1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2025-01-12 08:25:18 +00:00

Merge pull request #354 from catborise/master

fixes and others
This commit is contained in:
Anatoliy Guskov 2020-08-22 11:20:58 +03:00 committed by GitHub
commit a1fec1ebb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 110812 additions and 56712 deletions

View file

@ -1,4 +1,4 @@
## WebVirtCloud # WebVirtCloud
###### Python3 & Django 2.2 ###### Python3 & Django 2.2
## Features ## Features
@ -25,10 +25,21 @@ wget -O - https://clck.ru/9VMRH | sudo tee -a /usr/local/bin/gstfsd
sudo service supervisor restart sudo service supervisor restart
``` ```
### Description ## Description
WebVirtCloud is a virtualization web interface for admins and users. It can delegate Virtual Machine's to users. A noVNC viewer presents a full graphical console to the guest domain. KVM is currently the only hypervisor supported. WebVirtCloud is a virtualization web interface for admins and users. It can delegate Virtual Machine's to users. A noVNC viewer presents a full graphical console to the guest domain. KVM is currently the only hypervisor supported.
## Quick Install with Installer (Beta)
Install an OS and run specified commands. Installer supported OSes: Ubuntu 18.04, Debian 10, Centos/OEL/RHEL 8.
It can be installed on a virtual machine, physical host or on a KVM host.
```bash
wget https://raw.githubusercontent.com/retspen/webvirtcloud/master/install.sh
chmod 744 install.sh
# run with sudo or root user
./install.sh
```
## Manual Installation
### Generate secret key ### Generate secret key
You should generate SECRET_KEY after cloning repo. Then put it into webvirtcloud/settings.py. You should generate SECRET_KEY after cloning repo. Then put it into webvirtcloud/settings.py.
@ -95,7 +106,7 @@ sudo sed -r "s/SECRET_KEY = ''/SECRET_KEY = '"`python3 /srv/webvirtcloud/conf/ru
``` ```
#### Start installation webvirtcloud #### Start installation webvirtcloud
``` ```bash
virtualenv-3 venv virtualenv-3 venv
source venv/bin/activate source venv/bin/activate
pip3 install -r conf/requirements.txt pip3 install -r conf/requirements.txt
@ -306,7 +317,7 @@ Edit WS_PUBLIC_PORT at settings.py file to expose redirect to 80 or 443. Default
WS_PUBLIC_PORT = 80 WS_PUBLIC_PORT = 80
``` ```
### How To Update ## How To Update
```bash ```bash
# Go to Installation Directory # Go to Installation Directory
cd /srv/webvirtcloud cd /srv/webvirtcloud
@ -333,7 +344,7 @@ Run tests
python manage.py test python manage.py test
``` ```
### Screenshots ## Screenshots
Instance Detail: Instance Detail:
<img src="doc/images/instance.PNG" width="96%" align="center"/> <img src="doc/images/instance.PNG" width="96%" align="center"/>
Instance List:</br> Instance List:</br>
@ -343,6 +354,6 @@ Other: </br>
<img src="doc/images/hosts.PNG" width="47%"/> <img src="doc/images/hosts.PNG" width="47%"/>
<img src="doc/images/log.PNG" width="49%"/> <img src="doc/images/log.PNG" width="49%"/>
### License ## License
WebVirtCloud is licensed under the [Apache Licence, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). WebVirtCloud is licensed under the [Apache Licence, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html).

View file

@ -73,6 +73,7 @@ class UserForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(UserForm, self).__init__(*args, **kwargs) super(UserForm, self).__init__(*args, **kwargs)
if self.instance.id:
password = ReadOnlyPasswordHashField(label=_("Password"), password = ReadOnlyPasswordHashField(label=_("Password"),
help_text=format_lazy(_("""Raw passwords are not stored, so there is no way to see help_text=format_lazy(_("""Raw passwords are not stored, so there is no way to see
this user's password, but you can change the password this user's password, but you can change the password

View file

@ -0,0 +1,37 @@
# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
}

View file

@ -0,0 +1,85 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
#mail {
# # See sample authentication script at:
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
# # auth_http localhost/auth.php;
# # pop3_capabilities "TOP" "USER";
# # imap_capabilities "IMAP5rev1" "UIDPLUS";
#
# server {
# listen localhost:110;
# protocol pop3;
# proxy on;
# }
#
# server {
# listen localhost:143;
# protocol imap;
# proxy on;
# }
#}

View file

@ -0,0 +1,85 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
#mail {
# # See sample authentication script at:
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
# # auth_http localhost/auth.php;
# # pop3_capabilities "TOP" "USER";
# # imap_capabilities "IMAP5rev1" "UIDPLUS";
#
# server {
# listen localhost:110;
# protocol pop3;
# proxy on;
# }
#
# server {
# listen localhost:143;
# protocol imap;
# proxy on;
# }
#}

View file

@ -17,7 +17,11 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<!-- Icons (see Makefile for what the sizes are for) --> <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame
Remove this if you use the .htaccess -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- Icons (see app/images/icons/Makefile for what the sizes are for) -->
<link rel="icon" sizes="16x16" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-16x16.png" %}"> <link rel="icon" sizes="16x16" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-16x16.png" %}">
<link rel="icon" sizes="24x24" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-24x24.png" %}"> <link rel="icon" sizes="24x24" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-24x24.png" %}">
<link rel="icon" sizes="32x32" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-32x32.png" %}"> <link rel="icon" sizes="32x32" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-32x32.png" %}">
@ -54,12 +58,8 @@
<!-- Stylesheets --> <!-- Stylesheets -->
<link rel="stylesheet" href="{% static "js/novnc/app/styles/base.css" %}" /> <link rel="stylesheet" href="{% static "js/novnc/app/styles/base.css" %}" />
<!--
<script type='text/javascript' src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script>
-->
<!-- this is included as a normal file in order to catch script-loading errors as well --> <!-- this is included as a normal file in order to catch script-loading errors as well -->
<script type="text/javascript" src="{% static "js/novnc/app/error-handler.js" %}"></script> <script src="{% static "js/novnc/app/error-handler.js" %}"></script>
<!-- begin scripts --> <!-- begin scripts -->
<!-- promise polyfills promises for IE11 --> <!-- promise polyfills promises for IE11 -->
@ -91,7 +91,7 @@
<div class="noVNC_scroll"> <div class="noVNC_scroll">
<h1 class="noVNC_logo" translate="no"><span>no</span><br />VNC</h1> <h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>
<!-- Drag/Pan the viewport --> <!-- Drag/Pan the viewport -->
<input type="image" alt="viewport drag" src="{% static "js/novnc/app/images/drag.svg" %}" <input type="image" alt="viewport drag" src="{% static "js/novnc/app/images/drag.svg" %}"
@ -99,20 +99,11 @@
<!--noVNC Touch Device only buttons--> <!--noVNC Touch Device only buttons-->
<div id="noVNC_mobile_buttons"> <div id="noVNC_mobile_buttons">
<input type="image" alt="No mousebutton" src="{% static "js/novnc/app/images/mouse_none.svg" %}"
id="noVNC_mouse_button0" class="noVNC_button" title="Active Mouse Button" />
<input type="image" alt="Left mousebutton" src="{% static "js/novnc/app/images/mouse_left.svg" %}"
id="noVNC_mouse_button1" class="noVNC_button" title="Active Mouse Button" />
<input type="image" alt="Middle mousebutton" src="{% static "js/novnc/app/images/mouse_middle.svg" %}"
id="noVNC_mouse_button2" class="noVNC_button" title="Active Mouse Button" />
<input type="image" alt="Right mousebutton" src="{% static "js/novnc/app/images/mouse_right.svg" %}"
id="noVNC_mouse_button4" class="noVNC_button" title="Active Mouse Button" />
<input type="image" alt="Keyboard" src="{% static "js/novnc/app/images/keyboard.svg" %}" <input type="image" alt="Keyboard" src="{% static "js/novnc/app/images/keyboard.svg" %}"
id="noVNC_keyboard_button" class="noVNC_button" value="Keyboard" title="Show Keyboard" /> id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
</div> </div>
<!-- Extra manual keys --> <!-- Extra manual keys -->
<div id="noVNC_extra_keys">
<input type="image" alt="Extra keys" src="{% static "js/novnc/app/images/toggleextrakeys.svg" %}" <input type="image" alt="Extra keys" src="{% static "js/novnc/app/images/toggleextrakeys.svg" %}"
id="noVNC_toggle_extra_keys_button" class="noVNC_button" title="Show Extra Keys" /> id="noVNC_toggle_extra_keys_button" class="noVNC_button" title="Show Extra Keys" />
<div class="noVNC_vcenter"> <div class="noVNC_vcenter">
@ -131,7 +122,6 @@
id="noVNC_send_ctrl_alt_del_button" class="noVNC_button" title="Send Ctrl-Alt-Del" /> id="noVNC_send_ctrl_alt_del_button" class="noVNC_button" title="Send Ctrl-Alt-Del" />
</div> </div>
</div> </div>
</div>
<!-- Shutdown/Reboot --> <!-- Shutdown/Reboot -->
<input type="image" alt="Shutdown/Reboot" src="{% static "js/novnc/app/images/power.svg" %}" <input type="image" alt="Shutdown/Reboot" src="{% static "js/novnc/app/images/power.svg" %}"
@ -199,59 +189,53 @@
</li> </li>
<li> <li>
<div class="noVNC_expander">Advanced</div> <div class="noVNC_expander">Advanced</div>
<div> <div><ul>
<ul> <li>
<label for="noVNC_setting_quality">Quality:</label>
<input id="noVNC_setting_quality" type="range" min="0" max="9" value="6">
</li>
<li>
<label for="noVNC_setting_compression">Compression level:</label>
<input id="noVNC_setting_compression" type="range" min="0" max="9" value="2">
</li>
<li><hr></li>
<li> <li>
<label for="noVNC_setting_repeaterID">Repeater ID:</label> <label for="noVNC_setting_repeaterID">Repeater ID:</label>
<input id="noVNC_setting_repeaterID" type="input" value="" /> <input id="noVNC_setting_repeaterID" type="text" value="">
</li> </li>
<li> <li>
<div class="noVNC_expander">WebSocket</div> <div class="noVNC_expander">WebSocket</div>
<div> <div><ul>
<ul>
<li> <li>
<label><input id="noVNC_setting_encrypt" <label><input id="noVNC_setting_encrypt" type="checkbox"> Encrypt</label>
type="checkbox" />Encrypt</label>
</li> </li>
<li> <li>
<label for="noVNC_setting_host">Host:</label> <label for="noVNC_setting_host">Host:</label>
<input id="noVNC_setting_host" value="{{ ws_host }}" /> <input id="noVNC_setting_host">
</li> </li>
<li> <li>
<label for="noVNC_setting_port">Port:</label> <label for="noVNC_setting_port">Port:</label>
<input id="noVNC_setting_port" value="{{ ws_port }}" <input id="noVNC_setting_port" type="number">
type="number" />
</li> </li>
<li> <li>
<label for="noVNC_setting_path">Path:</label> <label for="noVNC_setting_path">Path:</label>
<!-- <input id="noVNC_setting_path" type="input" value="websockify"/> --> <input id="noVNC_setting_path" type="text" value="websockify">
<input id="noVNC_setting_path" type="input" value="{{ ws_path }}" />
</li> </li>
</ul> </ul></div>
</div>
</li> </li>
<li><hr></li>
<li> <li>
<hr> <label><input id="noVNC_setting_reconnect" type="checkbox"> Automatic Reconnect</label>
</li>
<li>
<label><input id="noVNC_setting_reconnect" type="checkbox" />Automatic
Reconnect</label>
<input id="noVNC_setting_autoconnect" type="checkbox" value="true" hidden />
</li> </li>
<li> <li>
<label for="noVNC_setting_reconnect_delay">Reconnect Delay (ms):</label> <label for="noVNC_setting_reconnect_delay">Reconnect Delay (ms):</label>
<input id="noVNC_setting_reconnect_delay" type="number" /> <input id="noVNC_setting_reconnect_delay" type="number">
</li> </li>
<li><hr></li>
<li> <li>
<hr> <label><input id="noVNC_setting_show_dot" type="checkbox"> Show Dot when No Cursor</label>
</li>
<li>
<label><input id="noVNC_setting_show_dot" type="checkbox">Show Dot when No
Cursor</label>
</li>
<li>
<hr>
</li> </li>
<li><hr></li>
<!-- Logging selection dropdown --> <!-- Logging selection dropdown -->
<li> <li>
<label>Logging: <label>Logging:
@ -259,8 +243,12 @@
</select> </select>
</label> </label>
</li> </li>
</ul> </ul></div>
</div> </li>
<li class="noVNC_version_separator"><hr></li>
<li class="noVNC_version_wrapper">
<span>Version:</span>
<span class="noVNC_version"></span>
</li> </li>
</ul> </ul>
</div> </div>
@ -292,10 +280,14 @@
<!-- Password Dialog --> <!-- Password Dialog -->
<div class="noVNC_center noVNC_connect_layer"> <div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_password_dlg" class="noVNC_panel"> <div id="noVNC_credentials_dlg" class="noVNC_panel">
<form aria-label="noVNC password form"> <form aria-label="noVNC password form">
<ul> <ul>
<li> <li id="noVNC_username_block">
<label>Username:</label>
<input id="noVNC_username_input">
</li>
<li id="noVNC_password_block">
<label>Password:</label> <label>Password:</label>
{% if perms.instances.passwordless_console %} {% if perms.instances.passwordless_console %}
<input id="noVNC_password_input" type="password" value='{{ console_passwd }}' /> <input id="noVNC_password_input" type="password" value='{{ console_passwd }}' />
@ -304,7 +296,7 @@
{% endif %} {% endif %}
</li> </li>
<li> <li>
<input id="noVNC_password_button" type="submit" value="Send Password" class="noVNC_submit" /> <input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit" />
</li> </li>
</ul> </ul>
</form> </form>
@ -326,8 +318,8 @@
html attributes which attempt to disable text suggestions on the html attributes which attempt to disable text suggestions on the
on-screen keyboard. Let's hope Chrome implements the ime-mode on-screen keyboard. Let's hope Chrome implements the ime-mode
style for example --> style for example -->
<textarea id="noVNC_keyboardinput" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" <textarea id="noVNC_keyboardinput" autocapitalize="off" autocomplete="off" spellcheck="false"
mozactionhint="Enter" tabindex="-1"></textarea> tabindex="-1"></textarea>
</div> </div>
<audio id="noVNC_bell"> <audio id="noVNC_bell">

View file

@ -205,7 +205,6 @@
rfb.addEventListener("disconnect", disconnectedFromServer); rfb.addEventListener("disconnect", disconnectedFromServer);
rfb.addEventListener("credentialsrequired", credentialsAreRequired); rfb.addEventListener("credentialsrequired", credentialsAreRequired);
rfb.addEventListener("desktopname", updateDesktopName); rfb.addEventListener("desktopname", updateDesktopName);
rfb.addEventListener("capabilities", function () { updatePowerButtons(); });
// Set parameters that can be changed on an active connection // Set parameters that can be changed on an active connection
rfb.scaleViewport = {{ scale }}; rfb.scaleViewport = {{ scale }};

13
install.sh Normal file
View file

@ -0,0 +1,13 @@
# ensure running as root
if [ "$(id -u)" != "0" ]; then
#Debian doesnt have sudo if root has a password.
if ! hash sudo 2>/dev/null; then
exec su -c "$0" "$@"
else
exec sudo "$0" "$@"
fi
fi
wget https://raw.githubusercontent.com/retspen/webvirtcloud/master/webvirtcloud.sh
chmod 744 webvirtcloud.sh
./webvirtcloud.sh 2>&1 | tee -a /var/log/webvirtcloud-install.log

View file

@ -389,7 +389,7 @@ def set_root_pass(request, pk):
messages.error(request, result['message']) messages.error(request, result['message'])
else: else:
msg = _("Please shutdown down your instance and then try again") msg = _("Please shutdown down your instance and then try again")
messages.error(msg) messages.error(request, msg)
return redirect(reverse('instances:instance', args=[instance.id]) + '#access') return redirect(reverse('instances:instance', args=[instance.id]) + '#access')
@ -412,10 +412,10 @@ def add_public_key(request, pk):
if result['return'] == 'success': if result['return'] == 'success':
messages.success(request, msg) messages.success(request, msg)
else: else:
messages.error(msg) messages.error(request, msg)
else: else:
msg = _("Please shutdown down your instance and then try again") msg = _("Please shutdown down your instance and then try again")
messages.error(msg) messages.error(request, msg)
return redirect(reverse('instances:instance', args=[instance.id]) + '#access') return redirect(reverse('instances:instance', args=[instance.id]) + '#access')
@ -434,7 +434,7 @@ def resizevm_cpu(request, pk):
quota_msg = utils.check_user_quota(request.user, 0, int(new_vcpu) - vcpu, 0, 0) quota_msg = utils.check_user_quota(request.user, 0, int(new_vcpu) - vcpu, 0, 0)
if not request.user.is_superuser and quota_msg: if not request.user.is_superuser and quota_msg:
msg = _(f"User {quota_msg} quota reached, cannot resize CPU of '{instance.name}'!") msg = _(f"User {quota_msg} quota reached, cannot resize CPU of '{instance.name}'!")
messages.error(msg) messages.error(request, msg)
else: else:
cur_vcpu = new_cur_vcpu cur_vcpu = new_cur_vcpu
vcpu = new_vcpu vcpu = new_vcpu
@ -468,7 +468,7 @@ def resize_memory(request, pk):
quota_msg = utils.check_user_quota(request.user, 0, 0, int(new_memory) - memory, 0) quota_msg = utils.check_user_quota(request.user, 0, 0, int(new_memory) - memory, 0)
if not request.user.is_superuser and quota_msg: if not request.user.is_superuser and quota_msg:
msg = _(f"User {quota_msg} quota reached, cannot resize memory of '{instance.name}'!") msg = _(f"User {quota_msg} quota reached, cannot resize memory of '{instance.name}'!")
messages.error(msg) messages.error(request, msg)
else: else:
cur_memory = new_cur_memory cur_memory = new_cur_memory
memory = new_memory memory = new_memory
@ -504,7 +504,7 @@ def resize_disk(request, pk):
quota_msg = utils.check_user_quota(request.user, 0, 0, 0, disk_new_sum - disk_sum) quota_msg = utils.check_user_quota(request.user, 0, 0, 0, disk_new_sum - disk_sum)
if not request.user.is_superuser and quota_msg: if not request.user.is_superuser and quota_msg:
msg = _(f"User {quota_msg} quota reached, cannot resize disks of '{instance.name}'!") msg = _(f"User {quota_msg} quota reached, cannot resize disks of '{instance.name}'!")
messages.error(msg) messages.error(request, msg)
else: else:
instance.proxy.resize_disk(disks_new) instance.proxy.resize_disk(disks_new)
msg = _("Disk resize") msg = _("Disk resize")
@ -1242,6 +1242,7 @@ def create_instance(request, compute_id, arch, machine):
flavors = Flavor.objects.filter().order_by('id') flavors = Flavor.objects.filter().order_by('id')
appsettings = AppSettings.objects.all() appsettings = AppSettings.objects.all()
try:
conn = wvmCreate(compute.hostname, compute.login, compute.password, compute.type) conn = wvmCreate(compute.hostname, compute.login, compute.password, compute.type)
default_firmware = app_settings.INSTANCE_FIRMWARE_DEFAULT_TYPE default_firmware = app_settings.INSTANCE_FIRMWARE_DEFAULT_TYPE
@ -1285,11 +1286,9 @@ def create_instance(request, compute_id, arch, machine):
if conn: if conn:
if not storages: if not storages:
msg = _("You haven't defined any storage pools") raise libvirtError(_("You haven't defined any storage pools"))
messages.error(request, msg)
if not networks: if not networks:
msg = _("You haven't defined any network pools") raise libvirtError(_("You haven't defined any network pools"))
messages.error(request, msg)
if request.method == 'POST': if request.method == 'POST':
if 'create' in request.POST: if 'create' in request.POST:
@ -1304,14 +1303,13 @@ def create_instance(request, compute_id, arch, machine):
meta_prealloc = True meta_prealloc = True
if instances: if instances:
if data['name'] in instances: if data['name'] in instances:
msg = _("A virtual machine with this name already exists") raise libvirtError(_("A virtual machine with this name already exists"))
messages.error(request, msg)
if Instance.objects.filter(name__exact=data['name']): if Instance.objects.filter(name__exact=data['name']):
messages.warning(request, _("There is an instance with same name. Are you sure?")) raise libvirtError(_("There is an instance with same name. Remove it and try again!"))
if data['hdd_size']: if data['hdd_size']:
if not data['mac']: if not data['mac']:
error_msg = _("No Virtual Machine MAC has been entered") raise libvirtError(_("No Virtual Machine MAC has been entered"))
messages.error(request, msg)
else: else:
path = conn.create_volume(data['storage'], data['name'], data['hdd_size'], default_disk_format, path = conn.create_volume(data['storage'], data['name'], data['hdd_size'], default_disk_format,
meta_prealloc, default_disk_owner_uid, default_disk_owner_gid) meta_prealloc, default_disk_owner_uid, default_disk_owner_gid)
@ -1334,8 +1332,7 @@ def create_instance(request, compute_id, arch, machine):
templ_path = conn.get_volume_path(data['template']) templ_path = conn.get_volume_path(data['template'])
dest_vol = conn.get_volume_path(data["name"] + ".img", data['storage']) dest_vol = conn.get_volume_path(data["name"] + ".img", data['storage'])
if dest_vol: if dest_vol:
error_msg = _("Image has already exist. Please check volumes or change instance name") raise libvirtError(_("Image has already exist. Please check volumes or change instance name"))
messages.error(error_msg)
else: else:
clone_path = conn.clone_from_template(data['name'], templ_path, data['storage'], meta_prealloc, clone_path = conn.clone_from_template(data['name'], templ_path, data['storage'], meta_prealloc,
default_disk_owner_uid, default_disk_owner_gid) default_disk_owner_uid, default_disk_owner_gid)
@ -1355,8 +1352,7 @@ def create_instance(request, compute_id, arch, machine):
is_disk_created = True is_disk_created = True
else: else:
if not data['images']: if not data['images']:
error_msg = _("First you need to create or select an image") raise libvirtError(_("First you need to create or select an image"))
messages.error(request, error_msg)
else: else:
for idx, vol in enumerate(data['images'].split(',')): for idx, vol in enumerate(data['images'].split(',')):
path = conn.get_volume_path(vol) path = conn.get_volume_path(vol)
@ -1375,7 +1371,7 @@ def create_instance(request, compute_id, arch, machine):
volume_list.append(volume) volume_list.append(volume)
if data['cache_mode'] not in conn.get_cache_modes(): if data['cache_mode'] not in conn.get_cache_modes():
error_msg = _("Invalid cache mode") error_msg = _("Invalid cache mode")
messages.error(error_msg) raise libvirtError
if 'UEFI' in data["firmware"]: if 'UEFI' in data["firmware"]:
firmware["loader"] = data["firmware"].split(":")[1].strip() firmware["loader"] = data["firmware"].split(":")[1].strip()
@ -1422,6 +1418,8 @@ def create_instance(request, compute_id, arch, machine):
conn.delete_volume(vol['path']) conn.delete_volume(vol['path'])
messages.error(request, lib_err) messages.error(request, lib_err)
conn.close() conn.close()
except libvirtError as lib_err:
messages.error(request, lib_err)
return render(request, 'create_instance_w2.html', locals()) return render(request, 'create_instance_w2.html', locals())

View file

@ -1,3 +1,11 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
// NB: this should *not* be included as a module until we have // NB: this should *not* be included as a module until we have
// native support in the browsers, so that our error handler // native support in the browsers, so that our error handler
// can catch script-loading errors. // can catch script-loading errors.

View file

@ -15,18 +15,18 @@
inkscape:export-xdpi="90" inkscape:export-xdpi="90"
sodipodi:docname="windows.svg" sodipodi:docname="windows.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png" inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:version="0.92.3 (2405546, 2018-03-11)" inkscape:version="0.92.4 (unknown)"
x="0px" x="0px"
y="0px" y="0px"
viewBox="-293 384 25 23" viewBox="-293 384 25 25"
xml:space="preserve" xml:space="preserve"
width="25" width="25"
height="23"><metadata height="25"><metadata
id="metadata21"><rdf:RDF><cc:Work id="metadata21"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs19" /><sodipodi:namedview id="defs19" /><sodipodi:namedview
pagecolor="#ffffff" pagecolor="#959595"
bordercolor="#666666" bordercolor="#666666"
borderopacity="1" borderopacity="1"
objecttolerance="10" objecttolerance="10"
@ -35,51 +35,31 @@
inkscape:pageopacity="0" inkscape:pageopacity="0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:window-width="1920" inkscape:window-width="1920"
inkscape:window-height="1017" inkscape:window-height="1136"
id="namedview17" id="namedview17"
showgrid="false" showgrid="true"
inkscape:pagecheckerboard="true" inkscape:pagecheckerboard="false"
inkscape:zoom="9.44" inkscape:zoom="32"
inkscape:cx="-0.84745763" inkscape:cx="3.926913"
inkscape:cy="12.5" inkscape:cy="13.255959"
inkscape:window-x="2552" inkscape:window-x="1920"
inkscape:window-y="122" inkscape:window-y="27"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="svg2" /> inkscape:current-layer="svg2"><inkscape:grid
type="xygrid"
id="grid818" /></sodipodi:namedview>
<style <style
type="text/css" type="text/css"
id="style2"> id="style2">
.st0{fill:#FFFFFF;} .st0{fill:#FFFFFF;}
</style> </style>
<g
id="g14"
transform="matrix(1.2624869,0,0,1.3601695,73.614445,-144.84322)">
<g
id="g12">
<path <path
class="st0" style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
d="m -277.4,396 c -0.7,0 -1.3,0 -2,0 -0.4,0 -0.5,-0.1 -0.5,-0.5 0,-1 0,-2 0,-3 0,-0.3 0.2,-0.5 0.5,-0.5 1.3,-0.1 2.6,-0.3 3.9,-0.4 0.4,0 0.7,0.1 0.7,0.6 0,1.1 0,2.2 0,3.3 0,0.4 -0.2,0.6 -0.6,0.6 -0.7,-0.1 -1.4,-0.1 -2,-0.1 z" d="M 21 4 L 11 5.1757812 L 11 12 L 21 12 L 21 4 z M 10 5.2949219 L 4 6 L 4 12 L 10 12 L 10 5.2949219 z "
id="path4" transform="translate(-293,384)"
inkscape:connector-curvature="0" id="path853" /><path
style="fill:#ffffff" /> id="path858"
<path d="m -272,405 -10,-1.17578 V 397 h 10 z M -283,403.70508 -289,403 v -6 h 6 z"
class="st0" style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -274.9,399.3 c 0,0.6 0,1.1 0,1.7 0,0.4 -0.1,0.6 -0.6,0.6 -1.4,-0.1 -2.8,-0.3 -4.1,-0.4 -0.3,0 -0.4,-0.3 -0.4,-0.5 0,-1 0,-2 0,-3 0,-0.4 0.2,-0.5 0.6,-0.5 1.3,0 2.6,0 3.9,0 0.5,0 0.6,0.2 0.6,0.6 0,0.4 0,0.9 0,1.5 z" inkscape:connector-curvature="0" /></svg>
id="path6"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m -283.5,396 c -0.6,0 -1.3,0 -1.9,0 -0.4,0 -0.6,-0.1 -0.6,-0.6 0,-0.8 0,-1.5 0,-2.3 0,-0.4 0.2,-0.6 0.6,-0.7 1.3,-0.1 2.7,-0.3 4,-0.4 0.4,0 0.5,0.1 0.5,0.5 0,1 0,1.9 0,2.9 0,0.4 -0.2,0.5 -0.5,0.5 -0.8,0.1 -1.5,0.1 -2.1,0.1 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m -283.5,397 c 0.6,0 1.3,0 1.9,0 0.4,0 0.6,0.1 0.6,0.5 0,1 0,1.9 0,2.9 0,0.4 -0.2,0.5 -0.5,0.5 -1.3,-0.1 -2.7,-0.3 -4,-0.4 -0.4,0 -0.6,-0.2 -0.6,-0.7 0,-0.7 0,-1.5 0,-2.2 0,-0.5 0.2,-0.7 0.7,-0.7 0.6,0.1 1.2,0.1 1.9,0.1 z"
id="path10"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1 @@
DO NOT MODIFY THE FILES IN THIS FOLDER, THEY ARE AUTOMATICALLY GENERATED FROM THE PO-FILES.

View file

@ -0,0 +1,73 @@
{
"Connecting...": "接続しています...",
"Disconnecting...": "切断しています...",
"Reconnecting...": "再接続しています...",
"Internal error": "内部エラー",
"Must set host": "ホストを設定する必要があります",
"Connected (encrypted) to ": "接続しました (暗号化済み): ",
"Connected (unencrypted) to ": "接続しました (暗号化されていません): ",
"Something went wrong, connection is closed": "何かが問題で、接続が閉じられました",
"Failed to connect to server": "サーバーへの接続に失敗しました",
"Disconnected": "切断しました",
"New connection has been rejected with reason: ": "新規接続は次の理由で拒否されました: ",
"New connection has been rejected": "新規接続は拒否されました",
"Password is required": "パスワードが必要です",
"noVNC encountered an error:": "noVNC でエラーが発生しました:",
"Hide/Show the control bar": "コントロールバーを隠す/表示する",
"Move/Drag Viewport": "ビューポートを移動/ドラッグ",
"viewport drag": "ビューポートをドラッグ",
"Active Mouse Button": "アクティブなマウスボタン",
"No mousebutton": "マウスボタンなし",
"Left mousebutton": "左マウスボタン",
"Middle mousebutton": "中マウスボタン",
"Right mousebutton": "右マウスボタン",
"Keyboard": "キーボード",
"Show Keyboard": "キーボードを表示",
"Extra keys": "追加キー",
"Show Extra Keys": "追加キーを表示",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl キーを切り替え",
"Alt": "Alt",
"Toggle Alt": "Alt キーを切り替え",
"Toggle Windows": "Windows キーを切り替え",
"Windows": "Windows",
"Send Tab": "Tab キーを送信",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Escape キーを送信",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del を送信",
"Shutdown/Reboot": "シャットダウン/再起動",
"Shutdown/Reboot...": "シャットダウン/再起動...",
"Power": "電源",
"Shutdown": "シャットダウン",
"Reboot": "再起動",
"Reset": "リセット",
"Clipboard": "クリップボード",
"Clear": "クリア",
"Fullscreen": "全画面表示",
"Settings": "設定",
"Shared Mode": "共有モード",
"View Only": "表示のみ",
"Clip to Window": "ウィンドウにクリップ",
"Scaling Mode:": "スケーリングモード:",
"None": "なし",
"Local Scaling": "ローカルスケーリング",
"Remote Resizing": "リモートでリサイズ",
"Advanced": "高度",
"Repeater ID:": "リピーター ID:",
"WebSocket": "WebSocket",
"Encrypt": "暗号化",
"Host:": "ホスト:",
"Port:": "ポート:",
"Path:": "パス:",
"Automatic Reconnect": "自動再接続",
"Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):",
"Show Dot when No Cursor": "カーソルがないときにドットを表示",
"Logging:": "ロギング:",
"Disconnect": "切断",
"Connect": "接続",
"Password:": "パスワード:",
"Send Password": "パスワードを送信",
"Cancel": "キャンセル"
}

View file

@ -11,16 +11,11 @@
"Disconnected": "Frånkopplad", "Disconnected": "Frånkopplad",
"New connection has been rejected with reason: ": "Ny anslutning har blivit nekad med följande skäl: ", "New connection has been rejected with reason: ": "Ny anslutning har blivit nekad med följande skäl: ",
"New connection has been rejected": "Ny anslutning har blivit nekad", "New connection has been rejected": "Ny anslutning har blivit nekad",
"Password is required": "Lösenord krävs", "Credentials are required": "Användaruppgifter krävs",
"noVNC encountered an error:": "noVNC stötte på ett problem:", "noVNC encountered an error:": "noVNC stötte på ett problem:",
"Hide/Show the control bar": "Göm/Visa kontrollbaren", "Hide/Show the control bar": "Göm/Visa kontrollbaren",
"Drag": "Dra",
"Move/Drag Viewport": "Flytta/Dra Vyn", "Move/Drag Viewport": "Flytta/Dra Vyn",
"viewport drag": "dra vy",
"Active Mouse Button": "Aktiv musknapp",
"No mousebutton": "Ingen musknapp",
"Left mousebutton": "Vänster musknapp",
"Middle mousebutton": "Mitten-musknapp",
"Right mousebutton": "Höger musknapp",
"Keyboard": "Tangentbord", "Keyboard": "Tangentbord",
"Show Keyboard": "Visa Tangentbord", "Show Keyboard": "Visa Tangentbord",
"Extra keys": "Extraknappar", "Extra keys": "Extraknappar",
@ -55,6 +50,8 @@
"Local Scaling": "Lokal Skalning", "Local Scaling": "Lokal Skalning",
"Remote Resizing": "Ändra Storlek", "Remote Resizing": "Ändra Storlek",
"Advanced": "Avancerat", "Advanced": "Avancerat",
"Quality:": "Kvalitet:",
"Compression level:": "Kompressionsnivå:",
"Repeater ID:": "Repeater-ID:", "Repeater ID:": "Repeater-ID:",
"WebSocket": "WebSocket", "WebSocket": "WebSocket",
"Encrypt": "Kryptera", "Encrypt": "Kryptera",
@ -65,9 +62,11 @@
"Reconnect Delay (ms):": "Fördröjning (ms):", "Reconnect Delay (ms):": "Fördröjning (ms):",
"Show Dot when No Cursor": "Visa prick när ingen muspekare finns", "Show Dot when No Cursor": "Visa prick när ingen muspekare finns",
"Logging:": "Loggning:", "Logging:": "Loggning:",
"Version:": "Version:",
"Disconnect": "Koppla från", "Disconnect": "Koppla från",
"Connect": "Anslut", "Connect": "Anslut",
"Username:": "Användarnamn:",
"Password:": "Lösenord:", "Password:": "Lösenord:",
"Send Password": "Skicka lösenord", "Send Credentials": "Skicka Användaruppgifter",
"Cancel": "Avbryt" "Cancel": "Avbryt"
} }

View file

@ -1,19 +1,19 @@
{ {
"Connecting...": "接中...", "Connecting...": "接中...",
"Disconnecting...": "正在断连接...", "Disconnecting...": "正在连接...",
"Reconnecting...": "重新接中...", "Reconnecting...": "重新接中...",
"Internal error": "内部错误", "Internal error": "内部错误",
"Must set host": "请提供主机名", "Must set host": "请提供主机名",
"Connected (encrypted) to ": "已加密链接到", "Connected (encrypted) to ": "已连接到(加密)",
"Connected (unencrypted) to ": "未加密链接到", "Connected (unencrypted) to ": "已连接到(未加密)",
"Something went wrong, connection is closed": "发生错误,接已关闭", "Something went wrong, connection is closed": "发生错误,接已关闭",
"Failed to connect to server": "无法接到服务器", "Failed to connect to server": "无法接到服务器",
"Disconnected": "链接断", "Disconnected": "已断开连接",
"New connection has been rejected with reason: ": "接被拒绝,原因:", "New connection has been rejected with reason: ": "接被拒绝,原因:",
"New connection has been rejected": "接被拒绝", "New connection has been rejected": "接被拒绝",
"Password is required": "请提供密码", "Password is required": "请提供密码",
"noVNC encountered an error:": "noVNC 遇到一个错误:", "noVNC encountered an error:": "noVNC 遇到一个错误:",
"Hide/Show the control bar": "显示/隐藏控制", "Hide/Show the control bar": "显示/隐藏控制",
"Move/Drag Viewport": "拖放显示范围", "Move/Drag Viewport": "拖放显示范围",
"viewport drag": "显示范围拖放", "viewport drag": "显示范围拖放",
"Active Mouse Button": "启动鼠标按鍵", "Active Mouse Button": "启动鼠标按鍵",
@ -43,10 +43,10 @@
"Reset": "重置", "Reset": "重置",
"Clipboard": "剪贴板", "Clipboard": "剪贴板",
"Clear": "清除", "Clear": "清除",
"Fullscreen": "全屏", "Fullscreen": "全屏",
"Settings": "设置", "Settings": "设置",
"Shared Mode": "分享模式", "Shared Mode": "分享模式",
"View Only": "仅检视", "View Only": "仅查看",
"Clip to Window": "限制/裁切窗口大小", "Clip to Window": "限制/裁切窗口大小",
"Scaling Mode:": "缩放模式:", "Scaling Mode:": "缩放模式:",
"None": "无", "None": "无",
@ -59,11 +59,11 @@
"Host:": "主机:", "Host:": "主机:",
"Port:": "端口:", "Port:": "端口:",
"Path:": "路径:", "Path:": "路径:",
"Automatic Reconnect": "自动重新接", "Automatic Reconnect": "自动重新接",
"Reconnect Delay (ms):": "重新接间隔 (ms)", "Reconnect Delay (ms):": "重新接间隔 (ms)",
"Logging:": "日志级别:", "Logging:": "日志级别:",
"Disconnect": "终端链接", "Disconnect": "中断连接",
"Connect": "接", "Connect": "接",
"Password:": "密码:", "Password:": "密码:",
"Cancel": "取消" "Cancel": "取消"
} }

View file

@ -1,6 +1,6 @@
/* /*
* noVNC base CSS * noVNC base CSS
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* noVNC is licensed under the MPL 2.0 (see LICENSE.txt) * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
* This file is licensed under the 2-Clause BSD license (see LICENSE.txt). * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
*/ */
@ -22,13 +22,12 @@
body { body {
margin:0; margin:0;
padding:0; padding:0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: Helvetica;
/*Background image with light grey curve.*/ /*Background image with light grey curve.*/
background-color:#494949; background-color:#494949;
background-repeat:no-repeat; background-repeat:no-repeat;
background-position:right bottom; background-position:right bottom;
height:100%; height:100%;
display: flex;
touch-action: none; touch-action: none;
} }
@ -84,8 +83,20 @@ html {
* ---------------------------------------- * ----------------------------------------
*/ */
input[type=input], input[type=password], input[type=number], input:not([type]),
input:not([type]), textarea { input[type=date],
input[type=datetime-local],
input[type=email],
input[type=month],
input[type=number],
input[type=password],
input[type=search],
input[type=tel],
input[type=text],
input[type=time],
input[type=url],
input[type=week],
textarea {
/* Disable default rendering */ /* Disable default rendering */
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
@ -99,7 +110,11 @@ input:not([type]), textarea {
background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240)); background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240));
} }
input[type=button], input[type=submit], select { input[type=button],
input[type=color],
input[type=reset],
input[type=submit],
select {
/* Disable default rendering */ /* Disable default rendering */
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
@ -117,7 +132,10 @@ input[type=button], input[type=submit], select {
vertical-align: middle; vertical-align: middle;
} }
input[type=button], input[type=submit] { input[type=button],
input[type=color],
input[type=reset],
input[type=submit] {
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
} }
@ -127,35 +145,72 @@ option {
background: white; background: white;
} }
input[type=input]:focus, input[type=password]:focus, input:not([type]):focus,
input:not([type]):focus, input[type=button]:focus, input[type=button]:focus,
input[type=color]:focus,
input[type=date]:focus,
input[type=datetime-local]:focus,
input[type=email]:focus,
input[type=month]:focus,
input[type=number]:focus,
input[type=password]:focus,
input[type=reset]:focus,
input[type=search]:focus,
input[type=submit]:focus, input[type=submit]:focus,
textarea:focus, select:focus { input[type=tel]:focus,
input[type=text]:focus,
input[type=time]:focus,
input[type=url]:focus,
input[type=week]:focus,
select:focus,
textarea:focus {
box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5); box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5);
border-color: rgb(74, 144, 217); border-color: rgb(74, 144, 217);
outline: none; outline: none;
} }
input[type=button]::-moz-focus-inner, input[type=button]::-moz-focus-inner,
input[type=color]::-moz-focus-inner,
input[type=reset]::-moz-focus-inner,
input[type=submit]::-moz-focus-inner { input[type=submit]::-moz-focus-inner {
border: none; border: none;
} }
input[type=input]:disabled, input[type=password]:disabled, input:not([type]):disabled,
input:not([type]):disabled, input[type=button]:disabled, input[type=button]:disabled,
input[type=submit]:disabled, input[type=number]:disabled, input[type=color]:disabled,
textarea:disabled, select:disabled { input[type=date]:disabled,
input[type=datetime-local]:disabled,
input[type=email]:disabled,
input[type=month]:disabled,
input[type=number]:disabled,
input[type=password]:disabled,
input[type=reset]:disabled,
input[type=search]:disabled,
input[type=submit]:disabled,
input[type=tel]:disabled,
input[type=text]:disabled,
input[type=time]:disabled,
input[type=url]:disabled,
input[type=week]:disabled,
select:disabled,
textarea:disabled {
color: rgb(128, 128, 128); color: rgb(128, 128, 128);
background: rgb(240, 240, 240); background: rgb(240, 240, 240);
} }
input[type=button]:active, input[type=submit]:active, input[type=button]:active,
input[type=color]:active,
input[type=reset]:active,
input[type=submit]:active,
select:active { select:active {
border-bottom-width: 1px; border-bottom-width: 1px;
margin-top: 3px; margin-top: 3px;
} }
:root:not(.noVNC_touch) input[type=button]:hover:not(:disabled), :root:not(.noVNC_touch) input[type=button]:hover:not(:disabled),
:root:not(.noVNC_touch) input[type=color]:hover:not(:disabled),
:root:not(.noVNC_touch) input[type=reset]:hover:not(:disabled),
:root:not(.noVNC_touch) input[type=submit]:hover:not(:disabled), :root:not(.noVNC_touch) input[type=submit]:hover:not(:disabled),
:root:not(.noVNC_touch) select:hover:not(:disabled) { :root:not(.noVNC_touch) select:hover:not(:disabled) {
background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250)); background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
@ -580,7 +635,7 @@ select:active {
} }
/* Extra manual keys */ /* Extra manual keys */
:root:not(.noVNC_connected) #noVNC_extra_keys { :root:not(.noVNC_connected) #noVNC_toggle_extra_keys_button {
display: none; display: none;
} }
@ -632,6 +687,16 @@ select:active {
width: 100px; width: 100px;
} }
/* Version */
.noVNC_version_wrapper {
font-size: small;
}
.noVNC_version {
margin-left: 1rem;
}
/* Connection Controls */ /* Connection Controls */
:root:not(.noVNC_connected) #noVNC_disconnect_button { :root:not(.noVNC_connected) #noVNC_disconnect_button {
display: none; display: none;
@ -781,19 +846,23 @@ select:active {
* ---------------------------------------- * ----------------------------------------
*/ */
#noVNC_password_dlg { #noVNC_credentials_dlg {
position: relative; position: relative;
transform: translateY(-50px); transform: translateY(-50px);
} }
#noVNC_password_dlg.noVNC_open { #noVNC_credentials_dlg.noVNC_open {
transform: translateY(0); transform: translateY(0);
} }
#noVNC_password_dlg ul { #noVNC_credentials_dlg ul {
list-style: none; list-style: none;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
} }
.noVNC_hidden {
display: none;
}
/* ---------------------------------------- /* ----------------------------------------
* Main Area * Main Area

View file

@ -60,3 +60,8 @@ html {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
#noVNC_container {
flex: 1; /* fill remaining space */
overflow: hidden;
}

View file

@ -1,6 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
@ -8,7 +8,7 @@
import * as Log from '../core/util/logging.js'; import * as Log from '../core/util/logging.js';
import _, { l10n } from './localization.js'; import _, { l10n } from './localization.js';
import { isTouchDevice, isSafari, isIOS, isAndroid, dragThreshold } import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold }
from '../core/util/browser.js'; from '../core/util/browser.js';
import { setCapture, getPointerEvent } from '../core/util/events.js'; import { setCapture, getPointerEvent } from '../core/util/events.js';
import KeyTable from "../core/input/keysym.js"; import KeyTable from "../core/input/keysym.js";
@ -17,6 +17,8 @@ import Keyboard from "../core/input/keyboard.js";
import RFB from "../core/rfb.js"; import RFB from "../core/rfb.js";
import * as WebUtil from "./webutil.js"; import * as WebUtil from "./webutil.js";
const PAGE_TITLE = "noVNC";
const UI = { const UI = {
connected: false, connected: false,
@ -35,9 +37,9 @@ const UI = {
lastKeyboardinput: null, lastKeyboardinput: null,
defaultKeyboardinputLen: 100, defaultKeyboardinputLen: 100,
inhibit_reconnect: true, inhibitReconnect: true,
reconnect_callback: null, reconnectCallback: null,
reconnect_password: null, reconnectPassword: null,
prime() { prime() {
return WebUtil.initSettings().then(() => { return WebUtil.initSettings().then(() => {
@ -59,6 +61,17 @@ const UI = {
// Translate the DOM // Translate the DOM
l10n.translateDOM(); l10n.translateDOM();
WebUtil.fetchJSON('/static/js/novnc/package.json')
.then((packageInfo) => {
Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
})
.catch((err) => {
Log.Error("Couldn't fetch package.json: " + err);
Array.from(document.getElementsByClassName('noVNC_version_wrapper'))
.concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))
.forEach(el => el.style.display = 'none');
});
// Adapt the interface for touch screen devices // Adapt the interface for touch screen devices
if (isTouchDevice) { if (isTouchDevice) {
document.documentElement.classList.add("noVNC_touch"); document.documentElement.classList.add("noVNC_touch");
@ -148,6 +161,8 @@ const UI = {
UI.initSetting('encrypt', (window.location.protocol === "https:")); UI.initSetting('encrypt', (window.location.protocol === "https:"));
UI.initSetting('view_clip', false); UI.initSetting('view_clip', false);
UI.initSetting('resize', 'off'); UI.initSetting('resize', 'off');
UI.initSetting('quality', 6);
UI.initSetting('compression', 2);
UI.initSetting('shared', true); UI.initSetting('shared', true);
UI.initSetting('view_only', false); UI.initSetting('view_only', false);
UI.initSetting('show_dot', false); UI.initSetting('show_dot', false);
@ -219,14 +234,6 @@ const UI = {
}, },
addTouchSpecificHandlers() { addTouchSpecificHandlers() {
document.getElementById("noVNC_mouse_button0")
.addEventListener('click', () => UI.setMouseButton(1));
document.getElementById("noVNC_mouse_button1")
.addEventListener('click', () => UI.setMouseButton(2));
document.getElementById("noVNC_mouse_button2")
.addEventListener('click', () => UI.setMouseButton(4));
document.getElementById("noVNC_mouse_button4")
.addEventListener('click', () => UI.setMouseButton(0));
document.getElementById("noVNC_keyboard_button") document.getElementById("noVNC_keyboard_button")
.addEventListener('click', UI.toggleVirtualKeyboard); .addEventListener('click', UI.toggleVirtualKeyboard);
@ -282,33 +289,6 @@ const UI = {
.addEventListener('click', UI.sendEsc); .addEventListener('click', UI.sendEsc);
document.getElementById("noVNC_send_ctrl_alt_del_button") document.getElementById("noVNC_send_ctrl_alt_del_button")
.addEventListener('click', UI.sendCtrlAltDel); .addEventListener('click', UI.sendCtrlAltDel);
document.getElementById('ctrlaltdel')
.addEventListener('click', UI.sendCtrlAltDel);
document.getElementById('ctrlaltf1')
.addEventListener('click', () => UI.sendCtrlAltFN(0));
document.getElementById('ctrlaltf2')
.addEventListener('click', () => UI.sendCtrlAltFN(1));
document.getElementById('ctrlaltf3')
.addEventListener('click', () => UI.sendCtrlAltFN(2));
document.getElementById('ctrlaltf4')
.addEventListener('click', () => UI.sendCtrlAltFN(3));
document.getElementById('ctrlaltf5')
.addEventListener('click', () => UI.sendCtrlAltFN(4));
document.getElementById('ctrlaltf6')
.addEventListener('click', () => UI.sendCtrlAltFN(5));
document.getElementById('ctrlaltf7')
.addEventListener('click', () => UI.sendCtrlAltFN(6));
document.getElementById('ctrlaltf8')
.addEventListener('click', () => UI.sendCtrlAltFN(7));
document.getElementById('ctrlaltf9')
.addEventListener('click', () => UI.sendCtrlAltFN(8));
document.getElementById('ctrlaltf10')
.addEventListener('click', () => UI.sendCtrlAltFN(9));
document.getElementById('ctrlaltf11')
.addEventListener('click', () => UI.sendCtrlAltFN(10));
document.getElementById('ctrlaltf12')
.addEventListener('click', () => UI.sendCtrlAltFN(11));
}, },
addMachineHandlers() { addMachineHandlers() {
@ -330,8 +310,8 @@ const UI = {
document.getElementById("noVNC_cancel_reconnect_button") document.getElementById("noVNC_cancel_reconnect_button")
.addEventListener('click', UI.cancelReconnect); .addEventListener('click', UI.cancelReconnect);
document.getElementById("noVNC_password_button") document.getElementById("noVNC_credentials_button")
.addEventListener('click', UI.setPassword); .addEventListener('click', UI.setCredentials);
}, },
addClipboardHandlers() { addClipboardHandlers() {
@ -361,6 +341,10 @@ const UI = {
UI.addSettingChangeHandler('resize'); UI.addSettingChangeHandler('resize');
UI.addSettingChangeHandler('resize', UI.applyResizeMode); UI.addSettingChangeHandler('resize', UI.applyResizeMode);
UI.addSettingChangeHandler('resize', UI.updateViewClip); UI.addSettingChangeHandler('resize', UI.updateViewClip);
UI.addSettingChangeHandler('quality');
UI.addSettingChangeHandler('quality', UI.updateQuality);
UI.addSettingChangeHandler('compression');
UI.addSettingChangeHandler('compression', UI.updateCompression);
UI.addSettingChangeHandler('view_clip'); UI.addSettingChangeHandler('view_clip');
UI.addSettingChangeHandler('view_clip', UI.updateViewClip); UI.addSettingChangeHandler('view_clip', UI.updateViewClip);
UI.addSettingChangeHandler('shared'); UI.addSettingChangeHandler('shared');
@ -381,8 +365,6 @@ const UI = {
addFullscreenHandlers() { addFullscreenHandlers() {
document.getElementById("noVNC_fullscreen_button") document.getElementById("noVNC_fullscreen_button")
.addEventListener('click', UI.toggleFullscreen); .addEventListener('click', UI.toggleFullscreen);
document.getElementById("fullscreen_button")
.addEventListener('click', UI.toggleFullscreen);
window.addEventListener('fullscreenchange', UI.updateFullscreenButton); window.addEventListener('fullscreenchange', UI.updateFullscreenButton);
window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton); window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton);
@ -404,25 +386,25 @@ const UI = {
document.documentElement.classList.remove("noVNC_disconnecting"); document.documentElement.classList.remove("noVNC_disconnecting");
document.documentElement.classList.remove("noVNC_reconnecting"); document.documentElement.classList.remove("noVNC_reconnecting");
const transition_elem = document.getElementById("noVNC_transition_text"); const transitionElem = document.getElementById("noVNC_transition_text");
switch (state) { switch (state) {
case 'init': case 'init':
break; break;
case 'connecting': case 'connecting':
transition_elem.textContent = _("Connecting..."); transitionElem.textContent = _("Connecting...");
document.documentElement.classList.add("noVNC_connecting"); document.documentElement.classList.add("noVNC_connecting");
break; break;
case 'connected': case 'connected':
document.documentElement.classList.add("noVNC_connected"); document.documentElement.classList.add("noVNC_connected");
break; break;
case 'disconnecting': case 'disconnecting':
transition_elem.textContent = _("Disconnecting..."); transitionElem.textContent = _("Disconnecting...");
document.documentElement.classList.add("noVNC_disconnecting"); document.documentElement.classList.add("noVNC_disconnecting");
break; break;
case 'disconnected': case 'disconnected':
break; break;
case 'reconnecting': case 'reconnecting':
transition_elem.textContent = _("Reconnecting..."); transitionElem.textContent = _("Reconnecting...");
document.documentElement.classList.add("noVNC_reconnecting"); document.documentElement.classList.add("noVNC_reconnecting");
break; break;
default: default:
@ -440,7 +422,6 @@ const UI = {
UI.disableSetting('port'); UI.disableSetting('port');
UI.disableSetting('path'); UI.disableSetting('path');
UI.disableSetting('repeaterID'); UI.disableSetting('repeaterID');
UI.setMouseButton(1);
// Hide the controlbar after 2 seconds // Hide the controlbar after 2 seconds
UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
@ -455,38 +436,35 @@ const UI = {
UI.keepControlbar(); UI.keepControlbar();
} }
// State change closes the password dialog // State change closes dialogs as they may not be relevant
document.getElementById('noVNC_password_dlg') // anymore
UI.closeAllPanels();
document.getElementById('noVNC_credentials_dlg')
.classList.remove('noVNC_open'); .classList.remove('noVNC_open');
}, },
showStatus(text, status_type, time) { showStatus(text, statusType, time) {
const statusElem = document.getElementById('noVNC_status'); const statusElem = document.getElementById('noVNC_status');
clearTimeout(UI.statusTimeout); if (typeof statusType === 'undefined') {
statusType = 'normal';
if (typeof status_type === 'undefined') {
status_type = 'normal';
} }
// Don't overwrite more severe visible statuses and never // Don't overwrite more severe visible statuses and never
// errors. Only shows the first error. // errors. Only shows the first error.
let visible_status_type = 'none';
if (statusElem.classList.contains("noVNC_open")) { if (statusElem.classList.contains("noVNC_open")) {
if (statusElem.classList.contains("noVNC_status_error")) { if (statusElem.classList.contains("noVNC_status_error")) {
visible_status_type = 'error';
} else if (statusElem.classList.contains("noVNC_status_warn")) {
visible_status_type = 'warn';
} else {
visible_status_type = 'normal';
}
}
if (visible_status_type === 'error' ||
(visible_status_type === 'warn' && status_type === 'normal')) {
return; return;
} }
if (statusElem.classList.contains("noVNC_status_warn") &&
statusType === 'normal') {
return;
}
}
switch (status_type) { clearTimeout(UI.statusTimeout);
switch (statusType) {
case 'error': case 'error':
statusElem.classList.remove("noVNC_status_warn"); statusElem.classList.remove("noVNC_status_warn");
statusElem.classList.remove("noVNC_status_normal"); statusElem.classList.remove("noVNC_status_normal");
@ -516,7 +494,7 @@ const UI = {
} }
// Error messages do not timeout // Error messages do not timeout
if (status_type !== 'error') { if (statusType !== 'error') {
UI.statusTimeout = window.setTimeout(UI.hideStatus, time); UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
} }
}, },
@ -536,6 +514,13 @@ const UI = {
}, },
idleControlbar() { idleControlbar() {
// Don't fade if a child of the control bar has focus
if (document.getElementById('noVNC_control_bar')
.contains(document.activeElement) && document.hasFocus()) {
UI.activateControlbar();
return;
}
document.getElementById('noVNC_control_bar_anchor') document.getElementById('noVNC_control_bar_anchor')
.classList.add("noVNC_idle"); .classList.add("noVNC_idle");
}, },
@ -553,6 +538,7 @@ const UI = {
UI.closeAllPanels(); UI.closeAllPanels();
document.getElementById('noVNC_control_bar') document.getElementById('noVNC_control_bar')
.classList.remove("noVNC_open"); .classList.remove("noVNC_open");
UI.rfb.focus();
}, },
toggleControlbar() { toggleControlbar() {
@ -850,6 +836,8 @@ const UI = {
UI.updateSetting('encrypt'); UI.updateSetting('encrypt');
UI.updateSetting('view_clip'); UI.updateSetting('view_clip');
UI.updateSetting('resize'); UI.updateSetting('resize');
UI.updateSetting('quality');
UI.updateSetting('compression');
UI.updateSetting('shared'); UI.updateSetting('shared');
UI.updateSetting('view_only'); UI.updateSetting('view_only');
UI.updateSetting('path'); UI.updateSetting('path');
@ -1006,7 +994,7 @@ const UI = {
if (typeof password === 'undefined') { if (typeof password === 'undefined') {
password = WebUtil.getConfigVar('password'); password = WebUtil.getConfigVar('password');
UI.reconnect_password = password; UI.reconnectPassword = password;
} }
if (password === null) { if (password === null) {
@ -1021,7 +1009,6 @@ const UI = {
return; return;
} }
UI.closeAllPanels();
UI.closeConnectPanel(); UI.closeConnectPanel();
UI.updateVisualState('connecting'); UI.updateVisualState('connecting');
@ -1038,7 +1025,6 @@ const UI = {
UI.rfb = new RFB(document.getElementById('noVNC_container'), url, UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
{ shared: UI.getSetting('shared'), { shared: UI.getSetting('shared'),
showDotCursor: UI.getSetting('show_dot'),
repeaterID: UI.getSetting('repeaterID'), repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } }); credentials: { password: password } });
UI.rfb.addEventListener("connect", UI.connectFinished); UI.rfb.addEventListener("connect", UI.connectFinished);
@ -1052,18 +1038,20 @@ const UI = {
UI.rfb.clipViewport = UI.getSetting('view_clip'); UI.rfb.clipViewport = UI.getSetting('view_clip');
UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.updateViewOnly(); // requires UI.rfb UI.updateViewOnly(); // requires UI.rfb
}, },
disconnect() { disconnect() {
UI.closeAllPanels();
UI.rfb.disconnect(); UI.rfb.disconnect();
UI.connected = false; UI.connected = false;
// Disable automatic reconnecting // Disable automatic reconnecting
UI.inhibit_reconnect = true; UI.inhibitReconnect = true;
UI.updateVisualState('disconnecting'); UI.updateVisualState('disconnecting');
@ -1071,20 +1059,20 @@ const UI = {
}, },
reconnect() { reconnect() {
UI.reconnect_callback = null; UI.reconnectCallback = null;
// if reconnect has been disabled in the meantime, do nothing. // if reconnect has been disabled in the meantime, do nothing.
if (UI.inhibit_reconnect) { if (UI.inhibitReconnect) {
return; return;
} }
UI.connect(null, UI.reconnect_password); UI.connect(null, UI.reconnectPassword);
}, },
cancelReconnect() { cancelReconnect() {
if (UI.reconnect_callback !== null) { if (UI.reconnectCallback !== null) {
clearTimeout(UI.reconnect_callback); clearTimeout(UI.reconnectCallback);
UI.reconnect_callback = null; UI.reconnectCallback = null;
} }
UI.updateVisualState('disconnected'); UI.updateVisualState('disconnected');
@ -1095,7 +1083,7 @@ const UI = {
connectFinished(e) { connectFinished(e) {
UI.connected = true; UI.connected = true;
UI.inhibit_reconnect = false; UI.inhibitReconnect = false;
let msg; let msg;
if (UI.getSetting('encrypt')) { if (UI.getSetting('encrypt')) {
@ -1129,17 +1117,19 @@ const UI = {
} else { } else {
UI.showStatus(_("Failed to connect to server"), 'error'); UI.showStatus(_("Failed to connect to server"), 'error');
} }
} else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) { } else if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
UI.updateVisualState('reconnecting'); UI.updateVisualState('reconnecting');
const delay = parseInt(UI.getSetting('reconnect_delay')); const delay = parseInt(UI.getSetting('reconnect_delay'));
UI.reconnect_callback = setTimeout(UI.reconnect, delay); UI.reconnectCallback = setTimeout(UI.reconnect, delay);
return; return;
} else { } else {
UI.updateVisualState('disconnected'); UI.updateVisualState('disconnected');
UI.showStatus(_("Disconnected"), 'normal'); UI.showStatus(_("Disconnected"), 'normal');
} }
document.title = PAGE_TITLE;
UI.openControlbar(); UI.openControlbar();
UI.openConnectPanel(); UI.openConnectPanel();
}, },
@ -1166,27 +1156,46 @@ const UI = {
credentials(e) { credentials(e) {
// FIXME: handle more types // FIXME: handle more types
document.getElementById('noVNC_password_dlg')
document.getElementById("noVNC_username_block").classList.remove("noVNC_hidden");
document.getElementById("noVNC_password_block").classList.remove("noVNC_hidden");
let inputFocus = "none";
if (e.detail.types.indexOf("username") === -1) {
document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
} else {
inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
}
if (e.detail.types.indexOf("password") === -1) {
document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
} else {
inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
}
document.getElementById('noVNC_credentials_dlg')
.classList.add('noVNC_open'); .classList.add('noVNC_open');
setTimeout(() => document setTimeout(() => document
.getElementById('noVNC_password_input').focus(), 100); .getElementById(inputFocus).focus(), 100);
Log.Warn("Server asked for a password"); Log.Warn("Server asked for credentials");
UI.showStatus(_("Password is required"), "warning"); UI.showStatus(_("Credentials are required"), "warning");
}, },
setPassword(e) { setCredentials(e) {
// Prevent actually submitting the form // Prevent actually submitting the form
e.preventDefault(); e.preventDefault();
const inputElem = document.getElementById('noVNC_password_input'); let inputElemUsername = document.getElementById('noVNC_username_input');
const password = inputElem.value; const username = inputElemUsername.value;
let inputElemPassword = document.getElementById('noVNC_password_input');
const password = inputElemPassword.value;
// Clear the input after reading the password // Clear the input after reading the password
inputElem.value = ""; inputElemPassword.value = "";
UI.rfb.sendCredentials({ password: password });
UI.reconnect_password = password; UI.rfb.sendCredentials({ username: username, password: password });
document.getElementById('noVNC_password_dlg') UI.reconnectPassword = password;
document.getElementById('noVNC_credentials_dlg')
.classList.remove('noVNC_open'); .classList.remove('noVNC_open');
}, },
@ -1269,8 +1278,9 @@ const UI = {
// Can't be clipping if viewport is scaled to fit // Can't be clipping if viewport is scaled to fit
UI.forceSetting('view_clip', false); UI.forceSetting('view_clip', false);
UI.rfb.clipViewport = false; UI.rfb.clipViewport = false;
} else if (isIOS() || isAndroid()) { } else if (!hasScrollbarGutter) {
// iOS and Android usually have shit scrollbars // Some platforms have scrollbars that are difficult
// to use in our case, so we always use our own panning
UI.forceSetting('view_clip', true); UI.forceSetting('view_clip', true);
UI.rfb.clipViewport = true; UI.rfb.clipViewport = true;
} else { } else {
@ -1313,30 +1323,40 @@ const UI = {
viewDragButton.classList.remove("noVNC_selected"); viewDragButton.classList.remove("noVNC_selected");
} }
// Different behaviour for touch vs non-touch
// The button is disabled instead of hidden on touch devices
if (isTouchDevice) {
viewDragButton.classList.remove("noVNC_hidden");
if (UI.rfb.clipViewport) {
viewDragButton.disabled = false;
} else {
viewDragButton.disabled = true;
}
} else {
viewDragButton.disabled = false;
if (UI.rfb.clipViewport) { if (UI.rfb.clipViewport) {
viewDragButton.classList.remove("noVNC_hidden"); viewDragButton.classList.remove("noVNC_hidden");
} else { } else {
viewDragButton.classList.add("noVNC_hidden"); viewDragButton.classList.add("noVNC_hidden");
} }
}
}, },
/* ------^------- /* ------^-------
* /VIEWDRAG * /VIEWDRAG
* ============== * ==============
* QUALITY
* ------v------*/
updateQuality() {
if (!UI.rfb) return;
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
},
/* ------^-------
* /QUALITY
* ==============
* COMPRESSION
* ------v------*/
updateCompression() {
if (!UI.rfb) return;
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
},
/* ------^-------
* /COMPRESSION
* ==============
* KEYBOARD * KEYBOARD
* ------v------*/ * ------v------*/
@ -1531,20 +1551,20 @@ const UI = {
}, },
sendEsc() { sendEsc() {
UI.rfb.sendKey(KeyTable.XK_Escape, "Escape"); UI.sendKey(KeyTable.XK_Escape, "Escape");
}, },
sendTab() { sendTab() {
UI.rfb.sendKey(KeyTable.XK_Tab); UI.sendKey(KeyTable.XK_Tab, "Tab");
}, },
toggleCtrl() { toggleCtrl() {
const btn = document.getElementById('noVNC_toggle_ctrl_button'); const btn = document.getElementById('noVNC_toggle_ctrl_button');
if (btn.classList.contains("noVNC_selected")) { if (btn.classList.contains("noVNC_selected")) {
UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
btn.classList.remove("noVNC_selected"); btn.classList.remove("noVNC_selected");
} else { } else {
UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
btn.classList.add("noVNC_selected"); btn.classList.add("noVNC_selected");
} }
}, },
@ -1552,10 +1572,10 @@ const UI = {
toggleWindows() { toggleWindows() {
const btn = document.getElementById('noVNC_toggle_windows_button'); const btn = document.getElementById('noVNC_toggle_windows_button');
if (btn.classList.contains("noVNC_selected")) { if (btn.classList.contains("noVNC_selected")) {
UI.rfb.sendKey(KeyTable.XK_Super_L, "MetaLeft", false); UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", false);
btn.classList.remove("noVNC_selected"); btn.classList.remove("noVNC_selected");
} else { } else {
UI.rfb.sendKey(KeyTable.XK_Super_L, "MetaLeft", true); UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", true);
btn.classList.add("noVNC_selected"); btn.classList.add("noVNC_selected");
} }
}, },
@ -1563,20 +1583,39 @@ const UI = {
toggleAlt() { toggleAlt() {
const btn = document.getElementById('noVNC_toggle_alt_button'); const btn = document.getElementById('noVNC_toggle_alt_button');
if (btn.classList.contains("noVNC_selected")) { if (btn.classList.contains("noVNC_selected")) {
UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", false); UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
btn.classList.remove("noVNC_selected"); btn.classList.remove("noVNC_selected");
} else { } else {
UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", true); UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
btn.classList.add("noVNC_selected"); btn.classList.add("noVNC_selected");
} }
}, },
sendCtrlAltDel() { sendCtrlAltDel() {
UI.rfb.sendCtrlAltDel(); UI.rfb.sendCtrlAltDel();
// See below
UI.rfb.focus();
UI.idleControlbar();
}, },
sendCtrlAltFN: function(f) { sendKey(keysym, code, down) {
UI.rfb.sendCtrlAltFN(f); UI.rfb.sendKey(keysym, code, down);
// Move focus to the screen in order to be able to use the
// keyboard right after these extra keys.
// The exception is when a virtual keyboard is used, because
// if we focus the screen the virtual keyboard would be closed.
// In this case we focus our special virtual keyboard input
// element instead.
if (document.getElementById('noVNC_keyboard_button')
.classList.contains("noVNC_selected")) {
document.getElementById('noVNC_keyboardinput').focus();
} else {
UI.rfb.focus();
}
// fade out the controlbar to highlight that
// the focus has been moved to the screen
UI.idleControlbar();
}, },
/* ------^------- /* ------^-------
@ -1585,24 +1624,6 @@ const UI = {
* MISC * MISC
* ------v------*/ * ------v------*/
setMouseButton(num) {
const view_only = UI.rfb.viewOnly;
if (UI.rfb && !view_only) {
UI.rfb.touchButton = num;
}
const blist = [0, 1, 2, 4];
for (let b = 0; b < blist.length; b++) {
const button = document.getElementById('noVNC_mouse_button' +
blist[b]);
if (blist[b] === num && !view_only) {
button.classList.remove("noVNC_hidden");
} else {
button.classList.add("noVNC_hidden");
}
}
},
updateViewOnly() { updateViewOnly() {
if (!UI.rfb) return; if (!UI.rfb) return;
UI.rfb.viewOnly = UI.getSetting('view_only'); UI.rfb.viewOnly = UI.getSetting('view_only');
@ -1613,14 +1634,14 @@ const UI = {
.classList.add('noVNC_hidden'); .classList.add('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button') document.getElementById('noVNC_toggle_extra_keys_button')
.classList.add('noVNC_hidden'); .classList.add('noVNC_hidden');
document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton) document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden'); .classList.add('noVNC_hidden');
} else { } else {
document.getElementById('noVNC_keyboard_button') document.getElementById('noVNC_keyboard_button')
.classList.remove('noVNC_hidden'); .classList.remove('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button') document.getElementById('noVNC_toggle_extra_keys_button')
.classList.remove('noVNC_hidden'); .classList.remove('noVNC_hidden');
document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton) document.getElementById('noVNC_clipboard_button')
.classList.remove('noVNC_hidden'); .classList.remove('noVNC_hidden');
} }
}, },
@ -1631,13 +1652,13 @@ const UI = {
}, },
updateLogging() { updateLogging() {
WebUtil.init_logging(UI.getSetting('logging')); WebUtil.initLogging(UI.getSetting('logging'));
}, },
updateDesktopName(e) { updateDesktopName(e) {
UI.desktopName = e.detail.name; UI.desktopName = e.detail.name;
// Display the desktop name in the document title // Display the desktop name in the document title
document.title = e.detail.name + " - noVNC"; document.title = e.detail.name + " - " + PAGE_TITLE;
}, },
bell(e) { bell(e) {
@ -1673,7 +1694,7 @@ const UI = {
}; };
// Set up translations // Set up translations
const LINGUAS = ["cs", "de", "el", "es", "ko", "nl", "pl", "ru", "sv", "tr", "zh_CN", "zh_TW"]; const LINGUAS = ["cs", "de", "el", "es", "ja", "ko", "nl", "pl", "ru", "sv", "tr", "zh_CN", "zh_TW"];
l10n.setup(LINGUAS); l10n.setup(LINGUAS);
if (l10n.language === "en" || l10n.dictionary !== undefined) { if (l10n.language === "en" || l10n.dictionary !== undefined) {
UI.prime(); UI.prime();

View file

@ -1,21 +1,21 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*/ */
import { init_logging as main_init_logging } from '../core/util/logging.js'; import { initLogging as mainInitLogging } from '../core/util/logging.js';
// init log level reading the logging HTTP param // init log level reading the logging HTTP param
export function init_logging(level) { export function initLogging(level) {
"use strict"; "use strict";
if (typeof level !== "undefined") { if (typeof level !== "undefined") {
main_init_logging(level); mainInitLogging(level);
} else { } else {
const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/); const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);
main_init_logging(param || undefined); mainInitLogging(param || undefined);
} }
} }
@ -184,7 +184,7 @@ export function injectParamIfMissing(path, param, value) {
const elem = document.createElement('a'); const elem = document.createElement('a');
elem.href = path; elem.href = path;
const param_eq = encodeURIComponent(param) + "="; const paramEq = encodeURIComponent(param) + "=";
let query; let query;
if (elem.search) { if (elem.search) {
query = elem.search.slice(1).split('&'); query = elem.search.slice(1).split('&');
@ -192,8 +192,8 @@ export function injectParamIfMissing(path, param, value) {
query = []; query = [];
} }
if (!query.some(v => v.startsWith(param_eq))) { if (!query.some(v => v.startsWith(paramEq))) {
query.push(param_eq + encodeURIComponent(value)); query.push(paramEq + encodeURIComponent(value));
elem.search = "?" + query.join("&"); elem.search = "?" + query.join("&");
} }

View file

@ -57,12 +57,12 @@ export default {
/* eslint-enable comma-spacing */ /* eslint-enable comma-spacing */
decode(data, offset = 0) { decode(data, offset = 0) {
let data_length = data.indexOf('=') - offset; let dataLength = data.indexOf('=') - offset;
if (data_length < 0) { data_length = data.length - offset; } if (dataLength < 0) { dataLength = data.length - offset; }
/* Every four characters is 3 resulting numbers */ /* Every four characters is 3 resulting numbers */
const result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5); const resultLength = (dataLength >> 2) * 3 + Math.floor((dataLength % 4) / 1.5);
const result = new Array(result_length); const result = new Array(resultLength);
// Convert one by one. // Convert one by one.

View file

@ -1,8 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin * Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.

View file

@ -1,8 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin * Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
@ -19,10 +17,10 @@ export default class HextileDecoder {
decodeRect(x, y, width, height, sock, display, depth) { decodeRect(x, y, width, height, sock, display, depth) {
if (this._tiles === 0) { if (this._tiles === 0) {
this._tiles_x = Math.ceil(width / 16); this._tilesX = Math.ceil(width / 16);
this._tiles_y = Math.ceil(height / 16); this._tilesY = Math.ceil(height / 16);
this._total_tiles = this._tiles_x * this._tiles_y; this._totalTiles = this._tilesX * this._tilesY;
this._tiles = this._total_tiles; this._tiles = this._totalTiles;
} }
while (this._tiles > 0) { while (this._tiles > 0) {
@ -41,11 +39,11 @@ export default class HextileDecoder {
subencoding + ")"); subencoding + ")");
} }
const curr_tile = this._total_tiles - this._tiles; const currTile = this._totalTiles - this._tiles;
const tile_x = curr_tile % this._tiles_x; const tileX = currTile % this._tilesX;
const tile_y = Math.floor(curr_tile / this._tiles_x); const tileY = Math.floor(currTile / this._tilesX);
const tx = x + tile_x * 16; const tx = x + tileX * 16;
const ty = y + tile_y * 16; const ty = y + tileY * 16;
const tw = Math.min(16, (x + width) - tx); const tw = Math.min(16, (x + width) - tx);
const th = Math.min(16, (y + height) - ty); const th = Math.min(16, (y + height) - ty);

View file

@ -1,8 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin * Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
@ -26,15 +24,15 @@ export default class RawDecoder {
return false; return false;
} }
const cur_y = y + (height - this._lines); const curY = y + (height - this._lines);
const curr_height = Math.min(this._lines, const currHeight = Math.min(this._lines,
Math.floor(sock.rQlen / bytesPerLine)); Math.floor(sock.rQlen / bytesPerLine));
let data = sock.rQ; let data = sock.rQ;
let index = sock.rQi; let index = sock.rQi;
// Convert data if needed // Convert data if needed
if (depth == 8) { if (depth == 8) {
const pixels = width * curr_height; const pixels = width * currHeight;
const newdata = new Uint8Array(pixels * 4); const newdata = new Uint8Array(pixels * 4);
for (let i = 0; i < pixels; i++) { for (let i = 0; i < pixels; i++) {
newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3; newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3;
@ -46,9 +44,9 @@ export default class RawDecoder {
index = 0; index = 0;
} }
display.blitImage(x, cur_y, width, curr_height, data, index); display.blitImage(x, curY, width, currHeight, data, index);
sock.rQskipBytes(curr_height * bytesPerLine); sock.rQskipBytes(currHeight * bytesPerLine);
this._lines -= curr_height; this._lines -= currHeight;
if (this._lines > 0) { if (this._lines > 0) {
return false; return false;
} }

View file

@ -1,8 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin * Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.

View file

@ -1,9 +1,7 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin * Copyright (C) 2019 The noVNC Authors
* (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
@ -94,7 +92,7 @@ export default class TightDecoder {
return false; return false;
} }
display.imageRect(x, y, "image/jpeg", data); display.imageRect(x, y, width, height, "image/jpeg", data);
return true; return true;
} }
@ -162,10 +160,9 @@ export default class TightDecoder {
return false; return false;
} }
data = this._zlibs[streamId].inflate(data, true, uncompressedSize); this._zlibs[streamId].setInput(data);
if (data.length != uncompressedSize) { data = this._zlibs[streamId].inflate(uncompressedSize);
throw new Error("Incomplete zlib block"); this._zlibs[streamId].setInput(null);
}
} }
display.blitRgbImage(x, y, width, height, data, 0, false); display.blitRgbImage(x, y, width, height, data, 0, false);
@ -210,10 +207,9 @@ export default class TightDecoder {
return false; return false;
} }
data = this._zlibs[streamId].inflate(data, true, uncompressedSize); this._zlibs[streamId].setInput(data);
if (data.length != uncompressedSize) { data = this._zlibs[streamId].inflate(uncompressedSize);
throw new Error("Incomplete zlib block"); this._zlibs[streamId].setInput(null);
}
} }
// Convert indexed (palette based) image data to RGB // Convert indexed (palette based) image data to RGB

View file

@ -1,8 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin * Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
@ -18,7 +16,7 @@ export default class TightPNGDecoder extends TightDecoder {
return false; return false;
} }
display.imageRect(x, y, "image/png", data); display.imageRect(x, y, width, height, "image/png", data);
return true; return true;
} }

View file

@ -0,0 +1,85 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
export default class Deflator {
constructor() {
this.strm = new ZStream();
this.chunkSize = 1024 * 10 * 10;
this.outputBuffer = new Uint8Array(this.chunkSize);
this.windowBits = 5;
deflateInit(this.strm, this.windowBits);
}
deflate(inData) {
/* eslint-disable camelcase */
this.strm.input = inData;
this.strm.avail_in = this.strm.input.length;
this.strm.next_in = 0;
this.strm.output = this.outputBuffer;
this.strm.avail_out = this.chunkSize;
this.strm.next_out = 0;
/* eslint-enable camelcase */
let lastRet = deflate(this.strm, Z_FULL_FLUSH);
let outData = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
if (lastRet < 0) {
throw new Error("zlib deflate failed");
}
if (this.strm.avail_in > 0) {
// Read chunks until done
let chunks = [outData];
let totalLen = outData.length;
do {
/* eslint-disable camelcase */
this.strm.output = new Uint8Array(this.chunkSize);
this.strm.next_out = 0;
this.strm.avail_out = this.chunkSize;
/* eslint-enable camelcase */
lastRet = deflate(this.strm, Z_FULL_FLUSH);
if (lastRet < 0) {
throw new Error("zlib deflate failed");
}
let chunk = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
totalLen += chunk.length;
chunks.push(chunk);
} while (this.strm.avail_in > 0);
// Combine chunks into a single data
let newData = new Uint8Array(totalLen);
let offset = 0;
for (let i = 0; i < chunks.length; i++) {
newData.set(chunks[i], offset);
offset += chunks[i].length;
}
outData = newData;
}
/* eslint-disable camelcase */
this.strm.input = null;
this.strm.avail_in = 0;
this.strm.next_in = 0;
/* eslint-enable camelcase */
return outData;
}
}

View file

@ -1,6 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
@ -9,24 +9,24 @@
import * as Log from './util/logging.js'; import * as Log from './util/logging.js';
import Base64 from "./base64.js"; import Base64 from "./base64.js";
import { supportsImageMetadata } from './util/browser.js'; import { supportsImageMetadata } from './util/browser.js';
import { toSigned32bit } from './util/int.js';
export default class Display { export default class Display {
constructor(target) { constructor(target) {
this._drawCtx = null; this._drawCtx = null;
this._c_forceCanvas = false;
this._renderQ = []; // queue drawing actions for in-oder rendering this._renderQ = []; // queue drawing actions for in-oder rendering
this._flushing = false; this._flushing = false;
// the full frame buffer (logical canvas) size // the full frame buffer (logical canvas) size
this._fb_width = 0; this._fbWidth = 0;
this._fb_height = 0; this._fbHeight = 0;
this._prevDrawStyle = ""; this._prevDrawStyle = "";
this._tile = null; this._tile = null;
this._tile16x16 = null; this._tile16x16 = null;
this._tile_x = 0; this._tileX = 0;
this._tile_y = 0; this._tileY = 0;
Log.Debug(">> Display.constructor"); Log.Debug(">> Display.constructor");
@ -60,8 +60,6 @@ export default class Display {
Log.Debug("User Agent: " + navigator.userAgent); Log.Debug("User Agent: " + navigator.userAgent);
this.clear();
// Check canvas features // Check canvas features
if (!('createImageData' in this._drawCtx)) { if (!('createImageData' in this._drawCtx)) {
throw new Error("Canvas does not support createImageData"); throw new Error("Canvas does not support createImageData");
@ -74,7 +72,6 @@ export default class Display {
this._scale = 1.0; this._scale = 1.0;
this._clipViewport = false; this._clipViewport = false;
this.logo = null;
// ===== EVENT HANDLERS ===== // ===== EVENT HANDLERS =====
@ -98,11 +95,11 @@ export default class Display {
} }
get width() { get width() {
return this._fb_width; return this._fbWidth;
} }
get height() { get height() {
return this._fb_height; return this._fbHeight;
} }
// ===== PUBLIC METHODS ===== // ===== PUBLIC METHODS =====
@ -125,15 +122,15 @@ export default class Display {
if (deltaX < 0 && vp.x + deltaX < 0) { if (deltaX < 0 && vp.x + deltaX < 0) {
deltaX = -vp.x; deltaX = -vp.x;
} }
if (vx2 + deltaX >= this._fb_width) { if (vx2 + deltaX >= this._fbWidth) {
deltaX -= vx2 + deltaX - this._fb_width + 1; deltaX -= vx2 + deltaX - this._fbWidth + 1;
} }
if (vp.y + deltaY < 0) { if (vp.y + deltaY < 0) {
deltaY = -vp.y; deltaY = -vp.y;
} }
if (vy2 + deltaY >= this._fb_height) { if (vy2 + deltaY >= this._fbHeight) {
deltaY -= (vy2 + deltaY - this._fb_height + 1); deltaY -= (vy2 + deltaY - this._fbHeight + 1);
} }
if (deltaX === 0 && deltaY === 0) { if (deltaX === 0 && deltaY === 0) {
@ -156,18 +153,18 @@ export default class Display {
typeof(height) === "undefined") { typeof(height) === "undefined") {
Log.Debug("Setting viewport to full display region"); Log.Debug("Setting viewport to full display region");
width = this._fb_width; width = this._fbWidth;
height = this._fb_height; height = this._fbHeight;
} }
width = Math.floor(width); width = Math.floor(width);
height = Math.floor(height); height = Math.floor(height);
if (width > this._fb_width) { if (width > this._fbWidth) {
width = this._fb_width; width = this._fbWidth;
} }
if (height > this._fb_height) { if (height > this._fbHeight) {
height = this._fb_height; height = this._fbHeight;
} }
const vp = this._viewportLoc; const vp = this._viewportLoc;
@ -194,21 +191,21 @@ export default class Display {
if (this._scale === 0) { if (this._scale === 0) {
return 0; return 0;
} }
return x / this._scale + this._viewportLoc.x; return toSigned32bit(x / this._scale + this._viewportLoc.x);
} }
absY(y) { absY(y) {
if (this._scale === 0) { if (this._scale === 0) {
return 0; return 0;
} }
return y / this._scale + this._viewportLoc.y; return toSigned32bit(y / this._scale + this._viewportLoc.y);
} }
resize(width, height) { resize(width, height) {
this._prevDrawStyle = ""; this._prevDrawStyle = "";
this._fb_width = width; this._fbWidth = width;
this._fb_height = height; this._fbHeight = height;
const canvas = this._backbuffer; const canvas = this._backbuffer;
if (canvas.width !== width || canvas.height !== height) { if (canvas.width !== width || canvas.height !== height) {
@ -256,9 +253,9 @@ export default class Display {
// Update the visible canvas with the contents of the // Update the visible canvas with the contents of the
// rendering canvas // rendering canvas
flip(from_queue) { flip(fromQueue) {
if (this._renderQ.length !== 0 && !from_queue) { if (this._renderQ.length !== 0 && !fromQueue) {
this._renderQ_push({ this._renderQPush({
'type': 'flip' 'type': 'flip'
}); });
} else { } else {
@ -302,17 +299,6 @@ export default class Display {
} }
} }
clear() {
if (this._logo) {
this.resize(this._logo.width, this._logo.height);
this.imageRect(0, 0, this._logo.type, this._logo.data);
} else {
this.resize(240, 20);
this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height);
}
this.flip();
}
pending() { pending() {
return this._renderQ.length > 0; return this._renderQ.length > 0;
} }
@ -325,9 +311,9 @@ export default class Display {
} }
} }
fillRect(x, y, width, height, color, from_queue) { fillRect(x, y, width, height, color, fromQueue) {
if (this._renderQ.length !== 0 && !from_queue) { if (this._renderQ.length !== 0 && !fromQueue) {
this._renderQ_push({ this._renderQPush({
'type': 'fill', 'type': 'fill',
'x': x, 'x': x,
'y': y, 'y': y,
@ -342,14 +328,14 @@ export default class Display {
} }
} }
copyImage(old_x, old_y, new_x, new_y, w, h, from_queue) { copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {
if (this._renderQ.length !== 0 && !from_queue) { if (this._renderQ.length !== 0 && !fromQueue) {
this._renderQ_push({ this._renderQPush({
'type': 'copy', 'type': 'copy',
'old_x': old_x, 'oldX': oldX,
'old_y': old_y, 'oldY': oldY,
'x': new_x, 'x': newX,
'y': new_y, 'y': newY,
'width': w, 'width': w,
'height': h, 'height': h,
}); });
@ -367,27 +353,35 @@ export default class Display {
this._drawCtx.imageSmoothingEnabled = false; this._drawCtx.imageSmoothingEnabled = false;
this._drawCtx.drawImage(this._backbuffer, this._drawCtx.drawImage(this._backbuffer,
old_x, old_y, w, h, oldX, oldY, w, h,
new_x, new_y, w, h); newX, newY, w, h);
this._damage(new_x, new_y, w, h); this._damage(newX, newY, w, h);
} }
} }
imageRect(x, y, mime, arr) { imageRect(x, y, width, height, mime, arr) {
/* The internal logic cannot handle empty images, so bail early */
if ((width === 0) || (height === 0)) {
return;
}
const img = new Image(); const img = new Image();
img.src = "data: " + mime + ";base64," + Base64.encode(arr); img.src = "data: " + mime + ";base64," + Base64.encode(arr);
this._renderQ_push({
this._renderQPush({
'type': 'img', 'type': 'img',
'img': img, 'img': img,
'x': x, 'x': x,
'y': y 'y': y,
'width': width,
'height': height
}); });
} }
// start updating a tile // start updating a tile
startTile(x, y, width, height, color) { startTile(x, y, width, height, color) {
this._tile_x = x; this._tileX = x;
this._tile_y = y; this._tileY = y;
if (width === 16 && height === 16) { if (width === 16 && height === 16) {
this._tile = this._tile16x16; this._tile = this._tile16x16;
} else { } else {
@ -430,21 +424,21 @@ export default class Display {
// draw the current tile to the screen // draw the current tile to the screen
finishTile() { finishTile() {
this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y); this._drawCtx.putImageData(this._tile, this._tileX, this._tileY);
this._damage(this._tile_x, this._tile_y, this._damage(this._tileX, this._tileY,
this._tile.width, this._tile.height); this._tile.width, this._tile.height);
} }
blitImage(x, y, width, height, arr, offset, from_queue) { blitImage(x, y, width, height, arr, offset, fromQueue) {
if (this._renderQ.length !== 0 && !from_queue) { if (this._renderQ.length !== 0 && !fromQueue) {
// NB(directxman12): it's technically more performant here to use preallocated arrays, // NB(directxman12): it's technically more performant here to use preallocated arrays,
// but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
// this probably isn't getting called *nearly* as much // this probably isn't getting called *nearly* as much
const new_arr = new Uint8Array(width * height * 4); const newArr = new Uint8Array(width * height * 4);
new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
this._renderQ_push({ this._renderQPush({
'type': 'blit', 'type': 'blit',
'data': new_arr, 'data': newArr,
'x': x, 'x': x,
'y': y, 'y': y,
'width': width, 'width': width,
@ -455,16 +449,16 @@ export default class Display {
} }
} }
blitRgbImage(x, y, width, height, arr, offset, from_queue) { blitRgbImage(x, y, width, height, arr, offset, fromQueue) {
if (this._renderQ.length !== 0 && !from_queue) { if (this._renderQ.length !== 0 && !fromQueue) {
// NB(directxman12): it's technically more performant here to use preallocated arrays, // NB(directxman12): it's technically more performant here to use preallocated arrays,
// but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
// this probably isn't getting called *nearly* as much // this probably isn't getting called *nearly* as much
const new_arr = new Uint8Array(width * height * 3); const newArr = new Uint8Array(width * height * 3);
new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
this._renderQ_push({ this._renderQPush({
'type': 'blitRgb', 'type': 'blitRgb',
'data': new_arr, 'data': newArr,
'x': x, 'x': x,
'y': y, 'y': y,
'width': width, 'width': width,
@ -475,16 +469,16 @@ export default class Display {
} }
} }
blitRgbxImage(x, y, width, height, arr, offset, from_queue) { blitRgbxImage(x, y, width, height, arr, offset, fromQueue) {
if (this._renderQ.length !== 0 && !from_queue) { if (this._renderQ.length !== 0 && !fromQueue) {
// NB(directxman12): it's technically more performant here to use preallocated arrays, // NB(directxman12): it's technically more performant here to use preallocated arrays,
// but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
// this probably isn't getting called *nearly* as much // this probably isn't getting called *nearly* as much
const new_arr = new Uint8Array(width * height * 4); const newArr = new Uint8Array(width * height * 4);
new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
this._renderQ_push({ this._renderQPush({
'type': 'blitRgbx', 'type': 'blitRgbx',
'data': new_arr, 'data': newArr,
'x': x, 'x': x,
'y': y, 'y': y,
'width': width, 'width': width,
@ -589,23 +583,23 @@ export default class Display {
this._damage(x, y, img.width, img.height); this._damage(x, y, img.width, img.height);
} }
_renderQ_push(action) { _renderQPush(action) {
this._renderQ.push(action); this._renderQ.push(action);
if (this._renderQ.length === 1) { if (this._renderQ.length === 1) {
// If this can be rendered immediately it will be, otherwise // If this can be rendered immediately it will be, otherwise
// the scanner will wait for the relevant event // the scanner will wait for the relevant event
this._scan_renderQ(); this._scanRenderQ();
} }
} }
_resume_renderQ() { _resumeRenderQ() {
// "this" is the object that is ready, not the // "this" is the object that is ready, not the
// display object // display object
this.removeEventListener('load', this._noVNC_display._resume_renderQ); this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);
this._noVNC_display._scan_renderQ(); this._noVNCDisplay._scanRenderQ();
} }
_scan_renderQ() { _scanRenderQ() {
let ready = true; let ready = true;
while (ready && this._renderQ.length > 0) { while (ready && this._renderQ.length > 0) {
const a = this._renderQ[0]; const a = this._renderQ[0];
@ -614,7 +608,7 @@ export default class Display {
this.flip(true); this.flip(true);
break; break;
case 'copy': case 'copy':
this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true); this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
break; break;
case 'fill': case 'fill':
this.fillRect(a.x, a.y, a.width, a.height, a.color, true); this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
@ -629,11 +623,18 @@ export default class Display {
this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true);
break; break;
case 'img': case 'img':
if (a.img.complete) { /* IE tends to set "complete" prematurely, so check dimensions */
if (a.img.complete && (a.img.width !== 0) && (a.img.height !== 0)) {
if (a.img.width !== a.width || a.img.height !== a.height) {
Log.Error("Decoded image has incorrect dimensions. Got " +
a.img.width + "x" + a.img.height + ". Expected " +
a.width + "x" + a.height + ".");
return;
}
this.drawImage(a.img, a.x, a.y); this.drawImage(a.img, a.x, a.y);
} else { } else {
a.img._noVNC_display = this; a.img._noVNCDisplay = this;
a.img.addEventListener('load', this._resume_renderQ); a.img.addEventListener('load', this._resumeRenderQ);
// We need to wait for this image to 'load' // We need to wait for this image to 'load'
// to keep things in-order // to keep things in-order
ready = false; ready = false;

View file

@ -1,6 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
@ -20,12 +20,15 @@ export const encodings = {
pseudoEncodingLastRect: -224, pseudoEncodingLastRect: -224,
pseudoEncodingCursor: -239, pseudoEncodingCursor: -239,
pseudoEncodingQEMUExtendedKeyEvent: -258, pseudoEncodingQEMUExtendedKeyEvent: -258,
pseudoEncodingDesktopName: -307,
pseudoEncodingExtendedDesktopSize: -308, pseudoEncodingExtendedDesktopSize: -308,
pseudoEncodingXvp: -309, pseudoEncodingXvp: -309,
pseudoEncodingFence: -312, pseudoEncodingFence: -312,
pseudoEncodingContinuousUpdates: -313, pseudoEncodingContinuousUpdates: -313,
pseudoEncodingCompressLevel9: -247, pseudoEncodingCompressLevel9: -247,
pseudoEncodingCompressLevel0: -256, pseudoEncodingCompressLevel0: -256,
pseudoEncodingVMwareCursor: 0x574d5664,
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
}; };
export function encodingName(num) { export function encodingName(num) {

View file

@ -1,3 +1,11 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
import { inflateInit, inflate, inflateReset } from "../vendor/pako/lib/zlib/inflate.js"; import { inflateInit, inflate, inflateReset } from "../vendor/pako/lib/zlib/inflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js"; import ZStream from "../vendor/pako/lib/zlib/zstream.js";
@ -11,12 +19,22 @@ export default class Inflate {
inflateInit(this.strm, this.windowBits); inflateInit(this.strm, this.windowBits);
} }
inflate(data, flush, expected) { setInput(data) {
if (!data) {
//FIXME: flush remaining data.
/* eslint-disable camelcase */
this.strm.input = null;
this.strm.avail_in = 0;
this.strm.next_in = 0;
} else {
this.strm.input = data; this.strm.input = data;
this.strm.avail_in = this.strm.input.length; this.strm.avail_in = this.strm.input.length;
this.strm.next_in = 0; this.strm.next_in = 0;
this.strm.next_out = 0; /* eslint-enable camelcase */
}
}
inflate(expected) {
// resize our output buffer if it's too small // resize our output buffer if it's too small
// (we could just use multiple chunks, but that would cause an extra // (we could just use multiple chunks, but that would cause an extra
// allocation each time to flatten the chunks) // allocation each time to flatten the chunks)
@ -25,9 +43,19 @@ export default class Inflate {
this.strm.output = new Uint8Array(this.chunkSize); this.strm.output = new Uint8Array(this.chunkSize);
} }
this.strm.avail_out = this.chunkSize; /* eslint-disable camelcase */
this.strm.next_out = 0;
this.strm.avail_out = expected;
/* eslint-enable camelcase */
inflate(this.strm, flush); let ret = inflate(this.strm, 0); // Flush argument not used.
if (ret < 0) {
throw new Error("zlib inflate failed");
}
if (this.strm.next_out != expected) {
throw new Error("Incomplete zlib block");
}
return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
} }

View file

@ -43,12 +43,10 @@ addStandard("CapsLock", KeyTable.XK_Caps_Lock);
addLeftRight("Control", KeyTable.XK_Control_L, KeyTable.XK_Control_R); addLeftRight("Control", KeyTable.XK_Control_L, KeyTable.XK_Control_R);
// - Fn // - Fn
// - FnLock // - FnLock
addLeftRight("Hyper", KeyTable.XK_Super_L, KeyTable.XK_Super_R);
addLeftRight("Meta", KeyTable.XK_Super_L, KeyTable.XK_Super_R); addLeftRight("Meta", KeyTable.XK_Super_L, KeyTable.XK_Super_R);
addStandard("NumLock", KeyTable.XK_Num_Lock); addStandard("NumLock", KeyTable.XK_Num_Lock);
addStandard("ScrollLock", KeyTable.XK_Scroll_Lock); addStandard("ScrollLock", KeyTable.XK_Scroll_Lock);
addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R); addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R);
addLeftRight("Super", KeyTable.XK_Super_L, KeyTable.XK_Super_R);
// - Symbol // - Symbol
// - SymbolLock // - SymbolLock
@ -72,6 +70,9 @@ addNumpad("PageUp", KeyTable.XK_Prior, KeyTable.XK_KP_Prior);
// 2.5. Editing Keys // 2.5. Editing Keys
addStandard("Backspace", KeyTable.XK_BackSpace); addStandard("Backspace", KeyTable.XK_BackSpace);
// Browsers send "Clear" for the numpad 5 without NumLock because
// Windows uses VK_Clear for that key. But Unix expects KP_Begin for
// that scenario.
addNumpad("Clear", KeyTable.XK_Clear, KeyTable.XK_KP_Begin); addNumpad("Clear", KeyTable.XK_Clear, KeyTable.XK_KP_Begin);
addStandard("Copy", KeyTable.XF86XK_Copy); addStandard("Copy", KeyTable.XF86XK_Copy);
// - CrSel // - CrSel
@ -194,7 +195,8 @@ addStandard("F35", KeyTable.XK_F35);
addStandard("Close", KeyTable.XF86XK_Close); addStandard("Close", KeyTable.XF86XK_Close);
addStandard("MailForward", KeyTable.XF86XK_MailForward); addStandard("MailForward", KeyTable.XF86XK_MailForward);
addStandard("MailReply", KeyTable.XF86XK_Reply); addStandard("MailReply", KeyTable.XF86XK_Reply);
addStandard("MainSend", KeyTable.XF86XK_Send); addStandard("MailSend", KeyTable.XF86XK_Send);
// - MediaClose
addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward); addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward);
addStandard("MediaPause", KeyTable.XF86XK_AudioPause); addStandard("MediaPause", KeyTable.XF86XK_AudioPause);
addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay); addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay);
@ -218,11 +220,9 @@ addStandard("SpellCheck", KeyTable.XF86XK_Spell);
// - AudioBalanceLeft // - AudioBalanceLeft
// - AudioBalanceRight // - AudioBalanceRight
// - AudioBassDown
// - AudioBassBoostDown // - AudioBassBoostDown
// - AudioBassBoostToggle // - AudioBassBoostToggle
// - AudioBassBoostUp // - AudioBassBoostUp
// - AudioBassUp
// - AudioFaderFront // - AudioFaderFront
// - AudioFaderRear // - AudioFaderRear
// - AudioSurroundModeNext // - AudioSurroundModeNext
@ -243,12 +243,12 @@ addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute);
// 2.14. Application Keys // 2.14. Application Keys
addStandard("LaunchCalculator", KeyTable.XF86XK_Calculator); addStandard("LaunchApplication1", KeyTable.XF86XK_MyComputer);
addStandard("LaunchApplication2", KeyTable.XF86XK_Calculator);
addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar); addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar);
addStandard("LaunchMail", KeyTable.XF86XK_Mail); addStandard("LaunchMail", KeyTable.XF86XK_Mail);
addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia); addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia);
addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music); addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music);
addStandard("LaunchMyComputer", KeyTable.XF86XK_MyComputer);
addStandard("LaunchPhone", KeyTable.XF86XK_Phone); addStandard("LaunchPhone", KeyTable.XF86XK_Phone);
addStandard("LaunchScreenSaver", KeyTable.XF86XK_ScreenSaver); addStandard("LaunchScreenSaver", KeyTable.XF86XK_ScreenSaver);
addStandard("LaunchSpreadsheet", KeyTable.XF86XK_Excel); addStandard("LaunchSpreadsheet", KeyTable.XF86XK_Excel);

View file

@ -0,0 +1,567 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
*/
const GH_NOGESTURE = 0;
const GH_ONETAP = 1;
const GH_TWOTAP = 2;
const GH_THREETAP = 4;
const GH_DRAG = 8;
const GH_LONGPRESS = 16;
const GH_TWODRAG = 32;
const GH_PINCH = 64;
const GH_INITSTATE = 127;
const GH_MOVE_THRESHOLD = 50;
const GH_ANGLE_THRESHOLD = 90; // Degrees
// Timeout when waiting for gestures (ms)
const GH_MULTITOUCH_TIMEOUT = 250;
// Maximum time between press and release for a tap (ms)
const GH_TAP_TIMEOUT = 1000;
// Timeout when waiting for longpress (ms)
const GH_LONGPRESS_TIMEOUT = 1000;
// Timeout when waiting to decide between PINCH and TWODRAG (ms)
const GH_TWOTOUCH_TIMEOUT = 50;
export default class GestureHandler {
constructor() {
this._target = null;
this._state = GH_INITSTATE;
this._tracked = [];
this._ignored = [];
this._waitingRelease = false;
this._releaseStart = 0.0;
this._longpressTimeoutId = null;
this._twoTouchTimeoutId = null;
this._boundEventHandler = this._eventHandler.bind(this);
}
attach(target) {
this.detach();
this._target = target;
this._target.addEventListener('touchstart',
this._boundEventHandler);
this._target.addEventListener('touchmove',
this._boundEventHandler);
this._target.addEventListener('touchend',
this._boundEventHandler);
this._target.addEventListener('touchcancel',
this._boundEventHandler);
}
detach() {
if (!this._target) {
return;
}
this._stopLongpressTimeout();
this._stopTwoTouchTimeout();
this._target.removeEventListener('touchstart',
this._boundEventHandler);
this._target.removeEventListener('touchmove',
this._boundEventHandler);
this._target.removeEventListener('touchend',
this._boundEventHandler);
this._target.removeEventListener('touchcancel',
this._boundEventHandler);
this._target = null;
}
_eventHandler(e) {
let fn;
e.stopPropagation();
e.preventDefault();
switch (e.type) {
case 'touchstart':
fn = this._touchStart;
break;
case 'touchmove':
fn = this._touchMove;
break;
case 'touchend':
case 'touchcancel':
fn = this._touchEnd;
break;
}
for (let i = 0; i < e.changedTouches.length; i++) {
let touch = e.changedTouches[i];
fn.call(this, touch.identifier, touch.clientX, touch.clientY);
}
}
_touchStart(id, x, y) {
// Ignore any new touches if there is already an active gesture,
// or we're in a cleanup state
if (this._hasDetectedGesture() || (this._state === GH_NOGESTURE)) {
this._ignored.push(id);
return;
}
// Did it take too long between touches that we should no longer
// consider this a single gesture?
if ((this._tracked.length > 0) &&
((Date.now() - this._tracked[0].started) > GH_MULTITOUCH_TIMEOUT)) {
this._state = GH_NOGESTURE;
this._ignored.push(id);
return;
}
// If we're waiting for fingers to release then we should no longer
// recognize new touches
if (this._waitingRelease) {
this._state = GH_NOGESTURE;
this._ignored.push(id);
return;
}
this._tracked.push({
id: id,
started: Date.now(),
active: true,
firstX: x,
firstY: y,
lastX: x,
lastY: y,
angle: 0
});
switch (this._tracked.length) {
case 1:
this._startLongpressTimeout();
break;
case 2:
this._state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS);
this._stopLongpressTimeout();
break;
case 3:
this._state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH);
break;
default:
this._state = GH_NOGESTURE;
}
}
_touchMove(id, x, y) {
let touch = this._tracked.find(t => t.id === id);
// If this is an update for a touch we're not tracking, ignore it
if (touch === undefined) {
return;
}
// Update the touches last position with the event coordinates
touch.lastX = x;
touch.lastY = y;
let deltaX = x - touch.firstX;
let deltaY = y - touch.firstY;
// Update angle when the touch has moved
if ((touch.firstX !== touch.lastX) ||
(touch.firstY !== touch.lastY)) {
touch.angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;
}
if (!this._hasDetectedGesture()) {
// Ignore moves smaller than the minimum threshold
if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) {
return;
}
// Can't be a tap or long press as we've seen movement
this._state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS);
this._stopLongpressTimeout();
if (this._tracked.length !== 1) {
this._state &= ~(GH_DRAG);
}
if (this._tracked.length !== 2) {
this._state &= ~(GH_TWODRAG | GH_PINCH);
}
// We need to figure out which of our different two touch gestures
// this might be
if (this._tracked.length === 2) {
// The other touch is the one where the id doesn't match
let prevTouch = this._tracked.find(t => t.id !== id);
// How far the previous touch point has moved since start
let prevDeltaMove = Math.hypot(prevTouch.firstX - prevTouch.lastX,
prevTouch.firstY - prevTouch.lastY);
// We know that the current touch moved far enough,
// but unless both touches moved further than their
// threshold we don't want to disqualify any gestures
if (prevDeltaMove > GH_MOVE_THRESHOLD) {
// The angle difference between the direction of the touch points
let deltaAngle = Math.abs(touch.angle - prevTouch.angle);
deltaAngle = Math.abs(((deltaAngle + 180) % 360) - 180);
// PINCH or TWODRAG can be eliminated depending on the angle
if (deltaAngle > GH_ANGLE_THRESHOLD) {
this._state &= ~GH_TWODRAG;
} else {
this._state &= ~GH_PINCH;
}
if (this._isTwoTouchTimeoutRunning()) {
this._stopTwoTouchTimeout();
}
} else if (!this._isTwoTouchTimeoutRunning()) {
// We can't determine the gesture right now, let's
// wait and see if more events are on their way
this._startTwoTouchTimeout();
}
}
if (!this._hasDetectedGesture()) {
return;
}
this._pushEvent('gesturestart');
}
this._pushEvent('gesturemove');
}
_touchEnd(id, x, y) {
// Check if this is an ignored touch
if (this._ignored.indexOf(id) !== -1) {
// Remove this touch from ignored
this._ignored.splice(this._ignored.indexOf(id), 1);
// And reset the state if there are no more touches
if ((this._ignored.length === 0) &&
(this._tracked.length === 0)) {
this._state = GH_INITSTATE;
this._waitingRelease = false;
}
return;
}
// We got a touchend before the timer triggered,
// this cannot result in a gesture anymore.
if (!this._hasDetectedGesture() &&
this._isTwoTouchTimeoutRunning()) {
this._stopTwoTouchTimeout();
this._state = GH_NOGESTURE;
}
// Some gestures don't trigger until a touch is released
if (!this._hasDetectedGesture()) {
// Can't be a gesture that relies on movement
this._state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH);
// Or something that relies on more time
this._state &= ~GH_LONGPRESS;
this._stopLongpressTimeout();
if (!this._waitingRelease) {
this._releaseStart = Date.now();
this._waitingRelease = true;
// Can't be a tap that requires more touches than we current have
switch (this._tracked.length) {
case 1:
this._state &= ~(GH_TWOTAP | GH_THREETAP);
break;
case 2:
this._state &= ~(GH_ONETAP | GH_THREETAP);
break;
}
}
}
// Waiting for all touches to release? (i.e. some tap)
if (this._waitingRelease) {
// Were all touches released at roughly the same time?
if ((Date.now() - this._releaseStart) > GH_MULTITOUCH_TIMEOUT) {
this._state = GH_NOGESTURE;
}
// Did too long time pass between press and release?
if (this._tracked.some(t => (Date.now() - t.started) > GH_TAP_TIMEOUT)) {
this._state = GH_NOGESTURE;
}
let touch = this._tracked.find(t => t.id === id);
touch.active = false;
// Are we still waiting for more releases?
if (this._hasDetectedGesture()) {
this._pushEvent('gesturestart');
} else {
// Have we reached a dead end?
if (this._state !== GH_NOGESTURE) {
return;
}
}
}
if (this._hasDetectedGesture()) {
this._pushEvent('gestureend');
}
// Ignore any remaining touches until they are ended
for (let i = 0; i < this._tracked.length; i++) {
if (this._tracked[i].active) {
this._ignored.push(this._tracked[i].id);
}
}
this._tracked = [];
this._state = GH_NOGESTURE;
// Remove this touch from ignored if it's in there
if (this._ignored.indexOf(id) !== -1) {
this._ignored.splice(this._ignored.indexOf(id), 1);
}
// We reset the state if ignored is empty
if ((this._ignored.length === 0)) {
this._state = GH_INITSTATE;
this._waitingRelease = false;
}
}
_hasDetectedGesture() {
if (this._state === GH_NOGESTURE) {
return false;
}
// Check to see if the bitmask value is a power of 2
// (i.e. only one bit set). If it is, we have a state.
if (this._state & (this._state - 1)) {
return false;
}
// For taps we also need to have all touches released
// before we've fully detected the gesture
if (this._state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) {
if (this._tracked.some(t => t.active)) {
return false;
}
}
return true;
}
_startLongpressTimeout() {
this._stopLongpressTimeout();
this._longpressTimeoutId = setTimeout(() => this._longpressTimeout(),
GH_LONGPRESS_TIMEOUT);
}
_stopLongpressTimeout() {
clearTimeout(this._longpressTimeoutId);
this._longpressTimeoutId = null;
}
_longpressTimeout() {
if (this._hasDetectedGesture()) {
throw new Error("A longpress gesture failed, conflict with a different gesture");
}
this._state = GH_LONGPRESS;
this._pushEvent('gesturestart');
}
_startTwoTouchTimeout() {
this._stopTwoTouchTimeout();
this._twoTouchTimeoutId = setTimeout(() => this._twoTouchTimeout(),
GH_TWOTOUCH_TIMEOUT);
}
_stopTwoTouchTimeout() {
clearTimeout(this._twoTouchTimeoutId);
this._twoTouchTimeoutId = null;
}
_isTwoTouchTimeoutRunning() {
return this._twoTouchTimeoutId !== null;
}
_twoTouchTimeout() {
if (this._tracked.length === 0) {
throw new Error("A pinch or two drag gesture failed, no tracked touches");
}
// How far each touch point has moved since start
let avgM = this._getAverageMovement();
let avgMoveH = Math.abs(avgM.x);
let avgMoveV = Math.abs(avgM.y);
// The difference in the distance between where
// the touch points started and where they are now
let avgD = this._getAverageDistance();
let deltaTouchDistance = Math.abs(Math.hypot(avgD.first.x, avgD.first.y) -
Math.hypot(avgD.last.x, avgD.last.y));
if ((avgMoveV < deltaTouchDistance) &&
(avgMoveH < deltaTouchDistance)) {
this._state = GH_PINCH;
} else {
this._state = GH_TWODRAG;
}
this._pushEvent('gesturestart');
this._pushEvent('gesturemove');
}
_pushEvent(type) {
let detail = { type: this._stateToGesture(this._state) };
// For most gesture events the current (average) position is the
// most useful
let avg = this._getPosition();
let pos = avg.last;
// However we have a slight distance to detect gestures, so for the
// first gesture event we want to use the first positions we saw
if (type === 'gesturestart') {
pos = avg.first;
}
// For these gestures, we always want the event coordinates
// to be where the gesture began, not the current touch location.
switch (this._state) {
case GH_TWODRAG:
case GH_PINCH:
pos = avg.first;
break;
}
detail['clientX'] = pos.x;
detail['clientY'] = pos.y;
// FIXME: other coordinates?
// Some gestures also have a magnitude
if (this._state === GH_PINCH) {
let distance = this._getAverageDistance();
if (type === 'gesturestart') {
detail['magnitudeX'] = distance.first.x;
detail['magnitudeY'] = distance.first.y;
} else {
detail['magnitudeX'] = distance.last.x;
detail['magnitudeY'] = distance.last.y;
}
} else if (this._state === GH_TWODRAG) {
if (type === 'gesturestart') {
detail['magnitudeX'] = 0.0;
detail['magnitudeY'] = 0.0;
} else {
let movement = this._getAverageMovement();
detail['magnitudeX'] = movement.x;
detail['magnitudeY'] = movement.y;
}
}
let gev = new CustomEvent(type, { detail: detail });
this._target.dispatchEvent(gev);
}
_stateToGesture(state) {
switch (state) {
case GH_ONETAP:
return 'onetap';
case GH_TWOTAP:
return 'twotap';
case GH_THREETAP:
return 'threetap';
case GH_DRAG:
return 'drag';
case GH_LONGPRESS:
return 'longpress';
case GH_TWODRAG:
return 'twodrag';
case GH_PINCH:
return 'pinch';
}
throw new Error("Unknown gesture state: " + state);
}
_getPosition() {
if (this._tracked.length === 0) {
throw new Error("Failed to get gesture position, no tracked touches");
}
let size = this._tracked.length;
let fx = 0, fy = 0, lx = 0, ly = 0;
for (let i = 0; i < this._tracked.length; i++) {
fx += this._tracked[i].firstX;
fy += this._tracked[i].firstY;
lx += this._tracked[i].lastX;
ly += this._tracked[i].lastY;
}
return { first: { x: fx / size,
y: fy / size },
last: { x: lx / size,
y: ly / size } };
}
_getAverageMovement() {
if (this._tracked.length === 0) {
throw new Error("Failed to get gesture movement, no tracked touches");
}
let totalH, totalV;
totalH = totalV = 0;
let size = this._tracked.length;
for (let i = 0; i < this._tracked.length; i++) {
totalH += this._tracked[i].lastX - this._tracked[i].firstX;
totalV += this._tracked[i].lastY - this._tracked[i].firstY;
}
return { x: totalH / size,
y: totalV / size };
}
_getAverageDistance() {
if (this._tracked.length === 0) {
throw new Error("Failed to get gesture distance, no tracked touches");
}
// Distance between the first and last tracked touches
let first = this._tracked[0];
let last = this._tracked[this._tracked.length - 1];
let fdx = Math.abs(last.firstX - first.firstX);
let fdy = Math.abs(last.firstY - first.firstY);
let ldx = Math.abs(last.lastX - first.lastX);
let ldy = Math.abs(last.lastY - first.lastY);
return { first: { x: fdx, y: fdy },
last: { x: ldx, y: ldy } };
}
}

View file

@ -1,6 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt) * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/ */
@ -118,9 +118,7 @@ export default class Keyboard {
// We cannot handle keys we cannot track, but we also need // We cannot handle keys we cannot track, but we also need
// to deal with virtual keyboards which omit key info // to deal with virtual keyboards which omit key info
// (iOS omits tracking info on keyup events, which forces us to if (code === 'Unidentified') {
// special treat that platform here)
if ((code === 'Unidentified') || browser.isIOS()) {
if (keysym) { if (keysym) {
// If it's a virtual keyboard then it should be // If it's a virtual keyboard then it should be
// sufficient to just send press and release right // sufficient to just send press and release right
@ -137,7 +135,7 @@ export default class Keyboard {
// keys around a bit to make things more sane for the remote // keys around a bit to make things more sane for the remote
// server. This method is used by RealVNC and TigerVNC (and // server. This method is used by RealVNC and TigerVNC (and
// possibly others). // possibly others).
if (browser.isMac()) { if (browser.isMac() || browser.isIOS()) {
switch (keysym) { switch (keysym) {
case KeyTable.XK_Super_L: case KeyTable.XK_Super_L:
keysym = KeyTable.XK_Alt_L; keysym = KeyTable.XK_Alt_L;
@ -164,7 +162,7 @@ export default class Keyboard {
// state change events. That gets extra confusing for CapsLock // state change events. That gets extra confusing for CapsLock
// which toggles on each press, but not on release. So pretend // which toggles on each press, but not on release. So pretend
// it was a quick press and release of the button. // it was a quick press and release of the button.
if (browser.isMac() && (code === 'CapsLock')) { if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
stopEvent(e); stopEvent(e);
@ -276,13 +274,28 @@ export default class Keyboard {
} }
// See comment in _handleKeyDown() // See comment in _handleKeyDown()
if (browser.isMac() && (code === 'CapsLock')) { if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
return; return;
} }
this._sendKeyEvent(this._keyDownList[code], code, false); this._sendKeyEvent(this._keyDownList[code], code, false);
// Windows has a rather nasty bug where it won't send key
// release events for a Shift button if the other Shift is still
// pressed
if (browser.isWindows() && ((code === 'ShiftLeft') ||
(code === 'ShiftRight'))) {
if ('ShiftRight' in this._keyDownList) {
this._sendKeyEvent(this._keyDownList['ShiftRight'],
'ShiftRight', false);
}
if ('ShiftLeft' in this._keyDownList) {
this._sendKeyEvent(this._keyDownList['ShiftLeft'],
'ShiftLeft', false);
}
}
} }
_handleAltGrTimeout() { _handleAltGrTimeout() {
@ -299,8 +312,11 @@ export default class Keyboard {
Log.Debug("<< Keyboard.allKeysUp"); Log.Debug("<< Keyboard.allKeysUp");
} }
// Firefox Alt workaround, see below // Alt workaround for Firefox on Windows, see below
_checkAlt(e) { _checkAlt(e) {
if (e.skipCheckAlt) {
return;
}
if (e.altKey) { if (e.altKey) {
return; return;
} }
@ -315,6 +331,7 @@ export default class Keyboard {
const event = new KeyboardEvent('keyup', const event = new KeyboardEvent('keyup',
{ key: downList[code], { key: downList[code],
code: code }); code: code });
event.skipCheckAlt = true;
target.dispatchEvent(event); target.dispatchEvent(event);
}); });
} }
@ -331,9 +348,10 @@ export default class Keyboard {
// Release (key up) if window loses focus // Release (key up) if window loses focus
window.addEventListener('blur', this._eventHandlers.blur); window.addEventListener('blur', this._eventHandlers.blur);
// Firefox has broken handling of Alt, so we need to poll as // Firefox on Windows has broken handling of Alt, so we need to
// best we can for releases (still doesn't prevent the menu // poll as best we can for releases (still doesn't prevent the
// from popping up though as we can't call preventDefault()) // menu from popping up though as we can't call
// preventDefault())
if (browser.isWindows() && browser.isFirefox()) { if (browser.isWindows() && browser.isFirefox()) {
const handler = this._eventHandlers.checkalt; const handler = this._eventHandlers.checkalt;
['mousedown', 'mouseup', 'mousemove', 'wheel', ['mousedown', 'mouseup', 'mousemove', 'wheel',

View file

@ -1,276 +0,0 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
import * as Log from '../util/logging.js';
import { isTouchDevice } from '../util/browser.js';
import { setCapture, stopEvent, getPointerEvent } from '../util/events.js';
const WHEEL_STEP = 10; // Delta threshold for a mouse wheel step
const WHEEL_STEP_TIMEOUT = 50; // ms
const WHEEL_LINE_HEIGHT = 19;
export default class Mouse {
constructor(target) {
this._target = target || document;
this._doubleClickTimer = null;
this._lastTouchPos = null;
this._pos = null;
this._wheelStepXTimer = null;
this._wheelStepYTimer = null;
this._accumulatedWheelDeltaX = 0;
this._accumulatedWheelDeltaY = 0;
this._eventHandlers = {
'mousedown': this._handleMouseDown.bind(this),
'mouseup': this._handleMouseUp.bind(this),
'mousemove': this._handleMouseMove.bind(this),
'mousewheel': this._handleMouseWheel.bind(this),
'mousedisable': this._handleMouseDisable.bind(this)
};
// ===== PROPERTIES =====
this.touchButton = 1; // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
// ===== EVENT HANDLERS =====
this.onmousebutton = () => {}; // Handler for mouse button click/release
this.onmousemove = () => {}; // Handler for mouse movement
}
// ===== PRIVATE METHODS =====
_resetDoubleClickTimer() {
this._doubleClickTimer = null;
}
_handleMouseButton(e, down) {
this._updateMousePosition(e);
let pos = this._pos;
let bmask;
if (e.touches || e.changedTouches) {
// Touch device
// When two touches occur within 500 ms of each other and are
// close enough together a double click is triggered.
if (down == 1) {
if (this._doubleClickTimer === null) {
this._lastTouchPos = pos;
} else {
clearTimeout(this._doubleClickTimer);
// When the distance between the two touches is small enough
// force the position of the latter touch to the position of
// the first.
const xs = this._lastTouchPos.x - pos.x;
const ys = this._lastTouchPos.y - pos.y;
const d = Math.sqrt((xs * xs) + (ys * ys));
// The goal is to trigger on a certain physical width, the
// devicePixelRatio brings us a bit closer but is not optimal.
const threshold = 20 * (window.devicePixelRatio || 1);
if (d < threshold) {
pos = this._lastTouchPos;
}
}
this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500);
}
bmask = this.touchButton;
// If bmask is set
} else if (e.which) {
/* everything except IE */
bmask = 1 << e.button;
} else {
/* IE including 9 */
bmask = (e.button & 0x1) + // Left
(e.button & 0x2) * 2 + // Right
(e.button & 0x4) / 2; // Middle
}
Log.Debug("onmousebutton " + (down ? "down" : "up") +
", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
this.onmousebutton(pos.x, pos.y, down, bmask);
stopEvent(e);
}
_handleMouseDown(e) {
// Touch events have implicit capture
if (e.type === "mousedown") {
setCapture(this._target);
}
this._handleMouseButton(e, 1);
}
_handleMouseUp(e) {
this._handleMouseButton(e, 0);
}
// Mouse wheel events are sent in steps over VNC. This means that the VNC
// protocol can't handle a wheel event with specific distance or speed.
// Therefor, if we get a lot of small mouse wheel events we combine them.
_generateWheelStepX() {
if (this._accumulatedWheelDeltaX < 0) {
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 5);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 5);
} else if (this._accumulatedWheelDeltaX > 0) {
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 6);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 6);
}
this._accumulatedWheelDeltaX = 0;
}
_generateWheelStepY() {
if (this._accumulatedWheelDeltaY < 0) {
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 3);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 3);
} else if (this._accumulatedWheelDeltaY > 0) {
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 4);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 4);
}
this._accumulatedWheelDeltaY = 0;
}
_resetWheelStepTimers() {
window.clearTimeout(this._wheelStepXTimer);
window.clearTimeout(this._wheelStepYTimer);
this._wheelStepXTimer = null;
this._wheelStepYTimer = null;
}
_handleMouseWheel(e) {
this._resetWheelStepTimers();
this._updateMousePosition(e);
let dX = e.deltaX;
let dY = e.deltaY;
// Pixel units unless it's non-zero.
// Note that if deltamode is line or page won't matter since we aren't
// sending the mouse wheel delta to the server anyway.
// The difference between pixel and line can be important however since
// we have a threshold that can be smaller than the line height.
if (e.deltaMode !== 0) {
dX *= WHEEL_LINE_HEIGHT;
dY *= WHEEL_LINE_HEIGHT;
}
this._accumulatedWheelDeltaX += dX;
this._accumulatedWheelDeltaY += dY;
// Generate a mouse wheel step event when the accumulated delta
// for one of the axes is large enough.
// Small delta events that do not pass the threshold get sent
// after a timeout.
if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) {
this._generateWheelStepX();
} else {
this._wheelStepXTimer =
window.setTimeout(this._generateWheelStepX.bind(this),
WHEEL_STEP_TIMEOUT);
}
if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) {
this._generateWheelStepY();
} else {
this._wheelStepYTimer =
window.setTimeout(this._generateWheelStepY.bind(this),
WHEEL_STEP_TIMEOUT);
}
stopEvent(e);
}
_handleMouseMove(e) {
this._updateMousePosition(e);
this.onmousemove(this._pos.x, this._pos.y);
stopEvent(e);
}
_handleMouseDisable(e) {
/*
* Stop propagation if inside canvas area
* Note: This is only needed for the 'click' event as it fails
* to fire properly for the target element so we have
* to listen on the document element instead.
*/
if (e.target == this._target) {
stopEvent(e);
}
}
// Update coordinates relative to target
_updateMousePosition(e) {
e = getPointerEvent(e);
const bounds = this._target.getBoundingClientRect();
let x;
let y;
// Clip to target bounds
if (e.clientX < bounds.left) {
x = 0;
} else if (e.clientX >= bounds.right) {
x = bounds.width - 1;
} else {
x = e.clientX - bounds.left;
}
if (e.clientY < bounds.top) {
y = 0;
} else if (e.clientY >= bounds.bottom) {
y = bounds.height - 1;
} else {
y = e.clientY - bounds.top;
}
this._pos = {x: x, y: y};
}
// ===== PUBLIC METHODS =====
grab() {
if (isTouchDevice) {
this._target.addEventListener('touchstart', this._eventHandlers.mousedown);
this._target.addEventListener('touchend', this._eventHandlers.mouseup);
this._target.addEventListener('touchmove', this._eventHandlers.mousemove);
}
this._target.addEventListener('mousedown', this._eventHandlers.mousedown);
this._target.addEventListener('mouseup', this._eventHandlers.mouseup);
this._target.addEventListener('mousemove', this._eventHandlers.mousemove);
this._target.addEventListener('wheel', this._eventHandlers.mousewheel);
/* Prevent middle-click pasting (see above for why we bind to document) */
document.addEventListener('click', this._eventHandlers.mousedisable);
/* preventDefault() on mousedown doesn't stop this event for some
reason so we have to explicitly block it */
this._target.addEventListener('contextmenu', this._eventHandlers.mousedisable);
}
ungrab() {
this._resetWheelStepTimers();
if (isTouchDevice) {
this._target.removeEventListener('touchstart', this._eventHandlers.mousedown);
this._target.removeEventListener('touchend', this._eventHandlers.mouseup);
this._target.removeEventListener('touchmove', this._eventHandlers.mousemove);
}
this._target.removeEventListener('mousedown', this._eventHandlers.mousedown);
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup);
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove);
this._target.removeEventListener('wheel', this._eventHandlers.mousewheel);
document.removeEventListener('click', this._eventHandlers.mousedisable);
this._target.removeEventListener('contextmenu', this._eventHandlers.mousedisable);
}
}

View file

@ -1,3 +1,4 @@
import KeyTable from "./keysym.js";
import keysyms from "./keysymdef.js"; import keysyms from "./keysymdef.js";
import vkeys from "./vkeys.js"; import vkeys from "./vkeys.js";
import fixedkeys from "./fixedkeys.js"; import fixedkeys from "./fixedkeys.js";
@ -91,6 +92,8 @@ export function getKey(evt) {
// Mozilla isn't fully in sync with the spec yet // Mozilla isn't fully in sync with the spec yet
switch (evt.key) { switch (evt.key) {
case 'OS': return 'Meta'; case 'OS': return 'Meta';
case 'LaunchMyComputer': return 'LaunchApplication1';
case 'LaunchCalculator': return 'LaunchApplication2';
} }
// iOS leaks some OS names // iOS leaks some OS names
@ -102,9 +105,21 @@ export function getKey(evt) {
case 'UIKeyInputEscape': return 'Escape'; case 'UIKeyInputEscape': return 'Escape';
} }
// IE and Edge have broken handling of AltGraph so we cannot // Broken behaviour in Chrome
// trust them for printable characters if ((evt.key === '\x00') && (evt.code === 'NumpadDecimal')) {
if ((evt.key.length !== 1) || (!browser.isIE() && !browser.isEdge())) { return 'Delete';
}
// IE and Edge need special handling, but for everyone else we
// can trust the value provided
if (!browser.isIE() && !browser.isEdge()) {
return evt.key;
}
// IE and Edge have broken handling of AltGraph so we can only
// trust them for non-printable characters (and unfortunately
// they also specify 'Unidentified' for some problem keys)
if ((evt.key.length !== 1) && (evt.key !== 'Unidentified')) {
return evt.key; return evt.key;
} }
} }
@ -141,10 +156,39 @@ export function getKeysym(evt) {
location = 2; location = 2;
} }
// And for Clear
if ((key === 'Clear') && (location === 3)) {
let code = getKeycode(evt);
if (code === 'NumLock') {
location = 0;
}
}
if ((location === undefined) || (location > 3)) { if ((location === undefined) || (location > 3)) {
location = 0; location = 0;
} }
// The original Meta key now gets confused with the Windows key
// https://bugs.chromium.org/p/chromium/issues/detail?id=1020141
// https://bugzilla.mozilla.org/show_bug.cgi?id=1232918
if (key === 'Meta') {
let code = getKeycode(evt);
if (code === 'AltLeft') {
return KeyTable.XK_Meta_L;
} else if (code === 'AltRight') {
return KeyTable.XK_Meta_R;
}
}
// macOS has Clear instead of NumLock, but the remote system is
// probably not macOS, so lying here is probably best...
if (key === 'Clear') {
let code = getKeycode(evt);
if (code === 'NumLock') {
return KeyTable.XK_Num_Lock;
}
}
return DOMKeyTable[key][location]; return DOMKeyTable[key][location];
} }

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,11 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*
* Browser feature support detection
*/ */
import * as Log from './logging.js'; import * as Log from './logging.js';
@ -31,7 +33,7 @@ try {
const target = document.createElement('canvas'); const target = document.createElement('canvas');
target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default';
if (target.style.cursor) { if (target.style.cursor.indexOf("url") === 0) {
Log.Info("Data URI scheme cursor supported"); Log.Info("Data URI scheme cursor supported");
_supportsCursorURIs = true; _supportsCursorURIs = true;
} else { } else {
@ -52,6 +54,38 @@ try {
} }
export const supportsImageMetadata = _supportsImageMetadata; export const supportsImageMetadata = _supportsImageMetadata;
let _hasScrollbarGutter = true;
try {
// Create invisible container
const container = document.createElement('div');
container.style.visibility = 'hidden';
container.style.overflow = 'scroll'; // forcing scrollbars
document.body.appendChild(container);
// Create a div and place it in the container
const child = document.createElement('div');
container.appendChild(child);
// Calculate the difference between the container's full width
// and the child's width - the difference is the scrollbars
const scrollbarWidth = (container.offsetWidth - child.offsetWidth);
// Clean up
container.parentNode.removeChild(container);
_hasScrollbarGutter = scrollbarWidth != 0;
} catch (exc) {
Log.Error("Scrollbar test exception: " + exc);
}
export const hasScrollbarGutter = _hasScrollbarGutter;
/*
* The functions for detection of platforms and browsers below are exported
* but the use of these should be minimized as much as possible.
*
* It's better to use feature detection than platform detection.
*/
export function isMac() { export function isMac() {
return navigator && !!(/mac/i).exec(navigator.platform); return navigator && !!(/mac/i).exec(navigator.platform);
} }
@ -67,10 +101,6 @@ export function isIOS() {
!!(/ipod/i).exec(navigator.platform)); !!(/ipod/i).exec(navigator.platform));
} }
export function isAndroid() {
return navigator && !!(/android/i).exec(navigator.userAgent);
}
export function isSafari() { export function isSafari() {
return navigator && (navigator.userAgent.indexOf('Safari') !== -1 && return navigator && (navigator.userAgent.indexOf('Safari') !== -1 &&
navigator.userAgent.indexOf('Chrome') === -1); navigator.userAgent.indexOf('Chrome') === -1);

View file

@ -1,6 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt) * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/ */
@ -20,7 +20,6 @@ export default class Cursor {
this._canvas.style.pointerEvents = 'none'; this._canvas.style.pointerEvents = 'none';
// Can't use "display" because of Firefox bug #1445997 // Can't use "display" because of Firefox bug #1445997
this._canvas.style.visibility = 'hidden'; this._canvas.style.visibility = 'hidden';
document.body.appendChild(this._canvas);
} }
this._position = { x: 0, y: 0 }; this._position = { x: 0, y: 0 };
@ -31,9 +30,6 @@ export default class Cursor {
'mouseleave': this._handleMouseLeave.bind(this), 'mouseleave': this._handleMouseLeave.bind(this),
'mousemove': this._handleMouseMove.bind(this), 'mousemove': this._handleMouseMove.bind(this),
'mouseup': this._handleMouseUp.bind(this), 'mouseup': this._handleMouseUp.bind(this),
'touchstart': this._handleTouchStart.bind(this),
'touchmove': this._handleTouchMove.bind(this),
'touchend': this._handleTouchEnd.bind(this),
}; };
} }
@ -45,6 +41,8 @@ export default class Cursor {
this._target = target; this._target = target;
if (useFallback) { if (useFallback) {
document.body.appendChild(this._canvas);
// FIXME: These don't fire properly except for mouse // FIXME: These don't fire properly except for mouse
/// movement in IE. We want to also capture element /// movement in IE. We want to also capture element
// movement, size changes, visibility, etc. // movement, size changes, visibility, etc.
@ -53,17 +51,16 @@ export default class Cursor {
this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options); this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options); this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options); this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
// There is no "touchleave" so we monitor touchstart globally
window.addEventListener('touchstart', this._eventHandlers.touchstart, options);
this._target.addEventListener('touchmove', this._eventHandlers.touchmove, options);
this._target.addEventListener('touchend', this._eventHandlers.touchend, options);
} }
this.clear(); this.clear();
} }
detach() { detach() {
if (!this._target) {
return;
}
if (useFallback) { if (useFallback) {
const options = { capture: true, passive: true }; const options = { capture: true, passive: true };
this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options); this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
@ -71,9 +68,7 @@ export default class Cursor {
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options); this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options); this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
window.removeEventListener('touchstart', this._eventHandlers.touchstart, options); document.body.removeChild(this._canvas);
this._target.removeEventListener('touchmove', this._eventHandlers.touchmove, options);
this._target.removeEventListener('touchend', this._eventHandlers.touchend, options);
} }
this._target = null; this._target = null;
@ -124,6 +119,27 @@ export default class Cursor {
this._hotSpot.y = 0; this._hotSpot.y = 0;
} }
// Mouse events might be emulated, this allows
// moving the cursor in such cases
move(clientX, clientY) {
if (!useFallback) {
return;
}
// clientX/clientY are relative the _visual viewport_,
// but our position is relative the _layout viewport_,
// so try to compensate when we can
if (window.visualViewport) {
this._position.x = clientX + window.visualViewport.offsetLeft;
this._position.y = clientY + window.visualViewport.offsetTop;
} else {
this._position.x = clientX;
this._position.y = clientY;
}
this._updatePosition();
let target = document.elementFromPoint(clientX, clientY);
this._updateVisibility(target);
}
_handleMouseOver(event) { _handleMouseOver(event) {
// This event could be because we're entering the target, or // This event could be because we're entering the target, or
// moving around amongst its sub elements. Let the move handler // moving around amongst its sub elements. Let the move handler
@ -132,7 +148,8 @@ export default class Cursor {
} }
_handleMouseLeave(event) { _handleMouseLeave(event) {
this._hideCursor(); // Check if we should show the cursor on the element we are leaving to
this._updateVisibility(event.relatedTarget);
} }
_handleMouseMove(event) { _handleMouseMove(event) {
@ -150,27 +167,29 @@ export default class Cursor {
// now and adjust visibility based on that. // now and adjust visibility based on that.
let target = document.elementFromPoint(event.clientX, event.clientY); let target = document.elementFromPoint(event.clientX, event.clientY);
this._updateVisibility(target); this._updateVisibility(target);
// Captures end with a mouseup but we can't know the event order of
// mouseup vs releaseCapture.
//
// In the cases when releaseCapture comes first, the code above is
// enough.
//
// In the cases when the mouseup comes first, we need wait for the
// browser to flush all events and then check again if the cursor
// should be visible.
if (this._captureIsActive()) {
window.setTimeout(() => {
// We might have detached at this point
if (!this._target) {
return;
} }
// Refresh the target from elementFromPoint since queued events
_handleTouchStart(event) { // might have altered the DOM
// Just as for mouseover, we let the move handler deal with it target = document.elementFromPoint(event.clientX,
this._handleTouchMove(event); event.clientY);
}
_handleTouchMove(event) {
this._updateVisibility(event.target);
this._position.x = event.changedTouches[0].clientX - this._hotSpot.x;
this._position.y = event.changedTouches[0].clientY - this._hotSpot.y;
this._updatePosition();
}
_handleTouchEnd(event) {
// Same principle as for mouseup
let target = document.elementFromPoint(event.changedTouches[0].clientX,
event.changedTouches[0].clientY);
this._updateVisibility(target); this._updateVisibility(target);
}, 0);
}
} }
_showCursor() { _showCursor() {
@ -189,6 +208,9 @@ export default class Cursor {
// (i.e. are we over the target, or a child of the target without a // (i.e. are we over the target, or a child of the target without a
// different cursor set) // different cursor set)
_shouldShowCursor(target) { _shouldShowCursor(target) {
if (!target) {
return false;
}
// Easy case // Easy case
if (target === this._target) { if (target === this._target) {
return true; return true;
@ -207,6 +229,11 @@ export default class Cursor {
} }
_updateVisibility(target) { _updateVisibility(target) {
// When the cursor target has capture we want to show the cursor.
// So, if a capture is active - look at the captured element instead.
if (this._captureIsActive()) {
target = document.captureElement;
}
if (this._shouldShowCursor(target)) { if (this._shouldShowCursor(target)) {
this._showCursor(); this._showCursor();
} else { } else {
@ -218,4 +245,9 @@ export default class Cursor {
this._canvas.style.left = this._position.x + "px"; this._canvas.style.left = this._position.x + "px";
this._canvas.style.top = this._position.y + "px"; this._canvas.style.top = this._position.y + "px";
} }
_captureIsActive() {
return document.captureElement &&
document.documentElement.contains(document.captureElement);
}
} }

View file

@ -0,0 +1,32 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
/*
* HTML element utility functions
*/
export function clientToElement(x, y, elem) {
const bounds = elem.getBoundingClientRect();
let pos = { x: 0, y: 0 };
// Clip to target bounds
if (x < bounds.left) {
pos.x = 0;
} else if (x >= bounds.right) {
pos.x = bounds.width - 1;
} else {
pos.x = x - bounds.left;
}
if (y < bounds.top) {
pos.y = 0;
} else if (y >= bounds.bottom) {
pos.y = bounds.height - 1;
} else {
pos.y = y - bounds.top;
}
return pos;
}

View file

@ -21,7 +21,8 @@ export function stopEvent(e) {
// Emulate Element.setCapture() when not supported // Emulate Element.setCapture() when not supported
let _captureRecursion = false; let _captureRecursion = false;
let _captureElem = null; let _elementForUnflushedEvents = null;
document.captureElement = null;
function _captureProxy(e) { function _captureProxy(e) {
// Recursion protection as we'll see our own event // Recursion protection as we'll see our own event
if (_captureRecursion) return; if (_captureRecursion) return;
@ -30,7 +31,11 @@ function _captureProxy(e) {
const newEv = new e.constructor(e.type, e); const newEv = new e.constructor(e.type, e);
_captureRecursion = true; _captureRecursion = true;
_captureElem.dispatchEvent(newEv); if (document.captureElement) {
document.captureElement.dispatchEvent(newEv);
} else {
_elementForUnflushedEvents.dispatchEvent(newEv);
}
_captureRecursion = false; _captureRecursion = false;
// Avoid double events // Avoid double events
@ -48,58 +53,56 @@ function _captureProxy(e) {
} }
// Follow cursor style of target element // Follow cursor style of target element
function _captureElemChanged() { function _capturedElemChanged() {
const captureElem = document.getElementById("noVNC_mouse_capture_elem"); const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
captureElem.style.cursor = window.getComputedStyle(_captureElem).cursor; proxyElem.style.cursor = window.getComputedStyle(document.captureElement).cursor;
} }
const _captureObserver = new MutationObserver(_captureElemChanged); const _captureObserver = new MutationObserver(_capturedElemChanged);
let _captureIndex = 0; export function setCapture(target) {
if (target.setCapture) {
export function setCapture(elem) { target.setCapture();
if (elem.setCapture) { document.captureElement = target;
elem.setCapture();
// IE releases capture on 'click' events which might not trigger // IE releases capture on 'click' events which might not trigger
elem.addEventListener('mouseup', releaseCapture); target.addEventListener('mouseup', releaseCapture);
} else { } else {
// Release any existing capture in case this method is // Release any existing capture in case this method is
// called multiple times without coordination // called multiple times without coordination
releaseCapture(); releaseCapture();
let captureElem = document.getElementById("noVNC_mouse_capture_elem"); let proxyElem = document.getElementById("noVNC_mouse_capture_elem");
if (captureElem === null) { if (proxyElem === null) {
captureElem = document.createElement("div"); proxyElem = document.createElement("div");
captureElem.id = "noVNC_mouse_capture_elem"; proxyElem.id = "noVNC_mouse_capture_elem";
captureElem.style.position = "fixed"; proxyElem.style.position = "fixed";
captureElem.style.top = "0px"; proxyElem.style.top = "0px";
captureElem.style.left = "0px"; proxyElem.style.left = "0px";
captureElem.style.width = "100%"; proxyElem.style.width = "100%";
captureElem.style.height = "100%"; proxyElem.style.height = "100%";
captureElem.style.zIndex = 10000; proxyElem.style.zIndex = 10000;
captureElem.style.display = "none"; proxyElem.style.display = "none";
document.body.appendChild(captureElem); document.body.appendChild(proxyElem);
// This is to make sure callers don't get confused by having // This is to make sure callers don't get confused by having
// our blocking element as the target // our blocking element as the target
captureElem.addEventListener('contextmenu', _captureProxy); proxyElem.addEventListener('contextmenu', _captureProxy);
captureElem.addEventListener('mousemove', _captureProxy); proxyElem.addEventListener('mousemove', _captureProxy);
captureElem.addEventListener('mouseup', _captureProxy); proxyElem.addEventListener('mouseup', _captureProxy);
} }
_captureElem = elem; document.captureElement = target;
_captureIndex++;
// Track cursor and get initial cursor // Track cursor and get initial cursor
_captureObserver.observe(elem, {attributes: true}); _captureObserver.observe(target, {attributes: true});
_captureElemChanged(); _capturedElemChanged();
captureElem.style.display = ""; proxyElem.style.display = "";
// We listen to events on window in order to keep tracking if it // We listen to events on window in order to keep tracking if it
// happens to leave the viewport // happens to leave the viewport
@ -112,26 +115,26 @@ export function releaseCapture() {
if (document.releaseCapture) { if (document.releaseCapture) {
document.releaseCapture(); document.releaseCapture();
document.captureElement = null;
} else { } else {
if (!_captureElem) { if (!document.captureElement) {
return; return;
} }
// There might be events already queued, so we need to wait for // There might be events already queued. The event proxy needs
// them to flush. E.g. contextmenu in Microsoft Edge // access to the captured element for these queued events.
window.setTimeout((expected) => { // E.g. contextmenu (right-click) in Microsoft Edge
// Only clear it if it's the expected grab (i.e. no one //
// else has initiated a new grab) // Before removing the capturedElem pointer we save it to a
if (_captureIndex === expected) { // temporary variable that the unflushed events can use.
_captureElem = null; _elementForUnflushedEvents = document.captureElement;
} document.captureElement = null;
}, 0, _captureIndex);
_captureObserver.disconnect(); _captureObserver.disconnect();
const captureElem = document.getElementById("noVNC_mouse_capture_elem"); const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
captureElem.style.display = "none"; proxyElem.style.display = "none";
window.removeEventListener('mousemove', _captureProxy); window.removeEventListener('mousemove', _captureProxy);
window.removeEventListener('mouseup', _captureProxy); window.removeEventListener('mouseup', _captureProxy);

View file

@ -1,6 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.

View file

@ -0,0 +1,15 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
export function toUnsigned32bit(toConvert) {
return toConvert >>> 0;
}
export function toSigned32bit(toConvert) {
return toConvert | 0;
}

View file

@ -1,6 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
@ -10,18 +10,18 @@
* Logging/debug routines * Logging/debug routines
*/ */
let _log_level = 'warn'; let _logLevel = 'warn';
let Debug = () => {}; let Debug = () => {};
let Info = () => {}; let Info = () => {};
let Warn = () => {}; let Warn = () => {};
let Error = () => {}; let Error = () => {};
export function init_logging(level) { export function initLogging(level) {
if (typeof level === 'undefined') { if (typeof level === 'undefined') {
level = _log_level; level = _logLevel;
} else { } else {
_log_level = level; _logLevel = level;
} }
Debug = Info = Warn = Error = () => {}; Debug = Info = Warn = Error = () => {};
@ -46,11 +46,11 @@ export function init_logging(level) {
} }
} }
export function get_logging() { export function getLogging() {
return _log_level; return _logLevel;
} }
export { Debug, Info, Warn, Error }; export { Debug, Info, Warn, Error };
// Initialize logging level // Initialize logging level
init_logging(); initLogging();

View file

@ -1,6 +1,6 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt) * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/ */
@ -52,3 +52,10 @@ if (typeof Object.assign != 'function') {
window.CustomEvent = CustomEvent; window.CustomEvent = CustomEvent;
} }
})(); })();
/* Number.isInteger() (taken from MDN) */
Number.isInteger = Number.isInteger || function isInteger(value) {
return typeof value === 'number' &&
isFinite(value) &&
Math.floor(value) === value;
};

View file

@ -1,14 +1,28 @@
/* /*
* noVNC: HTML5 VNC client * noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*/ */
/* // Decode from UTF-8
* Decode from UTF-8 export function decodeUTF8(utf8string, allowLatin1=false) {
*/ try {
export function decodeUTF8(utf8string) {
return decodeURIComponent(escape(utf8string)); return decodeURIComponent(escape(utf8string));
} catch (e) {
if (e instanceof URIError) {
if (allowLatin1) {
// If we allow Latin1 we can ignore any decoding fails
// and in these cases return the original string
return utf8string;
}
}
throw e;
}
}
// Encode to UTF-8
export function encodeUTF8(DOMString) {
return unescape(encodeURIComponent(DOMString));
} }

View file

@ -1,6 +1,6 @@
/* /*
* Websock: high-performance binary WebSockets * Websock: high-performance binary WebSockets
* Copyright (C) 2018 The noVNC Authors * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
* *
* Websock is similar to the standard WebSocket object but with extra * Websock is similar to the standard WebSocket object but with extra
@ -17,6 +17,8 @@ import * as Log from './util/logging.js';
// this has performance issues in some versions Chromium, and // this has performance issues in some versions Chromium, and
// doesn't gain a tremendous amount of performance increase in Firefox // doesn't gain a tremendous amount of performance increase in Firefox
// at the moment. It may be valuable to turn it on in the future. // at the moment. It may be valuable to turn it on in the future.
// Also copyWithin() for TypedArrays is not supported in IE 11 or
// Safari 13 (at the moment we want to support Safari 11).
const ENABLE_COPYWITHIN = false; const ENABLE_COPYWITHIN = false;
const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB
@ -27,7 +29,6 @@ export default class Websock {
this._rQi = 0; // Receive queue index this._rQi = 0; // Receive queue index
this._rQlen = 0; // Next write position in the receive queue this._rQlen = 0; // Next write position in the receive queue
this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB) this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB)
this._rQmax = this._rQbufferSize / 8;
// called in init: this._rQ = new Uint8Array(this._rQbufferSize); // called in init: this._rQ = new Uint8Array(this._rQbufferSize);
this._rQ = null; // Receive queue this._rQ = null; // Receive queue
@ -143,7 +144,7 @@ export default class Websock {
flush() { flush() {
if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) {
this._websocket.send(this._encode_message()); this._websocket.send(this._encodeMessage());
this._sQlen = 0; this._sQlen = 0;
} }
} }
@ -154,7 +155,7 @@ export default class Websock {
this.flush(); this.flush();
} }
send_string(str) { sendString(str) {
this.send(str.split('').map(chr => chr.charCodeAt(0))); this.send(str.split('').map(chr => chr.charCodeAt(0)));
} }
@ -167,13 +168,13 @@ export default class Websock {
this._eventHandlers[evt] = handler; this._eventHandlers[evt] = handler;
} }
_allocate_buffers() { _allocateBuffers() {
this._rQ = new Uint8Array(this._rQbufferSize); this._rQ = new Uint8Array(this._rQbufferSize);
this._sQ = new Uint8Array(this._sQbufferSize); this._sQ = new Uint8Array(this._sQbufferSize);
} }
init() { init() {
this._allocate_buffers(); this._allocateBuffers();
this._rQi = 0; this._rQi = 0;
this._websocket = null; this._websocket = null;
} }
@ -184,7 +185,7 @@ export default class Websock {
this._websocket = new WebSocket(uri, protocols); this._websocket = new WebSocket(uri, protocols);
this._websocket.binaryType = 'arraybuffer'; this._websocket.binaryType = 'arraybuffer';
this._websocket.onmessage = this._recv_message.bind(this); this._websocket.onmessage = this._recvMessage.bind(this);
this._websocket.onopen = () => { this._websocket.onopen = () => {
Log.Debug('>> WebSock.onopen'); Log.Debug('>> WebSock.onopen');
if (this._websocket.protocol) { if (this._websocket.protocol) {
@ -219,42 +220,46 @@ export default class Websock {
} }
// private methods // private methods
_encode_message() { _encodeMessage() {
// Put in a binary arraybuffer // Put in a binary arraybuffer
// according to the spec, you can send ArrayBufferViews with the send method // according to the spec, you can send ArrayBufferViews with the send method
return new Uint8Array(this._sQ.buffer, 0, this._sQlen); return new Uint8Array(this._sQ.buffer, 0, this._sQlen);
} }
_expand_compact_rQ(min_fit) { // We want to move all the unread data to the start of the queue,
const resizeNeeded = min_fit || this.rQlen > this._rQbufferSize / 2; // e.g. compacting.
// The function also expands the receive que if needed, and for
// performance reasons we combine these two actions to avoid
// unneccessary copying.
_expandCompactRQ(minFit) {
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
// instead of resizing
const requiredBufferSize = (this._rQlen - this._rQi + minFit) * 8;
const resizeNeeded = this._rQbufferSize < requiredBufferSize;
if (resizeNeeded) { if (resizeNeeded) {
if (!min_fit) { // Make sure we always *at least* double the buffer size, and have at least space for 8x
// just double the size if we need to do compaction // the current amount of data
this._rQbufferSize *= 2; this._rQbufferSize = Math.max(this._rQbufferSize * 2, requiredBufferSize);
} else {
// otherwise, make sure we satisy rQlen - rQi + min_fit < rQbufferSize / 8
this._rQbufferSize = (this.rQlen + min_fit) * 8;
}
} }
// we don't want to grow unboundedly // we don't want to grow unboundedly
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
this._rQbufferSize = MAX_RQ_GROW_SIZE; this._rQbufferSize = MAX_RQ_GROW_SIZE;
if (this._rQbufferSize - this.rQlen < min_fit) { if (this._rQbufferSize - this.rQlen < minFit) {
throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
} }
} }
if (resizeNeeded) { if (resizeNeeded) {
const old_rQbuffer = this._rQ.buffer; const oldRQbuffer = this._rQ.buffer;
this._rQmax = this._rQbufferSize / 8;
this._rQ = new Uint8Array(this._rQbufferSize); this._rQ = new Uint8Array(this._rQbufferSize);
this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi));
} else { } else {
if (ENABLE_COPYWITHIN) { if (ENABLE_COPYWITHIN) {
this._rQ.copyWithin(0, this._rQi); this._rQ.copyWithin(0, this._rQi, this._rQlen);
} else { } else {
this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi, this._rQlen - this._rQi));
} }
} }
@ -262,26 +267,25 @@ export default class Websock {
this._rQi = 0; this._rQi = 0;
} }
_decode_message(data) { // push arraybuffer values onto the end of the receive que
// push arraybuffer values onto the end _DecodeMessage(data) {
const u8 = new Uint8Array(data); const u8 = new Uint8Array(data);
if (u8.length > this._rQbufferSize - this._rQlen) { if (u8.length > this._rQbufferSize - this._rQlen) {
this._expand_compact_rQ(u8.length); this._expandCompactRQ(u8.length);
} }
this._rQ.set(u8, this._rQlen); this._rQ.set(u8, this._rQlen);
this._rQlen += u8.length; this._rQlen += u8.length;
} }
_recv_message(e) { _recvMessage(e) {
this._decode_message(e.data); this._DecodeMessage(e.data);
if (this.rQlen > 0) { if (this.rQlen > 0) {
this._eventHandlers.message(); this._eventHandlers.message();
// Compact the receive queue
if (this._rQlen == this._rQi) { if (this._rQlen == this._rQi) {
// All data has now been processed, this means we
// can reset the receive queue.
this._rQlen = 0; this._rQlen = 0;
this._rQi = 0; this._rQi = 0;
} else if (this._rQlen > this._rQmax) {
this._expand_compact_rQ();
} }
} else { } else {
Log.Debug("Ignoring empty message"); Log.Debug("Ignoring empty message");

View file

@ -0,0 +1,88 @@
{
"name": "@novnc/novnc",
"version": "1.2.0",
"description": "An HTML5 VNC client",
"browser": "lib/rfb",
"directories": {
"lib": "lib",
"doc": "docs",
"test": "tests"
},
"files": [
"lib",
"AUTHORS",
"VERSION",
"docs/API.md",
"docs/LIBRARY.md",
"docs/LICENSE*",
"core",
"vendor/pako"
],
"scripts": {
"lint": "eslint app core po/po2js po/xgettext-html tests utils",
"test": "karma start karma.conf.js",
"prepublish": "node ./utils/use_require.js --as commonjs --clean"
},
"repository": {
"type": "git",
"url": "git+https://github.com/novnc/noVNC.git"
},
"author": "Joel Martin <github@martintribe.org> (https://github.com/kanaka)",
"contributors": [
"Solly Ross <sross@redhat.com> (https://github.com/directxman12)",
"Peter Åstrand <astrand@cendio.se> (https://github.com/astrand)",
"Samuel Mannehed <samuel@cendio.se> (https://github.com/samhed)",
"Pierre Ossman <ossman@cendio.se> (https://github.com/CendioOssman)"
],
"license": "MPL-2.0",
"bugs": {
"url": "https://github.com/novnc/noVNC/issues"
},
"homepage": "https://github.com/novnc/noVNC",
"devDependencies": {
"@babel/core": "*",
"@babel/plugin-syntax-dynamic-import": "*",
"@babel/plugin-transform-modules-amd": "*",
"@babel/plugin-transform-modules-commonjs": "*",
"@babel/plugin-transform-modules-systemjs": "*",
"@babel/plugin-transform-modules-umd": "*",
"@babel/preset-env": "*",
"@babel/cli": "*",
"babel-plugin-import-redirect": "*",
"browserify": "*",
"babelify": "*",
"core-js": "*",
"chai": "*",
"commander": "*",
"es-module-loader": "*",
"eslint": "*",
"fs-extra": "*",
"jsdom": "*",
"karma": "*",
"karma-mocha": "*",
"karma-chrome-launcher": "*",
"@chiragrupani/karma-chromium-edge-launcher": "*",
"karma-firefox-launcher": "*",
"karma-ie-launcher": "*",
"karma-mocha-reporter": "*",
"karma-safari-launcher": "*",
"karma-script-launcher": "*",
"karma-sinon-chai": "*",
"mocha": "*",
"node-getopt": "*",
"po2json": "*",
"requirejs": "*",
"rollup": "*",
"rollup-plugin-node-resolve": "*",
"sinon": "*",
"sinon-chai": "*"
},
"dependencies": {},
"keywords": [
"vnc",
"rfb",
"novnc",
"websockify"
]
}

View file

@ -6,8 +6,8 @@ It's based heavily on
https://github.com/ModuleLoader/browser-es-module-loader, but uses https://github.com/ModuleLoader/browser-es-module-loader, but uses
WebWorkers to compile the modules in the background. WebWorkers to compile the modules in the background.
To generate, run `rollup -c` in this directory, and then run `browserify To generate, run `npx rollup -c` in this directory, and then run
src/babel-worker.js > dist/babel-worker.js`. `./genworker.js`.
LICENSE LICENSE
------- -------

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
(function (global, factory) { (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) : typeof define === 'function' && define.amd ? define(factory) :
(global.BrowserESModuleLoader = factory()); (global = global || self, global.BrowserESModuleLoader = factory());
}(this, (function () { 'use strict'; }(this, (function () { 'use strict';
/* /*
@ -12,6 +12,7 @@ var isNode = typeof process !== 'undefined' && process.versions && process.versi
var isWindows = typeof process !== 'undefined' && typeof process.platform === 'string' && process.platform.match(/^win/); var isWindows = typeof process !== 'undefined' && typeof process.platform === 'string' && process.platform.match(/^win/);
var envGlobal = typeof self !== 'undefined' ? self : global; var envGlobal = typeof self !== 'undefined' ? self : global;
/* /*
* Simple Symbol() shim * Simple Symbol() shim
*/ */
@ -22,10 +23,6 @@ function createSymbol (name) {
var toStringTag = hasSymbol && Symbol.toStringTag; var toStringTag = hasSymbol && Symbol.toStringTag;
/* /*
* Environment baseURI * Environment baseURI
*/ */
@ -95,7 +92,7 @@ function LoaderError__Check_error_message_for_loader_stack (childErr, newMessage
return err; return err;
} }
var resolvedPromise$1 = Promise.resolve(); var resolvedPromise = Promise.resolve();
/* /*
* Simple Array values shim * Simple Array values shim
@ -155,7 +152,7 @@ Loader.prototype.import = function (key, parent) {
throw new TypeError('Loader import method must be passed a module key string'); throw new TypeError('Loader import method must be passed a module key string');
// custom resolveInstantiate combined hook for better perf // custom resolveInstantiate combined hook for better perf
var loader = this; var loader = this;
return resolvedPromise$1 return resolvedPromise
.then(function () { .then(function () {
return loader[RESOLVE_INSTANTIATE](key, parent); return loader[RESOLVE_INSTANTIATE](key, parent);
}) })
@ -197,7 +194,7 @@ function ensureResolution (resolvedKey) {
Loader.prototype.resolve = function (key, parent) { Loader.prototype.resolve = function (key, parent) {
var loader = this; var loader = this;
return resolvedPromise$1 return resolvedPromise
.then(function() { .then(function() {
return loader[RESOLVE](key, parent); return loader[RESOLVE](key, parent);
}) })
@ -318,8 +315,7 @@ function ModuleNamespace (baseObject/*, evaluate*/) {
else { */ else { */
Object.keys(baseObject).forEach(extendNamespace, this); Object.keys(baseObject).forEach(extendNamespace, this);
//} //}
} }// 8.4.2
// 8.4.2
ModuleNamespace.prototype = Object.create(null); ModuleNamespace.prototype = Object.create(null);
if (toStringTag) if (toStringTag)
@ -491,7 +487,8 @@ function resolveIfNotPlain (relUrl, parentUrl) {
} }
} }
var resolvedPromise = Promise.resolve(); var resolvedPromise$1 = Promise.resolve();
/* /*
* Register Loader * Register Loader
* *
@ -505,7 +502,7 @@ var resolvedPromise = Promise.resolve();
var REGISTER_INTERNAL = createSymbol('register-internal'); var REGISTER_INTERNAL = createSymbol('register-internal');
function RegisterLoader$1 () { function RegisterLoader () {
Loader.call(this); Loader.call(this);
var registryDelete = this.registry.delete; var registryDelete = this.registry.delete;
@ -534,17 +531,17 @@ function RegisterLoader$1 () {
this.trace = false; this.trace = false;
} }
RegisterLoader$1.prototype = Object.create(Loader.prototype); RegisterLoader.prototype = Object.create(Loader.prototype);
RegisterLoader$1.prototype.constructor = RegisterLoader$1; RegisterLoader.prototype.constructor = RegisterLoader;
var INSTANTIATE = RegisterLoader$1.instantiate = createSymbol('instantiate'); var INSTANTIATE = RegisterLoader.instantiate = createSymbol('instantiate');
// default normalize is the WhatWG style normalizer // default normalize is the WhatWG style normalizer
RegisterLoader$1.prototype[RegisterLoader$1.resolve = Loader.resolve] = function (key, parentKey) { RegisterLoader.prototype[RegisterLoader.resolve = Loader.resolve] = function (key, parentKey) {
return resolveIfNotPlain(key, parentKey || baseURI); return resolveIfNotPlain(key, parentKey || baseURI);
}; };
RegisterLoader$1.prototype[INSTANTIATE] = function (key, processAnonRegister) {}; RegisterLoader.prototype[INSTANTIATE] = function (key, processAnonRegister) {};
// once evaluated, the linkRecord is set to undefined leaving just the other load record properties // once evaluated, the linkRecord is set to undefined leaving just the other load record properties
// this allows tracking new binding listeners for es modules through importerSetters // this allows tracking new binding listeners for es modules through importerSetters
@ -599,7 +596,7 @@ function createLoadRecord (state, key, registration) {
}; };
} }
RegisterLoader$1.prototype[Loader.resolveInstantiate] = function (key, parentKey) { RegisterLoader.prototype[Loader.resolveInstantiate] = function (key, parentKey) {
var loader = this; var loader = this;
var state = this[REGISTER_INTERNAL]; var state = this[REGISTER_INTERNAL];
var registry = this.registry[REGISTRY]; var registry = this.registry[REGISTRY];
@ -686,7 +683,7 @@ function createProcessAnonRegister (loader, load, state) {
function instantiate (loader, load, link, registry, state) { function instantiate (loader, load, link, registry, state) {
return link.instantiatePromise || (link.instantiatePromise = return link.instantiatePromise || (link.instantiatePromise =
// if there is already an existing registration, skip running instantiate // if there is already an existing registration, skip running instantiate
(load.registration ? resolvedPromise : resolvedPromise.then(function () { (load.registration ? resolvedPromise$1 : resolvedPromise$1.then(function () {
state.lastRegister = undefined; state.lastRegister = undefined;
return loader[INSTANTIATE](load.key, loader[INSTANTIATE].length > 1 && createProcessAnonRegister(loader, load, state)); return loader[INSTANTIATE](load.key, loader[INSTANTIATE].length > 1 && createProcessAnonRegister(loader, load, state));
})) }))
@ -907,9 +904,9 @@ function deepInstantiateDeps (loader, load, link, registry, state) {
var seen = []; var seen = [];
function addDeps (load, link) { function addDeps (load, link) {
if (!link) if (!link)
return resolvedPromise; return resolvedPromise$1;
if (seen.indexOf(load) !== -1) if (seen.indexOf(load) !== -1)
return resolvedPromise; return resolvedPromise$1;
seen.push(load); seen.push(load);
return instantiateDeps(loader, load, link, registry, state) return instantiateDeps(loader, load, link, registry, state)
@ -926,14 +923,13 @@ function deepInstantiateDeps (loader, load, link, registry, state) {
return Promise.all(depPromises); return Promise.all(depPromises);
}); });
} }
return addDeps(load, link); return addDeps(load, link);
} }
/* /*
* System.register * System.register
*/ */
RegisterLoader$1.prototype.register = function (key, deps, declare) { RegisterLoader.prototype.register = function (key, deps, declare) {
var state = this[REGISTER_INTERNAL]; var state = this[REGISTER_INTERNAL];
// anonymous modules get stored as lastAnon // anonymous modules get stored as lastAnon
@ -951,7 +947,7 @@ RegisterLoader$1.prototype.register = function (key, deps, declare) {
/* /*
* System.registerDyanmic * System.registerDyanmic
*/ */
RegisterLoader$1.prototype.registerDynamic = function (key, deps, executingRequire, execute) { RegisterLoader.prototype.registerDynamic = function (key, deps, executingRequire, execute) {
var state = this[REGISTER_INTERNAL]; var state = this[REGISTER_INTERNAL];
// anonymous modules get stored as lastAnon // anonymous modules get stored as lastAnon
@ -1279,7 +1275,7 @@ function BrowserESModuleLoader(baseKey) {
if (baseKey) if (baseKey)
this.baseKey = resolveIfNotPlain(baseKey, baseURI) || resolveIfNotPlain('./' + baseKey, baseURI); this.baseKey = resolveIfNotPlain(baseKey, baseURI) || resolveIfNotPlain('./' + baseKey, baseURI);
RegisterLoader$1.call(this); RegisterLoader.call(this);
var loader = this; var loader = this;
@ -1293,11 +1289,11 @@ function BrowserESModuleLoader(baseKey) {
prevRegister.apply(this, arguments); prevRegister.apply(this, arguments);
}; };
} }
BrowserESModuleLoader.prototype = Object.create(RegisterLoader$1.prototype); BrowserESModuleLoader.prototype = Object.create(RegisterLoader.prototype);
// normalize is never given a relative name like "./x", that part is already handled // normalize is never given a relative name like "./x", that part is already handled
BrowserESModuleLoader.prototype[RegisterLoader$1.resolve] = function(key, parent) { BrowserESModuleLoader.prototype[RegisterLoader.resolve] = function(key, parent) {
var resolved = RegisterLoader$1.prototype[RegisterLoader$1.resolve].call(this, key, parent || this.baseKey) || key; var resolved = RegisterLoader.prototype[RegisterLoader.resolve].call(this, key, parent || this.baseKey) || key;
if (!resolved) if (!resolved)
throw new RangeError('ES module loader does not resolve plain module names, resolving "' + key + '" to ' + parent); throw new RangeError('ES module loader does not resolve plain module names, resolving "' + key + '" to ' + parent);
@ -1431,8 +1427,7 @@ babelWorker.onmessage = function (evt) {
// instantiate just needs to run System.register // instantiate just needs to run System.register
// so we fetch the source, convert into the Babel System module format, then evaluate it // so we fetch the source, convert into the Babel System module format, then evaluate it
BrowserESModuleLoader.prototype[RegisterLoader$1.instantiate] = function(key, processAnonRegister) { BrowserESModuleLoader.prototype[RegisterLoader.instantiate] = function(key, processAnonRegister) {
var loader = this;
// load as ES with Babel converting into System.register // load as ES with Babel converting into System.register
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
#!/usr/bin/env node
var fs = require("fs");
var browserify = require("browserify");
browserify("src/babel-worker.js")
.transform("babelify", {
presets: [ [ "@babel/preset-env", { targets: "ie >= 11" } ] ],
global: true,
ignore: [ "../../node_modules/core-js" ]
})
.bundle()
.pipe(fs.createWriteStream("dist/babel-worker.js"));

View file

@ -1,16 +1,15 @@
import nodeResolve from 'rollup-plugin-node-resolve'; import nodeResolve from 'rollup-plugin-node-resolve';
export default { export default {
entry: 'src/browser-es-module-loader.js', input: 'src/browser-es-module-loader.js',
dest: 'dist/browser-es-module-loader.js', output: {
file: 'dist/browser-es-module-loader.js',
format: 'umd', format: 'umd',
moduleName: 'BrowserESModuleLoader', name: 'BrowserESModuleLoader',
sourceMap: true, sourcemap: true,
},
plugins: [ plugins: [
nodeResolve(), nodeResolve(),
], ],
// skip rollup warnings (specifically the eval warning)
onwarn: function() {}
}; };

View file

@ -1,12 +1,10 @@
/*import { transform as babelTransform } from 'babel-core'; // Polyfills needed for Babel to function
import babelTransformDynamicImport from 'babel-plugin-syntax-dynamic-import'; require("core-js");
import babelTransformES2015ModulesSystemJS from 'babel-plugin-transform-es2015-modules-systemjs';*/
// sadly, due to how rollup works, we can't use es6 imports here var babelTransform = require('@babel/core').transform;
var babelTransform = require('babel-core').transform; var babelTransformDynamicImport = require('@babel/plugin-syntax-dynamic-import');
var babelTransformDynamicImport = require('babel-plugin-syntax-dynamic-import'); var babelTransformModulesSystemJS = require('@babel/plugin-transform-modules-systemjs');
var babelTransformES2015ModulesSystemJS = require('babel-plugin-transform-es2015-modules-systemjs'); var babelPresetEnv = require('@babel/preset-env');
var babelPresetES2015 = require('babel-preset-es2015');
self.onmessage = function (evt) { self.onmessage = function (evt) {
// transform source with Babel // transform source with Babel
@ -17,8 +15,8 @@ self.onmessage = function (evt) {
moduleIds: false, moduleIds: false,
sourceMaps: 'inline', sourceMaps: 'inline',
babelrc: false, babelrc: false,
plugins: [babelTransformDynamicImport, babelTransformES2015ModulesSystemJS], plugins: [babelTransformDynamicImport, babelTransformModulesSystemJS],
presets: [babelPresetES2015], presets: [ [ babelPresetEnv, { targets: 'ie >= 11' } ] ],
}); });
self.postMessage({key: evt.data.key, code: output.code, source: evt.data.source}); self.postMessage({key: evt.data.key, code: output.code, source: evt.data.source});

View file

@ -1,5 +1,4 @@
import RegisterLoader from 'es-module-loader/core/register-loader.js'; import RegisterLoader from 'es-module-loader/core/register-loader.js';
import { InternalModuleNamespace as ModuleNamespace } from 'es-module-loader/core/loader-polyfill.js';
import { baseURI, global, isBrowser } from 'es-module-loader/core/common.js'; import { baseURI, global, isBrowser } from 'es-module-loader/core/common.js';
import { resolveIfNotPlain } from 'es-module-loader/core/resolve.js'; import { resolveIfNotPlain } from 'es-module-loader/core/resolve.js';
@ -35,7 +34,7 @@ if (typeof document != 'undefined' && document.getElementsByTagName) {
// throw so it still shows up in the console // throw so it still shows up in the console
throw err; throw err;
}; }
var ready = function() { var ready = function() {
document.removeEventListener('DOMContentLoaded', ready, false ); document.removeEventListener('DOMContentLoaded', ready, false );
@ -63,7 +62,7 @@ if (typeof document != 'undefined' && document.getElementsByTagName) {
} }
} }
} }
}; }
// simple DOM ready // simple DOM ready
if (document.readyState !== 'loading') if (document.readyState !== 'loading')
@ -105,10 +104,10 @@ function xhrFetch(url, resolve, reject) {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
var load = function(source) { var load = function(source) {
resolve(xhr.responseText); resolve(xhr.responseText);
}; }
var error = function() { var error = function() {
reject(new Error('XHR error' + (xhr.status ? ' (' + xhr.status + (xhr.statusText ? ' ' + xhr.statusText : '') + ')' : '') + ' loading ' + url)); reject(new Error('XHR error' + (xhr.status ? ' (' + xhr.status + (xhr.statusText ? ' ' + xhr.statusText : '') + ')' : '') + ' loading ' + url));
}; }
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { if (xhr.readyState === 4) {
@ -235,7 +234,7 @@ BrowserESModuleLoader.prototype[RegisterLoader.instantiate] = function(key, proc
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
// anonymous module // anonymous module
if (anonSources[key]) { if (anonSources[key]) {
resolve(anonSources[key]); resolve(anonSources[key])
anonSources[key] = undefined; anonSources[key] = undefined;
} }
// otherwise we fetch // otherwise we fetch

View file

@ -4,7 +4,7 @@ export function shrinkBuf (buf, size) {
if (buf.subarray) { return buf.subarray(0, size); } if (buf.subarray) { return buf.subarray(0, size); }
buf.length = size; buf.length = size;
return buf; return buf;
} };
export function arraySet (dest, src, src_offs, len, dest_offs) { export function arraySet (dest, src, src_offs, len, dest_offs) {

View file

@ -9,51 +9,51 @@ import msg from "./messages.js";
/* Allowed flush values; see deflate() and inflate() below for details */ /* Allowed flush values; see deflate() and inflate() below for details */
var Z_NO_FLUSH = 0; export const Z_NO_FLUSH = 0;
var Z_PARTIAL_FLUSH = 1; export const Z_PARTIAL_FLUSH = 1;
//var Z_SYNC_FLUSH = 2; //export const Z_SYNC_FLUSH = 2;
var Z_FULL_FLUSH = 3; export const Z_FULL_FLUSH = 3;
var Z_FINISH = 4; export const Z_FINISH = 4;
var Z_BLOCK = 5; export const Z_BLOCK = 5;
//var Z_TREES = 6; //export const Z_TREES = 6;
/* Return codes for the compression/decompression functions. Negative values /* Return codes for the compression/decompression functions. Negative values
* are errors, positive values are used for special but normal events. * are errors, positive values are used for special but normal events.
*/ */
var Z_OK = 0; export const Z_OK = 0;
var Z_STREAM_END = 1; export const Z_STREAM_END = 1;
//var Z_NEED_DICT = 2; //export const Z_NEED_DICT = 2;
//var Z_ERRNO = -1; //export const Z_ERRNO = -1;
var Z_STREAM_ERROR = -2; export const Z_STREAM_ERROR = -2;
var Z_DATA_ERROR = -3; export const Z_DATA_ERROR = -3;
//var Z_MEM_ERROR = -4; //export const Z_MEM_ERROR = -4;
var Z_BUF_ERROR = -5; export const Z_BUF_ERROR = -5;
//var Z_VERSION_ERROR = -6; //export const Z_VERSION_ERROR = -6;
/* compression levels */ /* compression levels */
//var Z_NO_COMPRESSION = 0; //export const Z_NO_COMPRESSION = 0;
//var Z_BEST_SPEED = 1; //export const Z_BEST_SPEED = 1;
//var Z_BEST_COMPRESSION = 9; //export const Z_BEST_COMPRESSION = 9;
var Z_DEFAULT_COMPRESSION = -1; export const Z_DEFAULT_COMPRESSION = -1;
var Z_FILTERED = 1; export const Z_FILTERED = 1;
var Z_HUFFMAN_ONLY = 2; export const Z_HUFFMAN_ONLY = 2;
var Z_RLE = 3; export const Z_RLE = 3;
var Z_FIXED = 4; export const Z_FIXED = 4;
var Z_DEFAULT_STRATEGY = 0; export const Z_DEFAULT_STRATEGY = 0;
/* Possible values of the data_type field (though see inflate()) */ /* Possible values of the data_type field (though see inflate()) */
//var Z_BINARY = 0; //export const Z_BINARY = 0;
//var Z_TEXT = 1; //export const Z_TEXT = 1;
//var Z_ASCII = 1; // = Z_TEXT //export const Z_ASCII = 1; // = Z_TEXT
var Z_UNKNOWN = 2; export const Z_UNKNOWN = 2;
/* The deflate compression method */ /* The deflate compression method */
var Z_DEFLATED = 8; export const Z_DEFLATED = 8;
/*============================================================================*/ /*============================================================================*/

View file

@ -277,7 +277,7 @@ export default function inflate_fast(strm, start) {
} }
else if ((op & 64) === 0) { /* 2nd level distance code */ else if ((op & 64) === 0) { /* 2nd level distance code */
here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))]; here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
continue; continue dodist;
} }
else { else {
strm.msg = 'invalid distance code'; strm.msg = 'invalid distance code';
@ -290,7 +290,7 @@ export default function inflate_fast(strm, start) {
} }
else if ((op & 64) === 0) { /* 2nd level length code */ else if ((op & 64) === 0) { /* 2nd level length code */
here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))]; here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
continue; continue dolen;
} }
else if (op & 32) { /* end-of-block */ else if (op & 32) { /* end-of-block */
//Tracevv((stderr, "inflate: end of block\n")); //Tracevv((stderr, "inflate: end of block\n"));
@ -320,5 +320,5 @@ export default function inflate_fast(strm, start) {
strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end)); strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end));
state.hold = hold; state.hold = hold;
state.bits = bits; state.bits = bits;
return;
}; };

View file

@ -13,30 +13,30 @@ var DISTS = 2;
/* Allowed flush values; see deflate() and inflate() below for details */ /* Allowed flush values; see deflate() and inflate() below for details */
//var Z_NO_FLUSH = 0; //export const Z_NO_FLUSH = 0;
//var Z_PARTIAL_FLUSH = 1; //export const Z_PARTIAL_FLUSH = 1;
//var Z_SYNC_FLUSH = 2; //export const Z_SYNC_FLUSH = 2;
//var Z_FULL_FLUSH = 3; //export const Z_FULL_FLUSH = 3;
var Z_FINISH = 4; export const Z_FINISH = 4;
var Z_BLOCK = 5; export const Z_BLOCK = 5;
var Z_TREES = 6; export const Z_TREES = 6;
/* Return codes for the compression/decompression functions. Negative values /* Return codes for the compression/decompression functions. Negative values
* are errors, positive values are used for special but normal events. * are errors, positive values are used for special but normal events.
*/ */
var Z_OK = 0; export const Z_OK = 0;
var Z_STREAM_END = 1; export const Z_STREAM_END = 1;
var Z_NEED_DICT = 2; export const Z_NEED_DICT = 2;
//var Z_ERRNO = -1; //export const Z_ERRNO = -1;
var Z_STREAM_ERROR = -2; export const Z_STREAM_ERROR = -2;
var Z_DATA_ERROR = -3; export const Z_DATA_ERROR = -3;
var Z_MEM_ERROR = -4; export const Z_MEM_ERROR = -4;
var Z_BUF_ERROR = -5; export const Z_BUF_ERROR = -5;
//var Z_VERSION_ERROR = -6; //export const Z_VERSION_ERROR = -6;
/* The deflate compression method */ /* The deflate compression method */
var Z_DEFLATED = 8; export const Z_DEFLATED = 8;
/* STATES ====================================================================*/ /* STATES ====================================================================*/

File diff suppressed because one or more lines are too long

View file

@ -283,6 +283,10 @@ class wvmCreate(wvmConnect):
else: else:
xml += """<target dev='sd%s'/>""" % sd_disk_letters.pop(0) xml += """<target dev='sd%s'/>""" % sd_disk_letters.pop(0)
xml += """</disk>""" xml += """</disk>"""
if volume.get('bus') == 'scsi':
xml += f"""<controller type='scsi' model='{volume.get('scsi_model')}'/>"""
if add_cd: if add_cd:
xml += """<disk type='file' device='cdrom'> xml += """<disk type='file' device='cdrom'>
<driver name='qemu' type='raw'/> <driver name='qemu' type='raw'/>
@ -298,9 +302,6 @@ class wvmCreate(wvmConnect):
xml += """<target dev='vd%s' bus='%s'/>""" % (vd_disk_letters.pop(0), 'virtio') xml += """<target dev='vd%s' bus='%s'/>""" % (vd_disk_letters.pop(0), 'virtio')
xml += """</disk>""" xml += """</disk>"""
if volume.get('bus') == 'scsi':
xml += f"""<controller type='scsi' model='{volume.get('scsi_model')}'/>"""
for net in networks.split(','): for net in networks.split(','):
xml += """<interface type='network'>""" xml += """<interface type='network'>"""
if mac: if mac:

494
webvirtcloud.sh Normal file
View file

@ -0,0 +1,494 @@
#!/bin/bash
#/ Usage: webvirtcloud.sh [-vh]
#/
#/ Install Webvirtcloud virtualization web interface.
#/
#/ OPTIONS:
#/ -v | --verbose Enable verbose output.
#/ -h | --help Show this message.
########################################################
# Webvirtcloud Install Script #
# Script created by Mike Tucker(mtucker6784@gmail.com) #
# adapted by catborise #
# catborise@gmail.com #
# #
# Feel free to modify, but please give #
# credit where it's due. Thanks! #
########################################################
# Parse arguments
while true; do
case "$1" in
-h|--help)
show_help=true
shift
;;
-v|--verbose)
set -x
verbose=true
shift
;;
-*)
echo "Error: invalid argument: '$1'" 1>&2
exit 1
;;
*)
break
;;
esac
done
print_usage () {
grep '^#/' <"$0" | cut -c 4-
exit 1
}
if [ -n "$show_help" ]; then
print_usage
else
for x in "$@"; do
if [ "$x" = "--help" ] || [ "$x" = "-h" ]; then
print_usage
fi
done
fi
# ensure running as root
if [ "$(id -u)" != "0" ]; then
#Debian doesnt have sudo if root has a password.
if ! hash sudo 2>/dev/null; then
exec su -c "$0" "$@"
else
exec sudo "$0" "$@"
fi
fi
clear
readonly APP_USER="wvcuser"
readonly APP_REPO_URL="https://github.com/retspen/webvirtcloud.git"
readonly APP_NAME="webvirtcloud"
readonly APP_PATH="/srv/$APP_NAME"
readonly PYTHON="python3"
progress () {
spin[0]="-"
spin[1]="\\"
spin[2]="|"
spin[3]="/"
echo -n " "
while kill -0 "$pid" > /dev/null 2>&1; do
for i in "${spin[@]}"; do
echo -ne "\\b$i"
sleep .3
done
done
echo ""
}
log () {
if [ -n "$verbose" ]; then
eval "$@" |& tee -a /var/log/webvirtcloud-install.log
else
eval "$@" |& tee -a /var/log/webvirtcloud-install.log >/dev/null 2>&1
fi
}
install_packages () {
case $distro in
ubuntu|debian)
for p in $PACKAGES; do
if dpkg -s "$p" >/dev/null 2>&1; then
echo " * $p already installed"
else
echo " * Installing $p"
log "DEBIAN_FRONTEND=noninteractive apt-get install -y $p"
fi
done;
;;
centos)
for p in $PACKAGES; do
if yum list installed "$p" >/dev/null 2>&1; then
echo " * $p already installed"
else
echo " * Installing $p"
log "yum -y install $p"
fi
done;
;;
fedora)
for p in $PACKAGES; do
if dnf list installed "$p" >/dev/null 2>&1; then
echo " * $p already installed"
else
echo " * Installing $p"
log "dnf -y install $p"
fi
done;
;;
esac
}
configure_nginx () {
# Remove default configuration
rm /etc/nginx/nginx.conf
if [ -f /etc/nginx/sites-enabled/default ]; then
rm /etc/nginx/sites-enabled/default
fi
chown -R $nginx_group:$nginx_group /var/lib/nginx
# Copy new configuration and webvirtcloud.conf
echo " * Copying Nginx configuration"
cp $APP_PATH/conf/nginx/"$distro"_nginx.conf /etc/nginx/nginx.conf
cp $APP_PATH/conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/
if ! [ -z "$fqdn" ]; then
sed -i "s|\\(#server_name\\).*|server_name = $fqdn|" "$nginxfile"
fi
sed -i "s|\\(server 127.0.0.1:\\).*|\\1$novncd_port;|" "$nginxfile"
}
configure_supervisor () {
# Copy template supervisor service for gunicorn and novnc
echo " * Copying supervisor configuration"
cp $APP_PATH/conf/supervisor/webvirtcloud.conf $supervisor_conf_path/$supervisor_file_name
sed -i "s|^\\(user=\\).*|\\1$nginx_group|" "$supervisor_conf_path/$supervisor_file_name"
}
create_user () {
echo "* Creating webvirtcloud user."
if [ "$distro" == "ubuntu" ] || [ "$distro" == "debian" ] ; then
adduser --quiet --disabled-password --gecos '""' "$APP_USER"
else
adduser "$APP_USER"
fi
usermod -a -G "$nginx_group" "$APP_USER"
}
run_as_app_user () {
if ! hash sudo 2>/dev/null; then
su -c "$@" $APP_USER
else
sudo -i -u $APP_USER "$@"
fi
}
activate_python_environment () {
cd $APP_PATH
virtualenv -p $PYTHON venv
source venv/bin/activate
}
generate_secret_key() {
$PYTHON - <<END
import random
print(''.join(random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)))
END
}
install_webvirtcloud () {
create_user
echo "* Cloning $APP_NAME from github to the web directory."
log "git clone $APP_REPO_URL $APP_PATH"
echo "* Configuring settings.py file."
cp "$APP_PATH/webvirtcloud/settings.py.template" "$APP_PATH/webvirtcloud/settings.py"
local secret_key=$(generate_secret_key)
echo "* Secret for Django generated: $secret_key"
#TODO escape SED delimiter in variables
sed -i "s|^\\(TIME_ZONE = \\).*|\\1$tzone|" "$APP_PATH/webvirtcloud/settings.py"
sed -i "s|^\\(SECRET_KEY = \\).*|\\1\'$secret_key\'|" "$APP_PATH/webvirtcloud/settings.py"
sed -i "s|^\\(WS_PORT = \\).*|\\1$novncd_port|" "$APP_PATH/webvirtcloud/settings.py"
sed -i "s|^\\(WS_PUBLIC_PORT = \\).*|\\1$novncd_public_port|" "$APP_PATH/webvirtcloud/settings.py"
sed -i "s|^\\(WS_HOST = \\).*|\\1\'$novncd_host\'|" "$APP_PATH/webvirtcloud/settings.py"
echo "* Activate virtual environment."
activate_python_environment
echo "* Install App's Python requirements."
pip3 install -U pip
pip3 install -r conf/requirements.txt -q
chown -R "$nginx_group":"$nginx_group" "$APP_PATH"
echo "* Django Migrate."
log "$PYTHON $APP_PATH/manage.py migrate"
$PYTHON $APP_PATH/manage.py migrate
$PYTHON $APP_PATH/manage.py makemigrations
chown -R "$nginx_group":"$nginx_group" "$APP_PATH"
}
set_firewall () {
if [ "$(firewall-cmd --state)" == "running" ]; then
echo "* Configuring firewall to allow HTTP & novnc traffic."
log "firewall-cmd --zone=public --add-port=http/tcp --permanent"
log "firewall-cmd --zone=public --add-port=$novncd_port/tcp --permanent"
#firewall-cmd --zone=public --add-port=$novncd_port/tcp --permanent
log "firewall-cmd --zone=public --add-port=$novncd_public_port/tcp --permanent"
#firewall-cmd --zone=public --add-port=$novncd_public_port/tcp --permanent
log "firewall-cmd --reload"
#firewall-cmd --reload
fi
}
set_selinux () {
#Check if SELinux is enforcing
if [ "$(getenforce)" == "Enforcing" ]; then
echo "* Configuring SELinux."
#Sets SELinux context type so that scripts running in the web server process are allowed read/write access
chcon -R -h -t httpd_sys_rw_content_t "$APP_PATH/"
setsebool -P httpd_can_network_connect 1
fi
}
set_hosts () {
echo "* Setting up hosts file."
echo >> /etc/hosts "127.0.0.1 $(hostname) $fqdn"
}
restart_supervisor () {
echo "* Setting Supervisor to start on boot and restart."
log "systemctl enable $supervisor_service"
#systemctl enable $supervisor_service
log "systemctl restart $supervisor_service"
#systemctl restart $supervisor_service
}
restart_nginx () {
echo "* Setting Nginx to start on boot and starting Nginx."
log "systemctl enable nginx.service"
#systemctl enable nginx.service
log "systemctl restart nginx.service"
#systemctl restart nginx.service
}
if [[ -f /etc/lsb-release || -f /etc/debian_version ]]; then
distro="$(lsb_release -is)"
version="$(lsb_release -rs)"
codename="$(lsb_release -cs)"
elif [ -f /etc/os-release ]; then
# shellcheck disable=SC1091
distro="$(source /etc/os-release && echo "$ID")"
# shellcheck disable=SC1091
version="$(source /etc/os-release && echo "$VERSION_ID")"
#Order is important here. If /etc/os-release and /etc/centos-release exist, we're on centos 7.
#If only /etc/centos-release exist, we're on centos6(or earlier). Centos-release is less parsable,
#so lets assume that it's version 6 (Plus, who would be doing a new install of anything on centos5 at this point..)
#/etc/os-release properly detects fedora
elif [ -f /etc/centos-release ]; then
distro="centos"
version="8"
else
distro="unsupported"
fi
echo '
WEBVIRTCLOUD
'
echo ""
echo " Welcome to Webvirtcloud Installer for CentOS, Fedora, Debian and Ubuntu!"
echo ""
shopt -s nocasematch
case $distro in
*ubuntu*)
echo " The installer has detected $distro version $version codename $codename."
distro=ubuntu
nginx_group=www-data
nginxfile=/etc/nginx/conf.d/$APP_NAME.conf
supervisor_service=supervisord
supervisor_conf_path=/etc/supervisor/conf.d
supervisor_file_name=webvirtcloud.conf
;;
*debian*)
echo " The installer has detected $distro version $version codename $codename."
distro=debian
nginx_group=www-data
nginxfile=/etc/nginx/conf.d/$APP_NAME.conf
supervisor_service=supervisor
supervisor_conf_path=/etc/supervisor/conf.d
supervisor_file_name=webvirtcloud.conf
;;
*centos*|*redhat*|*ol*|*rhel*)
echo " The installer has detected $distro version $version."
distro=centos
nginx_group=nginx
nginxfile=/etc/nginx/conf.d/$APP_NAME.conf
supervisor_service=supervisord
supervisor_conf_path=/etc/supervisord.d
supervisor_file_name=webvirtcloud.ini
;;
*)
echo " The installer was unable to determine your OS. Exiting for safety."
exit 1
;;
esac
setupfqdn=default
until [[ $setupfqdn == "yes" ]] || [[ $setupfqdn == "no" ]]; do
echo -n " Q. Do you want to configure fqdn for Nginx? (y/n) "
read -r setupfqdn
case $setupfqdn in
[yY] | [yY][Ee][Ss] )
echo -n " Q. What is the FQDN of your server? ($(hostname --fqdn)): "
read -r fqdn
if [ -z "$fqdn" ]; then
readonly fqdn="$(hostname --fqdn)"
fi
setupfqdn="yes"
echo " Setting to $fqdn"
echo ""
;;
[nN] | [n|N][O|o] )
setupfqdn="no"
;;
*) echo " Invalid answer. Please type y or n"
;;
esac
done
echo -n " Q. Do you want to change NOVNC service port number?(Default: 6080) "
read -r novncd_port
if [ -z "$novncd_port" ]; then
readonly novncd_port=6080
fi
echo " Setting novnc service port $novncd_port"
echo ""
echo -n " Q. Do you want to change NOVNC public port number for reverse proxy(e.g: 80 or 443)?(Default: 6080) "
read -r novncd_public_port
if [ -z "$novncd_public_port" ]; then
readonly novncd_public_port=6080
fi
echo " Setting novnc public port $novncd_public_port"
echo ""
echo -n " Q. Do you want to change NOVNC host listen ip?(Default: 0.0.0.0) "
read -r novncd_host
if [ -z "$novncd_host" ]; then
readonly novncd_host="0.0.0.0"
fi
echo " Setting novnc host ip $novncd_host"
echo ""
case $distro in
debian)
if [[ "$version" -ge 9 ]]; then
# Install for Debian 9.x / 10.x
tzone=\'$(cat /etc/timezone)\'
echo -n "* Updating installed packages."
log "apt-get update && apt-get -y upgrade" & pid=$!
progress
echo "* Installing OS requirements."
PACKAGES="git virtualenv python3-virtualenv python3-dev python3-lxml libvirt-dev zlib1g-dev libxslt1-dev nginx supervisor libsasl2-modules gcc pkg-config python3-guestfs uuid"
install_packages
set_hosts
install_webvirtcloud
echo "* Configuring Nginx."
configure_nginx
echo "* Configuring Supervisor."
configure_supervisor
restart_supervisor
restart_nginx
fi
;;
ubuntu)
if [ "$version" -ge "18.04" ]; then
# Install for Ubuntu 18 / 20
tzone=\'$(cat /etc/timezone)\'
echo -n "* Updating installed packages."
log "apt-get update && apt-get -y upgrade" & pid=$!
progress
echo "* Installing OS requirements."
PACKAGES="git virtualenv python3-virtualenv python3-pip python3-dev python3-lxml libvirt-dev zlib1g-dev libxslt1-dev nginx supervisor libsasl2-modules gcc pkg-config python3-guestfs"
install_packages
set_hosts
install_webvirtcloud
echo "* Configuring Nginx."
configure_nginx
echo "* Configuring Supervisor."
configure_supervisor
restart_supervisor
restart_nginx
fi
;;
centos)
if [[ "$version" =~ ^8 ]]; then
# Install for CentOS/Redhat 8
tzone=\'$(timedatectl|grep "Time zone"| awk '{print $3}')\'
echo "* Adding wget & epel-release repository."
log "yum -y install wget epel-release"
echo "* Installing OS requirements."
PACKAGES="git python3-virtualenv python3-devel libvirt-devel glibc gcc nginx supervisor python3-lxml python3-libguestfs iproute-tc cyrus-sasl-md5 python3-libguestfs"
install_packages
set_hosts
install_webvirtcloud
echo "* Configuring Nginx."
configure_nginx
echo "* Configuring Supervisor."
configure_supervisor
set_firewall
set_selinux
restart_supervisor
restart_nginx
else
echo "Unsupported CentOS version. Version found: $version"
exit 1
fi
;;
esac
echo ""
echo " ***Open http://$fqdn to login to webvirtcloud.***"
echo ""
echo ""
echo "* Cleaning up..."
rm -f webvirtcloud.sh
rm -f install.sh
echo "* Finished!"
sleep 1