Commit 8b7ca38
Changed files (146)
cmd
govnc
pkg
input
proxy
transport
web
noVNC
app
images
icons
locale
core
input
vendor
cmd/govnc/main.go
@@ -0,0 +1,275 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "log/slog"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+
+ "github.com/chzyer/readline"
+ "golang.org/x/sync/errgroup"
+
+ "goVNC/internal/cli"
+ "goVNC/pkg/input"
+ "goVNC/pkg/rfb"
+ "goVNC/pkg/transport"
+ "goVNC/pkg/web"
+)
+
+func main() {
+ if err := run(); err != nil {
+ slog.Error("fatal", slog.String("error", err.Error()))
+ os.Exit(1)
+ }
+}
+
+func run() error {
+ // Parse flags
+ cfg, err := cli.ParseFlags(os.Args[1:])
+ if err != nil {
+ return err
+ }
+
+ // Configure logging
+ setupLogging(cfg.Output.Verbose, cfg.Output.Silent)
+
+ // Validate URL
+ if cfg.URL == "" {
+ return fmt.Errorf("no URL provided. Use: govnc <URL> or set VNC_URL")
+ }
+
+ // Create context with signal handling
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // Handle signals
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+ go func() {
+ <-sigCh
+ slog.Info("shutting down...")
+ cancel()
+ }()
+
+ // Connect to VNC server
+ slog.Info("connecting", slog.String("url", cfg.URL))
+
+ dialer := transport.NewWebSocketDialer()
+ dialOpts := cfg.Connection.ToDialOptions()
+
+ t, err := dialer.Dial(ctx, cfg.URL, dialOpts)
+ if err != nil {
+ return fmt.Errorf("connecting: %w", err)
+ }
+ defer t.Close()
+
+ // Create RFB client
+ clientCfg := cfg.VNC.ToClientConfig()
+ client := rfb.NewClient(t, clientCfg)
+
+ // Connect and handshake
+ session, err := client.Connect(ctx)
+ if err != nil {
+ return fmt.Errorf("handshake: %w", err)
+ }
+
+ slog.Info("connected",
+ slog.String("name", session.Name),
+ slog.Int("width", int(session.Width)),
+ slog.Int("height", int(session.Height)))
+
+ // Handle one-shot modes
+ if cfg.Input.TypeText != "" {
+ return handleTypeAndExit(ctx, client, cfg.Input.TypeText)
+ }
+
+ if cfg.Clipboard.ClipSend != "" {
+ return handleClipSendAndExit(ctx, client, cfg.Clipboard.ClipSend)
+ }
+
+ // Server mode
+ if cfg.Server.HTTPAddr != "" {
+ return runServerMode(ctx, client, cfg)
+ }
+
+ // Interactive mode
+ return runInteractiveMode(ctx, client, cfg, session)
+}
+
+func setupLogging(verbose int, silent bool) {
+ var level slog.Level
+ if silent {
+ level = slog.LevelError + 1 // Suppress all logs
+ } else {
+ switch verbose {
+ case 0:
+ level = slog.LevelInfo
+ case 1:
+ level = slog.LevelDebug
+ default:
+ level = slog.LevelDebug - 4 // Trace level
+ }
+ }
+
+ handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
+ Level: level,
+ })
+ slog.SetDefault(slog.New(handler))
+}
+
+func handleTypeAndExit(ctx context.Context, client *rfb.Client, text string) error {
+ slog.Info("typing", slog.Int("len", len(text)))
+ if err := client.Type(ctx, text); err != nil {
+ return fmt.Errorf("typing: %w", err)
+ }
+ slog.Info("done")
+ return nil
+}
+
+func handleClipSendAndExit(ctx context.Context, client *rfb.Client, text string) error {
+ // Handle @file syntax
+ if strings.HasPrefix(text, "@") {
+ data, err := os.ReadFile(text[1:])
+ if err != nil {
+ return fmt.Errorf("reading file: %w", err)
+ }
+ text = string(data)
+ }
+
+ slog.Info("sending clipboard", slog.Int("len", len(text)))
+ if err := client.SetClipboard(ctx, text); err != nil {
+ return fmt.Errorf("sending clipboard: %w", err)
+ }
+ slog.Info("done")
+ return nil
+}
+
+func runServerMode(ctx context.Context, client *rfb.Client, cfg *cli.Config) error {
+ serverCfg := cfg.Server.ToServerConfig()
+ server := web.NewServer(client, serverCfg)
+
+ // Set up clipboard handler if output dir specified
+ if cfg.Clipboard.ClipOutDir != "" {
+ clipMgr, err := input.NewClipboardManager(client, &input.ClipboardConfig{
+ SaveDir: cfg.Clipboard.ClipOutDir,
+ })
+ if err != nil {
+ return fmt.Errorf("creating clipboard manager: %w", err)
+ }
+ client.OnClipboard(clipMgr.OnReceive)
+ }
+
+ g, ctx := errgroup.WithContext(ctx)
+
+ // Run VNC message listener
+ g.Go(func() error {
+ return client.Listen(ctx)
+ })
+
+ // Run web server
+ g.Go(func() error {
+ return server.ListenAndServe(ctx)
+ })
+
+ slog.Info("server mode started",
+ slog.String("http", cfg.Server.HTTPAddr),
+ slog.String("api", cfg.Server.APIPrefix))
+
+ return g.Wait()
+}
+
+func runInteractiveMode(ctx context.Context, client *rfb.Client, cfg *cli.Config, session *rfb.Session) error {
+ g, ctx := errgroup.WithContext(ctx)
+
+ // Set up clipboard manager
+ clipMgr, err := input.NewClipboardManager(client, &input.ClipboardConfig{
+ WatchDir: cfg.Clipboard.ClipInDir,
+ SaveDir: cfg.Clipboard.ClipOutDir,
+ })
+ if err != nil {
+ return fmt.Errorf("creating clipboard manager: %w", err)
+ }
+ client.OnClipboard(clipMgr.OnReceive)
+
+ // Run VNC message listener
+ g.Go(func() error {
+ return client.Listen(ctx)
+ })
+
+ // Run clipboard watcher if configured
+ if cfg.Clipboard.ClipInDir != "" {
+ g.Go(func() error {
+ return clipMgr.Start(ctx)
+ })
+ }
+
+ // Run stdin reader
+ if cfg.Input.InputFile == "" || cfg.Input.InputFile == "-" {
+ g.Go(func() error {
+ return readStdin(ctx, client, session.Name)
+ })
+ } else if cfg.Input.InputFile != "" {
+ g.Go(func() error {
+ return readInputFile(ctx, client, cfg.Input.InputFile)
+ })
+ }
+
+ return g.Wait()
+}
+
+func readStdin(ctx context.Context, client *rfb.Client, serverName string) error {
+ prompt := serverName + "> "
+ rl, err := readline.NewEx(&readline.Config{
+ Prompt: prompt,
+ HistoryFile: ".govnc_history",
+ InterruptPrompt: "^C",
+ EOFPrompt: "exit",
+ })
+ if err != nil {
+ return fmt.Errorf("creating readline: %w", err)
+ }
+ defer rl.Close()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ }
+
+ line, err := rl.Readline()
+ if err == readline.ErrInterrupt {
+ continue
+ }
+ if err == io.EOF {
+ return nil
+ }
+ if err != nil {
+ return fmt.Errorf("reading line: %w", err)
+ }
+
+ line = line + "\n"
+ slog.Debug("typing", slog.String("line", strings.TrimSpace(line)))
+ if err := client.Type(ctx, line); err != nil {
+ return fmt.Errorf("typing line: %w", err)
+ }
+ }
+}
+
+func readInputFile(ctx context.Context, client *rfb.Client, path string) error {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("reading input file: %w", err)
+ }
+
+ slog.Info("typing from file", slog.String("file", path), slog.Int("len", len(data)))
+ if err := client.Type(ctx, string(data)); err != nil {
+ return fmt.Errorf("typing: %w", err)
+ }
+
+ return nil
+}
internal/cli/config.go
@@ -0,0 +1,239 @@
+// Package cli provides CLI infrastructure for govnc.
+package cli
+
+import (
+ "net/http"
+ "time"
+
+ "goVNC/pkg/proxy"
+ "goVNC/pkg/rfb"
+ "goVNC/pkg/transport"
+ "goVNC/pkg/web"
+)
+
+// Config holds all CLI configuration.
+type Config struct {
+ // Target WebSocket URL
+ URL string
+
+ // Connection options
+ Connection ConnectionConfig
+
+ // VNC session options
+ VNC VNCConfig
+
+ // Input options
+ Input InputConfig
+
+ // Clipboard options
+ Clipboard ClipboardConfig
+
+ // Server mode options
+ Server ServerConfig
+
+ // Output options
+ Output OutputConfig
+}
+
+// ConnectionConfig holds connection-related settings.
+type ConnectionConfig struct {
+ // Headers to send with the connection (repeatable).
+ Headers http.Header
+
+ // Cookie string to send.
+ Cookie string
+
+ // UserAgent to send.
+ UserAgent string
+
+ // Proxy URL for upstream proxy.
+ Proxy string
+
+ // ConnectTimeout for connection establishment.
+ ConnectTimeout time.Duration
+
+ // Insecure skips TLS verification.
+ Insecure bool
+}
+
+// VNCConfig holds VNC-specific settings.
+type VNCConfig struct {
+ // User for VNC authentication (user:password format).
+ User string
+
+ // Password for VNC authentication.
+ Password string
+
+ // AuthNone forces no authentication.
+ AuthNone bool
+
+ // Shared requests a shared session.
+ Shared bool
+
+ // Exclusive requests an exclusive session.
+ Exclusive bool
+
+ // Encodings to request.
+ Encodings []int32
+}
+
+// InputConfig holds input-related settings.
+type InputConfig struct {
+ // InputFile to read input from (- for stdin).
+ InputFile string
+
+ // TypeText to type and exit.
+ TypeText string
+
+ // KeyDelay between keystrokes in milliseconds.
+ KeyDelay int
+}
+
+// ClipboardConfig holds clipboard-related settings.
+type ClipboardConfig struct {
+ // ClipInDir to watch for outgoing clipboard files.
+ ClipInDir string
+
+ // ClipOutDir to save incoming clipboard files.
+ ClipOutDir string
+
+ // ClipSend text to send to clipboard and exit.
+ ClipSend string
+}
+
+// ServerConfig holds server mode settings.
+type ServerConfig struct {
+ // HTTPAddr to start HTTP/WS server on.
+ HTTPAddr string
+
+ // ProxyMode for client sessions.
+ ProxyMode string
+
+ // MaxClients for concurrent WebSocket clients.
+ MaxClients int
+
+ // NoVNCPath for static noVNC files.
+ NoVNCPath string
+
+ // APIPrefix for API routes.
+ APIPrefix string
+
+ // CORSOrigin for CORS headers.
+ CORSOrigin string
+}
+
+// OutputConfig holds output-related settings.
+type OutputConfig struct {
+ // Verbose level (0-3).
+ Verbose int
+
+ // Silent suppresses output.
+ Silent bool
+
+ // OutputFile to write to.
+ OutputFile string
+}
+
+// DefaultConfig returns a Config with sensible defaults.
+func DefaultConfig() *Config {
+ return &Config{
+ Connection: ConnectionConfig{
+ Headers: make(http.Header),
+ UserAgent: "govnc/1.0",
+ ConnectTimeout: 30 * time.Second,
+ },
+ VNC: VNCConfig{
+ Shared: true,
+ Encodings: rfb.DefaultEncodings(),
+ },
+ Server: ServerConfig{
+ HTTPAddr: ":8080",
+ ProxyMode: "shared",
+ MaxClients: 10,
+ APIPrefix: "/api",
+ CORSOrigin: "*",
+ },
+ }
+}
+
+// ToDialOptions converts ConnectionConfig to transport.DialOptions.
+func (c *ConnectionConfig) ToDialOptions() *transport.DialOptions {
+ opts := transport.NewDialOptions()
+ opts.Headers = c.Headers.Clone()
+ if c.Cookie != "" {
+ opts.AddHeader("Cookie", c.Cookie)
+ }
+ if c.UserAgent != "" {
+ opts.AddHeader("User-Agent", c.UserAgent)
+ }
+ opts.Timeout = c.ConnectTimeout
+ opts.Proxy = c.Proxy
+ // TODO: Handle Insecure flag with TLS config
+ return opts
+}
+
+// ToHandshakeConfig converts VNCConfig to rfb.HandshakeConfig.
+func (c *VNCConfig) ToHandshakeConfig() *rfb.HandshakeConfig {
+ cfg := rfb.DefaultHandshakeConfig()
+
+ // Determine shared mode
+ if c.Exclusive {
+ cfg.SharedSession = false
+ } else if c.Shared {
+ cfg.SharedSession = true
+ }
+
+ // Set password if provided
+ if c.Password != "" {
+ cfg.Password = c.Password
+ }
+
+ // Add VNC auth if password is provided
+ if c.Password != "" && !c.AuthNone {
+ cfg.AllowedSecurityTypes = []rfb.SecurityType{
+ rfb.SecurityTypeNone,
+ rfb.SecurityTypeVNCAuth,
+ }
+ }
+
+ return cfg
+}
+
+// ToClientConfig converts VNCConfig to rfb.ClientConfig.
+func (c *VNCConfig) ToClientConfig() *rfb.ClientConfig {
+ cfg := rfb.DefaultClientConfig()
+ cfg.Handshake = c.ToHandshakeConfig()
+ if len(c.Encodings) > 0 {
+ cfg.Encodings = c.Encodings
+ }
+ return cfg
+}
+
+// ToServerConfig converts ServerConfig to web.ServerConfig.
+func (c *ServerConfig) ToServerConfig() *web.ServerConfig {
+ cfg := web.DefaultServerConfig()
+ if c.HTTPAddr != "" {
+ cfg.ListenAddr = c.HTTPAddr
+ }
+ if c.APIPrefix != "" {
+ cfg.APIPrefix = c.APIPrefix
+ }
+ if c.NoVNCPath != "" {
+ cfg.NoVNCPath = c.NoVNCPath
+ }
+ if c.CORSOrigin != "" {
+ cfg.CORSOrigin = c.CORSOrigin
+ }
+ if c.MaxClients > 0 {
+ cfg.MaxClients = c.MaxClients
+ }
+
+ switch c.ProxyMode {
+ case "isolated":
+ cfg.ProxyMode = proxy.IsolatedMode
+ default:
+ cfg.ProxyMode = proxy.SharedMode
+ }
+
+ return cfg
+}
internal/cli/flags.go
@@ -0,0 +1,258 @@
+package cli
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "time"
+)
+
+// headerList is a flag.Value that collects multiple -H flags.
+type headerList []string
+
+func (h *headerList) String() string {
+ return strings.Join(*h, ", ")
+}
+
+func (h *headerList) Set(value string) error {
+ *h = append(*h, value)
+ return nil
+}
+
+// ParseFlags parses command-line flags and returns a Config.
+func ParseFlags(args []string) (*Config, error) {
+ cfg := DefaultConfig()
+
+ fs := flag.NewFlagSet("govnc", flag.ContinueOnError)
+
+ // Custom usage
+ fs.Usage = func() {
+ fmt.Fprint(os.Stderr, usage)
+ }
+
+ // Connection flags
+ var headers headerList
+ fs.Var(&headers, "H", "Add custom header (repeatable)")
+ fs.Var(&headers, "header", "Add custom header (repeatable)")
+ fs.StringVar(&cfg.Connection.Cookie, "b", "", "Send cookies")
+ fs.StringVar(&cfg.Connection.Cookie, "cookie", "", "Send cookies")
+ fs.StringVar(&cfg.Connection.UserAgent, "A", "govnc/1.0", "User-Agent header")
+ fs.StringVar(&cfg.Connection.UserAgent, "user-agent", "govnc/1.0", "User-Agent header")
+ fs.StringVar(&cfg.Connection.Proxy, "x", "", "Use proxy (http, socks5)")
+ fs.StringVar(&cfg.Connection.Proxy, "proxy", "", "Use proxy (http, socks5)")
+
+ var connectTimeout int
+ fs.IntVar(&connectTimeout, "connect-timeout", 30, "Connection timeout in seconds")
+ fs.BoolVar(&cfg.Connection.Insecure, "k", false, "Skip TLS verification")
+ fs.BoolVar(&cfg.Connection.Insecure, "insecure", false, "Skip TLS verification")
+
+ // Authentication flags
+ fs.StringVar(&cfg.VNC.User, "u", "", "VNC authentication (user:password)")
+ fs.StringVar(&cfg.VNC.User, "user", "", "VNC authentication (user:password)")
+ fs.BoolVar(&cfg.VNC.AuthNone, "auth-none", false, "Force no authentication")
+
+ // VNC flags
+ fs.BoolVar(&cfg.VNC.Shared, "shared", true, "Request shared session")
+ fs.BoolVar(&cfg.VNC.Exclusive, "exclusive", false, "Request exclusive session")
+
+ var encodings string
+ fs.StringVar(&encodings, "encoding", "", "Comma-separated encodings")
+
+ // Input flags
+ fs.StringVar(&cfg.Input.InputFile, "i", "", "Read input from file (- for stdin)")
+ fs.StringVar(&cfg.Input.InputFile, "input", "", "Read input from file (- for stdin)")
+ fs.StringVar(&cfg.Input.TypeText, "type", "", "Type string and exit")
+ fs.IntVar(&cfg.Input.KeyDelay, "delay", 0, "Delay between keystrokes in ms")
+
+ // Clipboard flags
+ fs.StringVar(&cfg.Clipboard.ClipInDir, "clip-in", "", "Watch directory for outgoing clipboard")
+ fs.StringVar(&cfg.Clipboard.ClipOutDir, "clip-out", "", "Save incoming clipboard to directory")
+ fs.StringVar(&cfg.Clipboard.ClipSend, "clip-send", "", "Send clipboard text and exit")
+
+ // Server flags
+ fs.StringVar(&cfg.Server.HTTPAddr, "http", "", "Start HTTP/WS server on address")
+ fs.StringVar(&cfg.Server.ProxyMode, "proxy-mode", "shared", "Client session mode (shared|isolated)")
+ fs.IntVar(&cfg.Server.MaxClients, "max-clients", 10, "Max concurrent WebSocket clients")
+ fs.StringVar(&cfg.Server.NoVNCPath, "novnc-path", "", "Path to noVNC static files")
+ fs.StringVar(&cfg.Server.APIPrefix, "api-prefix", "/api", "API route prefix")
+ fs.StringVar(&cfg.Server.CORSOrigin, "cors-origin", "*", "CORS allowed origin")
+
+ // Output flags
+ fs.IntVar(&cfg.Output.Verbose, "v", 0, "Verbose output (use multiple times for more)")
+ fs.BoolVar(&cfg.Output.Silent, "s", false, "Silent mode")
+ fs.BoolVar(&cfg.Output.Silent, "silent", false, "Silent mode")
+ fs.StringVar(&cfg.Output.OutputFile, "o", "", "Write output to file")
+ fs.StringVar(&cfg.Output.OutputFile, "output", "", "Write output to file")
+
+ // Help flags
+ var showHelp bool
+ fs.BoolVar(&showHelp, "h", false, "Show help")
+ fs.BoolVar(&showHelp, "help", false, "Show help")
+
+ var showVersion bool
+ fs.BoolVar(&showVersion, "version", false, "Show version")
+
+ // Parse flags
+ if err := fs.Parse(args); err != nil {
+ return nil, err
+ }
+
+ if showHelp {
+ fs.Usage()
+ os.Exit(0)
+ }
+
+ if showVersion {
+ fmt.Println("govnc version 1.0.0")
+ os.Exit(0)
+ }
+
+ // Process headers
+ for _, h := range headers {
+ parts := strings.SplitN(h, ":", 2)
+ if len(parts) == 2 {
+ cfg.Connection.Headers.Add(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
+ }
+ }
+
+ // Process timeout (connectTimeout is in seconds, convert to time.Duration)
+ if connectTimeout > 0 {
+ cfg.Connection.ConnectTimeout = time.Duration(connectTimeout) * time.Second
+ }
+
+ // Process user:password
+ if cfg.VNC.User != "" {
+ parts := strings.SplitN(cfg.VNC.User, ":", 2)
+ if len(parts) == 2 {
+ cfg.VNC.Password = parts[1]
+ }
+ cfg.VNC.User = parts[0]
+ }
+
+ // Process encodings
+ if encodings != "" {
+ cfg.VNC.Encodings = parseEncodings(encodings)
+ }
+
+ // Get URL from remaining args
+ remaining := fs.Args()
+ if len(remaining) > 0 {
+ cfg.URL = remaining[0]
+ }
+
+ // Check for URL in environment
+ if cfg.URL == "" {
+ cfg.URL = os.Getenv("VNC_URL")
+ }
+
+ // Check for cookies in environment
+ if cfg.Connection.Cookie == "" {
+ cfg.Connection.Cookie = os.Getenv("VNC_COOKIES")
+ }
+
+ // Check for password in environment
+ if cfg.VNC.Password == "" {
+ cfg.VNC.Password = os.Getenv("VNC_PASSWORD")
+ }
+
+ return cfg, nil
+}
+
+// parseEncodings parses a comma-separated list of encoding names.
+func parseEncodings(s string) []int32 {
+ names := strings.Split(s, ",")
+ var encodings []int32
+ for _, name := range names {
+ name = strings.TrimSpace(strings.ToLower(name))
+ switch name {
+ case "raw":
+ encodings = append(encodings, 0)
+ case "copyrect":
+ encodings = append(encodings, 1)
+ case "rre":
+ encodings = append(encodings, 2)
+ case "hextile":
+ encodings = append(encodings, 5)
+ case "tight":
+ encodings = append(encodings, 7)
+ case "zrle":
+ encodings = append(encodings, 16)
+ }
+ }
+ return encodings
+}
+
+const usage = `govnc - VNC over WebSocket client and proxy
+
+USAGE:
+ govnc [OPTIONS] <URL>
+
+CONNECTION OPTIONS:
+ -H, --header <Header: Value> Add custom header (repeatable)
+ -b, --cookie <data|@file> Send cookies
+ -A, --user-agent <string> User-Agent header (default: govnc/1.0)
+ -x, --proxy <host:port> Use proxy (http, socks5)
+ --connect-timeout <seconds> Connection timeout (default: 30)
+ -k, --insecure Skip TLS verification
+
+AUTHENTICATION:
+ -u, --user <user:password> VNC authentication
+ --auth-none Force no authentication
+
+VNC OPTIONS:
+ --shared Request shared session (default: true)
+ --exclusive Request exclusive session
+ --encoding <list> Comma-separated encodings (tight,zrle,raw,...)
+
+INPUT OPTIONS:
+ -i, --input <file|-> Read input from file (- for stdin)
+ --type <string> Type string and exit
+ --delay <ms> Delay between keystrokes
+
+CLIPBOARD OPTIONS:
+ --clip-in <dir> Watch directory for outgoing clipboard
+ --clip-out <dir> Save incoming clipboard to directory
+ --clip-send <text|@file> Send clipboard text and exit
+
+SERVER MODE:
+ --http <addr:port> Start HTTP/WS server
+ --proxy-mode <shared|isolated> Client session mode (default: shared)
+ --max-clients <n> Max concurrent WebSocket clients
+ --novnc-path <path> Path to noVNC static files
+ --api-prefix <prefix> API route prefix (default: /api)
+ --cors-origin <origin> CORS allowed origin (default: *)
+
+OUTPUT OPTIONS:
+ -v Verbose output (use multiple times)
+ -s, --silent Silent mode
+ -o, --output <file> Write output to file
+
+OTHER:
+ -h, --help Show help
+ --version Show version
+
+ENVIRONMENT VARIABLES:
+ VNC_URL Default WebSocket URL
+ VNC_COOKIES Default cookies
+ VNC_PASSWORD VNC password
+
+EXAMPLES:
+ # Basic interactive connection
+ govnc wss://vnc.example.com/websockify
+
+ # With auth headers
+ govnc -b "session=abc" -H "X-Auth: token" wss://vnc.example.com/ws
+
+ # Type command and exit
+ govnc --type "ls -la\n" wss://vnc.example.com/ws
+
+ # Start proxy server with noVNC web UI
+ govnc --http :8080 wss://vnc.example.com/ws
+ # Then: http://localhost:8080/noVNC/vnc.html
+
+ # Use REST API
+ curl -X PUT localhost:8080/api/keys -d '{"text":"hello\n"}'
+ curl -X PUT localhost:8080/api/clipboard -d '{"text":"copied"}'
+ curl localhost:8080/api/session
+`
pkg/input/clipboard.go
@@ -0,0 +1,212 @@
+package input
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+// ClipboardSender can send clipboard data to a VNC server.
+type ClipboardSender interface {
+ SetClipboard(ctx context.Context, text string) error
+}
+
+// ClipboardHandler is called when clipboard data is received from the server.
+type ClipboardHandler func(text string) error
+
+// ClipboardManager handles clipboard synchronization between local files and VNC.
+type ClipboardManager struct {
+ sender ClipboardSender
+ handler ClipboardHandler
+
+ watchDir string
+ sentDir string
+ saveDir string
+
+ mu sync.RWMutex
+ lastReceived string
+ lastRecvTime time.Time
+ watcher *fsnotify.Watcher
+}
+
+// ClipboardConfig configures the clipboard manager.
+type ClipboardConfig struct {
+ // WatchDir is the directory to watch for outgoing clipboard files.
+ // Files placed here will be sent to the VNC server.
+ WatchDir string
+
+ // SaveDir is the directory to save incoming clipboard data.
+ // If empty, incoming clipboard is not saved to files.
+ SaveDir string
+
+ // Handler is called when clipboard data is received from the server.
+ Handler ClipboardHandler
+}
+
+// NewClipboardManager creates a new clipboard manager.
+func NewClipboardManager(sender ClipboardSender, cfg *ClipboardConfig) (*ClipboardManager, error) {
+ cm := &ClipboardManager{
+ sender: sender,
+ handler: cfg.Handler,
+ }
+
+ if cfg.WatchDir != "" {
+ cm.watchDir = cfg.WatchDir
+ cm.sentDir = filepath.Join(cfg.WatchDir, "sent")
+
+ // Create directories
+ if err := os.MkdirAll(cm.watchDir, 0755); err != nil {
+ return nil, fmt.Errorf("creating watch dir: %w", err)
+ }
+ if err := os.MkdirAll(cm.sentDir, 0755); err != nil {
+ return nil, fmt.Errorf("creating sent dir: %w", err)
+ }
+ }
+
+ if cfg.SaveDir != "" {
+ cm.saveDir = cfg.SaveDir
+ if err := os.MkdirAll(cm.saveDir, 0755); err != nil {
+ return nil, fmt.Errorf("creating save dir: %w", err)
+ }
+ }
+
+ return cm, nil
+}
+
+// Start begins watching for clipboard files and processing them.
+func (cm *ClipboardManager) Start(ctx context.Context) error {
+ if cm.watchDir == "" {
+ // No watch directory configured, just wait for context
+ <-ctx.Done()
+ return ctx.Err()
+ }
+
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ return fmt.Errorf("creating watcher: %w", err)
+ }
+ cm.watcher = watcher
+ defer watcher.Close()
+
+ if err := watcher.Add(cm.watchDir); err != nil {
+ return fmt.Errorf("watching directory: %w", err)
+ }
+
+ slog.Info("watching clipboard directory", slog.String("dir", cm.watchDir))
+
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+
+ case event, ok := <-watcher.Events:
+ if !ok {
+ return nil
+ }
+
+ // Only process Create and Write events
+ if !event.Has(fsnotify.Create) && !event.Has(fsnotify.Write) {
+ continue
+ }
+
+ // Skip the sent/ subdirectory
+ if filepath.Dir(event.Name) != cm.watchDir {
+ continue
+ }
+
+ // Small delay to ensure file is fully written
+ time.Sleep(10 * time.Millisecond)
+
+ cm.processFile(ctx, event.Name)
+
+ case err, ok := <-watcher.Errors:
+ if !ok {
+ return nil
+ }
+ slog.Warn("watcher error", slog.String("error", err.Error()))
+ }
+ }
+}
+
+// processFile reads a file and sends its contents as clipboard data.
+func (cm *ClipboardManager) processFile(ctx context.Context, path string) {
+ info, err := os.Stat(path)
+ if err != nil || info.IsDir() {
+ return
+ }
+
+ data, err := os.ReadFile(path)
+ if err != nil {
+ slog.Warn("reading clipboard file",
+ slog.String("file", path),
+ slog.String("error", err.Error()))
+ return
+ }
+
+ slog.Info("sending clipboard from file",
+ slog.String("file", filepath.Base(path)),
+ slog.Int("len", len(data)))
+
+ if err := cm.sender.SetClipboard(ctx, string(data)); err != nil {
+ slog.Warn("sending clipboard", slog.String("error", err.Error()))
+ return
+ }
+
+ // Move to sent/ after successful send
+ sentPath := filepath.Join(cm.sentDir, filepath.Base(path))
+ if err := os.Rename(path, sentPath); err != nil {
+ slog.Warn("moving sent file", slog.String("error", err.Error()))
+ }
+}
+
+// Send sends text to the VNC clipboard.
+func (cm *ClipboardManager) Send(ctx context.Context, text string) error {
+ return cm.sender.SetClipboard(ctx, text)
+}
+
+// OnReceive handles incoming clipboard data from the server.
+// This is typically called by the RFB client's clipboard handler.
+func (cm *ClipboardManager) OnReceive(text string) {
+ cm.mu.Lock()
+ cm.lastReceived = text
+ cm.lastRecvTime = time.Now()
+ cm.mu.Unlock()
+
+ // Save to file if configured
+ if cm.saveDir != "" {
+ filename := filepath.Join(cm.saveDir, fmt.Sprintf("%x.clip", time.Now().Unix()))
+ if err := os.WriteFile(filename, []byte(text), 0644); err != nil {
+ slog.Warn("saving clipboard", slog.String("error", err.Error()))
+ } else {
+ slog.Debug("saved clipboard", slog.String("file", filename))
+ }
+ }
+
+ // Call handler if configured
+ if cm.handler != nil {
+ if err := cm.handler(text); err != nil {
+ slog.Warn("clipboard handler error", slog.String("error", err.Error()))
+ }
+ }
+}
+
+// GetLastReceived returns the last received clipboard data and timestamp.
+func (cm *ClipboardManager) GetLastReceived() (string, time.Time) {
+ cm.mu.RLock()
+ defer cm.mu.RUnlock()
+ return cm.lastReceived, cm.lastRecvTime
+}
+
+// Close stops watching and cleans up resources.
+func (cm *ClipboardManager) Close() error {
+ if cm.watcher != nil {
+ return cm.watcher.Close()
+ }
+ return nil
+}
pkg/input/keyboard.go
@@ -0,0 +1,205 @@
+// Package input provides input handling utilities for VNC clients.
+package input
+
+import (
+ "context"
+ "io"
+ "time"
+
+ "goVNC/pkg/rfb"
+)
+
+// KeySender can send key events to a VNC server.
+type KeySender interface {
+ KeyEvent(ctx context.Context, key uint32, down bool) error
+}
+
+// Keyboard handles keyboard input processing.
+type Keyboard struct {
+ sender KeySender
+ delay time.Duration
+}
+
+// KeyboardOption configures a Keyboard.
+type KeyboardOption func(*Keyboard)
+
+// WithKeyDelay sets a delay between keystrokes.
+func WithKeyDelay(d time.Duration) KeyboardOption {
+ return func(k *Keyboard) {
+ k.delay = d
+ }
+}
+
+// NewKeyboard creates a new keyboard handler.
+func NewKeyboard(sender KeySender, opts ...KeyboardOption) *Keyboard {
+ k := &Keyboard{
+ sender: sender,
+ }
+ for _, opt := range opts {
+ opt(k)
+ }
+ return k
+}
+
+// Press sends a key down event.
+func (k *Keyboard) Press(ctx context.Context, key uint32) error {
+ return k.sender.KeyEvent(ctx, key, true)
+}
+
+// Release sends a key up event.
+func (k *Keyboard) Release(ctx context.Context, key uint32) error {
+ return k.sender.KeyEvent(ctx, key, false)
+}
+
+// Tap presses and releases a key.
+func (k *Keyboard) Tap(ctx context.Context, key uint32) error {
+ if err := k.Press(ctx, key); err != nil {
+ return err
+ }
+ return k.Release(ctx, key)
+}
+
+// Type types a string, converting each character to key events.
+func (k *Keyboard) Type(ctx context.Context, text string) error {
+ for _, r := range text {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ }
+
+ key := rfb.RuneToKeysym(r)
+ if err := k.Tap(ctx, key); err != nil {
+ return err
+ }
+
+ if k.delay > 0 {
+ time.Sleep(k.delay)
+ }
+ }
+ return nil
+}
+
+// Combo executes a key combination (e.g., Ctrl+C).
+// All modifier keys are pressed, then the final key is tapped,
+// then all modifiers are released in reverse order.
+func (k *Keyboard) Combo(ctx context.Context, keys ...uint32) error {
+ if len(keys) == 0 {
+ return nil
+ }
+
+ // Press all but last key
+ modifiers := keys[:len(keys)-1]
+ finalKey := keys[len(keys)-1]
+
+ // Press modifiers
+ for _, mod := range modifiers {
+ if err := k.Press(ctx, mod); err != nil {
+ return err
+ }
+ }
+
+ // Tap final key
+ if err := k.Tap(ctx, finalKey); err != nil {
+ return err
+ }
+
+ // Release modifiers in reverse order
+ for i := len(modifiers) - 1; i >= 0; i-- {
+ if err := k.Release(ctx, modifiers[i]); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Writer returns an io.Writer that types input to the VNC session.
+func (k *Keyboard) Writer(ctx context.Context) io.Writer {
+ return &keyboardWriter{
+ ctx: ctx,
+ keyboard: k,
+ }
+}
+
+type keyboardWriter struct {
+ ctx context.Context
+ keyboard *Keyboard
+}
+
+func (w *keyboardWriter) Write(p []byte) (int, error) {
+ err := w.keyboard.Type(w.ctx, string(p))
+ if err != nil {
+ return 0, err
+ }
+ return len(p), nil
+}
+
+// ParseKeyName converts a key name to a keysym.
+// Supports names like "ctrl", "shift", "alt", "return", "tab", etc.
+func ParseKeyName(name string) (uint32, bool) {
+ switch name {
+ case "ctrl", "control":
+ return rfb.KeyControlL, true
+ case "shift":
+ return rfb.KeyShiftL, true
+ case "alt":
+ return rfb.KeyAltL, true
+ case "meta", "super", "win", "cmd":
+ return rfb.KeySuperL, true
+ case "return", "enter":
+ return rfb.KeyReturn, true
+ case "tab":
+ return rfb.KeyTab, true
+ case "escape", "esc":
+ return rfb.KeyEscape, true
+ case "backspace":
+ return rfb.KeyBackspace, true
+ case "delete", "del":
+ return rfb.KeyDelete, true
+ case "insert", "ins":
+ return rfb.KeyInsert, true
+ case "home":
+ return rfb.KeyHome, true
+ case "end":
+ return rfb.KeyEnd, true
+ case "pageup", "pgup":
+ return rfb.KeyPageUp, true
+ case "pagedown", "pgdn":
+ return rfb.KeyPageDown, true
+ case "left":
+ return rfb.KeyLeft, true
+ case "right":
+ return rfb.KeyRight, true
+ case "up":
+ return rfb.KeyUp, true
+ case "down":
+ return rfb.KeyDown, true
+ case "f1":
+ return rfb.KeyF1, true
+ case "f2":
+ return rfb.KeyF2, true
+ case "f3":
+ return rfb.KeyF3, true
+ case "f4":
+ return rfb.KeyF4, true
+ case "f5":
+ return rfb.KeyF5, true
+ case "f6":
+ return rfb.KeyF6, true
+ case "f7":
+ return rfb.KeyF7, true
+ case "f8":
+ return rfb.KeyF8, true
+ case "f9":
+ return rfb.KeyF9, true
+ case "f10":
+ return rfb.KeyF10, true
+ case "f11":
+ return rfb.KeyF11, true
+ case "f12":
+ return rfb.KeyF12, true
+ default:
+ return 0, false
+ }
+}
pkg/proxy/mux.go
@@ -0,0 +1,228 @@
+// Package proxy provides VNC proxy functionality for multiplexing clients.
+package proxy
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "sync"
+
+ "goVNC/pkg/rfb"
+ "goVNC/pkg/transport"
+)
+
+// SessionMode determines how clients interact with the VNC session.
+type SessionMode int
+
+const (
+ // SharedMode - all clients share the same VNC session view and can send input.
+ SharedMode SessionMode = iota
+
+ // IsolatedMode - input is queued and processed sequentially.
+ IsolatedMode
+)
+
+// Multiplexer manages multiple WebSocket clients connected to a single VNC session.
+type Multiplexer struct {
+ vnc *rfb.Client
+ mode SessionMode
+
+ clients map[string]*ClientConn
+ clientMu sync.RWMutex
+
+ // For broadcasting server messages to all clients
+ broadcast chan []byte
+
+ closed bool
+ closeMu sync.RWMutex
+}
+
+// ClientConn represents a connected client.
+type ClientConn struct {
+ ID string
+ Transport transport.Transport
+ send chan []byte
+ done chan struct{}
+}
+
+// NewMultiplexer creates a new multiplexer for the given VNC client.
+func NewMultiplexer(vnc *rfb.Client, mode SessionMode) *Multiplexer {
+ return &Multiplexer{
+ vnc: vnc,
+ mode: mode,
+ clients: make(map[string]*ClientConn),
+ broadcast: make(chan []byte, 100),
+ }
+}
+
+// AddClient registers a new client connection.
+func (m *Multiplexer) AddClient(id string, t transport.Transport) *ClientConn {
+ client := &ClientConn{
+ ID: id,
+ Transport: t,
+ send: make(chan []byte, 100),
+ done: make(chan struct{}),
+ }
+
+ m.clientMu.Lock()
+ m.clients[id] = client
+ m.clientMu.Unlock()
+
+ slog.Info("client connected", slog.String("id", id))
+ return client
+}
+
+// RemoveClient unregisters a client.
+func (m *Multiplexer) RemoveClient(id string) {
+ m.clientMu.Lock()
+ client, ok := m.clients[id]
+ if ok {
+ delete(m.clients, id)
+ close(client.done)
+ }
+ m.clientMu.Unlock()
+
+ if ok {
+ slog.Info("client disconnected", slog.String("id", id))
+ }
+}
+
+// GetClient returns a client by ID.
+func (m *Multiplexer) GetClient(id string) (*ClientConn, bool) {
+ m.clientMu.RLock()
+ defer m.clientMu.RUnlock()
+ client, ok := m.clients[id]
+ return client, ok
+}
+
+// ClientCount returns the number of connected clients.
+func (m *Multiplexer) ClientCount() int {
+ m.clientMu.RLock()
+ defer m.clientMu.RUnlock()
+ return len(m.clients)
+}
+
+// Broadcast sends a message to all connected clients.
+func (m *Multiplexer) Broadcast(data []byte) {
+ m.clientMu.RLock()
+ defer m.clientMu.RUnlock()
+
+ for _, client := range m.clients {
+ select {
+ case client.send <- data:
+ default:
+ slog.Warn("client send buffer full", slog.String("id", client.ID))
+ }
+ }
+}
+
+// HandleClientInput processes input from a client and forwards to VNC.
+func (m *Multiplexer) HandleClientInput(ctx context.Context, clientID string, data []byte) error {
+ if len(data) == 0 {
+ return nil
+ }
+
+ // Parse the message type
+ msgType := data[0]
+
+ switch msgType {
+ case rfb.MsgTypeKeyEvent:
+ // Forward key events to VNC
+ return m.vnc.Transport().Write(ctx, data)
+
+ case rfb.MsgTypePointerEvent:
+ // Forward pointer events to VNC
+ return m.vnc.Transport().Write(ctx, data)
+
+ case rfb.MsgTypeClientCutText:
+ // Forward clipboard to VNC
+ return m.vnc.Transport().Write(ctx, data)
+
+ case rfb.MsgTypeFramebufferUpdateRequest:
+ // Don't forward FBU requests - the main client handles these
+ return nil
+
+ case rfb.MsgTypeSetEncodings:
+ // Don't forward encoding changes
+ return nil
+
+ case rfb.MsgTypeSetPixelFormat:
+ // Don't forward pixel format changes
+ return nil
+
+ default:
+ slog.Debug("unknown client message type",
+ slog.String("client", clientID),
+ slog.Int("type", int(msgType)))
+ }
+
+ return nil
+}
+
+// RunClientWriter writes messages from the send channel to a client.
+func (m *Multiplexer) RunClientWriter(ctx context.Context, client *ClientConn) error {
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-client.done:
+ return nil
+ case data := <-client.send:
+ if err := client.Transport.Write(ctx, data); err != nil {
+ return fmt.Errorf("writing to client: %w", err)
+ }
+ }
+ }
+}
+
+// RunClientReader reads messages from a client and processes them.
+func (m *Multiplexer) RunClientReader(ctx context.Context, client *ClientConn) error {
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-client.done:
+ return nil
+ default:
+ }
+
+ data, err := client.Transport.Read(ctx)
+ if err != nil {
+ return fmt.Errorf("reading from client: %w", err)
+ }
+
+ if err := m.HandleClientInput(ctx, client.ID, data); err != nil {
+ slog.Warn("handling client input",
+ slog.String("client", client.ID),
+ slog.String("error", err.Error()))
+ }
+ }
+}
+
+// Close shuts down the multiplexer.
+func (m *Multiplexer) Close() error {
+ m.closeMu.Lock()
+ defer m.closeMu.Unlock()
+
+ if m.closed {
+ return nil
+ }
+ m.closed = true
+
+ // Close all client connections
+ m.clientMu.Lock()
+ for id, client := range m.clients {
+ close(client.done)
+ client.Transport.Close()
+ delete(m.clients, id)
+ }
+ m.clientMu.Unlock()
+
+ close(m.broadcast)
+ return nil
+}
+
+// VNCClient returns the underlying VNC client.
+func (m *Multiplexer) VNCClient() *rfb.Client {
+ return m.vnc
+}
pkg/rfb/client.go
@@ -0,0 +1,278 @@
+package rfb
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "sync"
+ "time"
+
+ "goVNC/pkg/transport"
+)
+
+// MessageHandler is called when a server message is received.
+type MessageHandler func(msg *ServerMessage)
+
+// ClipboardHandler is called when clipboard data is received from the server.
+type ClipboardHandler func(text string)
+
+// Client is an RFB protocol client.
+type Client struct {
+ transport transport.Transport
+ session *Session
+ config *ClientConfig
+
+ // Message handlers
+ onMessage MessageHandler
+ onClipboard ClipboardHandler
+
+ // Synchronization
+ mu sync.RWMutex
+ closed bool
+
+ // Last received clipboard (for API access)
+ lastClipboard string
+ lastClipboardTime time.Time
+ clipMu sync.RWMutex
+}
+
+// ClientConfig configures the RFB client.
+type ClientConfig struct {
+ // Handshake configuration
+ Handshake *HandshakeConfig
+
+ // Encodings to request from the server.
+ Encodings []int32
+
+ // ReadLimit is the maximum message size (default 64MB).
+ ReadLimit int64
+
+ // KeyTimeout is the timeout for key events (default 1s).
+ KeyTimeout time.Duration
+}
+
+// DefaultClientConfig returns default client configuration.
+func DefaultClientConfig() *ClientConfig {
+ return &ClientConfig{
+ Handshake: DefaultHandshakeConfig(),
+ Encodings: DefaultEncodings(),
+ ReadLimit: 64 * 1024 * 1024, // 64MB
+ KeyTimeout: 1 * time.Second,
+ }
+}
+
+// NewClient creates a new RFB client with the given transport.
+func NewClient(t transport.Transport, cfg *ClientConfig) *Client {
+ if cfg == nil {
+ cfg = DefaultClientConfig()
+ }
+ return &Client{
+ transport: t,
+ config: cfg,
+ }
+}
+
+// Connect performs the RFB handshake and initializes the session.
+func (c *Client) Connect(ctx context.Context) (*Session, error) {
+ c.mu.Lock()
+ if c.closed {
+ c.mu.Unlock()
+ return nil, fmt.Errorf("client is closed")
+ }
+
+ // Perform handshake
+ session, err := Handshake(ctx, c.transport, c.config.Handshake)
+ if err != nil {
+ c.mu.Unlock()
+ return nil, fmt.Errorf("handshake failed: %w", err)
+ }
+ c.session = session
+
+ // Set read limit
+ c.transport.SetReadLimit(c.config.ReadLimit)
+
+ // Send encodings
+ if len(c.config.Encodings) > 0 {
+ msg := EncodeSetEncodings(c.config.Encodings)
+ if err := c.transport.Write(ctx, msg); err != nil {
+ c.mu.Unlock()
+ return nil, fmt.Errorf("sending encodings: %w", err)
+ }
+ }
+
+ // Release lock before calling RequestFramebuffer (which also takes the lock)
+ c.mu.Unlock()
+
+ // Request initial framebuffer
+ if err := c.RequestFramebuffer(ctx, false); err != nil {
+ return nil, fmt.Errorf("requesting initial framebuffer: %w", err)
+ }
+
+ return session, nil
+}
+
+// Session returns the current session info.
+func (c *Client) Session() *Session {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ return c.session
+}
+
+// OnMessage sets the handler for all server messages.
+func (c *Client) OnMessage(handler MessageHandler) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ c.onMessage = handler
+}
+
+// OnClipboard sets the handler for clipboard messages.
+func (c *Client) OnClipboard(handler ClipboardHandler) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ c.onClipboard = handler
+}
+
+// Listen reads and processes server messages until the context is cancelled.
+func (c *Client) Listen(ctx context.Context) error {
+ for {
+ data, err := c.transport.Read(ctx)
+ if err != nil {
+ return fmt.Errorf("reading message: %w", err)
+ }
+
+ if len(data) == 0 {
+ slog.Warn("received empty message")
+ continue
+ }
+
+ msg, err := ParseServerMessage(data)
+ if err != nil {
+ slog.Warn("parsing message", slog.String("error", err.Error()))
+ continue
+ }
+
+ // Call message handler
+ c.mu.RLock()
+ onMessage := c.onMessage
+ onClipboard := c.onClipboard
+ c.mu.RUnlock()
+
+ if onMessage != nil {
+ onMessage(msg)
+ }
+
+ // Handle specific message types
+ switch msg.Type {
+ case MsgTypeFramebufferUpdate:
+ // Request next incremental update
+ if err := c.RequestFramebuffer(ctx, true); err != nil {
+ return fmt.Errorf("requesting framebuffer: %w", err)
+ }
+
+ case MsgTypeServerCutText:
+ cut, err := ParseServerCutText(data)
+ if err != nil {
+ slog.Warn("parsing clipboard", slog.String("error", err.Error()))
+ continue
+ }
+ // Store for API access
+ c.clipMu.Lock()
+ c.lastClipboard = cut.Text
+ c.lastClipboardTime = time.Now()
+ c.clipMu.Unlock()
+
+ if onClipboard != nil {
+ onClipboard(cut.Text)
+ }
+
+ case MsgTypeBell:
+ slog.Info("bell received")
+ }
+ }
+}
+
+// RequestFramebuffer sends a framebuffer update request.
+func (c *Client) RequestFramebuffer(ctx context.Context, incremental bool) error {
+ c.mu.RLock()
+ session := c.session
+ c.mu.RUnlock()
+
+ if session == nil {
+ return fmt.Errorf("not connected")
+ }
+
+ msg := EncodeFramebufferUpdateRequest(incremental, 0, 0, session.Width, session.Height)
+ return c.transport.Write(ctx, msg)
+}
+
+// KeyEvent sends a key press or release event.
+func (c *Client) KeyEvent(ctx context.Context, key uint32, down bool) error {
+ ctx, cancel := context.WithTimeout(ctx, c.config.KeyTimeout)
+ defer cancel()
+
+ msg := EncodeKeyEvent(down, key)
+ return c.transport.Write(ctx, msg)
+}
+
+// Tap sends a key press followed by release.
+func (c *Client) Tap(ctx context.Context, key uint32) error {
+ if err := c.KeyEvent(ctx, key, true); err != nil {
+ return err
+ }
+ return c.KeyEvent(ctx, key, false)
+}
+
+// Type sends a string as key events.
+func (c *Client) Type(ctx context.Context, text string) error {
+ for _, r := range text {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ }
+
+ key := RuneToKeysym(r)
+ if err := c.Tap(ctx, key); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// PointerEvent sends a mouse/pointer event.
+func (c *Client) PointerEvent(ctx context.Context, buttonMask uint8, x, y uint16) error {
+ msg := EncodePointerEvent(buttonMask, x, y)
+ return c.transport.Write(ctx, msg)
+}
+
+// SetClipboard sends clipboard text to the server.
+func (c *Client) SetClipboard(ctx context.Context, text string) error {
+ msg := EncodeClientCutText(text)
+ return c.transport.Write(ctx, msg)
+}
+
+// GetLastClipboard returns the last received clipboard data.
+func (c *Client) GetLastClipboard() (string, time.Time) {
+ c.clipMu.RLock()
+ defer c.clipMu.RUnlock()
+ return c.lastClipboard, c.lastClipboardTime
+}
+
+// Close closes the client and transport.
+func (c *Client) Close() error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if c.closed {
+ return nil
+ }
+ c.closed = true
+
+ return c.transport.Close()
+}
+
+// Transport returns the underlying transport.
+// This is useful for advanced operations or passing to the proxy.
+func (c *Client) Transport() transport.Transport {
+ return c.transport
+}
pkg/rfb/handshake.go
@@ -0,0 +1,237 @@
+package rfb
+
+import (
+ "context"
+ "crypto/des"
+ "fmt"
+ "strings"
+
+ "goVNC/pkg/transport"
+)
+
+const (
+ // RFBVersion is the RFB protocol version we support.
+ RFBVersion = "RFB 003.008\n"
+)
+
+// SecurityType represents an RFB security type.
+type SecurityType uint8
+
+const (
+ SecurityTypeInvalid SecurityType = 0
+ SecurityTypeNone SecurityType = 1
+ SecurityTypeVNCAuth SecurityType = 2
+ SecurityTypeTight SecurityType = 16
+ SecurityTypeVeNCrypt SecurityType = 19
+)
+
+// HandshakeConfig configures the RFB handshake.
+type HandshakeConfig struct {
+ // SharedSession requests a shared session if true.
+ SharedSession bool
+
+ // Password for VNC authentication (SecurityTypeVNCAuth).
+ Password string
+
+ // AllowedSecurityTypes lists allowed security types.
+ // If empty, only SecurityTypeNone is allowed.
+ AllowedSecurityTypes []SecurityType
+}
+
+// DefaultHandshakeConfig returns default handshake configuration.
+func DefaultHandshakeConfig() *HandshakeConfig {
+ return &HandshakeConfig{
+ SharedSession: true,
+ AllowedSecurityTypes: []SecurityType{SecurityTypeNone},
+ }
+}
+
+// Handshake performs the RFB protocol handshake and returns a Session.
+func Handshake(ctx context.Context, t transport.Transport, cfg *HandshakeConfig) (*Session, error) {
+ if cfg == nil {
+ cfg = DefaultHandshakeConfig()
+ }
+
+ // Step 1: Read server version
+ serverVersionData, err := t.Read(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("reading server version: %w", err)
+ }
+ serverVersion := strings.TrimSpace(string(serverVersionData))
+
+ // Step 2: Send client version
+ err = t.Write(ctx, []byte(RFBVersion))
+ if err != nil {
+ return nil, fmt.Errorf("sending client version: %w", err)
+ }
+
+ // Step 3: Read security types
+ secTypes, err := t.Read(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("reading security types: %w", err)
+ }
+
+ if len(secTypes) < 1 {
+ return nil, fmt.Errorf("empty security types message")
+ }
+
+ // Parse security types
+ numSecTypes := int(secTypes[0])
+ if numSecTypes == 0 {
+ // Version 3.3 error or failure
+ if len(secTypes) >= 5 {
+ errLen := int(secTypes[1])<<24 | int(secTypes[2])<<16 | int(secTypes[3])<<8 | int(secTypes[4])
+ if len(secTypes) >= 5+errLen {
+ return nil, fmt.Errorf("server rejected: %s", string(secTypes[5:5+errLen]))
+ }
+ }
+ return nil, fmt.Errorf("server rejected connection")
+ }
+
+ if len(secTypes) < 1+numSecTypes {
+ return nil, fmt.Errorf("security types message too short")
+ }
+
+ // Find a supported security type
+ var selectedSecurity SecurityType
+ for i := 0; i < numSecTypes; i++ {
+ offered := SecurityType(secTypes[1+i])
+ for _, allowed := range cfg.AllowedSecurityTypes {
+ if offered == allowed {
+ selectedSecurity = offered
+ break
+ }
+ }
+ if selectedSecurity != 0 {
+ break
+ }
+ }
+
+ if selectedSecurity == 0 {
+ return nil, fmt.Errorf("no supported security type (server offered: %v)", secTypes[1:1+numSecTypes])
+ }
+
+ // Step 4: Send security choice
+ err = t.Write(ctx, []byte{byte(selectedSecurity)})
+ if err != nil {
+ return nil, fmt.Errorf("sending security choice: %w", err)
+ }
+
+ // Step 5: Handle security type specific authentication
+ switch selectedSecurity {
+ case SecurityTypeNone:
+ // No authentication required
+ case SecurityTypeVNCAuth:
+ err = handleVNCAuth(ctx, t, cfg.Password)
+ if err != nil {
+ return nil, fmt.Errorf("VNC authentication: %w", err)
+ }
+ default:
+ return nil, fmt.Errorf("unsupported security type: %d", selectedSecurity)
+ }
+
+ // Step 6: Read security result
+ secResult, err := t.Read(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("reading security result: %w", err)
+ }
+
+ if len(secResult) < 4 {
+ // Some servers don't send security result for None auth in older versions
+ if selectedSecurity != SecurityTypeNone {
+ return nil, fmt.Errorf("security result too short: %d bytes", len(secResult))
+ }
+ // Treat as success for None auth
+ } else {
+ result := uint32(secResult[0])<<24 | uint32(secResult[1])<<16 | uint32(secResult[2])<<8 | uint32(secResult[3])
+ if result != 0 {
+ return nil, fmt.Errorf("security handshake failed: result=%d", result)
+ }
+ }
+
+ // Step 7: Send ClientInit
+ clientInit := byte(0)
+ if cfg.SharedSession {
+ clientInit = 1
+ }
+ err = t.Write(ctx, []byte{clientInit})
+ if err != nil {
+ return nil, fmt.Errorf("sending client init: %w", err)
+ }
+
+ // Step 8: Read ServerInit
+ serverInit, err := t.Read(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("reading server init: %w", err)
+ }
+
+ session, err := ParseServerInit(serverInit)
+ if err != nil {
+ return nil, fmt.Errorf("parsing server init: %w", err)
+ }
+ session.ServerVersion = serverVersion
+
+ return session, nil
+}
+
+// handleVNCAuth handles VNC authentication (DES challenge-response).
+func handleVNCAuth(ctx context.Context, t transport.Transport, password string) error {
+ // Read 16-byte challenge
+ challenge, err := t.Read(ctx)
+ if err != nil {
+ return fmt.Errorf("reading challenge: %w", err)
+ }
+ if len(challenge) != 16 {
+ return fmt.Errorf("unexpected challenge length: %d", len(challenge))
+ }
+
+ // Encrypt challenge with password using DES
+ response := vncAuthEncrypt(challenge, password)
+
+ // Send 16-byte response
+ err = t.Write(ctx, response)
+ if err != nil {
+ return fmt.Errorf("sending response: %w", err)
+ }
+
+ return nil
+}
+
+// vncAuthEncrypt encrypts the VNC authentication challenge.
+// VNC uses a weird DES variant where key bits are reversed.
+func vncAuthEncrypt(challenge []byte, password string) []byte {
+ // Pad or truncate password to 8 bytes
+ key := make([]byte, 8)
+ copy(key, password)
+
+ // Reverse bits in each byte of the key (VNC quirk)
+ for i := range key {
+ key[i] = reverseBits(key[i])
+ }
+
+ // Create DES cipher
+ cipher, err := des.NewCipher(key)
+ if err != nil {
+ // Should not happen with 8-byte key
+ return challenge
+ }
+
+ // Encrypt with DES in ECB mode (2 blocks of 8 bytes)
+ response := make([]byte, 16)
+ cipher.Encrypt(response[0:8], challenge[0:8])
+ cipher.Encrypt(response[8:16], challenge[8:16])
+
+ return response
+}
+
+// reverseBits reverses the bits in a byte.
+// VNC authentication requires this quirk for the DES key.
+func reverseBits(b byte) byte {
+ var result byte
+ for i := 0; i < 8; i++ {
+ if b&(1<<i) != 0 {
+ result |= 1 << (7 - i)
+ }
+ }
+ return result
+}
pkg/rfb/handshake_test.go
@@ -0,0 +1,114 @@
+package rfb
+
+import (
+ "encoding/hex"
+ "testing"
+)
+
+func TestReverseBits(t *testing.T) {
+ tests := []struct {
+ input byte
+ want byte
+ }{
+ {0x00, 0x00},
+ {0xFF, 0xFF},
+ {0x01, 0x80},
+ {0x80, 0x01},
+ {0x0F, 0xF0},
+ {0xF0, 0x0F},
+ {0xAA, 0x55}, // 10101010 -> 01010101
+ {0x55, 0xAA}, // 01010101 -> 10101010
+ }
+
+ for _, tt := range tests {
+ got := reverseBits(tt.input)
+ if got != tt.want {
+ t.Errorf("reverseBits(%02x) = %02x, want %02x", tt.input, got, tt.want)
+ }
+ }
+}
+
+func TestVNCAuthEncrypt(t *testing.T) {
+ // Known test vectors for VNC authentication
+ // Challenge is 16 random bytes from server
+ // Password is converted to 8-byte key with reversed bits
+ // Then DES-ECB encrypt the challenge
+
+ // Test with empty password (all zeros key)
+ challenge := make([]byte, 16)
+ for i := range challenge {
+ challenge[i] = byte(i)
+ }
+
+ response := vncAuthEncrypt(challenge, "")
+ if len(response) != 16 {
+ t.Errorf("response length = %d, want 16", len(response))
+ }
+
+ // Test with a known password
+ // The response should be deterministic
+ response1 := vncAuthEncrypt(challenge, "password")
+ response2 := vncAuthEncrypt(challenge, "password")
+
+ if hex.EncodeToString(response1) != hex.EncodeToString(response2) {
+ t.Error("vncAuthEncrypt should be deterministic")
+ }
+
+ // Different passwords should produce different responses
+ response3 := vncAuthEncrypt(challenge, "different")
+ if hex.EncodeToString(response1) == hex.EncodeToString(response3) {
+ t.Error("different passwords should produce different responses")
+ }
+}
+
+func TestParseServerInit(t *testing.T) {
+ // Build a valid ServerInit message
+ // width(2) + height(2) + pixel_format(16) + name_len(4) + name
+ data := make([]byte, 24+4) // minimum + "test"
+ data[0] = 0x07 // width high byte
+ data[1] = 0x80 // width low byte (1920)
+ data[2] = 0x04 // height high byte
+ data[3] = 0x38 // height low byte (1080)
+ // pixel format at data[4:20]
+ data[4] = 32 // bits per pixel
+ data[5] = 24 // depth
+ data[6] = 0 // big endian (false)
+ data[7] = 1 // true colour
+ data[8] = 0 // red max high
+ data[9] = 255 // red max low
+ // ... rest of pixel format
+ data[20] = 0 // name len high bytes
+ data[21] = 0
+ data[22] = 0
+ data[23] = 4 // name len = 4
+ data[24] = 't'
+ data[25] = 'e'
+ data[26] = 's'
+ data[27] = 't'
+
+ session, err := ParseServerInit(data)
+ if err != nil {
+ t.Fatalf("ParseServerInit() error = %v", err)
+ }
+
+ if session.Width != 1920 {
+ t.Errorf("Width = %d, want 1920", session.Width)
+ }
+ if session.Height != 1080 {
+ t.Errorf("Height = %d, want 1080", session.Height)
+ }
+ if session.Name != "test" {
+ t.Errorf("Name = %q, want %q", session.Name, "test")
+ }
+ if session.PixelFormat.BitsPerPixel != 32 {
+ t.Errorf("BitsPerPixel = %d, want 32", session.PixelFormat.BitsPerPixel)
+ }
+}
+
+func TestParseServerInitTooShort(t *testing.T) {
+ data := make([]byte, 10) // too short
+ _, err := ParseServerInit(data)
+ if err == nil {
+ t.Error("ParseServerInit() should fail with short data")
+ }
+}
pkg/rfb/messages.go
@@ -0,0 +1,225 @@
+// Package rfb implements the RFB (Remote Framebuffer) protocol for VNC.
+package rfb
+
+import (
+ "encoding/binary"
+ "fmt"
+)
+
+// Server-to-client message types
+const (
+ MsgTypeFramebufferUpdate = 0
+ MsgTypeSetColourMapEntries = 1
+ MsgTypeBell = 2
+ MsgTypeServerCutText = 3
+)
+
+// Client-to-server message types
+const (
+ MsgTypeSetPixelFormat = 0
+ MsgTypeSetEncodings = 2
+ MsgTypeFramebufferUpdateRequest = 3
+ MsgTypeKeyEvent = 4
+ MsgTypePointerEvent = 5
+ MsgTypeClientCutText = 6
+)
+
+// Common VNC key codes (X11 keysyms)
+const (
+ KeyBackspace = 0xff08
+ KeyTab = 0xff09
+ KeyReturn = 0xff0d
+ KeyEscape = 0xff1b
+ KeyInsert = 0xff63
+ KeyDelete = 0xffff
+ KeyHome = 0xff50
+ KeyEnd = 0xff57
+ KeyPageUp = 0xff55
+ KeyPageDown = 0xff56
+ KeyLeft = 0xff51
+ KeyUp = 0xff52
+ KeyRight = 0xff53
+ KeyDown = 0xff54
+ KeyF1 = 0xffbe
+ KeyF2 = 0xffbf
+ KeyF3 = 0xffc0
+ KeyF4 = 0xffc1
+ KeyF5 = 0xffc2
+ KeyF6 = 0xffc3
+ KeyF7 = 0xffc4
+ KeyF8 = 0xffc5
+ KeyF9 = 0xffc6
+ KeyF10 = 0xffc7
+ KeyF11 = 0xffc8
+ KeyF12 = 0xffc9
+ KeyShiftL = 0xffe1
+ KeyShiftR = 0xffe2
+ KeyControlL = 0xffe3
+ KeyControlR = 0xffe4
+ KeyMetaL = 0xffe7
+ KeyMetaR = 0xffe8
+ KeyAltL = 0xffe9
+ KeyAltR = 0xffea
+ KeySuperL = 0xffeb
+ KeySuperR = 0xffec
+)
+
+// Standard encodings
+const (
+ EncodingRaw = 0
+ EncodingCopyRect = 1
+ EncodingRRE = 2
+ EncodingHextile = 5
+ EncodingZRLE = 16
+ EncodingTight = 7
+ EncodingTightPNG = -260
+
+ // Pseudo-encodings
+ EncodingCursor = -239
+ EncodingDesktopSize = -223
+ EncodingLastRect = -224
+ EncodingDesktopName = -307
+ EncodingExtendedDesktopSize = -308
+)
+
+// DefaultEncodings returns the default encoding list for noVNC compatibility.
+func DefaultEncodings() []int32 {
+ return []int32{
+ EncodingTight,
+ EncodingZRLE,
+ EncodingHextile,
+ EncodingCopyRect,
+ EncodingRaw,
+ EncodingDesktopSize,
+ EncodingLastRect,
+ EncodingCursor,
+ }
+}
+
+// EncodeSetEncodings creates a SetEncodings message.
+func EncodeSetEncodings(encodings []int32) []byte {
+ msg := make([]byte, 4+4*len(encodings))
+ msg[0] = MsgTypeSetEncodings
+ // msg[1] padding
+ binary.BigEndian.PutUint16(msg[2:4], uint16(len(encodings)))
+ for i, enc := range encodings {
+ binary.BigEndian.PutUint32(msg[4+i*4:], uint32(enc))
+ }
+ return msg
+}
+
+// EncodeFramebufferUpdateRequest creates a FramebufferUpdateRequest message.
+func EncodeFramebufferUpdateRequest(incremental bool, x, y, width, height uint16) []byte {
+ msg := make([]byte, 10)
+ msg[0] = MsgTypeFramebufferUpdateRequest
+ if incremental {
+ msg[1] = 1
+ }
+ binary.BigEndian.PutUint16(msg[2:4], x)
+ binary.BigEndian.PutUint16(msg[4:6], y)
+ binary.BigEndian.PutUint16(msg[6:8], width)
+ binary.BigEndian.PutUint16(msg[8:10], height)
+ return msg
+}
+
+// EncodeKeyEvent creates a KeyEvent message.
+func EncodeKeyEvent(down bool, key uint32) []byte {
+ msg := make([]byte, 8)
+ msg[0] = MsgTypeKeyEvent
+ if down {
+ msg[1] = 1
+ }
+ // msg[2:4] padding
+ binary.BigEndian.PutUint32(msg[4:8], key)
+ return msg
+}
+
+// EncodePointerEvent creates a PointerEvent message.
+func EncodePointerEvent(buttonMask uint8, x, y uint16) []byte {
+ msg := make([]byte, 6)
+ msg[0] = MsgTypePointerEvent
+ msg[1] = buttonMask
+ binary.BigEndian.PutUint16(msg[2:4], x)
+ binary.BigEndian.PutUint16(msg[4:6], y)
+ return msg
+}
+
+// EncodeClientCutText creates a ClientCutText message.
+func EncodeClientCutText(text string) []byte {
+ msg := make([]byte, 8+len(text))
+ msg[0] = MsgTypeClientCutText
+ // msg[1:4] padding
+ binary.BigEndian.PutUint32(msg[4:8], uint32(len(text)))
+ copy(msg[8:], text)
+ return msg
+}
+
+// ServerMessage represents a parsed server-to-client message.
+type ServerMessage struct {
+ Type uint8
+ Raw []byte
+}
+
+// FramebufferUpdate contains parsed framebuffer update data.
+type FramebufferUpdate struct {
+ NumRects uint16
+ Raw []byte
+}
+
+// ServerCutText contains clipboard data from the server.
+type ServerCutText struct {
+ Text string
+}
+
+// ParseServerMessage parses a raw server message.
+func ParseServerMessage(data []byte) (*ServerMessage, error) {
+ if len(data) == 0 {
+ return nil, fmt.Errorf("empty message")
+ }
+ return &ServerMessage{
+ Type: data[0],
+ Raw: data,
+ }, nil
+}
+
+// ParseFramebufferUpdate parses a FramebufferUpdate message.
+func ParseFramebufferUpdate(data []byte) (*FramebufferUpdate, error) {
+ if len(data) < 4 {
+ return nil, fmt.Errorf("framebuffer update too short: %d", len(data))
+ }
+ return &FramebufferUpdate{
+ NumRects: binary.BigEndian.Uint16(data[2:4]),
+ Raw: data,
+ }, nil
+}
+
+// ParseServerCutText parses a ServerCutText message.
+func ParseServerCutText(data []byte) (*ServerCutText, error) {
+ if len(data) < 8 {
+ return nil, fmt.Errorf("server cut text too short: %d", len(data))
+ }
+ textLen := binary.BigEndian.Uint32(data[4:8])
+ if len(data) < 8+int(textLen) {
+ return nil, fmt.Errorf("server cut text incomplete: need %d, got %d", 8+textLen, len(data))
+ }
+ return &ServerCutText{
+ Text: string(data[8 : 8+textLen]),
+ }, nil
+}
+
+// RuneToKeysym converts a rune to a VNC keysym.
+func RuneToKeysym(r rune) uint32 {
+ switch r {
+ case '\n':
+ return KeyReturn
+ case '\t':
+ return KeyTab
+ case '\b':
+ return KeyBackspace
+ case 0x1b:
+ return KeyEscape
+ default:
+ // For ASCII characters, the keysym equals the Unicode code point
+ return uint32(r)
+ }
+}
pkg/rfb/messages_test.go
@@ -0,0 +1,160 @@
+package rfb
+
+import (
+ "testing"
+)
+
+func TestEncodeKeyEvent(t *testing.T) {
+ tests := []struct {
+ name string
+ down bool
+ key uint32
+ want []byte
+ }{
+ {
+ name: "key down 'a'",
+ down: true,
+ key: 0x61,
+ want: []byte{0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61},
+ },
+ {
+ name: "key up 'a'",
+ down: false,
+ key: 0x61,
+ want: []byte{0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61},
+ },
+ {
+ name: "key down Return",
+ down: true,
+ key: KeyReturn,
+ want: []byte{0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0xff, 0x0d},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := EncodeKeyEvent(tt.down, tt.key)
+ if len(got) != len(tt.want) {
+ t.Errorf("EncodeKeyEvent() len = %d, want %d", len(got), len(tt.want))
+ return
+ }
+ for i := range got {
+ if got[i] != tt.want[i] {
+ t.Errorf("EncodeKeyEvent()[%d] = %02x, want %02x", i, got[i], tt.want[i])
+ }
+ }
+ })
+ }
+}
+
+func TestEncodeClientCutText(t *testing.T) {
+ text := "Hello"
+ got := EncodeClientCutText(text)
+
+ // Type
+ if got[0] != MsgTypeClientCutText {
+ t.Errorf("type = %d, want %d", got[0], MsgTypeClientCutText)
+ }
+
+ // Length (big-endian)
+ length := uint32(got[4])<<24 | uint32(got[5])<<16 | uint32(got[6])<<8 | uint32(got[7])
+ if length != uint32(len(text)) {
+ t.Errorf("length = %d, want %d", length, len(text))
+ }
+
+ // Text
+ gotText := string(got[8:])
+ if gotText != text {
+ t.Errorf("text = %q, want %q", gotText, text)
+ }
+}
+
+func TestRuneToKeysym(t *testing.T) {
+ tests := []struct {
+ r rune
+ want uint32
+ }{
+ {'\n', KeyReturn},
+ {'\t', KeyTab},
+ {'\b', KeyBackspace},
+ {'a', 0x61},
+ {'A', 0x41},
+ {'1', 0x31},
+ }
+
+ for _, tt := range tests {
+ got := RuneToKeysym(tt.r)
+ if got != tt.want {
+ t.Errorf("RuneToKeysym(%q) = %#x, want %#x", tt.r, got, tt.want)
+ }
+ }
+}
+
+func TestParseServerCutText(t *testing.T) {
+ // Build a valid ServerCutText message
+ text := "clipboard content"
+ msg := make([]byte, 8+len(text))
+ msg[0] = MsgTypeServerCutText
+ // padding msg[1:4]
+ msg[4] = 0
+ msg[5] = 0
+ msg[6] = 0
+ msg[7] = byte(len(text))
+ copy(msg[8:], text)
+
+ cut, err := ParseServerCutText(msg)
+ if err != nil {
+ t.Fatalf("ParseServerCutText() error = %v", err)
+ }
+ if cut.Text != text {
+ t.Errorf("Text = %q, want %q", cut.Text, text)
+ }
+}
+
+func TestEncodeSetEncodings(t *testing.T) {
+ encodings := []int32{EncodingTight, EncodingRaw}
+ got := EncodeSetEncodings(encodings)
+
+ // Type
+ if got[0] != MsgTypeSetEncodings {
+ t.Errorf("type = %d, want %d", got[0], MsgTypeSetEncodings)
+ }
+
+ // Count
+ count := uint16(got[2])<<8 | uint16(got[3])
+ if count != uint16(len(encodings)) {
+ t.Errorf("count = %d, want %d", count, len(encodings))
+ }
+
+ // Expected length: 4 header + 4 per encoding
+ wantLen := 4 + 4*len(encodings)
+ if len(got) != wantLen {
+ t.Errorf("len = %d, want %d", len(got), wantLen)
+ }
+}
+
+func TestEncodeFramebufferUpdateRequest(t *testing.T) {
+ got := EncodeFramebufferUpdateRequest(true, 0, 0, 1920, 1080)
+
+ // Type
+ if got[0] != MsgTypeFramebufferUpdateRequest {
+ t.Errorf("type = %d, want %d", got[0], MsgTypeFramebufferUpdateRequest)
+ }
+
+ // Incremental
+ if got[1] != 1 {
+ t.Errorf("incremental = %d, want 1", got[1])
+ }
+
+ // Width
+ width := uint16(got[6])<<8 | uint16(got[7])
+ if width != 1920 {
+ t.Errorf("width = %d, want 1920", width)
+ }
+
+ // Height
+ height := uint16(got[8])<<8 | uint16(got[9])
+ if height != 1080 {
+ t.Errorf("height = %d, want 1080", height)
+ }
+}
pkg/rfb/session.go
@@ -0,0 +1,99 @@
+package rfb
+
+import (
+ "encoding/binary"
+ "fmt"
+)
+
+// Session holds the state of an RFB session after handshake.
+type Session struct {
+ // Width of the framebuffer in pixels.
+ Width uint16
+
+ // Height of the framebuffer in pixels.
+ Height uint16
+
+ // Name of the VNC server/desktop.
+ Name string
+
+ // PixelFormat describes the pixel format of the framebuffer.
+ PixelFormat PixelFormat
+
+ // ServerVersion is the RFB version string from the server.
+ ServerVersion string
+}
+
+// PixelFormat describes the format of pixel data.
+type PixelFormat struct {
+ BitsPerPixel uint8
+ Depth uint8
+ BigEndian bool
+ TrueColour bool
+ RedMax uint16
+ GreenMax uint16
+ BlueMax uint16
+ RedShift uint8
+ GreenShift uint8
+ BlueShift uint8
+}
+
+// ParseServerInit parses a ServerInit message and returns a Session.
+// ServerInit format: width(2) + height(2) + pixel_format(16) + name_len(4) + name
+func ParseServerInit(data []byte) (*Session, error) {
+ if len(data) < 24 {
+ return nil, fmt.Errorf("server init too short: %d bytes", len(data))
+ }
+
+ width := binary.BigEndian.Uint16(data[0:2])
+ height := binary.BigEndian.Uint16(data[2:4])
+
+ pf := PixelFormat{
+ BitsPerPixel: data[4],
+ Depth: data[5],
+ BigEndian: data[6] != 0,
+ TrueColour: data[7] != 0,
+ RedMax: binary.BigEndian.Uint16(data[8:10]),
+ GreenMax: binary.BigEndian.Uint16(data[10:12]),
+ BlueMax: binary.BigEndian.Uint16(data[12:14]),
+ RedShift: data[14],
+ GreenShift: data[15],
+ BlueShift: data[16],
+ // data[17:20] is padding
+ }
+
+ nameLen := binary.BigEndian.Uint32(data[20:24])
+ if len(data) < 24+int(nameLen) {
+ return nil, fmt.Errorf("server init name incomplete: need %d, got %d", 24+nameLen, len(data))
+ }
+ name := string(data[24 : 24+nameLen])
+
+ return &Session{
+ Width: width,
+ Height: height,
+ Name: name,
+ PixelFormat: pf,
+ }, nil
+}
+
+// EncodePixelFormat creates a SetPixelFormat message.
+func EncodePixelFormat(pf *PixelFormat) []byte {
+ msg := make([]byte, 20)
+ msg[0] = MsgTypeSetPixelFormat
+ // msg[1:4] padding
+ msg[4] = pf.BitsPerPixel
+ msg[5] = pf.Depth
+ if pf.BigEndian {
+ msg[6] = 1
+ }
+ if pf.TrueColour {
+ msg[7] = 1
+ }
+ binary.BigEndian.PutUint16(msg[8:10], pf.RedMax)
+ binary.BigEndian.PutUint16(msg[10:12], pf.GreenMax)
+ binary.BigEndian.PutUint16(msg[12:14], pf.BlueMax)
+ msg[14] = pf.RedShift
+ msg[15] = pf.GreenShift
+ msg[16] = pf.BlueShift
+ // msg[17:20] padding
+ return msg
+}
pkg/transport/transport.go
@@ -0,0 +1,78 @@
+// Package transport provides transport layer abstractions for VNC connections.
+package transport
+
+import (
+ "context"
+ "crypto/tls"
+ "net/http"
+ "time"
+)
+
+// Transport represents a connection transport for RFB protocol messages.
+// Unlike TCP streams, this interface is message-oriented to support WebSocket
+// framing where each RFB message is a discrete WebSocket message.
+type Transport interface {
+ // Read reads a single RFB message from the transport.
+ // Returns the message data or an error.
+ Read(ctx context.Context) ([]byte, error)
+
+ // Write writes a single RFB message to the transport.
+ Write(ctx context.Context, data []byte) error
+
+ // Close closes the transport connection.
+ Close() error
+
+ // SetReadLimit sets the maximum message size for reads.
+ SetReadLimit(limit int64)
+}
+
+// DialOptions configures transport connection establishment.
+type DialOptions struct {
+ // Headers to send with the connection request (for WebSocket).
+ Headers http.Header
+
+ // Cookies to send with the connection request.
+ Cookies []*http.Cookie
+
+ // Proxy URL for upstream proxy (http, https, socks5).
+ Proxy string
+
+ // Timeout for connection establishment.
+ Timeout time.Duration
+
+ // TLSConfig for TLS connections.
+ TLSConfig *tls.Config
+
+ // UserAgent to send with the connection request.
+ UserAgent string
+
+ // Subprotocols for WebSocket negotiation.
+ Subprotocols []string
+}
+
+// Dialer creates new transport connections.
+type Dialer interface {
+ // Dial establishes a new transport connection.
+ Dial(ctx context.Context, target string, opts *DialOptions) (Transport, error)
+}
+
+// NewDialOptions creates DialOptions with sensible defaults.
+func NewDialOptions() *DialOptions {
+ return &DialOptions{
+ Headers: make(http.Header),
+ Timeout: 30 * time.Second,
+ }
+}
+
+// AddHeader adds a header to the dial options.
+func (o *DialOptions) AddHeader(key, value string) {
+ if o.Headers == nil {
+ o.Headers = make(http.Header)
+ }
+ o.Headers.Add(key, value)
+}
+
+// SetCookie sets a cookie string (parsed as Cookie header).
+func (o *DialOptions) SetCookie(cookie string) {
+ o.AddHeader("Cookie", cookie)
+}
pkg/transport/websocket.go
@@ -0,0 +1,107 @@
+package transport
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/coder/websocket"
+)
+
+// WebSocketTransport implements Transport over WebSocket connections.
+// This is used for noVNC-style VNC connections where each RFB message
+// is sent as a discrete WebSocket binary message.
+type WebSocketTransport struct {
+ conn *websocket.Conn
+}
+
+// WebSocketDialer implements Dialer for WebSocket connections.
+type WebSocketDialer struct{}
+
+// NewWebSocketDialer creates a new WebSocket dialer.
+func NewWebSocketDialer() *WebSocketDialer {
+ return &WebSocketDialer{}
+}
+
+// Dial establishes a WebSocket connection to the target URL.
+func (d *WebSocketDialer) Dial(ctx context.Context, target string, opts *DialOptions) (Transport, error) {
+ wsURL, err := url.Parse(target)
+ if err != nil {
+ return nil, fmt.Errorf("parsing url=%q: %w", target, err)
+ }
+
+ dialOpts := &websocket.DialOptions{}
+
+ if opts != nil {
+ // Build HTTP headers
+ if opts.Headers != nil {
+ dialOpts.HTTPHeader = opts.Headers.Clone()
+ } else {
+ dialOpts.HTTPHeader = make(http.Header)
+ }
+
+ // Add User-Agent if specified
+ if opts.UserAgent != "" {
+ dialOpts.HTTPHeader.Set("User-Agent", opts.UserAgent)
+ }
+
+ // Add subprotocols if specified
+ if len(opts.Subprotocols) > 0 {
+ dialOpts.Subprotocols = opts.Subprotocols
+ }
+
+ // TODO: Support proxy via custom HTTP client
+ // TODO: Support custom TLS config
+ }
+
+ // Apply timeout to context if specified
+ if opts != nil && opts.Timeout > 0 {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, opts.Timeout)
+ defer cancel()
+ }
+
+ conn, _, err := websocket.Dial(ctx, wsURL.String(), dialOpts)
+ if err != nil {
+ return nil, fmt.Errorf("connecting to url=%q: %w", wsURL.String(), err)
+ }
+
+ return &WebSocketTransport{conn: conn}, nil
+}
+
+// Read reads a single message from the WebSocket connection.
+func (t *WebSocketTransport) Read(ctx context.Context) ([]byte, error) {
+ _, data, err := t.conn.Read(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+}
+
+// Write writes a single message to the WebSocket connection.
+func (t *WebSocketTransport) Write(ctx context.Context, data []byte) error {
+ return t.conn.Write(ctx, websocket.MessageBinary, data)
+}
+
+// Close closes the WebSocket connection.
+func (t *WebSocketTransport) Close() error {
+ return t.conn.CloseNow()
+}
+
+// SetReadLimit sets the maximum message size for reads.
+func (t *WebSocketTransport) SetReadLimit(limit int64) {
+ t.conn.SetReadLimit(limit)
+}
+
+// Conn returns the underlying WebSocket connection.
+// This is useful for advanced operations not exposed by the Transport interface.
+func (t *WebSocketTransport) Conn() *websocket.Conn {
+ return t.conn
+}
+
+// Compile-time interface checks
+var (
+ _ Transport = (*WebSocketTransport)(nil)
+ _ Dialer = (*WebSocketDialer)(nil)
+)
pkg/web/noVNC/app/images/icons/Makefile
@@ -0,0 +1,42 @@
+BROWSER_SIZES := 16 24 32 48 64
+#ANDROID_SIZES := 72 96 144 192
+# FIXME: The ICO is limited to 8 icons due to a Chrome bug:
+# https://bugs.chromium.org/p/chromium/issues/detail?id=1381393
+ANDROID_SIZES := 96 144 192
+WEB_ICON_SIZES := $(BROWSER_SIZES) $(ANDROID_SIZES)
+
+#IOS_1X_SIZES := 20 29 40 76 # No such devices exist anymore
+IOS_2X_SIZES := 40 58 80 120 152 167
+IOS_3X_SIZES := 60 87 120 180
+ALL_IOS_SIZES := $(IOS_1X_SIZES) $(IOS_2X_SIZES) $(IOS_3X_SIZES)
+
+ALL_ICONS := \
+ $(ALL_IOS_SIZES:%=novnc-ios-%.png) \
+ novnc.ico
+
+all: $(ALL_ICONS)
+
+# Our testing shows that the ICO file need to be sorted in largest to
+# smallest to get the apporpriate behviour
+WEB_ICON_SIZES_REVERSE := $(shell echo $(WEB_ICON_SIZES) | tr ' ' '\n' | sort -nr | tr '\n' ' ')
+WEB_BASE_ICONS := $(WEB_ICON_SIZES_REVERSE:%=novnc-%.png)
+.INTERMEDIATE: $(WEB_BASE_ICONS)
+
+novnc.ico: $(WEB_BASE_ICONS)
+ convert $(WEB_BASE_ICONS) "$@"
+
+# General conversion
+novnc-%.png: novnc-icon.svg
+ convert -depth 8 -background transparent \
+ -size $*x$* "$(lastword $^)" "$@"
+
+# iOS icons use their own SVG
+novnc-ios-%.png: novnc-ios-icon.svg
+ convert -depth 8 -background transparent \
+ -size $*x$* "$(lastword $^)" "$@"
+
+# The smallest sizes are generated using a different SVG
+novnc-16.png novnc-24.png novnc-32.png: novnc-icon-sm.svg
+
+clean:
+ rm -f *.png
pkg/web/noVNC/app/images/icons/novnc-icon-sm.svg
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="16"
+ height="16"
+ viewBox="0 0 16 16"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="novnc-icon-sm.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="45.254834"
+ inkscape:cx="9.722703"
+ inkscape:cy="5.5311896"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:object-nodes="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4169" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1036.3621)">
+ <rect
+ style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect4167"
+ width="16"
+ height="15.999992"
+ x="0"
+ y="1036.3622"
+ ry="2.6666584" />
+ <path
+ style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 2.6666667,1036.3621 C 1.1893373,1036.3621 0,1037.5515 0,1039.0288 l 0,10.6666 c 0,1.4774 1.1893373,2.6667 2.6666667,2.6667 l 4,0 C 11.837333,1052.3621 16,1046.7128 16,1039.6955 l 0,-0.6667 c 0,-1.4773 -1.189337,-2.6667 -2.666667,-2.6667 l -10.6666663,0 z"
+ id="rect4173"
+ inkscape:connector-curvature="0" />
+ <g
+ id="g4381">
+ <g
+ transform="translate(0.25,0.25)"
+ style="fill:#000000;fill-opacity:1"
+ id="g4365">
+ <g
+ style="fill:#000000;fill-opacity:1"
+ id="g4367">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4369"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 4.3289754,1039.3621 c 0.1846149,0 0.3419956,0.071 0.4716623,0.2121 C 4.933546,1039.7121 5,1039.8793 5,1040.0759 l 0,3.2862 -1,0 0,-2.964 c 0,-0.024 -0.011592,-0.036 -0.034038,-0.036 l -1.931924,0 C 2.011349,1040.3621 2,1040.3741 2,1040.3981 l 0,2.964 -1,0 0,-4 z"
+ sodipodi:nodetypes="scsccsssscccs" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4371"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 6.6710244,1039.3621 2.6579513,0 c 0.184775,0 0.3419957,0.071 0.471662,0.2121 C 9.933546,1039.7121 10,1039.8793 10,1040.0759 l 0,2.5724 c 0,0.1966 -0.066454,0.3655 -0.1993623,0.5069 -0.1296663,0.1379 -0.286887,0.2069 -0.471662,0.2069 l -2.6579513,0 c -0.184775,0 -0.3436164,-0.069 -0.4765247,-0.2069 C 6.0648334,1043.0138 6,1042.8449 6,1042.6483 l 0,-2.5724 c 0,-0.1966 0.064833,-0.3638 0.1944997,-0.5017 0.1329083,-0.1414 0.2917497,-0.2121 0.4765247,-0.2121 z m 2.2949386,1 -1.931926,0 C 7.011344,1040.3621 7,1040.3741 7,1040.3981 l 0,1.928 c 0,0.024 0.011347,0.036 0.034037,0.036 l 1.931926,0 c 0.02269,0 0.034037,-0.012 0.034037,-0.036 l 0,-1.928 c 0,-0.024 -0.011347,-0.036 -0.034037,-0.036 z"
+ sodipodi:nodetypes="sscsscsscsscssssssssss" />
+ </g>
+ <g
+ style="fill:#000000;fill-opacity:1"
+ id="g4373">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4375"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 3,1047.1121 1,-2.75 1,0 -1.5,4 -1,0 -1.5,-4 1,0 z"
+ sodipodi:nodetypes="cccccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4377"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 9,1046.8621 0,-2.5 1,0 0,4 -1,0 -2,-2.5 0,2.5 -1,0 0,-4 1,0 z"
+ sodipodi:nodetypes="ccccccccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4379"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 15,1045.3621 -2.96596,0 c -0.02269,0 -0.03404,0.012 -0.03404,0.036 l 0,1.928 c 0,0.024 0.01135,0.036 0.03404,0.036 l 2.96596,0 0,1 -3.324113,0 c -0.188017,0 -0.348479,-0.068 -0.481388,-0.2037 C 11.064833,1048.0192 11,1047.8511 11,1047.6542 l 0,-2.5842 c 0,-0.1969 0.06483,-0.3633 0.194499,-0.4991 0.132909,-0.1392 0.293371,-0.2088 0.481388,-0.2088 l 3.324113,0 z"
+ sodipodi:nodetypes="cssssccscsscscc" />
+ </g>
+ </g>
+ <g
+ id="g4356">
+ <g
+ id="g4347">
+ <path
+ sodipodi:nodetypes="scsccsssscccs"
+ d="m 4.3289754,1039.3621 c 0.1846149,0 0.3419956,0.071 0.4716623,0.2121 C 4.933546,1039.7121 5,1039.8793 5,1040.0759 l 0,3.2862 -1,0 0,-2.964 c 0,-0.024 -0.011592,-0.036 -0.034038,-0.036 l -1.931924,0 c -0.022689,0 -0.034038,0.012 -0.034038,0.036 l 0,2.964 -1,0 0,-4 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4143"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="sscsscsscsscssssssssss"
+ d="m 6.6710244,1039.3621 2.6579513,0 c 0.184775,0 0.3419957,0.071 0.471662,0.2121 C 9.933546,1039.7121 10,1039.8793 10,1040.0759 l 0,2.5724 c 0,0.1966 -0.066454,0.3655 -0.1993623,0.5069 -0.1296663,0.1379 -0.286887,0.2069 -0.471662,0.2069 l -2.6579513,0 c -0.184775,0 -0.3436164,-0.069 -0.4765247,-0.2069 C 6.0648334,1043.0138 6,1042.8449 6,1042.6483 l 0,-2.5724 c 0,-0.1966 0.064833,-0.3638 0.1944997,-0.5017 0.1329083,-0.1414 0.2917497,-0.2121 0.4765247,-0.2121 z m 2.2949386,1 -1.931926,0 C 7.011344,1040.3621 7,1040.3741 7,1040.3981 l 0,1.928 c 0,0.024 0.011347,0.036 0.034037,0.036 l 1.931926,0 c 0.02269,0 0.034037,-0.012 0.034037,-0.036 l 0,-1.928 c 0,-0.024 -0.011347,-0.036 -0.034037,-0.036 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4145"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ id="g4351">
+ <path
+ sodipodi:nodetypes="cccccccc"
+ d="m 3,1047.1121 1,-2.75 1,0 -1.5,4 -1,0 -1.5,-4 1,0 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4147"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="ccccccccccc"
+ d="m 9,1046.8621 0,-2.5 1,0 0,4 -1,0 -2,-2.5 0,2.5 -1,0 0,-4 1,0 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4149"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="cssssccscsscscc"
+ d="m 15,1045.3621 -2.96596,0 c -0.02269,0 -0.03404,0.012 -0.03404,0.036 l 0,1.928 c 0,0.024 0.01135,0.036 0.03404,0.036 l 2.96596,0 0,1 -3.324113,0 c -0.188017,0 -0.348479,-0.068 -0.481388,-0.2037 C 11.064833,1048.0192 11,1047.8511 11,1047.6542 l 0,-2.5842 c 0,-0.1969 0.06483,-0.3633 0.194499,-0.4991 0.132909,-0.1392 0.293371,-0.2088 0.481388,-0.2088 l 3.324113,0 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4151"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
pkg/web/noVNC/app/images/icons/novnc-icon.svg
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="48"
+ height="48"
+ viewBox="0 0 48 48.000001"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="novnc-icon.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="11.313708"
+ inkscape:cx="27.187245"
+ inkscape:cy="17.700974"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:object-nodes="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4169" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1004.3621)">
+ <rect
+ style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect4167"
+ width="48"
+ height="48"
+ x="0"
+ y="1004.3621"
+ ry="7.9999785" />
+ <path
+ style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="m 8,1004.3621 c -4.4319881,0 -8,3.568 -8,8 l 0,32 c 0,4.432 3.5680119,8 8,8 l 12,0 c 15.512,0 28,-16.948 28,-38 l 0,-2 c 0,-4.432 -3.568012,-8 -8,-8 l -32,0 z"
+ id="rect4173"
+ inkscape:connector-curvature="0" />
+ <g
+ id="g4300"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ transform="translate(0.5,0.5)">
+ <g
+ id="g4302"
+ style="fill:#000000;fill-opacity:1;stroke:none">
+ <path
+ sodipodi:nodetypes="scsccsssscccs"
+ d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4304"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="sscsscsscsscssssssssss"
+ d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4306"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ id="g4308"
+ style="fill:#000000;fill-opacity:1;stroke:none">
+ <path
+ sodipodi:nodetypes="cccccccc"
+ d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4310"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="ccccccccccc"
+ d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4312"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="cssssccscsscscc"
+ d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4314"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ <g
+ id="g4291"
+ style="stroke:none">
+ <g
+ id="g4282"
+ style="stroke:none">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4143"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
+ sodipodi:nodetypes="scsccsssscccs" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4145"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
+ sodipodi:nodetypes="sscsscsscsscssssssssss" />
+ </g>
+ <g
+ id="g4286"
+ style="stroke:none">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4147"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
+ sodipodi:nodetypes="cccccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4149"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
+ sodipodi:nodetypes="ccccccccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4151"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
+ sodipodi:nodetypes="cssssccscsscscc" />
+ </g>
+ </g>
+ </g>
+</svg>
pkg/web/noVNC/app/images/icons/novnc-ios-120.png
Binary file
pkg/web/noVNC/app/images/icons/novnc-ios-152.png
Binary file
pkg/web/noVNC/app/images/icons/novnc-ios-167.png
Binary file
pkg/web/noVNC/app/images/icons/novnc-ios-180.png
Binary file
pkg/web/noVNC/app/images/icons/novnc-ios-40.png
Binary file
pkg/web/noVNC/app/images/icons/novnc-ios-58.png
Binary file
pkg/web/noVNC/app/images/icons/novnc-ios-60.png
Binary file
pkg/web/noVNC/app/images/icons/novnc-ios-80.png
Binary file
pkg/web/noVNC/app/images/icons/novnc-ios-87.png
Binary file
pkg/web/noVNC/app/images/icons/novnc-ios-icon.svg
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="48"
+ height="48"
+ viewBox="0 0 48 48.000001"
+ id="svg2"
+ version="1.1"
+ inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+ sodipodi:docname="novnc-ios-icon.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="11.313708"
+ inkscape:cx="27.356195"
+ inkscape:cy="17.810253"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:object-nodes="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:window-width="2560"
+ inkscape:window-height="1371"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:showpageshadow="2"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4169" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1004.3621)">
+ <rect
+ style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect4167"
+ width="48"
+ height="48"
+ x="0"
+ y="1004.3621"
+ inkscape:label="background" />
+ <path
+ style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="m 0,1004.3621 v 48 h 20 c 15.512,0 28,-16.948 28,-38 v -10 z"
+ id="rect4173"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc"
+ inkscape:label="darker_grey_plate" />
+ <g
+ id="g4300"
+ style="display:inline;fill:#000000;fill-opacity:1;stroke:none"
+ transform="translate(0.5,0.5)"
+ inkscape:label="shadows">
+ <g
+ id="g4302"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ inkscape:label="no">
+ <path
+ sodipodi:nodetypes="scsccsssscccs"
+ d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 v 6.8586 h -2 v -6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 H 7.1021125 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 v 6.8914 H 5 v -9 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4304"
+ inkscape:connector-curvature="0"
+ inkscape:label="n" />
+ <path
+ sodipodi:nodetypes="sscsscsscsscssssssssss"
+ d="m 17.013073,1016.3621 h 4.973854 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 v 4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 h -4.973854 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 v -4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 h -4.795776 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 v 4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 h 4.795776 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 v -4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4306"
+ inkscape:connector-curvature="0"
+ inkscape:label="o" />
+ </g>
+ <g
+ id="g4308"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ inkscape:label="VNC">
+ <path
+ sodipodi:nodetypes="cccccccc"
+ d="m 12,1036.9177 4.768114,-8.5556 H 19 l -6,11 h -2 l -6,-11 h 2.2318854 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4310"
+ inkscape:connector-curvature="0"
+ inkscape:label="V" />
+ <path
+ sodipodi:nodetypes="ccccccccccc"
+ d="m 29,1036.3621 v -8 h 2 v 11 h -2 l -7,-8 v 8 h -2 v -11 h 2 z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4312"
+ inkscape:connector-curvature="0"
+ inkscape:label="N" />
+ <path
+ sodipodi:nodetypes="cssssccscsscscc"
+ d="m 43,1030.3621 h -8.897887 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 v 6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 H 43 v 2 h -8.972339 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 v -6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 H 43 Z"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path4314"
+ inkscape:connector-curvature="0"
+ inkscape:label="C" />
+ </g>
+ </g>
+ <g
+ id="g4291"
+ style="stroke:none"
+ inkscape:label="noVNC">
+ <g
+ id="g4282"
+ style="stroke:none"
+ inkscape:label="no">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4143"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
+ sodipodi:nodetypes="scsccsssscccs"
+ inkscape:label="n" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4145"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
+ sodipodi:nodetypes="sscsscsscsscssssssssss"
+ inkscape:label="o" />
+ </g>
+ <g
+ id="g4286"
+ style="stroke:none"
+ inkscape:label="VNC">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4147"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
+ sodipodi:nodetypes="cccccccc"
+ inkscape:label="V" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4149"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
+ sodipodi:nodetypes="ccccccccccc"
+ inkscape:label="N" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4151"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
+ sodipodi:nodetypes="cssssccscsscscc"
+ inkscape:label="C" />
+ </g>
+ </g>
+ </g>
+</svg>
pkg/web/noVNC/app/images/icons/novnc.ico
Binary file
pkg/web/noVNC/app/images/alt.svg
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="alt.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="16"
+ inkscape:cx="18.205425"
+ inkscape:cy="17.531398"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <g
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text5290">
+ <path
+ d="m 9.9560547,1042.3329 -2.9394531,0 -0.4638672,1.3281 -1.8896485,0 2.7001953,-7.29 2.241211,0 2.7001958,7.29 -1.889649,0 -0.4589843,-1.3281 z m -2.4707031,-1.3526 1.9970703,0 -0.9960938,-2.9003 -1.0009765,2.9003 z"
+ style="font-size:10px;fill:#ffffff;fill-opacity:1"
+ id="path5340" />
+ <path
+ d="m 13.188477,1036.0634 1.748046,0 0,7.5976 -1.748046,0 0,-7.5976 z"
+ style="font-size:10px;fill:#ffffff;fill-opacity:1"
+ id="path5342" />
+ <path
+ d="m 18.535156,1036.6395 0,1.5528 1.801758,0 0,1.25 -1.801758,0 0,2.3193 q 0,0.3809 0.151367,0.5176 0.151368,0.1318 0.600586,0.1318 l 0.898438,0 0,1.25 -1.499024,0 q -1.035156,0 -1.469726,-0.4297 -0.429688,-0.4345 -0.429688,-1.4697 l 0,-2.3193 -0.86914,0 0,-1.25 0.86914,0 0,-1.5528 1.748047,0 z"
+ style="font-size:10px;fill:#ffffff;fill-opacity:1"
+ id="path5344" />
+ </g>
+ </g>
+</svg>
pkg/web/noVNC/app/images/clipboard.svg
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="clipboard.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="15.366606"
+ inkscape:cy="16.42981"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <path
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 9,6 6,6 C 5.4459889,6 5,6.4459889 5,7 l 0,13 c 0,0.554011 0.4459889,1 1,1 l 13,0 c 0.554011,0 1,-0.445989 1,-1 L 20,7 C 20,6.4459889 19.554011,6 19,6 l -3,0"
+ transform="translate(0,1027.3622)"
+ id="rect6083"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cssssssssc" />
+ <rect
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect6085"
+ width="7"
+ height="4"
+ x="9"
+ y="1031.3622"
+ ry="1.00002" />
+ <path
+ style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
+ d="m 8.5071212,1038.8622 7.9999998,0"
+ id="path6087"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
+ d="m 8.5071212,1041.8622 3.9999998,0"
+ id="path6089"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
+ d="m 8.5071212,1044.8622 5.9999998,0"
+ id="path6091"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/connect.svg
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="connect.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="37.14834"
+ inkscape:cy="1.9525926"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <g
+ id="g5103"
+ transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,-729.15757,315.8823)">
+ <path
+ sodipodi:nodetypes="cssssc"
+ inkscape:connector-curvature="0"
+ id="rect5096"
+ d="m 11,1040.3622 -5,0 c -1.108,0 -2,-0.892 -2,-2 l 0,-4 c 0,-1.108 0.892,-2 2,-2 l 5,0"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="m 14,1032.3622 5,0 c 1.108,0 2,0.892 2,2 l 0,4 c 0,1.108 -0.892,2 -2,2 l -5,0"
+ id="path5099"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cssssc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path5101"
+ d="m 9,1036.3622 7,0"
+ style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ </g>
+</svg>
pkg/web/noVNC/app/images/ctrl.svg
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="ctrl.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="16"
+ inkscape:cx="18.205425"
+ inkscape:cy="17.531398"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <g
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text5290">
+ <path
+ d="m 9.1210938,1043.1898 q -0.5175782,0.2686 -1.0791016,0.4053 -0.5615235,0.1367 -1.171875,0.1367 -1.8212891,0 -2.8857422,-1.0156 -1.0644531,-1.0205 -1.0644531,-2.7637 0,-1.748 1.0644531,-2.7637 1.0644531,-1.0205 2.8857422,-1.0205 0.6103515,0 1.171875,0.1368 0.5615234,0.1367 1.0791016,0.4052 l 0,1.5088 q -0.522461,-0.3564 -1.0302735,-0.5224 -0.5078125,-0.1661 -1.0693359,-0.1661 -1.0058594,0 -1.5820313,0.6446 -0.5761719,0.6445 -0.5761719,1.7773 0,1.1279 0.5761719,1.7725 0.5761719,0.6445 1.5820313,0.6445 0.5615234,0 1.0693359,-0.166 0.5078125,-0.166 1.0302735,-0.5225 l 0,1.5088 z"
+ style="font-size:10px;fill:#ffffff;fill-opacity:1"
+ id="path5370" />
+ <path
+ d="m 12.514648,1036.5687 0,1.5528 1.801758,0 0,1.25 -1.801758,0 0,2.3193 q 0,0.3809 0.151368,0.5176 0.151367,0.1318 0.600586,0.1318 l 0.898437,0 0,1.25 -1.499023,0 q -1.035157,0 -1.469727,-0.4297 -0.429687,-0.4345 -0.429687,-1.4697 l 0,-2.3193 -0.8691411,0 0,-1.25 0.8691411,0 0,-1.5528 1.748046,0 z"
+ style="font-size:10px;fill:#ffffff;fill-opacity:1"
+ id="path5372" />
+ <path
+ d="m 19.453125,1039.6107 q -0.229492,-0.1074 -0.458984,-0.1562 -0.22461,-0.054 -0.454102,-0.054 -0.673828,0 -1.040039,0.4345 -0.361328,0.4297 -0.361328,1.2354 l 0,2.5195 -1.748047,0 0,-5.4687 1.748047,0 0,0.8984 q 0.336914,-0.5371 0.771484,-0.7813 0.439453,-0.249 1.049805,-0.249 0.08789,0 0.19043,0.01 0.102539,0 0.297851,0.029 l 0.0049,1.582 z"
+ style="font-size:10px;fill:#ffffff;fill-opacity:1"
+ id="path5374" />
+ <path
+ d="m 20.332031,1035.9926 1.748047,0 0,7.5976 -1.748047,0 0,-7.5976 z"
+ style="font-size:10px;fill:#ffffff;fill-opacity:1"
+ id="path5376" />
+ </g>
+ </g>
+</svg>
pkg/web/noVNC/app/images/ctrlaltdel.svg
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="ctrlaltdel.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="8"
+ inkscape:cx="11.135667"
+ inkscape:cy="16.407428"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect5253"
+ width="5"
+ height="5.0000172"
+ x="16"
+ y="1031.3622"
+ ry="1.0000174" />
+ <rect
+ y="1043.3622"
+ x="4"
+ height="5.0000172"
+ width="5"
+ id="rect5255"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ ry="1.0000174" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect5257"
+ width="5"
+ height="5.0000172"
+ x="13"
+ y="1043.3622"
+ ry="1.0000174" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/disconnect.svg
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="disconnect.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="16"
+ inkscape:cx="25.05707"
+ inkscape:cy="11.594858"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <g
+ id="g5171"
+ transform="translate(-24.062499,-6.15775e-4)">
+ <path
+ id="path5110"
+ transform="translate(0,1027.3622)"
+ d="m 39.744141,3.4960938 c -0.769923,0 -1.539607,0.2915468 -2.121094,0.8730468 l -2.566406,2.5664063 1.414062,1.4140625 2.566406,-2.5664063 c 0.403974,-0.404 1.010089,-0.404 1.414063,0 l 2.828125,2.828125 c 0.40398,0.4039 0.403907,1.0101621 0,1.4140629 l -2.566406,2.566406 1.414062,1.414062 2.566406,-2.566406 c 1.163041,-1.1629 1.162968,-3.0791874 0,-4.2421874 L 41.865234,4.3691406 C 41.283747,3.7876406 40.514063,3.4960937 39.744141,3.4960938 Z M 39.017578,9.015625 a 1.0001,1.0001 0 0 0 -0.6875,0.3027344 l -0.445312,0.4453125 1.414062,1.4140621 0.445313,-0.445312 A 1.0001,1.0001 0 0 0 39.017578,9.015625 Z m -6.363281,0.7070312 a 1.0001,1.0001 0 0 0 -0.6875,0.3027348 L 28.431641,13.5625 c -1.163042,1.163 -1.16297,3.079187 0,4.242188 l 2.828125,2.828124 c 1.162974,1.163101 3.079213,1.163101 4.242187,0 l 3.535156,-3.535156 a 1.0001,1.0001 0 1 0 -1.414062,-1.414062 l -3.535156,3.535156 c -0.403974,0.404 -1.010089,0.404 -1.414063,0 l -2.828125,-2.828125 c -0.403981,-0.404 -0.403908,-1.010162 0,-1.414063 l 3.535156,-3.537109 A 1.0001,1.0001 0 0 0 32.654297,9.7226562 Z m 3.109375,2.1621098 -2.382813,2.384765 a 1.0001,1.0001 0 1 0 1.414063,1.414063 l 2.382812,-2.384766 -1.414062,-1.414062 z"
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ inkscape:connector-curvature="0" />
+ <rect
+ transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
+ y="752.29541"
+ x="-712.31262"
+ height="18.000017"
+ width="3"
+ id="rect5116"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ </g>
+ </g>
+</svg>
pkg/web/noVNC/app/images/drag.svg
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="drag.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="22.627417"
+ inkscape:cx="9.8789407"
+ inkscape:cy="9.5008608"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="m 7.039733,1049.3037 c -0.4309106,-0.1233 -0.7932634,-0.4631 -0.9705434,-0.9103 -0.04922,-0.1241 -0.057118,-0.2988 -0.071321,-1.5771 l -0.015972,-1.4375 -0.328125,-0.082 c -0.7668138,-0.1927 -1.1897046,-0.4275 -1.7031253,-0.9457 -0.4586773,-0.4629 -0.6804297,-0.8433 -0.867034,-1.4875 -0.067215,-0.232 -0.068001,-0.2642 -0.078682,-3.2188 -0.012078,-3.341 -0.020337,-3.2012 0.2099452,-3.5555 0.2246623,-0.3458 0.5798271,-0.5892 0.9667343,-0.6626 0.092506,-0.017 0.531898,-0.032 0.9764271,-0.032 l 0.8082347,0 1.157e-4,1.336 c 1.125e-4,1.2779 0.00281,1.3403 0.062214,1.4378 0.091785,0.1505 0.2357707,0.226 0.4314082,0.2261 0.285389,2e-4 0.454884,-0.1352 0.5058962,-0.4042 0.019355,-0.102 0.031616,-0.982 0.031616,-2.269 0,-1.9756 0.00357,-2.1138 0.059205,-2.2926 0.1645475,-0.5287 0.6307616,-0.9246 1.19078,-1.0113 0.8000572,-0.1238 1.5711277,0.4446 1.6860387,1.2429 0.01732,0.1203 0.03177,0.8248 0.03211,1.5657 6.19e-4,1.3449 7.22e-4,1.347 0.07093,1.4499 0.108355,0.1587 0.255268,0.2248 0.46917,0.2108 0.204069,-0.013 0.316116,-0.08 0.413642,-0.2453 0.06028,-0.1024 0.06307,-0.1778 0.07862,-2.1218 0.01462,-1.8283 0.02124,-2.0285 0.07121,-2.1549 0.260673,-0.659 0.934894,-1.0527 1.621129,-0.9465 0.640523,0.099 1.152269,0.6104 1.243187,1.2421 0.01827,0.1269 0.03175,0.9943 0.03211,2.0657 l 6.19e-4,1.8469 0.07031,0.103 c 0.108355,0.1587 0.255267,0.2248 0.46917,0.2108 0.204069,-0.013 0.316115,-0.08 0.413642,-0.2453 0.05951,-0.1011 0.06329,-0.1786 0.07907,-1.6218 0.01469,-1.3438 0.02277,-1.5314 0.07121,-1.6549 0.257975,-0.6576 0.934425,-1.0527 1.620676,-0.9465 0.640522,0.099 1.152269,0.6104 1.243186,1.2421 0.0186,0.1292 0.03179,1.0759 0.03222,2.3125 7.15e-4,2.0335 0.0025,2.0966 0.06283,2.1956 0.09178,0.1505 0.235771,0.226 0.431409,0.2261 0.285388,2e-4 0.454884,-0.1352 0.505897,-0.4042 0.01874,-0.099 0.03161,-0.8192 0.03161,-1.769 0,-1.4848 0.0043,-1.6163 0.0592,-1.7926 0.164548,-0.5287 0.630762,-0.9246 1.19078,-1.0113 0.800057,-0.1238 1.571128,0.4446 1.686039,1.2429 0.04318,0.2999 0.04372,9.1764 5.78e-4,9.4531 -0.04431,0.2841 -0.217814,0.6241 -0.420069,0.8232 -0.320102,0.315 -0.63307,0.4268 -1.194973,0.4268 l -0.35281,0 -2.51e-4,1.2734 c -1.25e-4,0.7046 -0.01439,1.3642 -0.03191,1.4766 -0.06665,0.4274 -0.372966,0.8704 -0.740031,1.0702 -0.349999,0.1905 0.01748,0.18 -6.242199,0.1776 -5.3622439,0 -5.7320152,-0.01 -5.9121592,-0.057 l 1.4e-5,0 z"
+ id="path4379"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/error.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="error.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="14.00357"
+ inkscape:cy="12.443398"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 7 3 C 4.7839905 3 3 4.7839905 3 7 L 3 18 C 3 20.21601 4.7839905 22 7 22 L 18 22 C 20.21601 22 22 20.21601 22 18 L 22 7 C 22 4.7839905 20.21601 3 18 3 L 7 3 z M 7.6992188 6 A 1.6916875 1.6924297 0 0 1 8.9121094 6.5117188 L 12.5 10.101562 L 16.087891 6.5117188 A 1.6916875 1.6924297 0 0 1 17.251953 6 A 1.6916875 1.6924297 0 0 1 18.480469 8.90625 L 14.892578 12.496094 L 18.480469 16.085938 A 1.6916875 1.6924297 0 1 1 16.087891 18.478516 L 12.5 14.888672 L 8.9121094 18.478516 A 1.6916875 1.6924297 0 1 1 6.5214844 16.085938 L 10.109375 12.496094 L 6.5214844 8.90625 A 1.6916875 1.6924297 0 0 1 7.6992188 6 z "
+ transform="translate(0,1027.3622)"
+ id="rect4135" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/esc.svg
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="esc.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="16"
+ inkscape:cx="18.205425"
+ inkscape:cy="17.531398"
+ inkscape:document-units="px"
+ inkscape:current-layer="text5290"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <g
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text5290">
+ <path
+ d="m 3.9331055,1036.1464 5.0732422,0 0,1.4209 -3.1933594,0 0,1.3574 3.0029297,0 0,1.4209 -3.0029297,0 0,1.6699 3.3007812,0 0,1.4209 -5.180664,0 0,-7.29 z"
+ style="font-size:10px;fill:#ffffff;fill-opacity:1"
+ id="path5314" />
+ <path
+ d="m 14.963379,1038.1385 0,1.3282 q -0.561524,-0.2344 -1.083984,-0.3516 -0.522461,-0.1172 -0.986329,-0.1172 -0.498046,0 -0.742187,0.127 -0.239258,0.122 -0.239258,0.3808 0,0.21 0.180664,0.3223 0.185547,0.1123 0.65918,0.166 l 0.307617,0.044 q 1.342773,0.1709 1.806641,0.5615 0.463867,0.3906 0.463867,1.2256 0,0.874 -0.644531,1.3134 -0.644532,0.4395 -1.923829,0.4395 -0.541992,0 -1.123046,-0.088 -0.576172,-0.083 -1.186524,-0.2539 l 0,-1.3281 q 0.522461,0.2539 1.069336,0.3808 0.551758,0.127 1.118164,0.127 0.512695,0 0.771485,-0.1416 0.258789,-0.1416 0.258789,-0.4199 0,-0.2344 -0.180664,-0.3467 -0.175782,-0.1172 -0.708008,-0.1807 l -0.307617,-0.039 q -1.166993,-0.1465 -1.635743,-0.542 -0.46875,-0.3955 -0.46875,-1.2012 0,-0.8691 0.595703,-1.2891 0.595704,-0.4199 1.826172,-0.4199 0.483399,0 1.015625,0.073 0.532227,0.073 1.157227,0.2294 z"
+ style="font-size:10px;fill:#ffffff;fill-opacity:1"
+ id="path5316" />
+ <path
+ d="m 21.066895,1038.1385 0,1.4258 q -0.356446,-0.2441 -0.717774,-0.3613 -0.356445,-0.1172 -0.742187,-0.1172 -0.732422,0 -1.142579,0.4297 -0.405273,0.4248 -0.405273,1.1914 0,0.7666 0.405273,1.1963 0.410157,0.4248 1.142579,0.4248 0.410156,0 0.776367,-0.1221 0.371094,-0.122 0.683594,-0.3613 l 0,1.4307 q -0.410157,0.1513 -0.834961,0.2246 -0.419922,0.078 -0.844727,0.078 -1.479492,0 -2.314453,-0.7568 -0.834961,-0.7618 -0.834961,-2.1143 0,-1.3525 0.834961,-2.1094 0.834961,-0.7617 2.314453,-0.7617 0.429688,0 0.844727,0.078 0.419921,0.073 0.834961,0.2246 z"
+ style="font-size:10px;fill:#ffffff;fill-opacity:1"
+ id="path5318" />
+ </g>
+ </g>
+</svg>
pkg/web/noVNC/app/images/expander.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="9"
+ height="10"
+ viewBox="0 0 9 10"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="expander.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="45.254834"
+ inkscape:cx="9.8737281"
+ inkscape:cy="6.4583132"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ units="px"
+ inkscape:snap-object-midpoints="false"
+ inkscape:object-nodes="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1042.3622)">
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="M 2.0800781,1042.3633 A 2.0002,2.0002 0 0 0 0,1044.3613 l 0,6 a 2.0002,2.0002 0 0 0 3.0292969,1.7168 l 5,-3 a 2.0002,2.0002 0 0 0 0,-3.4316 l -5,-3 a 2.0002,2.0002 0 0 0 -0.9492188,-0.2832 z"
+ id="path4138"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/fullscreen.svg
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="fullscreen.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="16.400723"
+ inkscape:cy="15.083758"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <rect
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect5006"
+ width="17"
+ height="17.000017"
+ x="4"
+ y="1031.3622"
+ ry="3.0000174" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
+ d="m 7.5,1044.8622 4,0 -1.5,-1.5 1.5,-1.5 -1,-1 -1.5,1.5 -1.5,-1.5 0,4 z"
+ id="path5017"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path5025"
+ d="m 17.5,1034.8622 -4,0 1.5,1.5 -1.5,1.5 1,1 1.5,-1.5 1.5,1.5 0,-4 z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/handle.svg
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="5"
+ height="6"
+ viewBox="0 0 5 6"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="handle.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="32"
+ inkscape:cx="1.3551778"
+ inkscape:cy="8.7800329"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1046.3622)">
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 4.0000803,1049.3622 -3,-2 0,4 z"
+ id="path4247"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/handle_bg.svg
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="15"
+ height="50"
+ viewBox="0 0 15 50"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="handle_bg.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="16"
+ inkscape:cx="-10.001409"
+ inkscape:cy="24.512566"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1002.3622)">
+ <rect
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect4249"
+ width="1"
+ height="1.0000174"
+ x="9.5"
+ y="1008.8622"
+ ry="1.7382812e-05" />
+ <rect
+ ry="1.7382812e-05"
+ y="1013.8622"
+ x="9.5"
+ height="1.0000174"
+ width="1"
+ id="rect4255"
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <rect
+ ry="1.7382812e-05"
+ y="1008.8622"
+ x="4.5"
+ height="1.0000174"
+ width="1"
+ id="rect4261"
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <rect
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect4263"
+ width="1"
+ height="1.0000174"
+ x="4.5"
+ y="1013.8622"
+ ry="1.7382812e-05" />
+ <rect
+ ry="1.7382812e-05"
+ y="1039.8622"
+ x="9.5"
+ height="1.0000174"
+ width="1"
+ id="rect4265"
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <rect
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect4267"
+ width="1"
+ height="1.0000174"
+ x="9.5"
+ y="1044.8622"
+ ry="1.7382812e-05" />
+ <rect
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect4269"
+ width="1"
+ height="1.0000174"
+ x="4.5"
+ y="1039.8622"
+ ry="1.7382812e-05" />
+ <rect
+ ry="1.7382812e-05"
+ y="1044.8622"
+ x="4.5"
+ height="1.0000174"
+ width="1"
+ id="rect4271"
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <rect
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect4273"
+ width="1"
+ height="1.0000174"
+ x="9.5"
+ y="1018.8622"
+ ry="1.7382812e-05" />
+ <rect
+ ry="1.7382812e-05"
+ y="1018.8622"
+ x="4.5"
+ height="1.0000174"
+ width="1"
+ id="rect4275"
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <rect
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect4277"
+ width="1"
+ height="1.0000174"
+ x="9.5"
+ y="1034.8622"
+ ry="1.7382812e-05" />
+ <rect
+ ry="1.7382812e-05"
+ y="1034.8622"
+ x="4.5"
+ height="1.0000174"
+ width="1"
+ id="rect4279"
+ style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/info.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="info.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="15.720838"
+ inkscape:cy="8.9111233"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 12.5 3 A 9.5 9.4999914 0 0 0 3 12.5 A 9.5 9.4999914 0 0 0 12.5 22 A 9.5 9.4999914 0 0 0 22 12.5 A 9.5 9.4999914 0 0 0 12.5 3 z M 12.5 5 A 1.5 1.5000087 0 0 1 14 6.5 A 1.5 1.5000087 0 0 1 12.5 8 A 1.5 1.5000087 0 0 1 11 6.5 A 1.5 1.5000087 0 0 1 12.5 5 z M 10.521484 8.9785156 L 12.521484 8.9785156 A 1.50015 1.50015 0 0 1 14.021484 10.478516 L 14.021484 15.972656 A 1.50015 1.50015 0 0 1 14.498047 18.894531 C 14.498047 18.894531 13.74301 19.228309 12.789062 18.912109 C 12.312092 18.754109 11.776235 18.366625 11.458984 17.828125 C 11.141734 17.289525 11.021484 16.668469 11.021484 15.980469 L 11.021484 11.980469 L 10.521484 11.980469 A 1.50015 1.50015 0 1 1 10.521484 8.9804688 L 10.521484 8.9785156 z "
+ transform="translate(0,1027.3622)"
+ id="path4136" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/keyboard.svg
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="keyboard.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/keyboard.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#717171"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="31.285341"
+ inkscape:cy="8.8028469"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:object-paths="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-smooth-nodes="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="M 7,3 C 4.8012876,3 3,4.8013 3,7 3,11.166667 3,15.333333 3,19.5 3,20.8764 4.1236413,22 5.5,22 l 14,0 C 20.876358,22 22,20.8764 22,19.5 22,15.333333 22,11.166667 22,7 22,4.8013 20.198712,3 18,3 Z m 0,2 11,0 c 1.125307,0 2,0.8747 2,2 L 20,12 5,12 5,7 C 5,5.8747 5.8746931,5 7,5 Z M 6.5,14 C 6.777,14 7,14.223 7,14.5 7,14.777 6.777,15 6.5,15 6.223,15 6,14.777 6,14.5 6,14.223 6.223,14 6.5,14 Z m 2,0 C 8.777,14 9,14.223 9,14.5 9,14.777 8.777,15 8.5,15 8.223,15 8,14.777 8,14.5 8,14.223 8.223,14 8.5,14 Z m 2,0 C 10.777,14 11,14.223 11,14.5 11,14.777 10.777,15 10.5,15 10.223,15 10,14.777 10,14.5 10,14.223 10.223,14 10.5,14 Z m 2,0 C 12.777,14 13,14.223 13,14.5 13,14.777 12.777,15 12.5,15 12.223,15 12,14.777 12,14.5 12,14.223 12.223,14 12.5,14 Z m 2,0 C 14.777,14 15,14.223 15,14.5 15,14.777 14.777,15 14.5,15 14.223,15 14,14.777 14,14.5 14,14.223 14.223,14 14.5,14 Z m 2,0 C 16.777,14 17,14.223 17,14.5 17,14.777 16.777,15 16.5,15 16.223,15 16,14.777 16,14.5 16,14.223 16.223,14 16.5,14 Z m 2,0 C 18.777,14 19,14.223 19,14.5 19,14.777 18.777,15 18.5,15 18.223,15 18,14.777 18,14.5 18,14.223 18.223,14 18.5,14 Z m -13,2 C 5.777,16 6,16.223 6,16.5 6,16.777 5.777,17 5.5,17 5.223,17 5,16.777 5,16.5 5,16.223 5.223,16 5.5,16 Z m 2,0 C 7.777,16 8,16.223 8,16.5 8,16.777 7.777,17 7.5,17 7.223,17 7,16.777 7,16.5 7,16.223 7.223,16 7.5,16 Z m 2,0 C 9.777,16 10,16.223 10,16.5 10,16.777 9.777,17 9.5,17 9.223,17 9,16.777 9,16.5 9,16.223 9.223,16 9.5,16 Z m 2,0 C 11.777,16 12,16.223 12,16.5 12,16.777 11.777,17 11.5,17 11.223,17 11,16.777 11,16.5 11,16.223 11.223,16 11.5,16 Z m 2,0 C 13.777,16 14,16.223 14,16.5 14,16.777 13.777,17 13.5,17 13.223,17 13,16.777 13,16.5 13,16.223 13.223,16 13.5,16 Z m 2,0 C 15.777,16 16,16.223 16,16.5 16,16.777 15.777,17 15.5,17 15.223,17 15,16.777 15,16.5 15,16.223 15.223,16 15.5,16 Z m 2,0 C 17.777,16 18,16.223 18,16.5 18,16.777 17.777,17 17.5,17 17.223,17 17,16.777 17,16.5 17,16.223 17.223,16 17.5,16 Z m 2,0 C 19.777,16 20,16.223 20,16.5 20,16.777 19.777,17 19.5,17 19.223,17 19,16.777 19,16.5 19,16.223 19.223,16 19.5,16 Z M 6,18 c 0.554,0 1,0.446 1,1 0,0.554 -0.446,1 -1,1 -0.554,0 -1,-0.446 -1,-1 0,-0.554 0.446,-1 1,-1 z m 2.8261719,0 7.3476561,0 C 16.631643,18 17,18.368372 17,18.826172 l 0,0.347656 C 17,19.631628 16.631643,20 16.173828,20 L 8.8261719,20 C 8.3683573,20 8,19.631628 8,19.173828 L 8,18.826172 C 8,18.368372 8.3683573,18 8.8261719,18 Z m 10.1113281,0 0.125,0 C 19.581551,18 20,18.4184 20,18.9375 l 0,0.125 C 20,19.5816 19.581551,20 19.0625,20 l -0.125,0 C 18.418449,20 18,19.5816 18,19.0625 l 0,-0.125 C 18,18.4184 18.418449,18 18.9375,18 Z"
+ transform="translate(0,1027.3622)"
+ id="rect4160"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sccssccsssssccssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+ d="m 12.499929,1033.8622 -2,2 1.500071,0 0,2 1,0 0,-2 1.499929,0 z"
+ id="path4150"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccc" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/power.svg
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="power.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="9.3159849"
+ inkscape:cy="13.436208"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="M 9 6.8183594 C 6.3418164 8.1213032 4.5 10.849161 4.5 14 C 4.5 18.4065 8.0935666 22 12.5 22 C 16.906433 22 20.5 18.4065 20.5 14 C 20.5 10.849161 18.658184 8.1213032 16 6.8183594 L 16 9.125 C 17.514327 10.211757 18.5 11.984508 18.5 14 C 18.5 17.3256 15.825553 20 12.5 20 C 9.1744469 20 6.5 17.3256 6.5 14 C 6.5 11.984508 7.4856727 10.211757 9 9.125 L 9 6.8183594 z "
+ transform="translate(0,1027.3622)"
+ id="path6140" />
+ <path
+ style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 12.5,1031.8836 0,6.4786"
+ id="path6142"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/settings.svg
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="settings.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="22.627417"
+ inkscape:cx="14.69683"
+ inkscape:cy="8.8039511"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 11 3 L 11 5.1601562 A 7.5 7.5 0 0 0 8.3671875 6.2460938 L 6.84375 4.7226562 L 4.7226562 6.84375 L 6.2480469 8.3691406 A 7.5 7.5 0 0 0 5.1523438 11 L 3 11 L 3 14 L 5.1601562 14 A 7.5 7.5 0 0 0 6.2460938 16.632812 L 4.7226562 18.15625 L 6.84375 20.277344 L 8.3691406 18.751953 A 7.5 7.5 0 0 0 11 19.847656 L 11 22 L 14 22 L 14 19.839844 A 7.5 7.5 0 0 0 16.632812 18.753906 L 18.15625 20.277344 L 20.277344 18.15625 L 18.751953 16.630859 A 7.5 7.5 0 0 0 19.847656 14 L 22 14 L 22 11 L 19.839844 11 A 7.5 7.5 0 0 0 18.753906 8.3671875 L 20.277344 6.84375 L 18.15625 4.7226562 L 16.630859 6.2480469 A 7.5 7.5 0 0 0 14 5.1523438 L 14 3 L 11 3 z M 12.5 10 A 2.5 2.5 0 0 1 15 12.5 A 2.5 2.5 0 0 1 12.5 15 A 2.5 2.5 0 0 1 10 12.5 A 2.5 2.5 0 0 1 12.5 10 z "
+ transform="translate(0,1027.3622)"
+ id="rect4967" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/tab.svg
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="tab.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="16"
+ inkscape:cx="11.67335"
+ inkscape:cy="17.881696"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="m 3,1031.3622 0,8 2,0 0,-4 0,-4 -2,0 z m 2,4 4,4 0,-3 13,0 0,-2 -13,0 0,-3 -4,4 z"
+ id="rect5194"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path5211"
+ d="m 22,1048.3622 0,-8 -2,0 0,4 0,4 2,0 z m -2,-4 -4,-4 0,3 -13,0 0,2 13,0 0,3 4,-4 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/toggleextrakeys.svg
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="extrakeys.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="15.234555"
+ inkscape:cy="9.9710826"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m 8,1031.3622 c -2.1987124,0 -4,1.8013 -4,4 l 0,8.9996 c 0,2.1987 1.8012876,4 4,4 l 9,0 c 2.198712,0 4,-1.8013 4,-4 l 0,-8.9996 c 0,-2.1987 -1.801288,-4 -4,-4 z m 0,2 9,0 c 1.125307,0 2,0.8747 2,2 l 0,7.0005 c 0,1.1253 -0.874693,2 -2,2 l -9,0 c -1.1253069,0 -2,-0.8747 -2,-2 l 0,-7.0005 c 0,-1.1253 0.8746931,-2 2,-2 z"
+ id="rect5006"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssssssssssssssssss" />
+ <g
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text4167"
+ transform="matrix(0.96021948,0,0,0.96021948,0.18921715,41.80659)">
+ <path
+ d="m 14.292969,1040.6791 -2.939453,0 -0.463868,1.3281 -1.889648,0 2.700195,-7.29 2.241211,0 2.700196,7.29 -1.889649,0 -0.458984,-1.3281 z m -2.470703,-1.3526 1.99707,0 -0.996094,-2.9004 -1.000976,2.9004 z"
+ id="path4172"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+</svg>
pkg/web/noVNC/app/images/warning.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="25"
+ height="25"
+ viewBox="0 0 25 25"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="warning.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="16.457343"
+ inkscape:cy="12.179552"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:object-paths="true"
+ showguides="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-global="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ 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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1027.3622)">
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="M 12.513672 3.0019531 C 11.751609 2.9919531 11.052563 3.4242687 10.710938 4.1054688 L 3.2109375 19.105469 C 2.5461937 20.435369 3.5132277 21.9999 5 22 L 20 22 C 21.486772 21.9999 22.453806 20.435369 21.789062 19.105469 L 14.289062 4.1054688 C 13.951849 3.4330688 13.265888 3.0066531 12.513672 3.0019531 z M 12.478516 6.9804688 A 1.50015 1.50015 0 0 1 14 8.5 L 14 14.5 A 1.50015 1.50015 0 1 1 11 14.5 L 11 8.5 A 1.50015 1.50015 0 0 1 12.478516 6.9804688 z M 12.5 17 A 1.5 1.5 0 0 1 14 18.5 A 1.5 1.5 0 0 1 12.5 20 A 1.5 1.5 0 0 1 11 18.5 A 1.5 1.5 0 0 1 12.5 17 z "
+ transform="translate(0,1027.3622)"
+ id="path4208" />
+ </g>
+</svg>
pkg/web/noVNC/app/images/windows.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="svg2"
+ inkscape:export-ydpi="90"
+ inkscape:export-xdpi="90"
+ sodipodi:docname="windows.svg"
+ inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+ inkscape:version="0.92.4 (unknown)"
+ x="0px"
+ y="0px"
+ viewBox="-293 384 25 25"
+ xml:space="preserve"
+ width="25"
+ height="25"><metadata
+ id="metadata21"><rdf:RDF><cc:Work
+ 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
+ id="defs19" /><sodipodi:namedview
+ pagecolor="#959595"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1136"
+ id="namedview17"
+ showgrid="true"
+ inkscape:pagecheckerboard="false"
+ inkscape:zoom="32"
+ inkscape:cx="3.926913"
+ inkscape:cy="13.255959"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2"><inkscape:grid
+ type="xygrid"
+ id="grid818" /></sodipodi:namedview>
+<style
+ type="text/css"
+ id="style2">
+ .st0{fill:#FFFFFF;}
+</style>
+
+<path
+ 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 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 "
+ transform="translate(-293,384)"
+ id="path853" /><path
+ id="path858"
+ d="m -272,405 -10,-1.17578 V 397 h 10 z M -283,403.70508 -289,403 v -6 h 6 z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ inkscape:connector-curvature="0" /></svg>
\ No newline at end of file
pkg/web/noVNC/app/locale/cs.json
@@ -0,0 +1,71 @@
+{
+ "Connecting...": "Pลipojenรญ...",
+ "Disconnecting...": "Odpojenรญ...",
+ "Reconnecting...": "Obnova pลipojenรญ...",
+ "Internal error": "Vnitลnรญ chyba",
+ "Must set host": "Hostitel musรญ bรฝt nastavenรญ",
+ "Connected (encrypted) to ": "Pลipojenรญ (ลกifrovanรฉ) k ",
+ "Connected (unencrypted) to ": "Pลipojenรญ (neลกifrovanรฉ) k ",
+ "Something went wrong, connection is closed": "Nฤco se pokazilo, odpojeno",
+ "Failed to connect to server": "Chyba pลipojenรญ k serveru",
+ "Disconnected": "Odpojeno",
+ "New connection has been rejected with reason: ": "Novรฉ pลipojenรญ bylo odmรญtnuto s odลฏvodnฤnรญm: ",
+ "New connection has been rejected": "Novรฉ pลipojenรญ bylo odmรญtnuto",
+ "Password is required": "Je vyลพadovรกno heslo",
+ "noVNC encountered an error:": "noVNC narazilo na chybu:",
+ "Hide/Show the control bar": "Skrรฝt/zobrazit ovlรกdacรญ panel",
+ "Move/Drag viewport": "Pลesunout/pลetรกhnout vรฝลez",
+ "viewport drag": "pลesun vรฝลezu",
+ "Active Mouse Button": "Aktivnรญ tlaฤรญtka myลกi",
+ "No mousebutton": "ลฝรกdnรฉ",
+ "Left mousebutton": "Levรฉ tlaฤรญtko myลกi",
+ "Middle mousebutton": "Prostลednรญ tlaฤรญtko myลกi",
+ "Right mousebutton": "Pravรฉ tlaฤรญtko myลกi",
+ "Keyboard": "Klรกvesnice",
+ "Show keyboard": "Zobrazit klรกvesnici",
+ "Extra keys": "Extra klรกvesy",
+ "Show extra keys": "Zobrazit extra klรกvesy",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Pลepnout Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "Pลepnout Alt",
+ "Send Tab": "Odeslat tabulรกtor",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Odeslat Esc",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+ "Send Ctrl-Alt-Del": "Poslat Ctrl-Alt-Del",
+ "Shutdown/Reboot": "Vypnutรญ/Restart",
+ "Shutdown/Reboot...": "Vypnutรญ/Restart...",
+ "Power": "Napรกjenรญ",
+ "Shutdown": "Vypnout",
+ "Reboot": "Restart",
+ "Reset": "Reset",
+ "Clipboard": "Schrรกnka",
+ "Clear": "Vymazat",
+ "Fullscreen": "Celรก obrazovka",
+ "Settings": "Nastavenรญ",
+ "Shared mode": "Sdรญlenรฝ reลพim",
+ "View only": "Pouze prohlรญลพenรญ",
+ "Clip to window": "Pลizpลฏsobit oknu",
+ "Scaling mode:": "Pลizpลฏsobenรญ velikosti",
+ "None": "ลฝรกdnรฉ",
+ "Local scaling": "Mรญstnรญ",
+ "Remote resizing": "Vzdรกlenรฉ",
+ "Advanced": "Pokroฤilรฉ",
+ "Repeater ID:": "ID opakovaฤe",
+ "WebSocket": "WebSocket",
+ "Encrypt": "ล ifrovรกnรญ:",
+ "Host:": "Hostitel:",
+ "Port:": "Port:",
+ "Path:": "Cesta",
+ "Automatic reconnect": "Automatickรก obnova pลipojenรญ",
+ "Reconnect delay (ms):": "Zpoลพdฤnรญ pลipojenรญ (ms)",
+ "Show dot when no cursor": "Teฤka mรญsto chybฤjรญcรญho kurzoru myลกi",
+ "Logging:": "Logovรกnรญ:",
+ "Disconnect": "Odpojit",
+ "Connect": "Pลipojit",
+ "Password:": "Heslo",
+ "Send Password": "Odeslat heslo",
+ "Cancel": "Zruลกit"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/de.json
@@ -0,0 +1,74 @@
+{
+ "Connecting...": "Verbinden...",
+ "Disconnecting...": "Verbindung trennen...",
+ "Reconnecting...": "Verbindung wiederherstellen...",
+ "Internal error": "Interner Fehler",
+ "Must set host": "Richten Sie den Server ein",
+ "Connected (encrypted) to ": "Verbunden mit (verschlรผsselt) ",
+ "Connected (unencrypted) to ": "Verbunden mit (unverschlรผsselt) ",
+ "Something went wrong, connection is closed": "Etwas lief schief, Verbindung wurde getrennt",
+ "Disconnected": "Verbindung zum Server getrennt",
+ "New connection has been rejected with reason: ": "Verbindung wurde aus folgendem Grund abgelehnt: ",
+ "New connection has been rejected": "Verbindung wurde abgelehnt",
+ "Password is required": "Passwort ist erforderlich",
+ "noVNC encountered an error:": "Ein Fehler ist aufgetreten:",
+ "Hide/Show the control bar": "Kontrollleiste verstecken/anzeigen",
+ "Move/Drag viewport": "Ansichtsfenster verschieben/ziehen",
+ "viewport drag": "Ansichtsfenster ziehen",
+ "Active Mouse Button": "Aktive Maustaste",
+ "No mousebutton": "Keine Maustaste",
+ "Left mousebutton": "Linke Maustaste",
+ "Middle mousebutton": "Mittlere Maustaste",
+ "Right mousebutton": "Rechte Maustaste",
+ "Keyboard": "Tastatur",
+ "Show keyboard": "Tastatur anzeigen",
+ "Extra keys": "Zusatztasten",
+ "Show extra keys": "Zusatztasten anzeigen",
+ "Ctrl": "Strg",
+ "Toggle Ctrl": "Strg umschalten",
+ "Alt": "Alt",
+ "Toggle Alt": "Alt umschalten",
+ "Send Tab": "Tab senden",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Escape senden",
+ "Ctrl+Alt+Del": "Strg+Alt+Entf",
+ "Send Ctrl-Alt-Del": "Strg+Alt+Entf senden",
+ "Shutdown/Reboot": "Herunterfahren/Neustarten",
+ "Shutdown/Reboot...": "Herunterfahren/Neustarten...",
+ "Power": "Energie",
+ "Shutdown": "Herunterfahren",
+ "Reboot": "Neustarten",
+ "Reset": "Zurรผcksetzen",
+ "Clipboard": "Zwischenablage",
+ "Clear": "Lรถschen",
+ "Fullscreen": "Vollbild",
+ "Settings": "Einstellungen",
+ "Shared mode": "Geteilter Modus",
+ "View only": "Nur betrachten",
+ "Clip to window": "Auf Fenster begrenzen",
+ "Scaling mode:": "Skalierungsmodus:",
+ "None": "Keiner",
+ "Local scaling": "Lokales skalieren",
+ "Remote resizing": "Serverseitiges skalieren",
+ "Advanced": "Erweitert",
+ "Repeater ID:": "Repeater ID:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Verschlรผsselt",
+ "Host:": "Server:",
+ "Port:": "Port:",
+ "Path:": "Pfad:",
+ "Automatic reconnect": "Automatisch wiederverbinden",
+ "Reconnect delay (ms):": "Wiederverbindungsverzรถgerung (ms):",
+ "Logging:": "Protokollierung:",
+ "Disconnect": "Verbindung trennen",
+ "Connect": "Verbinden",
+ "Password:": "Passwort:",
+ "Cancel": "Abbrechen",
+ "Canvas not supported.": "Canvas nicht unterstรผtzt.",
+ "Disconnect timeout": "Zeitรผberschreitung beim Trennen",
+ "Local Downscaling": "Lokales herunterskalieren",
+ "Local Cursor": "Lokaler Mauszeiger",
+ "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstรผtzt",
+ "True Color": "True Color"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/el.json
@@ -0,0 +1,100 @@
+{
+ "HTTPS is required for full functionality": "ฮคฮฟ HTTPS ฮตฮฏฮฝฮฑฮน ฮฑฯฮฑฮนฯฮฟฯฮผฮตฮฝฮฟ ฮณฮนฮฑ ฯฮปฮฎฯฮท ฮปฮตฮนฯฮฟฯ
ฯฮณฮนฮบฯฯฮทฯฮฑ",
+ "Connecting...": "ฮฃฯ
ฮฝฮดฮญฮตฯฮฑฮน...",
+ "Disconnecting...": "Aฯฮฟฯฯ
ฮฝฮดฮญฮตฯฮฑฮน...",
+ "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": "ฮ ฮฝฮญฮฑ ฯฯฮฝฮดฮตฯฮท ฮฑฯฮฟฯฯฮฏฯฮธฮทฮบฮต ",
+ "Credentials are required": "ฮฯฮฑฮนฯฮฟฯฮฝฯฮฑฮน ฮดฮนฮฑฯฮนฯฯฮตฯ
ฯฮฎฯฮนฮฑ",
+ "noVNC encountered an error:": "ฯฮฟ noVNC ฮฑฮฝฯฮนฮผฮตฯฯฯฮนฯฮต ฮญฮฝฮฑ ฯฯฮฌฮปฮผฮฑ:",
+ "Hide/Show the control bar": "ฮฯฯฮบฯฯ
ฯฮท/ฮฮผฯฮฌฮฝฮนฯฮท ฮณฯฮฑฮผฮผฮฎฯ ฮตฮปฮญฮณฯฮฟฯ
",
+ "Drag": "ฮฃฯฯฯฮนฮผฮฟ",
+ "Move/Drag Viewport": "ฮฮตฯฮฑฮบฮฏฮฝฮทฯฮท/ฮฃฯฯฯฮนฮผฮฟ ฮฮตฮฑฯฮฟฯ ฯฮตฮดฮฏฮฟฯ
",
+ "Keyboard": "ฮ ฮปฮทฮบฯฯฮฟฮปฯฮณฮนฮฟ",
+ "Show Keyboard": "ฮฮผฯฮฌฮฝฮนฯฮท ฮ ฮปฮทฮบฯฯฮฟฮปฮฟฮณฮฏฮฟฯ
",
+ "Extra keys": "ฮฯฮนฯฮปฮญฮฟฮฝ ฯฮปฮฎฮบฯฯฮฑ",
+ "Show Extra Keys": "ฮฮผฯฮฌฮฝฮนฯฮท ฮฯฮนฯฮปฮญฮฟฮฝ ฮ ฮปฮฎฮบฯฯฯฮฝ",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "ฮฮฝฮฑฮปฮปฮฑฮณฮฎ Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "ฮฮฝฮฑฮปฮปฮฑฮณฮฎ Alt",
+ "Toggle 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": "ฮ ฯฯฯฮตฮนฯฮฟ",
+ "Edit clipboard content in the textarea below.": "ฮฯฮตฮพฮตฯฮณฮฑฯฯฮตฮฏฯฮต ฯฮฟ ฯฮตฯฮนฮตฯฯฮผฮตฮฝฮฟ ฯฮฟฯ
ฯฯฯฯฮตฮนฯฮฟฯ
ฯฯฮทฮฝ ฯฮตฯฮนฮฟฯฮฎ ฮบฮตฮนฮผฮญฮฝฮฟฯ
ฯฮฑฯฮฑฮบฮฌฯฯ.",
+ "Full Screen": "ฮ ฮปฮฎฯฮทฯ ฮฮธฯฮฝฮท",
+ "Settings": "ฮกฯ
ฮธฮผฮฏฯฮตฮนฯ",
+ "Shared Mode": "ฮฮฟฮนฮฝฯฯฯฮทฯฯฮท ฮฮตฮนฯฮฟฯ
ฯฮณฮฏฮฑ",
+ "View Only": "ฮฯฮฝฮฟ ฮฮญฮฑฯฮท",
+ "Clip to Window": "ฮฯฮฟฮบฮฟฯฮฎ ฯฯฮฟ ฯฯฮนฮฟ ฯฮฟฯ
ฮ ฮฑฯฮฌฮธฯ
ฯฮฟฯ
",
+ "Scaling Mode:": "ฮฮตฮนฯฮฟฯ
ฯฮณฮฏฮฑ ฮฮปฮนฮผฮฌฮบฯฯฮทฯ:",
+ "None": "ฮฮฑฮผฮฏฮฑ",
+ "Local Scaling": "ฮคฮฟฯฮนฮบฮฎ ฮฮปฮนฮผฮฌฮบฯฯฮท",
+ "Remote Resizing": "ฮฯฮฟฮผฮฑฮบฯฯ
ฯฮผฮญฮฝฮท ฮฮปฮปฮฑฮณฮฎ ฮผฮตฮณฮญฮธฮฟฯ
ฯ",
+ "Advanced": "ฮฮนฮฑ ฯฯฮฟฯฯฯฮทฮผฮญฮฝฮฟฯ
ฯ",
+ "Quality:": "ฮ ฮฟฮนฯฯฮทฯฮฑ:",
+ "Compression level:": "ฮฯฮฏฯฮตฮดฮฟ ฯฯ
ฮผฯฮฏฮตฯฮทฯ:",
+ "Repeater ID:": "Repeater ID:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "ฮฯฯ
ฯฯฮฟฮณฯฮฌฯฮทฯฮท",
+ "Host:": "ฮฮฝฮฟฮผฮฑ ฮดฮนฮฑฮบฮฟฮผฮนฯฯฮฎ:",
+ "Port:": "ฮ ฯฯฯฮฑ ฮดฮนฮฑฮบฮฟฮผฮนฯฯฮฎ:",
+ "Path:": "ฮฮนฮฑฮดฯฮฟฮผฮฎ:",
+ "Automatic Reconnect": "ฮฯ
ฯฯฮผฮฑฯฮท ฮตฯฮฑฮฝฮฑฯฯฮฝฮดฮตฯฮท",
+ "Reconnect Delay (ms):": "ฮฮฑฮธฯ
ฯฯฮญฯฮทฯฮท ฮตฯฮฑฮฝฮฑฯฯฮฝฮดฮตฯฮทฯ (ms):",
+ "Show Dot when No Cursor": "ฮฮผฯฮฌฮฝฮนฯฮท ฮคฮตฮปฮตฮฏฮฑฯ ฯฯฮฑฮฝ ฮดฮตฮฝ ฯ
ฯฮฌฯฯฮตฮน ฮฯฮฟฮผฮญฮฑฯ",
+ "Logging:": "ฮฮฑฯฮฑฮณฯฮฑฯฮฎ:",
+ "Version:": "ฮฮบฮดฮฟฯฮท:",
+ "Disconnect": "ฮฯฮฟฯฯฮฝฮดฮตฯฮท",
+ "Connect": "ฮฃฯฮฝฮดฮตฯฮท",
+ "Server identity": "ฮคฮฑฯ
ฯฯฯฮทฯฮฑ ฮฮนฮฑฮบฮฟฮผฮนฯฯฮฎ",
+ "The server has provided the following identifying information:": "ฮ ฮดฮนฮฑฮบฮฟฮผฮนฯฯฮฎฯ ฯฮฑฯฮตฮฏฯฮต ฯฮทฮฝ ฮฑฮบฯฮปฮฟฯ
ฮธฮท ฯฮปฮทฯฮฟฯฮฟฯฮฏฮฑ ฯฮฑฯ
ฯฮฟฯฮฟฮฏฮทฯฮทฯ:",
+ "Fingerprint:": "ฮฮฑฮบฯฯ
ฮปฮนฮบฯ ฮฑฯฮฟฯฯฯฯฮผฮฑ:",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "ฮ ฮฑฯฮฑฮบฮฑฮปฯ ฮตฯฮฑฮปฮทฮธฮตฯฯฮตฯฮต ฯฯฮน ฮท ฯฮปฮทฯฮฟฯฮฟฯฮฏฮฑ ฮตฮฏฮฝฮฑฮน ฯฯฯฯฮฎ ฮบฮฑฮน ฯฮนฮญฯฯฮต \"ฮฯฮฟฮดฮฟฯฮฎ\". ฮฮปฮปฮนฯฯ ฯฮนฮญฯฯฮต \"ฮฯฯฯฯฮนฯฮท\".",
+ "Approve": "ฮฯฮฟฮดฮฟฯฮฎ",
+ "Reject": "ฮฯฯฯฯฮนฯฮท",
+ "Credentials": "ฮฮนฮฑฯฮนฯฯฮตฯ
ฯฮฎฯฮนฮฑ",
+ "Username:": "ฮฯฮดฮนฮบฯฯ ฮงฯฮฎฯฯฮท:",
+ "Password:": "ฮฯฮดฮนฮบฯฯ ฮ ฯฯฯฮฒฮฑฯฮทฯ:",
+ "Send Credentials": "ฮฯฮฟฯฯฮฟฮปฮฎ ฮฮนฮฑฯฮนฯฯฮตฯ
ฯฮทฯฮฏฯฮฝ",
+ "Cancel": "ฮฮบฯฯฯฯฮท",
+ "Password is required": "ฮฯฮฑฮนฯฮตฮฏฯฮฑฮน ฮฟ ฮบฯฮดฮนฮบฯฯ ฯฯฯฯฮฒฮฑฯฮทฯ",
+ "viewport drag": "ฯฯฯฯฮนฮผฮฟ ฮธฮตฮฑฯฮฟฯ ฯฮตฮดฮฏฮฟฯ
",
+ "Active Mouse Button": "ฮฮฝฮตฯฮณฯ ฮ ฮปฮฎฮบฯฯฮฟ ฮ ฮฟฮฝฯฮนฮบฮนฮฟฯ",
+ "No mousebutton": "ฮงฯฯฮฏฯ ฮ ฮปฮฎฮบฯฯฮฟ ฮ ฮฟฮฝฯฮนฮบฮนฮฟฯ",
+ "Left mousebutton": "ฮฯฮนฯฯฮตฯฯ ฮ ฮปฮฎฮบฯฯฮฟ ฮ ฮฟฮฝฯฮนฮบฮนฮฟฯ",
+ "Middle mousebutton": "ฮฮตฯฮฑฮฏฮฟ ฮ ฮปฮฎฮบฯฯฮฟ ฮ ฮฟฮฝฯฮนฮบฮนฮฟฯ",
+ "Right mousebutton": "ฮฮตฮพฮฏ ฮ ฮปฮฎฮบฯฯฮฟ ฮ ฮฟฮฝฯฮนฮบฮนฮฟฯ",
+ "Clear": "ฮฮฑฮธฮฌฯฮนฯฮผฮฑ",
+ "Canvas not supported.": "ฮฮตฮฝ ฯ
ฯฮฟฯฯฮทฯฮฏฮถฮตฯฮฑฮน ฯฮฟ ฯฯฮฟฮนฯฮตฮฏฮฟ Canvas",
+ "Disconnect timeout": "ฮ ฮฑฯฮญฮปฮตฯ
ฯฮท ฯฯฮฟฮฝฮนฮบฮฟฯ ฮฟฯฮฏฮฟฯ
ฮฑฯฮฟฯฯฮฝฮดฮตฯฮทฯ",
+ "Local Downscaling": "ฮคฮฟฯฮนฮบฮฎ ฮฃฯ
ฯฯฮฏฮบฮฝฯฯฮท",
+ "Local Cursor": "ฮคฮฟฯฮนฮบฯฯ ฮฯฮฟฮผฮญฮฑฯ",
+ "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "ฮฯฮฑฯฮผฮฟฮณฮฎ ฮปฮตฮนฯฮฟฯ
ฯฮณฮฏฮฑฯ ฮฑฯฮฟฮบฮฟฯฮฎฯ ฮฑฯฮฟฯ ฮดฮตฮฝ ฯ
ฯฮฟฯฯฮทฯฮฏฮถฮฟฮฝฯฮฑฮน ฮฟฮน ฮปฯฯฮฏฮดฮตฯ ฮบฯฮปฮนฯฮทฯ ฯฮต ฯฮปฮฎฯฮท ฮฟฮธฯฮฝฮท ฯฯฮฟฮฝ IE",
+ "True Color": "ฮ ฯฮฑฮณฮผฮฑฯฮนฮบฮฌ ฮงฯฯฮผฮฑฯฮฑ",
+ "Style:": "ฮฃฯฯ
ฮป:",
+ "default": "ฯฯฮฟฮตฯฮนฮปฮตฮณฮผฮญฮฝฮฟ",
+ "Apply": "ฮฯฮฑฯฮผฮฟฮณฮฎ",
+ "Connection": "ฮฃฯฮฝฮดฮตฯฮท",
+ "Token:": "ฮฮนฮฑฮบฯฮนฯฮนฮบฯ:",
+ "Send Password": "ฮฯฮฟฯฯฮฟฮปฮฎ ฮฯฮดฮนฮบฮฟฯ ฮ ฯฯฯฮฒฮฑฯฮทฯ"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/es.json
@@ -0,0 +1,68 @@
+{
+ "Connecting...": "Conectando...",
+ "Connected (encrypted) to ": "Conectado (con encriptaciรณn) a",
+ "Connected (unencrypted) to ": "Conectado (sin encriptaciรณn) a",
+ "Disconnecting...": "Desconectando...",
+ "Disconnected": "Desconectado",
+ "Must set host": "Se debe configurar el host",
+ "Reconnecting...": "Reconectando...",
+ "Password is required": "La contraseรฑa es obligatoria",
+ "Disconnect timeout": "Tiempo de desconexiรณn agotado",
+ "noVNC encountered an error:": "noVNC ha encontrado un error:",
+ "Hide/Show the control bar": "Ocultar/Mostrar la barra de control",
+ "Move/Drag viewport": "Mover/Arrastrar la ventana",
+ "viewport drag": "Arrastrar la ventana",
+ "Active Mouse Button": "Botรณn activo del ratรณn",
+ "No mousebutton": "Ningรบn botรณn del ratรณn",
+ "Left mousebutton": "Botรณn izquierdo del ratรณn",
+ "Middle mousebutton": "Botรณn central del ratรณn",
+ "Right mousebutton": "Botรณn derecho del ratรณn",
+ "Keyboard": "Teclado",
+ "Show keyboard": "Mostrar teclado",
+ "Extra keys": "Teclas adicionales",
+ "Show Extra Keys": "Mostrar Teclas Adicionales",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Pulsar/Soltar Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "Pulsar/Soltar Alt",
+ "Send Tab": "Enviar Tabulaciรณn",
+ "Tab": "Tabulaciรณn",
+ "Esc": "Esc",
+ "Send Escape": "Enviar Escape",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+ "Send Ctrl-Alt-Del": "Enviar Ctrl+Alt+Del",
+ "Shutdown/Reboot": "Apagar/Reiniciar",
+ "Shutdown/Reboot...": "Apagar/Reiniciar...",
+ "Power": "Encender",
+ "Shutdown": "Apagar",
+ "Reboot": "Reiniciar",
+ "Reset": "Restablecer",
+ "Clipboard": "Portapapeles",
+ "Clear": "Vaciar",
+ "Fullscreen": "Pantalla Completa",
+ "Settings": "Configuraciones",
+ "Encrypt": "Encriptar",
+ "Shared Mode": "Modo Compartido",
+ "View only": "Solo visualizaciรณn",
+ "Clip to window": "Recortar al tamaรฑo de la ventana",
+ "Scaling mode:": "Modo de escalado:",
+ "None": "Ninguno",
+ "Local Scaling": "Escalado Local",
+ "Local Downscaling": "Reducciรณn de escala local",
+ "Remote resizing": "Cambio de tamaรฑo remoto",
+ "Advanced": "Avanzado",
+ "Local Cursor": "Cursor Local",
+ "Repeater ID:": "ID del Repetidor:",
+ "WebSocket": "WebSocket",
+ "Host:": "Host:",
+ "Port:": "Puerto:",
+ "Path:": "Ruta:",
+ "Automatic reconnect": "Reconexiรณn automรกtica",
+ "Reconnect delay (ms):": "Retraso en la reconexiรณn (ms):",
+ "Logging:": "Registrando:",
+ "Disconnect": "Desconectar",
+ "Connect": "Conectar",
+ "Password:": "Contraseรฑa:",
+ "Cancel": "Cancelar",
+ "Canvas not supported.": "Canvas no soportado."
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/fr.json
@@ -0,0 +1,82 @@
+{
+ "Running without HTTPS is not recommended, crashes or other issues are likely.": "Lancer sans HTTPS n'est pas recommandรฉ, crashs ou autres problรจmes en vue.",
+ "Connecting...": "En cours de connexion...",
+ "Disconnecting...": "Dรฉconnexion en cours...",
+ "Reconnecting...": "Reconnexion en cours...",
+ "Internal error": "Erreur interne",
+ "Failed to connect to server: ": "รchec de connexion au serveur ",
+ "Connected (encrypted) to ": "Connectรฉ (chiffrรฉ) ร ",
+ "Connected (unencrypted) to ": "Connectรฉ (non chiffrรฉ) ร ",
+ "Something went wrong, connection is closed": "Quelque chose s'est mal passรฉ, la connexion a รฉtรฉ fermรฉe",
+ "Failed to connect to server": "รchec de connexion au serveur",
+ "Disconnected": "Dรฉconnectรฉ",
+ "New connection has been rejected with reason: ": "Une nouvelle connexion a รฉtรฉ rejetรฉe avec motif : ",
+ "New connection has been rejected": "Une nouvelle connexion a รฉtรฉ rejetรฉe",
+ "Credentials are required": "Les identifiants sont requis",
+ "noVNC encountered an error:": "noVNC a rencontrรฉ une erreur :",
+ "Hide/Show the control bar": "Masquer/Afficher la barre de contrรดle",
+ "Drag": "Faire glisser",
+ "Move/Drag viewport": "Dรฉplacer la fenรชtre de visualisation",
+ "Keyboard": "Clavier",
+ "Show keyboard": "Afficher le clavier",
+ "Extra keys": "Touches supplรฉmentaires",
+ "Show extra keys": "Afficher les touches supplรฉmentaires",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Basculer Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "Basculer Alt",
+ "Toggle Windows": "Basculer Windows",
+ "Windows": "Fenรชtre",
+ "Send Tab": "Envoyer Tab",
+ "Tab": "Tabulation",
+ "Esc": "Esc",
+ "Send Escape": "Envoyer Escape",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+ "Send Ctrl-Alt-Del": "Envoyer Ctrl-Alt-Del",
+ "Shutdown/Reboot": "Arrรชter/Redรฉmarrer",
+ "Shutdown/Reboot...": "Arrรชter/Redรฉmarrer...",
+ "Power": "Alimentation",
+ "Shutdown": "Arrรชter",
+ "Reboot": "Redรฉmarrer",
+ "Reset": "Rรฉinitialiser",
+ "Clipboard": "Presse-papiers",
+ "Edit clipboard content in the textarea below.": "Editer le contenu du presse-papier dans la zone ci-dessous.",
+ "Full screen": "Plein รฉcran",
+ "Settings": "Paramรจtres",
+ "Shared mode": "Mode partagรฉ",
+ "View only": "Afficher uniquement",
+ "Clip to window": "Ajuster ร la fenรชtre",
+ "Scaling mode:": "Mode mise ร l'รฉchelle :",
+ "None": "Aucun",
+ "Local scaling": "Mise ร l'รฉchelle locale",
+ "Remote resizing": "Redimensionnement ร distance",
+ "Advanced": "Avancรฉ",
+ "Quality:": "Qualitรฉ :",
+ "Compression level:": "Niveau de compression :",
+ "Repeater ID:": "ID Rรฉpรฉteur :",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Chiffrer",
+ "Host:": "Hรดte :",
+ "Port:": "Port :",
+ "Path:": "Chemin :",
+ "Automatic reconnect": "Reconnecter automatiquement",
+ "Reconnect delay (ms):": "Dรฉlai de reconnexion (ms) :",
+ "Show dot when no cursor": "Afficher le point lorsqu'il n'y a pas de curseur",
+ "Logging:": "Se connecter :",
+ "Version:": "Version :",
+ "Disconnect": "Dรฉconnecter",
+ "Connect": "Connecter",
+ "Server identity": "Identitรฉ du serveur",
+ "The server has provided the following identifying information:": "Le serveur a fourni l'identification suivante :",
+ "Fingerprint:": "Empreinte digitale :",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "SVP, verifiez que l'information est correcte et pressez \"Accepter\". Sinon pressez \"Refuser\".",
+ "Approve": "Accepter",
+ "Reject": "Refuser",
+ "Credentials": "Envoyer les identifiants",
+ "Username:": "Nom d'utilisateur :",
+ "Password:": "Mot de passe :",
+ "Send credentials": "Envoyer les identifiants",
+ "Cancel": "Annuler",
+ "Must set host": "Doit dรฉfinir l'hรดte",
+ "Clear": "Effacer"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/hu.json
@@ -0,0 +1,80 @@
+{
+ "Running without HTTPS is not recommended, crashes or other issues are likely.": "HTTPS nรฉlkรผl futtatni nem ajรกnlott, รถsszeomlรกsok vagy mรกs problรฉmรกk vรกrhatรณk.",
+ "Connecting...": "Kapcsolรณdรกs...",
+ "Disconnecting...": "Kapcsolat bontรกsa...",
+ "Reconnecting...": "รjrakapcsolรณdรกs...",
+ "Internal error": "Belsล hiba",
+ "Failed to connect to server: ": "Nem sikerรผlt csatlakozni a szerverhez: ",
+ "Connected (encrypted) to ": "Kapcsolรณdva (titkosรญtva) ehhez: ",
+ "Connected (unencrypted) to ": "Kapcsolรณdva (titkosรญtatlanul) ehhez: ",
+ "Something went wrong, connection is closed": "Valami hiba tรถrtรฉnt, a kapcsolat lezรกrult",
+ "Failed to connect to server": "Nem sikerรผlt csatlakozni a szerverhez",
+ "Disconnected": "Kapcsolat bontva",
+ "New connection has been rejected with reason: ": "Az รบj kapcsolat elutasรญtva, indok: ",
+ "New connection has been rejected": "Az รบj kapcsolat elutasรญtva",
+ "Credentials are required": "Hitelesรญtล adatok szรผksรฉgesek",
+ "noVNC encountered an error:": "A noVNC hibรกt รฉszlelt:",
+ "Hide/Show the control bar": "Vezรฉrlลsรกv elrejtรฉse/megjelenรญtรฉse",
+ "Drag": "Hรบzรกs",
+ "Move/Drag viewport": "Nรฉzet mozgatรกsa/hรบzรกsa",
+ "Keyboard": "Billentyลฑzet",
+ "Show keyboard": "Billentyลฑzet megjelenรญtรฉse",
+ "Extra keys": "Extra billentyลฑk",
+ "Show extra keys": "Extra billentyลฑk megjelenรญtรฉse",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Ctrl lenyomรกsa/felengedรฉse",
+ "Alt": "Alt",
+ "Toggle Alt": "Alt lenyomรกsa/felengedรฉse",
+ "Toggle Windows": "Windows lenyomรกsa/felengedรฉse",
+ "Windows": "Windows",
+ "Send Tab": "Tab kรผldรฉse",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Escape kรผldรฉse",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+ "Send Ctrl-Alt-Del": "Ctrl-Alt-Del kรผldรฉse",
+ "Shutdown/Reboot": "Leรกllรญtรกs/รjraindรญtรกs",
+ "Shutdown/Reboot...": "Leรกllรญtรกs/รjraindรญtรกs...",
+ "Power": "Bekapcsolรกs",
+ "Shutdown": "Leรกllรญtรกs",
+ "Reboot": "รjraindรญtรกs",
+ "Reset": "Reset",
+ "Clipboard": "Vรกgรณlap",
+ "Edit clipboard content in the textarea below.": "Itt tudod mรณdosรญtani a vรกgรณlap tartalmรกt.",
+ "Full screen": "Teljes kรฉpernyล",
+ "Settings": "Beรกllรญtรกsok",
+ "Shared mode": "Megosztott mรณd",
+ "View only": "Csak megtekintรฉs",
+ "Clip to window": "Ablakhoz igazรญtรกs",
+ "Scaling mode:": "Mรฉretezรฉsi mรณd:",
+ "None": "Nincs",
+ "Local scaling": "Helyi mรฉretezรฉs",
+ "Remote resizing": "Tรกvoli รกtmรฉretezรฉs",
+ "Advanced": "Speciรกlis",
+ "Quality:": "Minลsรฉg:",
+ "Compression level:": "Tรถmรถrรญtรฉsi szint:",
+ "Repeater ID:": "Ismรฉtlล azonosรญtรณ:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Titkosรญtรกs",
+ "Host:": "Hoszt:",
+ "Port:": "Port:",
+ "Path:": "รtvonal:",
+ "Automatic reconnect": "Automatikus รบjracsatlakozรกs",
+ "Reconnect delay (ms):": "รjracsatlakozรกs kรฉsleltetรฉse (ms):",
+ "Show dot when no cursor": "Kurzor hiรกnyรกban pont mutatรกsa",
+ "Logging:": "Naplรณzรกs:",
+ "Version:": "Verziรณ:",
+ "Disconnect": "Kapcsolat bontรกsa",
+ "Connect": "Csatlakozรกs",
+ "Server identity": "Szerver azonosรญtรณ",
+ "The server has provided the following identifying information:": "A szerver a kรถvetkezล azonosรญtรณ informรกciรณt adta meg:",
+ "Fingerprint:": "Ujjlenyomat:",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Ellenลrizze, hogy az informรกciรณ helyes-e รฉs nyomja meg a \"Jรณvรกhagyรกs\" gombot. Ellenkezล esetben nyomja meg az \"Elutasรญtรกs\" gombot.",
+ "Approve": "Jรณvรกhagyรกs",
+ "Reject": "Elutasรญtรกs",
+ "Credentials": "Hitelesรญtล adatok",
+ "Username:": "Felhasznรกlรณnรฉv:",
+ "Password:": "Jelszรณ:",
+ "Send credentials": "Hitelesรญtล adatok kรผldรฉse",
+ "Cancel": "Mรฉgse"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/it.json
@@ -0,0 +1,68 @@
+{
+ "Connecting...": "Connessione in corso...",
+ "Disconnecting...": "Disconnessione...",
+ "Reconnecting...": "Riconnessione...",
+ "Internal error": "Errore interno",
+ "Must set host": "Devi impostare l'host",
+ "Connected (encrypted) to ": "Connesso (crittografato) a ",
+ "Connected (unencrypted) to ": "Connesso (non crittografato) a",
+ "Something went wrong, connection is closed": "Qualcosa รจ andato storto, la connessione รจ stata chiusa",
+ "Failed to connect to server": "Impossibile connettersi al server",
+ "Disconnected": "Disconnesso",
+ "New connection has been rejected with reason: ": "La nuova connessione รจ stata rifiutata con motivo: ",
+ "New connection has been rejected": "La nuova connessione รจ stata rifiutata",
+ "Credentials are required": "Le credenziali sono obbligatorie",
+ "noVNC encountered an error:": "noVNC ha riscontrato un errore:",
+ "Hide/Show the control bar": "Nascondi/Mostra la barra di controllo",
+ "Keyboard": "Tastiera",
+ "Show keyboard": "Mostra tastiera",
+ "Extra keys": "Tasti Aggiuntivi",
+ "Show Extra Keys": "Mostra Tasti Aggiuntivi",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Tieni premuto Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "Tieni premuto Alt",
+ "Toggle Windows": "Tieni premuto Windows",
+ "Windows": "Windows",
+ "Send Tab": "Invia Tab",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Invia Esc",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Canc",
+ "Send Ctrl-Alt-Del": "Invia Ctrl-Alt-Canc",
+ "Shutdown/Reboot": "Spegnimento/Riavvio",
+ "Shutdown/Reboot...": "Spegnimento/Riavvio...",
+ "Power": "Alimentazione",
+ "Shutdown": "Spegnimento",
+ "Reboot": "Riavvio",
+ "Reset": "Reset",
+ "Clipboard": "Clipboard",
+ "Clear": "Pulisci",
+ "Fullscreen": "Schermo intero",
+ "Settings": "Impostazioni",
+ "Shared mode": "Modalitร condivisa",
+ "View Only": "Sola Visualizzazione",
+ "Scaling mode:": "Modalitร di ridimensionamento:",
+ "None": "Nessuna",
+ "Local Scaling": "Ridimensionamento Locale",
+ "Remote Resizing": "Ridimensionamento Remoto",
+ "Advanced": "Avanzate",
+ "Quality:": "Qualitร :",
+ "Compression level:": "Livello Compressione:",
+ "Repeater ID:": "ID Ripetitore:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Crittografa",
+ "Host:": "Host:",
+ "Port:": "Porta:",
+ "Path:": "Percorso:",
+ "Automatic Reconnect": "Riconnessione Automatica",
+ "Reconnect Delay (ms):": "Ritardo Riconnessione (ms):",
+ "Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore",
+ "Version:": "Versione:",
+ "Disconnect": "Disconnetti",
+ "Connect": "Connetti",
+ "Username:": "Utente:",
+ "Password:": "Password:",
+ "Send Credentials": "Invia Credenziale",
+ "Cancel": "Annulla"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/ja.json
@@ -0,0 +1,81 @@
+{
+ "Running without HTTPS is not recommended, crashes or other issues are likely.": "HTTPSๆฅ็ถใชใใงๅฎ่กใใใใจใฏๆจๅฅจใใใพใใใใฏใฉใใทใฅใใใใใฎไปใฎๅ้กใ็บ็ใใใใใๅฏ่ฝๆงใใใใพใใ",
+ "Connecting...": "ๆฅ็ถใใฆใใพใ...",
+ "Disconnecting...": "ๅๆญใใฆใใพใ...",
+ "Reconnecting...": "ๅๆฅ็ถใใฆใใพใ...",
+ "Internal error": "ๅ
้จใจใฉใผ",
+ "Must set host": "ใในใใ่จญๅฎใใๅฟ
่ฆใใใใพใ",
+ "Failed to connect to server: ": "ใตใผใใผใธใฎๆฅ็ถใซๅคฑๆใใพใใ: ",
+ "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": "ๆฐ่ฆๆฅ็ถใฏๆๅฆใใใพใใ",
+ "Credentials are required": "่ณๆ ผๆ
ๅ ฑใๅฟ
่ฆใงใ",
+ "noVNC encountered an error:": "noVNC ใงใจใฉใผใ็บ็ใใพใใ:",
+ "Hide/Show the control bar": "ใณใณใใญใผใซใใผใ้ ใ/่กจ็คบใใ",
+ "Drag": "ใใฉใใฐ",
+ "Move/Drag viewport": "ใใฅใผใใผใใ็งปๅ/ใใฉใใฐ",
+ "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": "ใฏใชใใใใผใ",
+ "Edit clipboard content in the textarea below.": "ไปฅไธใฎๅ
ฅๅๆฌใใใฏใชใใใใผใใฎๅ
ๅฎนใ็ทจ้ใงใใพใใ",
+ "Full screen": "ๅ
จ็ป้ข่กจ็คบ",
+ "Settings": "่จญๅฎ",
+ "Shared mode": "ๅ
ฑๆใขใผใ",
+ "View only": "่กจ็คบๅฐ็จ",
+ "Clip to window": "ใฆใฃใณใใฆใซใฏใชใใ",
+ "Scaling mode:": "ในใฑใผใชใณใฐใขใผใ:",
+ "None": "ใชใ",
+ "Local scaling": "ใญใผใซใซใงในใฑใผใชใณใฐ",
+ "Remote resizing": "ใชใขใผใใงใชใตใคใบ",
+ "Advanced": "้ซๅบฆ",
+ "Quality:": "ๅ่ณช:",
+ "Compression level:": "ๅง็ธฎใฌใใซ:",
+ "Repeater ID:": "ใชใใผใฟใผ ID:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "ๆๅทๅ",
+ "Host:": "ใในใ:",
+ "Port:": "ใใผใ:",
+ "Path:": "ใใน:",
+ "Automatic reconnect": "่ชๅๅๆฅ็ถ",
+ "Reconnect delay (ms):": "ๅๆฅ็ถใใ้
ๅปถ (ใใช็ง):",
+ "Show dot when no cursor": "ใซใผใฝใซใใชใใจใใซใใใใ่กจ็คบใใ",
+ "Logging:": "ใญใฎใณใฐ:",
+ "Version:": "ใใผใธใงใณ:",
+ "Disconnect": "ๅๆญ",
+ "Connect": "ๆฅ็ถ",
+ "Server identity": "ใตใผใใผใฎ่ญๅฅๆ
ๅ ฑ",
+ "The server has provided the following identifying information:": "ใตใผใใผใฏไปฅไธใฎ่ญๅฅๆ
ๅ ฑใๆไพใใฆใใพใ:",
+ "Fingerprint:": "ใใฃใณใฌใผใใชใณใ:",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "ใใฎๆ
ๅ ฑใๆญฃใใๅ ดๅใฏใๆฟ่ชใใใใใใงใชใๅ ดๅใฏใๆๅฆใใๆผใใฆใใ ใใใ",
+ "Approve": "ๆฟ่ช",
+ "Reject": "ๆๅฆ",
+ "Credentials": "่ณๆ ผๆ
ๅ ฑ",
+ "Username:": "ใฆใผใถใผๅ:",
+ "Password:": "ใในใฏใผใ:",
+ "Send credentials": "่ณๆ ผๆ
ๅ ฑใ้ไฟก",
+ "Cancel": "ใญใฃใณใปใซ"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/ko.json
@@ -0,0 +1,70 @@
+{
+ "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 ์ผ๊ธฐ/๋๊ธฐ",
+ "Send Tab": "Tab ๋ณด๋ด๊ธฐ",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Esc ๋ณด๋ด๊ธฐ",
+ "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": "์น์์ผ",
+ "Encrypt": "์ํธํ",
+ "Host:": "ํธ์คํธ:",
+ "Port:": "ํฌํธ:",
+ "Path:": "์์น:",
+ "Automatic reconnect": "์๋ ์ฌ์ฐ๊ฒฐ",
+ "Reconnect delay (ms):": "์ฌ์ฐ๊ฒฐ ์ง์ฐ ์๊ฐ (ms)",
+ "Logging:": "๋ก๊น
",
+ "Disconnect": "์ฐ๊ฒฐ ํด์ ",
+ "Connect": "์ฐ๊ฒฐ",
+ "Password:": "๋น๋ฐ๋ฒํธ:",
+ "Send Password": "๋น๋ฐ๋ฒํธ ์ ์ก",
+ "Cancel": "์ทจ์"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/nl.json
@@ -0,0 +1,95 @@
+{
+ "Running without HTTPS is not recommended, crashes or other issues are likely.": "Het is niet aan te raden om zonder HTTPS te werken, crashes of andere problemen zijn dan waarschijnlijk.",
+ "Connecting...": "Aan het verbindenโฆ",
+ "Disconnecting...": "Bezig om verbinding te verbreken...",
+ "Reconnecting...": "Opnieuw verbinding maken...",
+ "Internal error": "Interne fout",
+ "Failed to connect to server: ": "Verbinding maken met server is mislukt",
+ "Connected (encrypted) to ": "Verbonden (versleuteld) met ",
+ "Connected (unencrypted) to ": "Verbonden (onversleuteld) met ",
+ "Something went wrong, connection is closed": "Er iets fout gelopen, verbinding werd verbroken",
+ "Failed to connect to server": "Verbinding maken met server is mislukt",
+ "Disconnected": "Verbinding verbroken",
+ "New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd met de volgende reden: ",
+ "New connection has been rejected": "Nieuwe verbinding is geweigerd",
+ "Credentials are required": "Inloggegevens zijn nodig",
+ "noVNC encountered an error:": "noVNC heeft een fout bemerkt:",
+ "Hide/Show the control bar": "Verberg/Toon de bedieningsbalk",
+ "Drag": "Sleep",
+ "Move/Drag viewport": "Verplaats/Versleep Kijkvenster",
+ "Keyboard": "Toetsenbord",
+ "Show keyboard": "Toon Toetsenbord",
+ "Extra keys": "Extra toetsen",
+ "Show extra keys": "Toon Extra Toetsen",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Ctrl omschakelen",
+ "Alt": "Alt",
+ "Toggle Alt": "Alt omschakelen",
+ "Toggle Windows": "Vensters omschakelen",
+ "Windows": "Vensters",
+ "Send Tab": "Tab Sturen",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Escape Sturen",
+ "Ctrl+Alt+Del": "Ctrl-Alt-Del",
+ "Send Ctrl-Alt-Del": "Ctrl-Alt-Del Sturen",
+ "Shutdown/Reboot": "Uitschakelen/Herstarten",
+ "Shutdown/Reboot...": "Uitschakelen/Herstarten...",
+ "Power": "Systeem",
+ "Shutdown": "Uitschakelen",
+ "Reboot": "Herstarten",
+ "Reset": "Resetten",
+ "Clipboard": "Klembord",
+ "Edit clipboard content in the textarea below.": "Edit de inhoud van het klembord in het tekstveld hieronder",
+ "Full screen": "Volledig Scherm",
+ "Settings": "Instellingen",
+ "Shared mode": "Gedeelde Modus",
+ "View only": "Alleen Kijken",
+ "Clip to window": "Randen buiten venster afsnijden",
+ "Scaling mode:": "Schaalmodus:",
+ "None": "Geen",
+ "Local scaling": "Lokaal Schalen",
+ "Remote resizing": "Op Afstand Formaat Wijzigen",
+ "Advanced": "Geavanceerd",
+ "Quality:": "Kwaliteit:",
+ "Compression level:": "Compressieniveau:",
+ "Repeater ID:": "Repeater ID:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Versleutelen",
+ "Host:": "Host:",
+ "Port:": "Poort:",
+ "Path:": "Pad:",
+ "Automatic reconnect": "Automatisch Opnieuw Verbinden",
+ "Reconnect delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):",
+ "Show dot when no cursor": "Geef stip weer indien geen cursor",
+ "Logging:": "Logmeldingen:",
+ "Version:": "Versie:",
+ "Disconnect": "Verbinding verbreken",
+ "Connect": "Verbinden",
+ "Server identity": "Serveridentiteit",
+ "The server has provided the following identifying information:": "De server geeft de volgende identificerende informatie:",
+ "Fingerprint:": "Vingerafdruk:",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Verifieer dat de informatie is correct en druk โOKโ. Druk anders op โAfwijzenโ.",
+ "Approve": "OK",
+ "Reject": "Afwijzen",
+ "Credentials": "Inloggegevens",
+ "Username:": "Gebruikersnaam:",
+ "Password:": "Wachtwoord:",
+ "Send credentials": "Stuur inloggegevens",
+ "Cancel": "Annuleren",
+ "Must set host": "Host moeten worden ingesteld",
+ "Password is required": "Wachtwoord is vereist",
+ "viewport drag": "kijkvenster slepen",
+ "Active Mouse Button": "Actieve Muisknop",
+ "No mousebutton": "Geen muisknop",
+ "Left mousebutton": "Linker muisknop",
+ "Middle mousebutton": "Middelste muisknop",
+ "Right mousebutton": "Rechter muisknop",
+ "Clear": "Wissen",
+ "Send Password": "Verzend Wachtwoord:",
+ "Disconnect timeout": "Timeout tijdens verbreken van verbinding",
+ "Local Downscaling": "Lokaal Neerschalen",
+ "Local Cursor": "Lokale Cursor",
+ "Canvas not supported.": "Canvas wordt niet ondersteund.",
+ "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-modus in IE niet worden ondersteund"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/pl.json
@@ -0,0 +1,80 @@
+{
+ "Connecting...": "ลฤ
czenie...",
+ "Disconnecting...": "Rozลฤ
czanie...",
+ "Reconnecting...": "ลฤ
czenie...",
+ "Internal error": "Bลฤ
d wewnฤtrzny",
+ "Must set host": "Host i port sฤ
wymagane",
+ "Connected (encrypted) to ": "Poลฤ
czenie (szyfrowane) z ",
+ "Connected (unencrypted) to ": "Poลฤ
czenie (nieszyfrowane) z ",
+ "Something went wrong, connection is closed": "Coล poszลo ลบle, poลฤ
czenie zostaลo zamkniฤte",
+ "Disconnected": "Rozลฤ
czony",
+ "New connection has been rejected with reason: ": "Nowe poลฤ
czenie zostaลo odrzucone z powodu: ",
+ "New connection has been rejected": "Nowe poลฤ
czenie zostaลo odrzucone",
+ "Password is required": "Hasลo jest wymagane",
+ "noVNC encountered an error:": "noVNC napotkaลo bลฤ
d:",
+ "Hide/Show the control bar": "Pokaลผ/Ukryj pasek ustawieล",
+ "Move/Drag Viewport": "Ruszaj/Przeciฤ
gaj Viewport",
+ "viewport drag": "przeciฤ
gnij viewport",
+ "Active Mouse Button": "Aktywny Przycisk Myszy",
+ "No mousebutton": "Brak przycisku myszy",
+ "Left mousebutton": "Lewy przycisk myszy",
+ "Middle mousebutton": "ลrodkowy przycisk myszy",
+ "Right mousebutton": "Prawy przycisk myszy",
+ "Keyboard": "Klawiatura",
+ "Show keyboard": "Pokaลผ klawiaturฤ",
+ "Extra keys": "Przyciski dodatkowe",
+ "Show extra keys": "Pokaลผ przyciski dodatkowe",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Przeลฤ
cz Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "Przeลฤ
cz Alt",
+ "Send Tab": "Wyลlij Tab",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Wyลlij Escape",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+ "Send Ctrl-Alt-Del": "Wyลlij Ctrl-Alt-Del",
+ "Shutdown/Reboot": "Wyลฤ
cz/Uruchom ponownie",
+ "Shutdown/Reboot...": "Wyลฤ
cz/Uruchom ponownie...",
+ "Power": "Wลฤ
czony",
+ "Shutdown": "Wyลฤ
cz",
+ "Reboot": "Uruchom ponownie",
+ "Reset": "Resetuj",
+ "Clipboard": "Schowek",
+ "Clear": "Wyczyลฤ",
+ "Fullscreen": "Peลny ekran",
+ "Settings": "Ustawienia",
+ "Shared Mode": "Tryb Wspรณลdzielenia",
+ "View Only": "Tylko Podglฤ
d",
+ "Clip to Window": "Przytnij do Okna",
+ "Scaling Mode:": "Tryb Skalowania:",
+ "None": "Brak",
+ "Local scaling": "Skalowanie lokalne",
+ "Remote resizing": "Skalowanie zdalne",
+ "Advanced": "Zaawansowane",
+ "Repeater ID:": "ID Repeatera:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Szyfrowanie",
+ "Host:": "Host:",
+ "Port:": "Port:",
+ "Path:": "ลcieลผka:",
+ "Automatic reconnect": "Automatycznie wznawiaj poลฤ
czenie",
+ "Reconnect delay (ms):": "Opรณลบnienie wznawiania (ms):",
+ "Logging:": "Poziom logowania:",
+ "Disconnect": "Rozลฤ
cz",
+ "Connect": "Poลฤ
cz",
+ "Password:": "Hasลo:",
+ "Cancel": "Anuluj",
+ "Canvas not supported.": "Element Canvas nie jest wspierany.",
+ "Disconnect timeout": "Timeout rozลฤ
czenia",
+ "Local Downscaling": "Downscaling lokalny",
+ "Local Cursor": "Lokalny kursor",
+ "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Wymuszam clipping mode poniewaลผ paski przewijania nie sฤ
wspierane przez IE w trybie peลnoekranowym",
+ "True Color": "True Color",
+ "Style:": "Styl:",
+ "default": "domyลlny",
+ "Apply": "Zapisz",
+ "Connection": "Poลฤ
czenie",
+ "Token:": "Token:",
+ "Send Password": "Wyลlij Hasลo"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/pt_BR.json
@@ -0,0 +1,72 @@
+{
+ "Connecting...": "Conectando...",
+ "Disconnecting...": "Desconectando...",
+ "Reconnecting...": "Reconectando...",
+ "Internal error": "Erro interno",
+ "Must set host": "ร necessรกrio definir o host",
+ "Connected (encrypted) to ": "Conectado (com criptografia) a ",
+ "Connected (unencrypted) to ": "Conectado (sem criptografia) a ",
+ "Something went wrong, connection is closed": "Algo deu errado. A conexรฃo foi encerrada.",
+ "Failed to connect to server": "Falha ao conectar-se ao servidor",
+ "Disconnected": "Desconectado",
+ "New connection has been rejected with reason: ": "A nova conexรฃo foi rejeitada pelo motivo: ",
+ "New connection has been rejected": "A nova conexรฃo foi rejeitada",
+ "Credentials are required": "Credenciais sรฃo obrigatรณrias",
+ "noVNC encountered an error:": "O noVNC encontrou um erro:",
+ "Hide/Show the control bar": "Esconder/mostrar a barra de controles",
+ "Drag": "Arrastar",
+ "Move/Drag viewport": "Mover/arrastar a janela",
+ "Keyboard": "Teclado",
+ "Show keyboard": "Mostrar teclado",
+ "Extra keys": "Teclas adicionais",
+ "Show extra keys": "Mostrar teclas adicionais",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Pressionar/soltar Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "Pressionar/soltar Alt",
+ "Toggle Windows": "Pressionar/soltar Windows",
+ "Windows": "Windows",
+ "Send Tab": "Enviar Tab",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Enviar Esc",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+ "Send Ctrl-Alt-Del": "Enviar Ctrl-Alt-Del",
+ "Shutdown/Reboot": "Desligar/reiniciar",
+ "Shutdown/Reboot...": "Desligar/reiniciar...",
+ "Power": "Ligar",
+ "Shutdown": "Desligar",
+ "Reboot": "Reiniciar",
+ "Reset": "Reiniciar (forรงado)",
+ "Clipboard": "รrea de transferรชncia",
+ "Clear": "Limpar",
+ "Fullscreen": "Tela cheia",
+ "Settings": "Configuraรงรตes",
+ "Shared mode": "Modo compartilhado",
+ "View only": "Apenas visualizar",
+ "Clip to window": "Recortar ร janela",
+ "Scaling mode:": "Modo de dimensionamento:",
+ "None": "Nenhum",
+ "Local scaling": "Local",
+ "Remote resizing": "Remoto",
+ "Advanced": "Avanรงado",
+ "Quality:": "Qualidade:",
+ "Compression level:": "Nรญvel de compressรฃo:",
+ "Repeater ID:": "ID do repetidor:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Criptografar",
+ "Host:": "Host:",
+ "Port:": "Porta:",
+ "Path:": "Caminho:",
+ "Automatic reconnect": "Reconexรฃo automรกtica",
+ "Reconnect delay (ms):": "Atraso da reconexรฃo (ms)",
+ "Show dot when no cursor": "Mostrar ponto quando nรฃo hรก cursor",
+ "Logging:": "Registros:",
+ "Version:": "Versรฃo:",
+ "Disconnect": "Desconectar",
+ "Connect": "Conectar",
+ "Username:": "Nome de usuรกrio:",
+ "Password:": "Senha:",
+ "Send credentials": "Enviar credenciais",
+ "Cancel": "Cancelar"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/README
@@ -0,0 +1,1 @@
+DO NOT MODIFY THE FILES IN THIS FOLDER, THEY ARE AUTOMATICALLY GENERATED FROM THE PO-FILES.
pkg/web/noVNC/app/locale/ru.json
@@ -0,0 +1,72 @@
+{
+ "Connecting...": "ะะพะดะบะปััะตะฝะธะต...",
+ "Disconnecting...": "ะัะบะปััะตะฝะธะต...",
+ "Reconnecting...": "ะะตัะตะฟะพะดะบะปััะตะฝะธะต...",
+ "Internal error": "ะะฝัััะตะฝะฝัั ะพัะธะฑะบะฐ",
+ "Must set host": "ะะฐะดะฐะนัะต ะธะผั ัะตัะฒะตัะฐ ะธะปะธ IP",
+ "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": "ะะพะฒะพะต ัะพะตะดะธะฝะตะฝะธะต ะพัะบะปะพะฝะตะฝะพ",
+ "Credentials are required": "ะขัะตะฑััััั ััะตัะฝัะต ะดะฐะฝะฝัะต",
+ "noVNC encountered an error:": "ะัะธะฑะบะฐ noVNC: ",
+ "Hide/Show the control bar": "ะกะบัััั/ะะพะบะฐะทะฐัั ะบะพะฝััะพะปัะฝัั ะฟะฐะฝะตะปั",
+ "Drag": "ะะตัะตะผะตััะธัั",
+ "Move/Drag viewport": "ะะตัะตะผะตััะธัั ะพะบะฝะพ",
+ "Keyboard": "ะะปะฐะฒะธะฐัััะฐ",
+ "Show keyboard": "ะะพะบะฐะทะฐัั ะบะปะฐะฒะธะฐัััั",
+ "Extra keys": "ะะพะฟะพะปะฝะธัะตะปัะฝัะต ะะฝะพะฟะบะธ",
+ "Show Extra Keys": "ะะพะบะฐะทะฐัั ะะพะฟะพะปะฝะธัะตะปัะฝัะต ะะฝะพะฟะบะธ",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "ะะฐะถะฐัั Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "ะะฐะถะฐัั Alt",
+ "Toggle 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": "ะะพะฟะพะปะฝะธัะตะปัะฝะพ",
+ "Quality:": "ะะฐัะตััะฒะพ",
+ "Compression level:": "ะฃัะพะฒะตะฝั ะกะถะฐัะธั",
+ "Repeater ID:": "ะะดะตะฝัะธัะธะบะฐัะพั ID:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "ะจะธััะพะฒะฐะฝะธะต",
+ "Host:": "ะกะตัะฒะตั:",
+ "Port:": "ะะพัั:",
+ "Path:": "ะััั:",
+ "Automatic reconnect": "ะะฒัะพะผะฐัะธัะตัะบะพะต ะฟะตัะตะฟะพะดะบะปััะตะฝะธะต",
+ "Reconnect delay (ms):": "ะะฐะดะตัะถะบะฐ ะฟะตัะตะฟะพะดะบะปััะตะฝะธั (ะผั):",
+ "Show dot when no cursor": "ะะพะบะฐะทะฐัั ัะพัะบั ะฒะผะตััะพ ะบัััะพัะฐ",
+ "Logging:": "ะะพะณ:",
+ "Version:": "ะะตััะธั",
+ "Disconnect": "ะัะบะปััะตะฝะธะต",
+ "Connect": "ะะพะดะบะปััะตะฝะธะต",
+ "Username:": "ะะผั ะะพะปัะทะพะฒะฐัะตะปั",
+ "Password:": "ะะฐัะพะปั:",
+ "Send Credentials": "ะะตัะตะดะฐัะฐ ะฃัะตัะฝัั
ะะฐะฝะฝัั
",
+ "Cancel": "ะัั
ะพะด"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/sv.json
@@ -0,0 +1,83 @@
+{
+ "Running without HTTPS is not recommended, crashes or other issues are likely.": "Det รคr ej rekommenderat att kรถra utan HTTPS, krascher och andra problem รคr troliga.",
+ "Connecting...": "Ansluter...",
+ "Disconnecting...": "Kopplar ner...",
+ "Reconnecting...": "ร
teransluter...",
+ "Internal error": "Internt fel",
+ "Failed to connect to server: ": "Misslyckades att ansluta till servern: ",
+ "Connected (encrypted) to ": "Ansluten (krypterat) till ",
+ "Connected (unencrypted) to ": "Ansluten (okrypterat) till ",
+ "Something went wrong, connection is closed": "Nรฅgot gick fel, anslutningen avslutades",
+ "Failed to connect to server": "Misslyckades att ansluta till servern",
+ "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": "Ny anslutning har blivit nekad",
+ "Credentials are required": "Anvรคndaruppgifter krรคvs",
+ "noVNC encountered an error:": "noVNC stรถtte pรฅ ett problem:",
+ "Hide/Show the control bar": "Gรถm/Visa kontrollbaren",
+ "Drag": "Dra",
+ "Move/Drag viewport": "Flytta/Dra vyn",
+ "Keyboard": "Tangentbord",
+ "Show keyboard": "Visa tangentbord",
+ "Extra keys": "Extraknappar",
+ "Show extra keys": "Visa extraknappar",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Vรคxla Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "Vรคxla Alt",
+ "Toggle Windows": "Vรคxla Windows",
+ "Windows": "Windows",
+ "Send Tab": "Skicka Tab",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Skicka Escape",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+ "Send Ctrl-Alt-Del": "Skicka Ctrl-Alt-Del",
+ "Shutdown/Reboot": "Stรคng av/Boota om",
+ "Shutdown/Reboot...": "Stรคng av/Boota om...",
+ "Power": "Strรถm",
+ "Shutdown": "Stรคng av",
+ "Reboot": "Boota om",
+ "Reset": "ร
terstรคll",
+ "Clipboard": "Urklipp",
+ "Edit clipboard content in the textarea below.": "Redigera urklippets innehรฅll i fรคltet nedan.",
+ "Full screen": "Fullskรคrm",
+ "Settings": "Instรคllningar",
+ "Shared mode": "Delat lรคge",
+ "View only": "Endast visning",
+ "Clip to window": "Begrรคnsa till fรถnster",
+ "Scaling mode:": "Skalningslรคge:",
+ "None": "Ingen",
+ "Local scaling": "Lokal skalning",
+ "Remote resizing": "รndra storlek",
+ "Advanced": "Avancerat",
+ "Quality:": "Kvalitet:",
+ "Compression level:": "Kompressionsnivรฅ:",
+ "Repeater ID:": "Repeater-ID:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Kryptera",
+ "Host:": "Vรคrd:",
+ "Port:": "Port:",
+ "Path:": "Sรถkvรคg:",
+ "Automatic reconnect": "Automatisk รฅteranslutning",
+ "Reconnect delay (ms):": "Fรถrdrรถjning (ms):",
+ "Show dot when no cursor": "Visa prick nรคr ingen muspekare finns",
+ "Logging:": "Loggning:",
+ "Version:": "Version:",
+ "Disconnect": "Koppla frรฅn",
+ "Connect": "Anslut",
+ "Server identity": "Server-identitet",
+ "The server has provided the following identifying information:": "Servern har gett fรถljande identifierande information:",
+ "Fingerprint:": "Fingeravtryck:",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Kontrollera att informationen รคr korrekt och tryck sedan \"Godkรคnn\". Tryck annars \"Neka\".",
+ "Approve": "Godkรคnn",
+ "Reject": "Neka",
+ "Credentials": "Anvรคndaruppgifter",
+ "Username:": "Anvรคndarnamn:",
+ "Password:": "Lรถsenord:",
+ "Send credentials": "Skicka anvรคndaruppgifter",
+ "Cancel": "Avbryt",
+ "Must set host": "Du mรฅste specifiera en vรคrd",
+ "HTTPS is required for full functionality": "HTTPS krรคvs fรถr full funktionalitet",
+ "Clear": "Rensa"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/tr.json
@@ -0,0 +1,69 @@
+{
+ "Connecting...": "Baฤlanฤฑyor...",
+ "Disconnecting...": "Baฤlantฤฑ kesiliyor...",
+ "Reconnecting...": "Yeniden baฤlantฤฑ kuruluyor...",
+ "Internal error": "ฤฐรง hata",
+ "Must set host": "Sunucuyu kur",
+ "Connected (encrypted) to ": "Baฤlฤฑ (ลifrelenmiล)",
+ "Connected (unencrypted) to ": "Baฤlandฤฑ (ลifrelenmemiล)",
+ "Something went wrong, connection is closed": "Bir ลeyler ters gitti, baฤlantฤฑ kesildi",
+ "Disconnected": "Baฤlantฤฑ kesildi",
+ "New connection has been rejected with reason: ": "Baฤlantฤฑ aลaฤฤฑdaki nedenlerden dolayฤฑ reddedildi: ",
+ "New connection has been rejected": "Baฤlantฤฑ reddedildi",
+ "Password is required": "ลifre gerekli",
+ "noVNC encountered an error:": "Bir hata oluลtu:",
+ "Hide/Show the control bar": "Denetim masasฤฑnฤฑ Gizle/Gรถster",
+ "Move/Drag Viewport": "Gรถrรผnรผmรผ Taลฤฑ/Sรผrรผkle",
+ "viewport drag": "Gรถrรผntรผ penceresini sรผrรผkle",
+ "Active Mouse Button": "Aktif Fare Dรผฤmesi",
+ "No mousebutton": "Fare dรผฤmesi yok",
+ "Left mousebutton": "Farenin sol dรผฤmesi",
+ "Middle mousebutton": "Farenin orta dรผฤmesi",
+ "Right mousebutton": "Farenin saฤ dรผฤmesi",
+ "Keyboard": "Klavye",
+ "Show Keyboard": "Klavye Dรผzenini Gรถster",
+ "Extra keys": "Ekstra tuลlar",
+ "Show extra keys": "Ekstra tuลlarฤฑ gรถster",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Ctrl Deฤiลtir ",
+ "Alt": "Alt",
+ "Toggle Alt": "Alt Deฤiลtir",
+ "Send Tab": "Sekme Gรถnder",
+ "Tab": "Sekme",
+ "Esc": "Esc",
+ "Send Escape": "Boลluk Gรถnder",
+ "Ctrl+Alt+Del": "Ctrl + Alt + Del",
+ "Send Ctrl-Alt-Del": "Ctrl-Alt-Del Gรถnder",
+ "Shutdown/Reboot": "Kapat/Yeniden Baลlat",
+ "Shutdown/Reboot...": "Kapat/Yeniden Baลlat...",
+ "Power": "Gรผรง",
+ "Shutdown": "Kapat",
+ "Reboot": "Yeniden Baลlat",
+ "Reset": "Sฤฑfฤฑrla",
+ "Clipboard": "Pano",
+ "Clear": "Temizle",
+ "Fullscreen": "Tam Ekran",
+ "Settings": "Ayarlar",
+ "Shared Mode": "Paylaลฤฑm Modu",
+ "View Only": "Sadece Gรถrรผntรผle",
+ "Clip to Window": "Pencereye Tฤฑkla",
+ "Scaling Mode:": "รlรงekleme Modu:",
+ "None": "Bilinmeyen",
+ "Local Scaling": "Yerel รlรงeklendirme",
+ "Remote Resizing": "Uzaktan Yeniden Boyutlandฤฑrma",
+ "Advanced": "Geliลmiล",
+ "Repeater ID:": "Tekralayฤฑcฤฑ ID:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "ลifrele",
+ "Host:": "Ana makine:",
+ "Port:": "Port:",
+ "Path:": "Yol:",
+ "Automatic Reconnect": "Otomatik Yeniden Baฤlan",
+ "Reconnect Delay (ms):": "Yeniden Baฤlanma Sรผreci (ms):",
+ "Logging:": "Giriล yapฤฑlฤฑyor:",
+ "Disconnect": "Bagฬlantฤฑyฤฑ Kes",
+ "Connect": "Bagฬlan",
+ "Password:": "Parola:",
+ "Cancel": "Vazgeรง",
+ "Canvas not supported.": "Tuval desteklenmiyor."
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/uk.json
@@ -0,0 +1,81 @@
+{
+ "Running without HTTPS is not recommended, crashes or other issues are likely.": "ะ ะพะฑะพัะฐ ะฑะตะท HTTPS ะฝะต ัะตะบะพะผะตะฝะดัััััั, ะนะผะพะฒััะฝั ะทะฑะพั ัะธ ัะฝัั ะฟัะพะฑะปะตะผะธ.",
+ "Connecting...": "ะ'ัะดะฝะฐะฝะฝั...",
+ "Disconnecting...": "ะัะด'ัะดะฝะฐะฝะฝั...",
+ "Reconnecting...": "ะะตัะตะท'ัะดะฝะฐะฝะฝั...",
+ "Internal error": "ะะฝัััััะฝั ะฟะพะผะธะปะบะฐ",
+ "Failed to connect to server: ": "ะะต ะฒะดะฐะปะพัั ะท'ัะดะฝะฐัะธัั ะท ัะตัะฒะตัะพะผ: ",
+ "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": "ะะพะฒะต ะท'ัะดะฝะฐะฝะฝั ะฒัะดั
ะธะปะตะฝะพ",
+ "Are you sure you want to disconnect the session?": "ะขะพัะฝะพ ะฒัะด'ัะดะฝะฐัะธ ัะตะฐะฝั?",
+ "Credentials are required": "ะขัะตะฑะฐ ะพัะพะฑะพะฒั ะดะฐะฝั",
+ "noVNC encountered an error:": "ะะพะผะธะปะบะฐ noVNC:",
+ "Hide/Show the control bar": "ะกั
ะพะฒะฐัะธ/ะฟะพะบะฐะทะฐัะธ ะฟะฐะฝะตะปั ะบะตััะฒะฐะฝะฝั",
+ "Drag": "ะะพััะฝััะธ",
+ "Move/Drag viewport": "ะะผัััะธัะธ ะพะฑะปะฐััั ะพะณะปัะดั",
+ "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": "ะััะตั ะพะฑะผัะฝั",
+ "Edit clipboard content in the textarea below.": "ะ ะตะดะฐะณัะนัะต ะฒะผััั ะฑััะตัะฐ ะพะฑะผัะฝั ะฒ ัะตะบััะพะฒัะน ะทะพะฝั ะฒะฝะธะทั.",
+ "Full screen": "ะะพะฒะฝะธะน ะตะบัะฐะฝ",
+ "Settings": "ะะฐัะฐะผะตััะธ",
+ "Shared mode": "ะกะฟัะปัะฝะธะน ัะตะถะธะผ",
+ "View only": "ะะธัะต ะฟะตัะตะณะปัะด",
+ "Clip to window": "ะะพ ัะพะทะผัััะฒ ะฒัะบะฝะฐ",
+ "Scaling mode:": "ะ ะตะถะธะผ ะผะฐัััะฐะฑัะฒะฐะฝะฝั:",
+ "None": "ะะธะผะบะฝะตะฝะพ",
+ "Local scaling": "ะะพะบะฐะปัะฝะต ะผะฐัััะฐะฑัะฒะฐะฝะฝั",
+ "Remote resizing": "ะัะดะดะฐะปะตะฝะต ะผะฐัััะฐะฑัะฒะฐะฝะฝั",
+ "Advanced": "ะะพะดะฐัะบะพะฒะพ",
+ "Quality:": "ะฏะบัััั:",
+ "Compression level:": "ะ ัะฒะตะฝั ััะธัะฝะตะฝะฝั:",
+ "Repeater ID:": "ะะดะตะฝัะธััะบะฐัะพั ัะตะฟััะตัะฐ:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "ะจะธัััะฒะฐะฝะฝั",
+ "Host:": "ะกะตัะฒะตั:",
+ "Port:": "ะะพัั:",
+ "Path:": "ะจะปัั
:",
+ "Automatic reconnect": "ะะฒัะพะผะฐัะธัะฝะต ะฟะตัะตะท'ัะดะฝะฐะฝะฝั",
+ "Reconnect delay (ms):": "ะะฐััะธะผะบะฐ ะฟะตัะตะท'ัะดะฝะฐะฝะฝั (ะผั):",
+ "Show dot when no cursor": "ะะพะบะฐะทัะฒะฐัะธ ะบัะฐะฟะบั, ะบะพะปะธ ะฝะตะผะฐ ะบัััะพัะฐ",
+ "Logging:": "ะััะฝะฐะป:",
+ "Version:": "ะะตัััั:",
+ "Disconnect": "ะัะด'ัะดะฝะฐัะธ",
+ "Connect": "ะ'ัะดะฝะฐัะธ",
+ "Server identity": "ะะดะตะฝัะธััะบะฐััั ัะตัะฒะตัะฐ",
+ "The server has provided the following identifying information:": "ะกะตัะฒะตั ะฝะฐะดะฐั ัะฐะบั ัะดะตะฝัะธััะบะฐััะนะฝั ะดะฐะฝั:",
+ "Fingerprint:": "ะัะดะฑะธัะพะบ:",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "ะะตัะตะฒัััะต, ัะธ ะดะฐะฝั ะบะพัะตะบัะฝั, ะน ะฝะฐัะธัะฝััั ยซะกั
ะฒะฐะปะธัะธยป. ะะฝะฐะบัะต ะฝะฐัะธัะฝััั ยซะัะดั
ะธะปะธัะธยป.",
+ "Approve": "ะกั
ะฒะฐะปะธัะธ",
+ "Reject": "ะัะดั
ะธะปะธัะธ",
+ "Credentials": "ะัะพะฑะพะฒั ะดะฐะฝั",
+ "Username:": "ะะพัะธัััะฒะฐััะบะต ัะผ'ั:",
+ "Password:": "ะะฐัะพะปั:",
+ "Send credentials": "ะะฐะดััะปะฐัะธ ะพัะพะฑะพะฒั ะดะฐะฝั",
+ "Cancel": "ะกะบะฐััะฒะฐัะธ"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/zh_CN.json
@@ -0,0 +1,93 @@
+{
+ "Running without HTTPS is not recommended, crashes or other issues are likely.": "ไธๅปบ่ฎฎๅจๆฒกๆ HTTPS ็ๆ
ๅตไธ่ฟ่ก๏ผๅฏ่ฝไผๅบ็ฐๅดฉๆบๆๅบ็ฐๅ
ถไป้ฎ้ขใ",
+ "Connecting...": "่ฟๆฅไธญ...",
+ "Disconnecting...": "ๆญฃๅจๆญๅผ่ฟๆฅ...",
+ "Reconnecting...": "้ๆฐ่ฟๆฅไธญ...",
+ "Internal error": "ๅ
้จ้่ฏฏ",
+ "Must set host": "ๅฟ
้กป่ฎพ็ฝฎไธปๆบ",
+ "Failed to connect to server: ": "ๆ ๆณ่ฟๆฅๅฐๆๅกๅจ๏ผ",
+ "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": "ๆฐ่ฟๆฅๅทฒ่ขซๆ็ป",
+ "Credentials are required": "้่ฆๅญ่ฏ",
+ "noVNC encountered an error:": "noVNC ้ๅฐไธไธช้่ฏฏ๏ผ",
+ "Hide/Show the control bar": "ๆพ็คบ/้่ๆงๅถๆ ",
+ "Drag": "ๆๅจ",
+ "Move/Drag viewport": "็งปๅจ/ๆๅจ็ชๅฃ",
+ "Keyboard": "้ฎ็",
+ "Show keyboard": "ๆพ็คบ้ฎ็",
+ "Extra keys": "้ขๅคๆ้ฎ",
+ "Show extra keys": "ๆพ็คบ้ขๅคๆ้ฎ",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "ๅๆข Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "ๅๆข Alt",
+ "Toggle 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": "ๅช่ดดๆฟ",
+ "Edit clipboard content in the textarea below.": "ๅจไธ้ข็ๆๆฌๅบๅไธญ็ผ่พๅช่ดดๆฟๅ
ๅฎนใ",
+ "Full screen": "ๅ
จๅฑ",
+ "Settings": "่ฎพ็ฝฎ",
+ "Shared mode": "ๅไบซๆจกๅผ",
+ "View only": "ไป
ๆฅ็",
+ "Clip to window": "้ๅถ/่ฃๅ็ชๅฃๅคงๅฐ",
+ "Scaling mode:": "็ผฉๆพๆจกๅผ๏ผ",
+ "None": "ๆ ",
+ "Local scaling": "ๆฌๅฐ็ผฉๆพ",
+ "Remote resizing": "่ฟ็จ่ฐๆดๅคงๅฐ",
+ "Advanced": "้ซ็บง",
+ "Quality:": "ๅ่ดจ๏ผ",
+ "Compression level:": "ๅ็ผฉ็บงๅซ๏ผ",
+ "Repeater ID:": "ไธญ็ปง็ซ ID",
+ "WebSocket": "WebSocket",
+ "Encrypt": "ๅ ๅฏ",
+ "Host:": "ไธปๆบ๏ผ",
+ "Port:": "็ซฏๅฃ๏ผ",
+ "Path:": "่ทฏๅพ๏ผ",
+ "Automatic reconnect": "่ชๅจ้ๆฐ่ฟๆฅ",
+ "Reconnect delay (ms):": "้ๆฐ่ฟๆฅ้ด้ (ms)๏ผ",
+ "Show dot when no cursor": "ๆ ๅ
ๆ ๆถๆพ็คบ็น",
+ "Logging:": "ๆฅๅฟ็บงๅซ๏ผ",
+ "Version:": "็ๆฌ๏ผ",
+ "Disconnect": "ๆญๅผ่ฟๆฅ",
+ "Connect": "่ฟๆฅ",
+ "Server identity": "ๆๅกๅจ่บซไปฝ",
+ "The server has provided the following identifying information:": "ๆๅกๅจๆไพไบไปฅไธ่ฏๅซไฟกๆฏ๏ผ",
+ "Fingerprint:": "ๆ็บน๏ผ",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "่ฏทๆ ธๅฎไฟกๆฏๆฏๅฆๆญฃ็กฎ๏ผๅนถๆ โๅๆโ๏ผๅฆๅๆ โๆ็ปโใ",
+ "Approve": "ๅๆ",
+ "Reject": "ๆ็ป",
+ "Credentials": "ๅญ่ฏ",
+ "Username:": "็จๆทๅ:",
+ "Password:": "ๅฏ็ ๏ผ",
+ "Send credentials": "ๅ้ๅญ่ฏ",
+ "Cancel": "ๅๆถ",
+ "Password is required": "่ฏทๆไพๅฏ็ ",
+ "Disconnect timeout": "่ถ
ๆถๆญๅผ",
+ "viewport drag": "็ชๅฃๆๅจ",
+ "Active Mouse Button": "ๅฏๅจ้ผ ๆ ๆ้ฎ",
+ "No mousebutton": "็ฆ็จ้ผ ๆ ๆ้ฎ",
+ "Left mousebutton": "้ผ ๆ ๅทฆ้ฎ",
+ "Middle mousebutton": "้ผ ๆ ไธญ้ฎ",
+ "Right mousebutton": "้ผ ๆ ๅณ้ฎ",
+ "Clear": "ๆธ
้ค",
+ "Local Downscaling": "้ไฝๆฌๅฐๅฐบๅฏธ",
+ "Local Cursor": "ๆฌๅฐๅ
ๆ ",
+ "Canvas not supported.": "ไธๆฏๆ Canvasใ"
+}
\ No newline at end of file
pkg/web/noVNC/app/locale/zh_TW.json
@@ -0,0 +1,69 @@
+{
+ "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",
+ "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):": "้ๆฐ้ฃ็ท้้ (ms)๏ผ",
+ "Logging:": "ๆฅ่ช็ดๅฅ๏ผ",
+ "Disconnect": "ไธญๆท้ฃ็ท",
+ "Connect": "้ฃ็ท",
+ "Password:": "ๅฏ็ขผ๏ผ",
+ "Cancel": "ๅๆถ"
+}
\ No newline at end of file
pkg/web/noVNC/app/sounds/bell.mp3
Binary file
pkg/web/noVNC/app/sounds/bell.oga
Binary file
pkg/web/noVNC/app/sounds/CREDITS
@@ -0,0 +1,4 @@
+bell
+ Copyright: Dr. Richard Boulanger et al
+ URL: http://www.archive.org/details/Berklee44v12
+ License: CC-BY Attribution 3.0 Unported
pkg/web/noVNC/app/styles/base.css
@@ -0,0 +1,927 @@
+/*
+ * noVNC base CSS
+ * Copyright (C) 2019 The noVNC authors
+ * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
+ * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
+ */
+
+/*
+ * Z index layers:
+ *
+ * 0: Main screen
+ * 10: Control bar
+ * 50: Transition blocker
+ * 60: Connection popups
+ * 100: Status bar
+ * ...
+ * 1000: Javascript crash
+ * ...
+ * 10000: Max (used for polyfills)
+ */
+
+/*
+ * State variables (set on :root):
+ *
+ * noVNC_loading: Page is still loading
+ * noVNC_connecting: Connecting to server
+ * noVNC_reconnecting: Re-establishing a connection
+ * noVNC_connected: Connected to server (most common state)
+ * noVNC_disconnecting: Disconnecting from server
+ */
+
+:root {
+ font-family: sans-serif;
+ line-height: 1.6;
+}
+
+body {
+ margin:0;
+ padding:0;
+ /*Background image with light grey curve.*/
+ background-color:#494949;
+ background-repeat:no-repeat;
+ background-position:right bottom;
+ height:100%;
+ touch-action: none;
+}
+
+html {
+ height:100%;
+}
+
+.noVNC_only_touch.noVNC_hidden {
+ display: none;
+}
+
+.noVNC_disabled {
+ color: var(--novnc-grey);
+}
+
+/* ----------------------------------------
+ * Spinner
+ * ----------------------------------------
+ */
+
+.noVNC_spinner {
+ position: relative;
+}
+.noVNC_spinner, .noVNC_spinner::before, .noVNC_spinner::after {
+ width: 10px;
+ height: 10px;
+ border-radius: 2px;
+ box-shadow: -60px 10px 0 rgba(255, 255, 255, 0);
+ animation: noVNC_spinner 1.0s linear infinite;
+}
+.noVNC_spinner::before {
+ content: "";
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ animation-delay: -0.1s;
+}
+.noVNC_spinner::after {
+ content: "";
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ animation-delay: 0.1s;
+}
+@keyframes noVNC_spinner {
+ 0% { box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); width: 20px; }
+ 25% { box-shadow: 20px 10px 0 rgba(255, 255, 255, 1); width: 10px; }
+ 50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; }
+}
+
+/* ----------------------------------------
+ * WebKit centering hacks
+ * ----------------------------------------
+ */
+
+.noVNC_center {
+ /*
+ * This is a workaround because webkit misrenders transforms and
+ * uses non-integer coordinates, resulting in blurry content.
+ * Ideally we'd use "top: 50%; transform: translateY(-50%);" on
+ * the objects instead.
+ */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
+.noVNC_center > * {
+ pointer-events: auto;
+}
+.noVNC_vcenter {
+ display: flex !important;
+ flex-direction: column;
+ justify-content: center;
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100%;
+ margin: 0 !important;
+ padding: 0 !important;
+ pointer-events: none;
+}
+.noVNC_vcenter > * {
+ pointer-events: auto;
+}
+
+/* ----------------------------------------
+ * Layering
+ * ----------------------------------------
+ */
+
+.noVNC_connect_layer {
+ z-index: 60;
+}
+
+/* ----------------------------------------
+ * Fallback error
+ * ----------------------------------------
+ */
+
+#noVNC_fallback_error {
+ z-index: 1000;
+ visibility: hidden;
+ /* Put a dark background in front of everything but the error,
+ and don't let mouse events pass through */
+ background: rgba(0, 0, 0, 0.8);
+ pointer-events: all;
+}
+#noVNC_fallback_error.noVNC_open {
+ visibility: visible;
+}
+
+#noVNC_fallback_error > div {
+ max-width: calc(100vw - 30px - 30px);
+ max-height: calc(100vh - 30px - 30px);
+ overflow: auto;
+
+ padding: 15px;
+
+ transition: 0.5s ease-in-out;
+
+ transform: translateY(-50px);
+ opacity: 0;
+
+ text-align: center;
+ font-weight: bold;
+ color: #fff;
+
+ border-radius: 12px;
+ box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+ background: rgba(200,55,55,0.8);
+}
+#noVNC_fallback_error.noVNC_open > div {
+ transform: translateY(0);
+ opacity: 1;
+}
+
+#noVNC_fallback_errormsg {
+ font-weight: normal;
+}
+
+#noVNC_fallback_errormsg .noVNC_message {
+ display: inline-block;
+ text-align: left;
+ font-family: monospace;
+ white-space: pre-wrap;
+}
+
+#noVNC_fallback_error .noVNC_location {
+ font-style: italic;
+ font-size: 0.8em;
+ color: rgba(255, 255, 255, 0.8);
+}
+
+#noVNC_fallback_error .noVNC_stack {
+ padding: 10px;
+ margin: 10px;
+ font-size: 0.8em;
+ text-align: left;
+ font-family: monospace;
+ white-space: pre;
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ background: rgba(0, 0, 0, 0.2);
+ overflow: auto;
+}
+
+/* ----------------------------------------
+ * Control bar
+ * ----------------------------------------
+ */
+
+#noVNC_control_bar_anchor {
+ /* The anchor is needed to get z-stacking to work */
+ position: fixed;
+ z-index: 10;
+
+ transition: 0.5s ease-in-out;
+
+ /* Edge misrenders animations wihthout this */
+ transform: translateX(0);
+}
+:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {
+ opacity: 0.8;
+}
+#noVNC_control_bar_anchor.noVNC_right {
+ left: auto;
+ right: 0;
+}
+
+#noVNC_control_bar {
+ position: relative;
+ left: -100%;
+
+ transition: 0.5s ease-in-out;
+
+ background-color: var(--novnc-blue);
+ border-radius: 0 12px 12px 0;
+
+ user-select: none;
+ -webkit-user-select: none;
+ -webkit-touch-callout: none; /* Disable iOS image long-press popup */
+}
+#noVNC_control_bar.noVNC_open {
+ box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+ left: 0;
+}
+#noVNC_control_bar::before {
+ /* This extra element is to get a proper shadow */
+ content: "";
+ position: absolute;
+ z-index: -1;
+ height: 100%;
+ width: 30px;
+ left: -30px;
+ transition: box-shadow 0.5s ease-in-out;
+}
+#noVNC_control_bar.noVNC_open::before {
+ box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+}
+.noVNC_right #noVNC_control_bar {
+ left: 100%;
+ border-radius: 12px 0 0 12px;
+}
+.noVNC_right #noVNC_control_bar.noVNC_open {
+ left: 0;
+}
+.noVNC_right #noVNC_control_bar::before {
+ visibility: hidden;
+}
+
+#noVNC_control_bar_handle {
+ position: absolute;
+ left: -15px;
+ top: 0;
+ transform: translateY(35px);
+ width: calc(100% + 30px);
+ height: 50px;
+ z-index: -1;
+ cursor: pointer;
+ border-radius: 6px;
+ background-color: var(--novnc-darkblue);
+ background-image: url("../images/handle_bg.svg");
+ background-repeat: no-repeat;
+ background-position: right;
+ box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);
+}
+#noVNC_control_bar_handle:after {
+ content: "";
+ transition: transform 0.5s ease-in-out;
+ background: url("../images/handle.svg");
+ position: absolute;
+ top: 22px; /* (50px-6px)/2 */
+ right: 5px;
+ width: 5px;
+ height: 6px;
+}
+#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
+ transform: translateX(1px) rotate(180deg);
+}
+:root:not(.noVNC_connected) #noVNC_control_bar_handle {
+ display: none;
+}
+.noVNC_right #noVNC_control_bar_handle {
+ background-position: left;
+}
+.noVNC_right #noVNC_control_bar_handle:after {
+ left: 5px;
+ right: 0;
+ transform: translateX(1px) rotate(180deg);
+}
+.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
+ transform: none;
+}
+/* Larger touch area for the handle, used when a touch screen is available */
+#noVNC_control_bar_handle div {
+ position: absolute;
+ right: -35px;
+ top: 0;
+ width: 50px;
+ height: 100%;
+ display: none;
+}
+@media (any-pointer: coarse) {
+ #noVNC_control_bar_handle div {
+ display: initial;
+ }
+}
+.noVNC_right #noVNC_control_bar_handle div {
+ left: -35px;
+ right: auto;
+}
+
+#noVNC_control_bar > .noVNC_scroll {
+ max-height: 100vh; /* Chrome is buggy with 100% */
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding: 0 10px;
+}
+
+#noVNC_control_bar > .noVNC_scroll > * {
+ display: block;
+ margin: 10px auto;
+}
+
+/* Control bar hint */
+#noVNC_hint_anchor {
+ position: fixed;
+ right: -50px;
+ left: auto;
+}
+#noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor {
+ left: -50px;
+ right: auto;
+}
+#noVNC_control_bar_hint {
+ position: relative;
+ transform: scale(0);
+ width: 100px;
+ height: 50%;
+ max-height: 600px;
+
+ visibility: hidden;
+ opacity: 0;
+ transition: 0.2s ease-in-out;
+ background: transparent;
+ box-shadow: 0 0 10px black, inset 0 0 10px 10px var(--novnc-darkblue);
+ border-radius: 12px;
+ transition-delay: 0s;
+}
+#noVNC_control_bar_hint.noVNC_active {
+ visibility: visible;
+ opacity: 1;
+ transition-delay: 0.2s;
+ transform: scale(1);
+}
+#noVNC_control_bar_hint.noVNC_notransition {
+ transition: none !important;
+}
+
+/* Control bar buttons */
+#noVNC_control_bar .noVNC_button {
+ min-width: unset;
+ padding: 4px 4px;
+ vertical-align: middle;
+ border:1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 6px;
+ background-color: transparent;
+}
+#noVNC_control_bar .noVNC_button.noVNC_selected {
+ border-color: rgba(0, 0, 0, 0.8);
+ background-color: rgba(0, 0, 0, 0.5);
+}
+#noVNC_control_bar .noVNC_button.noVNC_hidden {
+ display: none !important;
+}
+
+/* Panels */
+.noVNC_panel {
+ transform: translateX(25px);
+
+ transition: 0.5s ease-in-out;
+
+ box-sizing: border-box; /* so max-width don't have to care about padding */
+ max-width: calc(100vw - 75px - 25px); /* minus left and right margins */
+ max-height: 100vh; /* Chrome is buggy with 100% */
+ overflow-x: hidden;
+ overflow-y: auto;
+
+ visibility: hidden;
+ opacity: 0;
+
+ padding: 15px;
+
+ background: #fff;
+ border-radius: 12px;
+ color: #000;
+ border: 2px solid #E0E0E0;
+ box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+}
+.noVNC_panel.noVNC_open {
+ visibility: visible;
+ opacity: 1;
+ transform: translateX(75px);
+}
+.noVNC_right .noVNC_vcenter {
+ left: auto;
+ right: 0;
+}
+.noVNC_right .noVNC_panel {
+ transform: translateX(-25px);
+}
+.noVNC_right .noVNC_panel.noVNC_open {
+ transform: translateX(-75px);
+}
+
+.noVNC_panel > * {
+ display: block;
+ margin: 10px auto;
+}
+.noVNC_panel > *:first-child {
+ margin-top: 0 !important;
+}
+.noVNC_panel > *:last-child {
+ margin-bottom: 0 !important;
+}
+
+.noVNC_panel hr {
+ border: none;
+ border-top: 1px solid var(--novnc-lightgrey);
+ width: 100%; /* <hr> inside a flexbox will otherwise be 0px wide */
+}
+
+.noVNC_panel label {
+ display: block;
+ white-space: nowrap;
+ margin: 5px;
+}
+@media (max-width: 540px) {
+ /* Allow wrapping on small screens */
+ .noVNC_panel label {
+ white-space: unset;
+ }
+}
+
+.noVNC_panel li {
+ margin: 5px;
+}
+
+.noVNC_panel .noVNC_heading {
+ background-color: var(--novnc-blue);
+ border-radius: 6px;
+ padding: 5px 8px;
+ /* Compensate for padding in image */
+ padding-right: 11px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: white;
+ font-size: 20px;
+ font-weight: bold;
+ white-space: nowrap;
+}
+.noVNC_panel .noVNC_heading img {
+ vertical-align: bottom;
+}
+
+.noVNC_panel form {
+ display: flex;
+ flex-direction: column;
+ gap: 12px
+}
+
+.noVNC_panel .button_row {
+ margin-top: 10px;
+ display: flex;
+ gap: 10px;
+ justify-content: space-between;
+}
+.noVNC_panel .button_row *:only-child {
+ margin-left: auto; /* Align single buttons to the right */
+}
+
+/* Expanders */
+.noVNC_expander {
+ cursor: pointer;
+}
+.noVNC_expander::before {
+ content: url("../images/expander.svg");
+ display: inline-block;
+ margin-right: 5px;
+ transition: 0.2s ease-in-out;
+}
+.noVNC_expander.noVNC_open::before {
+ transform: rotateZ(90deg);
+}
+.noVNC_expander ~ * {
+ margin: 5px;
+ margin-left: 10px;
+ padding: 5px;
+ background: rgba(0, 0, 0, 0.04);
+ border-radius: 6px;
+}
+.noVNC_expander:not(.noVNC_open) ~ * {
+ display: none;
+}
+
+/* Control bar content */
+
+#noVNC_control_bar .noVNC_logo {
+ font-size: 13px;
+}
+
+.noVNC_logo + hr {
+ /* Remove all but top border */
+ border: none;
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+:root:not(.noVNC_connected) #noVNC_view_drag_button {
+ display: none;
+}
+
+/* noVNC Touch Device only buttons */
+:root:not(.noVNC_connected) #noVNC_mobile_buttons {
+ display: none;
+}
+@media not all and (any-pointer: coarse) {
+ /* FIXME: The button for the virtual keyboard is the only button in this
+ group of "mobile buttons". It is bad to assume that no touch
+ devices have physical keyboards available. Hopefully we can get
+ a media query for this:
+ https://github.com/w3c/csswg-drafts/issues/3871 */
+ :root.noVNC_connected #noVNC_mobile_buttons {
+ display: none;
+ }
+}
+
+/* Extra manual keys */
+:root:not(.noVNC_connected) #noVNC_toggle_extra_keys_button {
+ display: none;
+}
+
+#noVNC_modifiers {
+ background-color: var(--novnc-darkgrey);
+ border: none;
+ padding: 10px;
+}
+
+/* Shutdown/Reboot */
+:root:not(.noVNC_connected) #noVNC_power_button {
+ display: none;
+}
+#noVNC_power {
+}
+#noVNC_power_buttons {
+ display: none;
+}
+
+#noVNC_power input[type=button] {
+ width: 100%;
+}
+
+/* Clipboard */
+:root:not(.noVNC_connected) #noVNC_clipboard_button {
+ display: none;
+}
+#noVNC_clipboard_text {
+ width: 360px;
+ min-width: 150px;
+ height: 160px;
+ min-height: 70px;
+
+ box-sizing: border-box;
+ max-width: 100%;
+ /* minus approximate height of title, height of subtitle, and margin */
+ max-height: calc(100vh - 10em - 25px);
+}
+
+/* Settings */
+#noVNC_settings {
+}
+#noVNC_settings ul {
+ list-style: none;
+ padding: 0px;
+}
+#noVNC_settings button,
+#noVNC_settings select,
+#noVNC_settings textarea,
+#noVNC_settings input:not([type=checkbox]):not([type=radio]) {
+ margin-left: 6px;
+ /* Prevent inputs in settings from being too wide */
+ max-width: calc(100% - 6px - var(--input-xpadding) * 2);
+}
+
+#noVNC_setting_port {
+ width: 80px;
+}
+#noVNC_setting_path {
+ width: 100px;
+}
+
+/* Version */
+
+.noVNC_version_wrapper {
+ font-size: small;
+}
+
+.noVNC_version {
+ margin-left: 1rem;
+}
+
+/* Connection controls */
+:root:not(.noVNC_connected) #noVNC_disconnect_button {
+ display: none;
+}
+
+/* ----------------------------------------
+ * Status dialog
+ * ----------------------------------------
+ */
+
+#noVNC_status {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ z-index: 100;
+ transform: translateY(-100%);
+
+ cursor: pointer;
+
+ transition: 0.5s ease-in-out;
+
+ visibility: hidden;
+ opacity: 0;
+
+ padding: 5px;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-content: center;
+
+ line-height: 1.6;
+ word-wrap: break-word;
+ color: #fff;
+
+ border-bottom: 1px solid rgba(0, 0, 0, 0.9);
+}
+#noVNC_status.noVNC_open {
+ transform: translateY(0);
+ visibility: visible;
+ opacity: 1;
+}
+
+#noVNC_status::before {
+ content: "";
+ display: inline-block;
+ width: 25px;
+ height: 25px;
+ margin-right: 5px;
+}
+
+#noVNC_status.noVNC_status_normal {
+ background: rgba(128,128,128,0.9);
+}
+#noVNC_status.noVNC_status_normal::before {
+ content: url("../images/info.svg") " ";
+}
+#noVNC_status.noVNC_status_error {
+ background: rgba(200,55,55,0.9);
+}
+#noVNC_status.noVNC_status_error::before {
+ content: url("../images/error.svg") " ";
+}
+#noVNC_status.noVNC_status_warn {
+ background: rgba(180,180,30,0.9);
+}
+#noVNC_status.noVNC_status_warn::before {
+ content: url("../images/warning.svg") " ";
+}
+
+/* ----------------------------------------
+ * Connect dialog
+ * ----------------------------------------
+ */
+
+#noVNC_connect_dlg {
+ transition: 0.5s ease-in-out;
+
+ transform: scale(0, 0);
+ visibility: hidden;
+ opacity: 0;
+}
+#noVNC_connect_dlg.noVNC_open {
+ transform: scale(1, 1);
+ visibility: visible;
+ opacity: 1;
+}
+#noVNC_connect_dlg .noVNC_logo {
+ transition: 0.5s ease-in-out;
+ padding: 10px;
+ margin-bottom: 10px;
+
+ font-size: 80px;
+ text-align: center;
+
+ border-radius: 6px;
+}
+@media (max-width: 440px) {
+ #noVNC_connect_dlg {
+ max-width: calc(100vw - 100px);
+ }
+ #noVNC_connect_dlg .noVNC_logo {
+ font-size: calc(25vw - 30px);
+ }
+}
+#noVNC_connect_dlg div {
+ padding: 18px;
+
+ background-color: var(--novnc-darkgrey);
+ border-radius: 12px;
+ text-align: center;
+ font-size: 20px;
+
+ box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+}
+#noVNC_connect_button {
+ width: 100%;
+ padding: 6px 30px;
+ cursor: pointer;
+ border-color: transparent;
+ border-radius: 12px;
+ background-color: var(--novnc-blue);
+ color: white;
+
+ display: flex;
+ justify-content: center;
+ place-items: center;
+ gap: 4px;
+}
+
+#noVNC_connect_button img {
+ vertical-align: bottom;
+ height: 1.3em;
+}
+
+/* ----------------------------------------
+ * Server verification dialog
+ * ----------------------------------------
+ */
+
+#noVNC_verify_server_dlg {
+ position: relative;
+
+ transform: translateY(-50px);
+}
+#noVNC_verify_server_dlg.noVNC_open {
+ transform: translateY(0);
+}
+#noVNC_fingerprint_block {
+ margin: 10px;
+}
+
+/* ----------------------------------------
+ * Password dialog
+ * ----------------------------------------
+ */
+
+#noVNC_credentials_dlg {
+ position: relative;
+
+ transform: translateY(-50px);
+}
+#noVNC_credentials_dlg.noVNC_open {
+ transform: translateY(0);
+}
+#noVNC_username_block.noVNC_hidden,
+#noVNC_password_block.noVNC_hidden {
+ display: none;
+}
+
+
+/* ----------------------------------------
+ * Main area
+ * ----------------------------------------
+ */
+
+/* Transition screen */
+#noVNC_transition {
+ transition: 0.5s ease-in-out;
+
+ display: flex;
+ opacity: 0;
+ visibility: hidden;
+
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+
+ color: white;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 50;
+
+ /*display: flex;*/
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+}
+:root.noVNC_loading #noVNC_transition,
+:root.noVNC_connecting #noVNC_transition,
+:root.noVNC_disconnecting #noVNC_transition,
+:root.noVNC_reconnecting #noVNC_transition {
+ opacity: 1;
+ visibility: visible;
+}
+:root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button {
+ display: none;
+}
+#noVNC_transition_text {
+ font-size: 1.5em;
+}
+
+/* Main container */
+#noVNC_container {
+ width: 100%;
+ height: 100%;
+ background-color: #313131;
+ border-bottom-right-radius: 800px 600px;
+ /*border-top-left-radius: 800px 600px;*/
+
+ /* If selection isn't disabled, long-pressing stuff in the sidebar
+ can accidentally select the container or the canvas. This can
+ happen when attempting to move the handle. */
+ user-select: none;
+ -webkit-user-select: none;
+}
+
+#noVNC_keyboardinput {
+ width: 1px;
+ height: 1px;
+ background-color: #fff;
+ color: #fff;
+ border: 0;
+ position: absolute;
+ left: -40px;
+ z-index: -1;
+ ime-mode: disabled;
+}
+
+/*Default noVNC logo.*/
+/* From: http://fonts.googleapis.com/css?family=Orbitron:700 */
+@font-face {
+ font-family: 'Orbitron';
+ font-style: normal;
+ font-weight: 700;
+ src: local('?'), url('Orbitron700.woff') format('woff'),
+ url('Orbitron700.ttf') format('truetype');
+}
+
+.noVNC_logo {
+ color: var(--novnc-yellow);
+ font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
+ line-height: 0.9;
+ text-shadow: 0.1em 0.1em 0 black;
+}
+.noVNC_logo span{
+ color: var(--novnc-green);
+}
+
+#noVNC_bell {
+ display: none;
+}
+
+/* ----------------------------------------
+ * Media sizing
+ * ----------------------------------------
+ */
+
+@media screen and (max-width: 640px){
+ #noVNC_logo {
+ font-size: 150px;
+ }
+}
+
+@media screen and (min-width: 321px) and (max-width: 480px) {
+ #noVNC_logo {
+ font-size: 110px;
+ }
+}
+
+@media screen and (max-width: 320px) {
+ #noVNC_logo {
+ font-size: 90px;
+ }
+}
pkg/web/noVNC/app/styles/constants.css
@@ -0,0 +1,30 @@
+/*
+ * noVNC general CSS constant variables
+ * Copyright (C) 2025 The noVNC authors
+ * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
+ * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
+ */
+
+/* ---------- COLORS ----------- */
+
+:root {
+ --novnc-grey: rgb(128, 128, 128);
+ --novnc-lightgrey: rgb(192, 192, 192);
+ --novnc-darkgrey: rgb(92, 92, 92);
+
+ /* Transparent to make button colors adapt to the background */
+ --novnc-buttongrey: rgba(192, 192, 192, 0.5);
+
+ --novnc-blue: rgb(110, 132, 163);
+ --novnc-lightblue: rgb(74, 144, 217);
+ --novnc-darkblue: rgb(83, 99, 122);
+
+ --novnc-green: rgb(0, 128, 0);
+ --novnc-yellow: rgb(255, 255, 0);
+}
+
+/* ------ MISC PROPERTIES ------ */
+
+:root {
+ --input-xpadding: 1em;
+}
pkg/web/noVNC/app/styles/input.css
@@ -0,0 +1,628 @@
+/*
+ * noVNC general input element CSS
+ * Copyright (C) 2025 The noVNC authors
+ * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
+ * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
+ */
+
+/* ------- SHARED BETWEEN INPUT ELEMENTS -------- */
+
+input,
+textarea,
+button,
+select,
+input::file-selector-button {
+ padding: 0.5em var(--input-xpadding);
+ border-radius: 6px;
+ appearance: none;
+ text-overflow: ellipsis;
+
+ /* Respect standard font settings */
+ font: inherit;
+ line-height: 1.6;
+}
+input:disabled,
+textarea:disabled,
+button:disabled,
+select:disabled,
+label[disabled] {
+ opacity: 0.4;
+}
+
+input:focus-visible,
+textarea:focus-visible,
+button:focus-visible,
+select:focus-visible,
+input:focus-visible::file-selector-button {
+ outline: 2px solid var(--novnc-lightblue);
+ outline-offset: 1px;
+}
+
+/* ------- TEXT INPUT -------- */
+
+input:not([type]),
+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 {
+ border: 1px solid var(--novnc-lightgrey);
+ /* Account for borders on text inputs, buttons dont have borders */
+ padding: calc(0.5em - 1px) var(--input-xpadding);
+}
+input:not([type]):focus-visible,
+input[type=date]:focus-visible,
+input[type=datetime-local]:focus-visible,
+input[type=email]:focus-visible,
+input[type=month]:focus-visible,
+input[type=number]:focus-visible,
+input[type=password]:focus-visible,
+input[type=search]:focus-visible,
+input[type=tel]:focus-visible,
+input[type=text]:focus-visible,
+input[type=time]:focus-visible,
+input[type=url]:focus-visible,
+input[type=week]:focus-visible,
+textarea:focus-visible {
+ outline-offset: -1px;
+}
+
+textarea {
+ margin: unset; /* Remove Firefox's built in margin */
+ /* Prevent layout from shifting when scrollbars show */
+ scrollbar-gutter: stable;
+ /* Make textareas show at minimum one line. This does not work when
+ using box-sizing border-box, in which case, vertical padding and
+ border width needs to be taken into account. */
+ min-height: 1lh;
+ vertical-align: baseline; /* Firefox gives "text-bottom" by default */
+}
+
+/* ------- NUMBER PICKERS ------- */
+
+/* We can't style the number spinner buttons:
+ https://github.com/w3c/csswg-drafts/issues/8777 */
+input[type=number]::-webkit-inner-spin-button,
+input[type=number]::-webkit-outer-spin-button {
+ /* Get rid of increase/decrease buttons in WebKit */
+ appearance: none;
+}
+input[type=number] {
+ /* Get rid of increase/decrease buttons in Firefox */
+ appearance: textfield;
+}
+
+/* ------- BUTTON ACTIVATIONS -------- */
+
+/* A color overlay that depends on the activation level. The level can then be
+ set for different states on an element, for example hover and click on a
+ <button>. */
+input, button, select, option,
+input::file-selector-button,
+.button-activations {
+ --button-activation-level: 0;
+ /* Note that CSS variables aren't functions, beware when inheriting */
+ --button-activation-alpha: calc(0.08 * var(--button-activation-level));
+ /* FIXME: We want the image() function instead of the linear-gradient()
+ function below. But it's not supported in the browsers yet. */
+ --button-activation-overlay:
+ linear-gradient(rgba(0, 0, 0, var(--button-activation-alpha))
+ 100%, transparent);
+ --button-activation-overlay-light:
+ linear-gradient(rgba(255, 255, 255, calc(0.23 * var(--button-activation-level)))
+ 100%, transparent);
+}
+.button-activations {
+ background-image: var(--button-activation-overlay);
+
+ /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */
+ -webkit-tap-highlight-color: transparent;
+}
+/* When we want the light overlay on activations instead.
+ This is best used on elements with darker backgrounds. */
+.button-activations.light-overlay {
+ background-image: var(--button-activation-overlay-light);
+ /* Can't use the normal blend mode since that gives washed out colors. */
+ /* FIXME: For elements with these activation overlays we'd like only
+ the luminosity to change. The proprty "background-blend-mode" set
+ to "luminosity" sounds good, but it doesn't work as intended,
+ see: https://bugzilla.mozilla.org/show_bug.cgi?id=1806417 */
+ background-blend-mode: overlay;
+}
+
+input:hover, button:hover, select:hover, option:hover,
+input::file-selector-button:hover,
+.button-activations:hover {
+ --button-activation-level: 1;
+}
+/* Unfortunately we have to disable the :hover effect on touch devices,
+ otherwise the style lingers after tapping the button. */
+@media (any-pointer: coarse) {
+ input:hover, button:hover, select:hover, option:hover,
+ input::file-selector-button:hover,
+ .button-activations:hover {
+ --button-activation-level: 0;
+ }
+}
+input:active, button:active, select:active, option:active,
+input::file-selector-button:active,
+.button-activations:active {
+ --button-activation-level: 2;
+}
+input:disabled, button:disabled, select:disabled, select:disabled option,
+input:disabled::file-selector-button,
+.button-activations:disabled {
+ --button-activation-level: 0;
+}
+
+/* ------- BUTTONS -------- */
+
+input[type=button],
+input[type=color],
+input[type=image],
+input[type=reset],
+input[type=submit],
+input::file-selector-button,
+button,
+select {
+ min-width: 8em;
+ border: none;
+ color: black;
+ font-weight: bold;
+ background-color: var(--novnc-buttongrey);
+ background-image: var(--button-activation-overlay);
+ cursor: pointer;
+ /* Disable Chrome's touch tap highlight */
+ -webkit-tap-highlight-color: transparent;
+}
+input[type=button]:disabled,
+input[type=color]:disabled,
+input[type=image]:disabled,
+input[type=reset]:disabled,
+input[type=submit]:disabled,
+input:disabled::file-selector-button,
+button:disabled,
+select:disabled {
+ /* See Firefox bug:
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1798304 */
+ cursor: default;
+}
+
+input[type=button],
+input[type=color],
+input[type=reset],
+input[type=submit] {
+ /* Workaround for text-overflow bugs in Firefox and Chromium:
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1800077
+ https://bugs.chromium.org/p/chromium/issues/detail?id=1383144 */
+ overflow: clip;
+}
+
+/* ------- COLOR PICKERS ------- */
+
+input[type=color] {
+ min-width: unset;
+ box-sizing: content-box;
+ width: 1.4em;
+ height: 1.4em;
+}
+input[type=color]::-webkit-color-swatch-wrapper {
+ padding: 0;
+}
+/* -webkit-color-swatch & -moz-color-swatch cant be in a selector list:
+ https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */
+input[type=color]::-webkit-color-swatch {
+ border: none;
+ border-radius: 6px;
+}
+input[type=color]::-moz-color-swatch {
+ border: none;
+ border-radius: 6px;
+}
+
+/* -- SHARED BETWEEN CHECKBOXES, RADIOBUTTONS AND THE TOGGLE CLASS -- */
+
+input[type=radio],
+input[type=checkbox] {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ background-color: var(--novnc-buttongrey);
+ background-image: var(--button-activation-overlay);
+ /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */
+ -webkit-tap-highlight-color: transparent;
+ width: 16px;
+ --checkradio-height: 16px;
+ height: var(--checkradio-height);
+ padding: 0;
+ margin: 0 6px 0 0;
+ /* Don't have transitions for outline in order to be consistent
+ with other elements */
+ transition: all 0.2s, outline-color 0s, outline-offset 0s;
+
+ /* A transparent outline in order to work around a graphical clipping issue
+ in WebKit. See bug: https://bugs.webkit.org/show_bug.cgi?id=256003 */
+ outline: 1px solid transparent;
+ position: relative; /* Since ::before & ::after are absolute positioned */
+
+ /* We want to align with the middle of capital letters, this requires
+ a workaround. The default behavior is to align the bottom of the element
+ on top of the text baseline, this is too far up.
+ We want to push the element down half the difference in height between
+ it and a capital X. In our font, the height of a capital "X" is 0.698em.
+ */
+ vertical-align: calc(0px - (var(--checkradio-height) - 0.698em) / 2);
+ /* FIXME: Could write 1cap instead of 0.698em, but it's only supported in
+ Firefox as of 2023 */
+ /* FIXME: We probably want to use round() here, see bug 8148 */
+}
+input[type=radio]:focus-visible,
+input[type=checkbox]:focus-visible {
+ outline-color: var(--novnc-lightblue);
+}
+input[type=checkbox]::before,
+input[type=checkbox]:not(.toggle)::after,
+input[type=radio]::before,
+input[type=radio]::after {
+ content: "";
+ display: block; /* width & height doesn't work on inline elements */
+ transition: inherit;
+ /* Let's prevent the pseudo-elements from taking up layout space so that
+ the ::before and ::after pseudo-elements can be in the same place. This
+ is also required for vertical-align: baseline to work like we want it to
+ on radio/checkboxes. If the pseudo-elements take up layout space, the
+ baseline of text inside them will be used instead. */
+ position: absolute;
+}
+input[type=checkbox]:not(.toggle)::after,
+input[type=radio]::after {
+ width: 10px;
+ height: 2px;
+ background-color: transparent;
+ border-radius: 2px;
+}
+
+/* ------- CHECKBOXES ------- */
+
+input[type=checkbox]:not(.toggle) {
+ border-radius: 4px;
+}
+input[type=checkbox]:not(.toggle):checked,
+input[type=checkbox]:not(.toggle):indeterminate {
+ background-color: var(--novnc-blue);
+ background-image: var(--button-activation-overlay-light);
+ background-blend-mode: overlay;
+}
+input[type=checkbox]:not(.toggle)::before {
+ width: 25%;
+ height: 55%;
+ border-style: solid;
+ border-color: transparent;
+ border-width: 0 2px 2px 0;
+ border-radius: 1px;
+ transform: translateY(-1px) rotate(35deg);
+}
+input[type=checkbox]:not(.toggle):checked::before {
+ border-color: white;
+}
+input[type=checkbox]:not(.toggle):indeterminate::after {
+ background-color: white;
+}
+
+/* ------- RADIO BUTTONS ------- */
+
+input[type=radio] {
+ border-radius: 50%;
+ border: 1px solid transparent; /* To ensure a smooth transition */
+}
+input[type=radio]:checked {
+ border: 4px solid var(--novnc-blue);
+ background-color: white;
+ /* button-activation-overlay should be removed from the radio
+ element to not interfere with button-activation-overlay-light
+ that is set on the ::before element. */
+ background-image: none;
+}
+input[type=radio]::before {
+ width: inherit;
+ height: inherit;
+ border-radius: inherit;
+ /* We can achieve the highlight overlay effect on border colors by
+ setting button-activation-overlay-light on an element that stays
+ on top (z-axis) of the element with a border. */
+ background-image: var(--button-activation-overlay-light);
+ mix-blend-mode: overlay;
+ opacity: 0;
+}
+input[type=radio]:checked::before {
+ opacity: 1;
+}
+input[type=radio]:indeterminate::after {
+ background-color: black;
+}
+
+/* ------- TOGGLE SWITCHES ------- */
+
+/* These are meant to be used instead of checkboxes in some cases. If all of
+ the following critera are true you should use a toggle switch:
+
+ * The choice is a simple ON/OFF or ENABLE/DISABLE
+ * The choice doesn't give the feeling of "I agree" or "I confirm"
+ * There are not multiple related & grouped options
+ */
+
+input[type=checkbox].toggle {
+ display: inline-block;
+ --checkradio-height: 18px; /* Height value used in calc, see above */
+ width: 31px;
+ cursor: pointer;
+ user-select: none;
+ -webkit-user-select: none;
+ border-radius: 9px;
+}
+input[type=checkbox].toggle:disabled {
+ cursor: default;
+}
+input[type=checkbox].toggle:indeterminate {
+ background-color: var(--novnc-buttongrey);
+ background-image: var(--button-activation-overlay);
+}
+input[type=checkbox].toggle:checked {
+ background-color: var(--novnc-blue);
+ background-image: var(--button-activation-overlay-light);
+ background-blend-mode: overlay;
+}
+input[type=checkbox].toggle::before {
+ --circle-diameter: 10px;
+ --circle-offset: 4px;
+ width: var(--circle-diameter);
+ height: var(--circle-diameter);
+ top: var(--circle-offset);
+ left: var(--circle-offset);
+ background: white;
+ border-radius: 6px;
+}
+input[type=checkbox].toggle:checked::before {
+ left: calc(100% - var(--circle-offset) - var(--circle-diameter));
+}
+input[type=checkbox].toggle:indeterminate::before {
+ left: calc(50% - var(--circle-diameter) / 2);
+}
+
+/* ------- RANGE SLIDERS ------- */
+
+input[type=range] {
+ border: unset;
+ border-radius: 8px;
+ height: 15px;
+ padding: 0;
+ background: transparent;
+ /* Needed to get properly rounded corners on -moz-range-progress
+ when the thumb is all the way to the right. Without overflow
+ hidden, the pointy edges of the progress track shows to the
+ right of the thumb. */
+ overflow: hidden;
+}
+@supports selector(::-webkit-slider-thumb) {
+ input[type=range] {
+ /* Needs a fixed width to match clip-path */
+ width: 125px;
+ /* overflow: hidden is not ideal for hiding the left part of the box
+ shadow of -webkit-slider-thumb since it doesn't match the smaller
+ border-radius of the progress track. The below clip-path has two
+ circular sides to make the ends of the track have correctly rounded
+ corners. The clip path shape looks something like this:
+
+ +-------------------------------+
+ /---| |---\
+ | |
+ \---| |---/
+ +-------------------------------+
+
+ The larger middle part of the clip path is made to have room for the
+ thumb. By using margins on the track, we prevent the thumb from
+ touching the ends of the track.
+ */
+ clip-path: path(' \
+ M 4.5 3 \
+ L 4.5 0 \
+ L 120.5 0 \
+ L 120.5 3 \
+ A 1 1 0 0 1 120.5 12 \
+ L 120.5 15 \
+ L 4.5 15 \
+ L 4.5 12 \
+ A 1 1 0 0 1 4.5 3 \
+ ');
+ }
+}
+input[type=range]:hover {
+ cursor: grab;
+}
+input[type=range]:active {
+ cursor: grabbing;
+}
+input[type=range]:disabled {
+ cursor: default;
+}
+input[type=range]:focus-visible {
+ clip-path: none; /* Otherwise it hides the outline */
+}
+/* -webkit-slider.. & -moz-range.. cant be in selector lists:
+ https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */
+input[type=range]::-webkit-slider-runnable-track {
+ background-color: var(--novnc-buttongrey);
+ height: 7px;
+ border-radius: 4px;
+ margin: 0 3px;
+}
+input[type=range]::-moz-range-track {
+ background-color: var(--novnc-buttongrey);
+ height: 7px;
+ border-radius: 4px;
+}
+input[type=range]::-moz-range-progress {
+ background-color: var(--novnc-blue);
+ height: 9px;
+ /* Needs rounded corners only on the left side. Otherwise the rounding of
+ the progress track starts before the thumb, when the thumb is close to
+ the left edge. */
+ border-radius: 5px 0 0 5px;
+}
+input[type=range]::-webkit-slider-thumb {
+ appearance: none;
+ width: 15px;
+ height: 15px;
+ border-radius: 50%;
+ background-color: white;
+ background-image: var(--button-activation-overlay);
+ /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */
+ -webkit-tap-highlight-color: transparent;
+ border: 3px solid var(--novnc-blue);
+ margin-top: -4px; /* (track height / 2) - (thumb height /2) */
+
+ /* Since there is no way to style the left part of the range track in
+ webkit, we add a large shadow (1000px wide) to the left of the thumb and
+ then crop it with a clip-path shaped like this:
+ ___
+ +-------------------/ \
+ | progress |Thumb|
+ +-------------------\ ___ /
+
+ The large left part of the shadow is clipped by another clip-path on on
+ the main range input element. */
+ /* FIXME: We can remove the box shadow workaround when this is standardized:
+ https://github.com/w3c/csswg-drafts/issues/4410 */
+
+ box-shadow: calc(-100vw - 8px) 0 0 100vw var(--novnc-blue);
+ clip-path: path(' \
+ M -1000 3 \
+ L 3 3 \
+ L 15 7.5 \
+ A 1 1 0 0 1 0 7.5 \
+ A 1 1 0 0 1 15 7.5 \
+ L 3 12 \
+ L -1000 12 Z \
+ ');
+}
+input[type=range]::-moz-range-thumb {
+ appearance: none;
+ width: 15px;
+ height: 15px;
+ border-radius: 50%;
+ box-sizing: border-box;
+ background-color: white;
+ background-image: var(--button-activation-overlay);
+ border: 3px solid var(--novnc-blue);
+ margin-top: -7px;
+}
+
+/* ------- FILE CHOOSERS ------- */
+
+input[type=file] {
+ background-image: none;
+ border: none;
+}
+input::file-selector-button {
+ margin-right: 6px;
+}
+input[type=file]:focus-visible {
+ outline: none; /* We outline the button instead of the entire element */
+}
+
+/* ------- SELECT BUTTONS ------- */
+
+select {
+ --select-arrow: url('data:image/svg+xml;utf8, \
+ <svg width="11" height="6" version="1.1" viewBox="0 0 11 6" \
+ xmlns="http://www.w3.org/2000/svg"> \
+ <path d="m10.5.5-5 5-5-5" fill="none" \
+ stroke="black" stroke-width="1.5" \
+ stroke-linecap="round" stroke-linejoin="round"/> \
+ </svg>');
+
+ /* FIXME: A bug in Firefox, requires a workaround for the background:
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1810958 */
+ /* The dropdown list will show the select element's background above and
+ below the options in Firefox. We want the entire dropdown to be white. */
+ background-color: white;
+ /* However, we don't want the select element to actually show a white
+ background, so let's place a gradient above it with the color we want. */
+ --grey-background: linear-gradient(var(--novnc-buttongrey) 100%,
+ transparent);
+ background-image:
+ var(--select-arrow),
+ var(--button-activation-overlay),
+ var(--grey-background);
+ background-position: calc(100% - var(--input-xpadding)), left top, left top;
+ background-repeat: no-repeat;
+ padding-right: calc(2*var(--input-xpadding) + 11px);
+ overflow: auto;
+}
+/* FIXME: :active isn't set when the <select> is opened in Firefox:
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */
+select:active {
+ /* Rotated arrow */
+ background-image: url('data:image/svg+xml;utf8, \
+ <svg width="11" height="6" version="1.1" viewBox="0 0 11 6" \
+ xmlns="http://www.w3.org/2000/svg" transform="rotate(180)"> \
+ <path d="m10.5.5-5 5-5-5" fill="none" \
+ stroke="black" stroke-width="1.5" \
+ stroke-linecap="round" stroke-linejoin="round"/> \
+ </svg>'),
+ var(--button-activation-overlay),
+ var(--grey-background);
+}
+select:disabled {
+ background-image:
+ var(--select-arrow),
+ var(--grey-background);
+}
+/* Note that styling for <option> doesn't work in all browsers
+ since its often drawn directly by the OS. We are generally very
+ limited in what we can change here. */
+option {
+ /* Prevent Chrome from inheriting background-color from the <select> */
+ background-color: white;
+ color: black;
+ font-weight: normal;
+ background-image: var(--button-activation-overlay);
+}
+option:checked {
+ background-color: var(--novnc-lightgrey);
+}
+/* Change the look when the <select> isn't used as a dropdown. When "size"
+ or "multiple" are set, these elements behaves more like lists. */
+select[size]:not([size="1"]), select[multiple] {
+ background-color: white;
+ background-image: unset; /* Don't show the arrow and other gradients */
+ border: 1px solid var(--novnc-lightgrey);
+ padding: 0;
+ font-weight: normal; /* Without this, options get bold font in WebKit. */
+
+ /* As an exception to the "list"-look, multi-selects in Chrome on Android,
+ and Safari on iOS, are unfortunately designed to be shown as a single
+ line. We can mitigate this inconsistency by at least fixing the height
+ here. By setting a min-height that matches other input elements, it
+ doesn't look too much out of place:
+ (1px border * 2) + (6.5px padding * 2) + 24px line-height = 39px */
+ min-height: 39px;
+}
+select[size]:not([size="1"]):focus-visible,
+select[multiple]:focus-visible {
+ /* Text input style focus-visible highlight */
+ outline-offset: -1px;
+}
+select[size]:not([size="1"]) option, select[multiple] option {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 4px var(--input-xpadding);
+}
pkg/web/noVNC/app/styles/Orbitron700.ttf
Binary file
pkg/web/noVNC/app/styles/Orbitron700.woff
Binary file
pkg/web/noVNC/app/error-handler.js
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+// Fallback for all uncaught errors
+function handleError(event, err) {
+ try {
+ const msg = document.getElementById('noVNC_fallback_errormsg');
+
+ // Work around Firefox bug:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1685038
+ if (event.message === "ResizeObserver loop completed with undelivered notifications.") {
+ return false;
+ }
+
+ // Only show the initial error
+ if (msg.hasChildNodes()) {
+ return false;
+ }
+
+ let div = document.createElement("div");
+ div.classList.add('noVNC_message');
+ div.appendChild(document.createTextNode(event.message));
+ msg.appendChild(div);
+
+ if (event.filename) {
+ div = document.createElement("div");
+ div.className = 'noVNC_location';
+ let text = event.filename;
+ if (event.lineno !== undefined) {
+ text += ":" + event.lineno;
+ if (event.colno !== undefined) {
+ text += ":" + event.colno;
+ }
+ }
+ div.appendChild(document.createTextNode(text));
+ msg.appendChild(div);
+ }
+
+ if (err && err.stack) {
+ div = document.createElement("div");
+ div.className = 'noVNC_stack';
+ div.appendChild(document.createTextNode(err.stack));
+ msg.appendChild(div);
+ }
+
+ document.getElementById('noVNC_fallback_error')
+ .classList.add("noVNC_open");
+
+ } catch (exc) {
+ document.write("noVNC encountered an error.");
+ }
+
+ // Try to disable keyboard interaction, best effort
+ try {
+ // Remove focus from the currently focused element in order to
+ // prevent keyboard interaction from continuing
+ if (document.activeElement) { document.activeElement.blur(); }
+
+ // Don't let any element be focusable when showing the error
+ let keyboardFocusable = 'a[href], button, input, textarea, select, details, [tabindex]';
+ document.querySelectorAll(keyboardFocusable).forEach((elem) => {
+ elem.setAttribute("tabindex", "-1");
+ });
+ } catch (exc) {
+ // Do nothing
+ }
+
+ // Don't return true since this would prevent the error
+ // from being printed to the browser console.
+ return false;
+}
+
+window.addEventListener('error', evt => handleError(evt, evt.error));
+window.addEventListener('unhandledrejection', evt => handleError(evt.reason, evt.reason));
pkg/web/noVNC/app/localization.js
@@ -0,0 +1,206 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2018 The noVNC authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ */
+
+/*
+ * Localization utilities
+ */
+
+export class Localizer {
+ constructor() {
+ // Currently configured language
+ this.language = 'en';
+
+ // Current dictionary of translations
+ this._dictionary = undefined;
+ }
+
+ // Configure suitable language based on user preferences
+ async setup(supportedLanguages, baseURL) {
+ this.language = 'en'; // Default: US English
+ this._dictionary = undefined;
+
+ this._setupLanguage(supportedLanguages);
+ await this._setupDictionary(baseURL);
+ }
+
+ _setupLanguage(supportedLanguages) {
+ /*
+ * Navigator.languages only available in Chrome (32+) and FireFox (32+)
+ * Fall back to navigator.language for other browsers
+ */
+ let userLanguages;
+ if (typeof window.navigator.languages == 'object') {
+ userLanguages = window.navigator.languages;
+ } else {
+ userLanguages = [navigator.language || navigator.userLanguage];
+ }
+
+ for (let i = 0;i < userLanguages.length;i++) {
+ const userLang = userLanguages[i]
+ .toLowerCase()
+ .replace("_", "-")
+ .split("-");
+
+ // First pass: perfect match
+ for (let j = 0; j < supportedLanguages.length; j++) {
+ const supLang = supportedLanguages[j]
+ .toLowerCase()
+ .replace("_", "-")
+ .split("-");
+
+ if (userLang[0] !== supLang[0]) {
+ continue;
+ }
+ if (userLang[1] !== supLang[1]) {
+ continue;
+ }
+
+ this.language = supportedLanguages[j];
+ return;
+ }
+
+ // Second pass: English fallback
+ if (userLang[0] === 'en') {
+ return;
+ }
+
+ // Third pass pass: other fallback
+ for (let j = 0;j < supportedLanguages.length;j++) {
+ const supLang = supportedLanguages[j]
+ .toLowerCase()
+ .replace("_", "-")
+ .split("-");
+
+ if (userLang[0] !== supLang[0]) {
+ continue;
+ }
+ if (supLang[1] !== undefined) {
+ continue;
+ }
+
+ this.language = supportedLanguages[j];
+ return;
+ }
+ }
+ }
+
+ async _setupDictionary(baseURL) {
+ if (baseURL) {
+ if (!baseURL.endsWith("/")) {
+ baseURL = baseURL + "/";
+ }
+ } else {
+ baseURL = "";
+ }
+
+ if (this.language === "en") {
+ return;
+ }
+
+ let response = await fetch(baseURL + this.language + ".json");
+ if (!response.ok) {
+ throw Error("" + response.status + " " + response.statusText);
+ }
+
+ this._dictionary = await response.json();
+ }
+
+ // Retrieve localised text
+ get(id) {
+ if (typeof this._dictionary !== 'undefined' &&
+ this._dictionary[id]) {
+ return this._dictionary[id];
+ } else {
+ return id;
+ }
+ }
+
+ // Traverses the DOM and translates relevant fields
+ // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate
+ translateDOM() {
+ const self = this;
+
+ function process(elem, enabled) {
+ function isAnyOf(searchElement, items) {
+ return items.indexOf(searchElement) !== -1;
+ }
+
+ function translateString(str) {
+ // We assume surrounding whitespace, and whitespace around line
+ // breaks is just for source formatting
+ str = str.split("\n").map(s => s.trim()).join(" ").trim();
+ return self.get(str);
+ }
+
+ function translateAttribute(elem, attr) {
+ const str = translateString(elem.getAttribute(attr));
+ elem.setAttribute(attr, str);
+ }
+
+ function translateTextNode(node) {
+ const str = translateString(node.data);
+ node.data = str;
+ }
+
+ if (elem.hasAttribute("translate")) {
+ if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) {
+ enabled = true;
+ } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) {
+ enabled = false;
+ }
+ }
+
+ if (enabled) {
+ if (elem.hasAttribute("abbr") &&
+ elem.tagName === "TH") {
+ translateAttribute(elem, "abbr");
+ }
+ if (elem.hasAttribute("alt") &&
+ isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) {
+ translateAttribute(elem, "alt");
+ }
+ if (elem.hasAttribute("download") &&
+ isAnyOf(elem.tagName, ["A", "AREA"])) {
+ translateAttribute(elem, "download");
+ }
+ if (elem.hasAttribute("label") &&
+ isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP",
+ "OPTION", "TRACK"])) {
+ translateAttribute(elem, "label");
+ }
+ // FIXME: Should update "lang"
+ if (elem.hasAttribute("placeholder") &&
+ isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) {
+ translateAttribute(elem, "placeholder");
+ }
+ if (elem.hasAttribute("title")) {
+ translateAttribute(elem, "title");
+ }
+ if (elem.hasAttribute("value") &&
+ elem.tagName === "INPUT" &&
+ isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) {
+ translateAttribute(elem, "value");
+ }
+ }
+
+ for (let i = 0; i < elem.childNodes.length; i++) {
+ const node = elem.childNodes[i];
+ if (node.nodeType === node.ELEMENT_NODE) {
+ process(node, enabled);
+ } else if (node.nodeType === node.TEXT_NODE && enabled) {
+ translateTextNode(node);
+ }
+ }
+ }
+
+ process(document.body, true);
+ }
+}
+
+export const l10n = new Localizer();
+export default l10n.get.bind(l10n);
pkg/web/noVNC/app/ui.js
@@ -0,0 +1,1875 @@
+/*
+ * 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.
+ */
+
+import * as Log from '../core/util/logging.js';
+import _, { l10n } from './localization.js';
+import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
+ hasScrollbarGutter, dragThreshold, browserAsyncClipboardSupport }
+ from '../core/util/browser.js';
+import { setCapture, getPointerEvent } from '../core/util/events.js';
+import KeyTable from "../core/input/keysym.js";
+import keysyms from "../core/input/keysymdef.js";
+import Keyboard from "../core/input/keyboard.js";
+import RFB from "../core/rfb.js";
+import WakeLockManager from './wakelock.js';
+import * as WebUtil from "./webutil.js";
+
+const PAGE_TITLE = "noVNC";
+
+const LINGUAS = ["cs", "de", "el", "es", "fr", "hr", "hu", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "uk", "zh_CN", "zh_TW"];
+
+const UI = {
+
+ customSettings: {},
+
+ connected: false,
+ desktopName: "",
+
+ statusTimeout: null,
+ hideKeyboardTimeout: null,
+ idleControlbarTimeout: null,
+ closeControlbarTimeout: null,
+
+ controlbarGrabbed: false,
+ controlbarDrag: false,
+ controlbarMouseDownClientY: 0,
+ controlbarMouseDownOffsetY: 0,
+
+ lastKeyboardinput: null,
+ defaultKeyboardinputLen: 100,
+
+ inhibitReconnect: true,
+ reconnectCallback: null,
+ reconnectPassword: null,
+
+ wakeLockManager: new WakeLockManager(),
+
+ async start(options={}) {
+ UI.customSettings = options.settings || {};
+ if (UI.customSettings.defaults === undefined) {
+ UI.customSettings.defaults = {};
+ }
+ if (UI.customSettings.mandatory === undefined) {
+ UI.customSettings.mandatory = {};
+ }
+
+ // Set up translations
+ try {
+ await l10n.setup(LINGUAS, "app/locale/");
+ } catch (err) {
+ Log.Error("Failed to load translations: " + err);
+ }
+
+ // Initialize setting storage
+ await WebUtil.initSettings();
+
+ // Wait for the page to load
+ if (document.readyState !== "interactive" && document.readyState !== "complete") {
+ await new Promise((resolve, reject) => {
+ document.addEventListener('DOMContentLoaded', resolve);
+ });
+ }
+
+ UI.initSettings();
+
+ // Translate the DOM
+ l10n.translateDOM();
+
+ // We rely on modern APIs which might not be available in an
+ // insecure context
+ if (!window.isSecureContext) {
+ // FIXME: This gets hidden when connecting
+ UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error');
+ }
+
+ // Try to fetch version number
+ try {
+ let response = await fetch('./package.json');
+ if (!response.ok) {
+ throw Error("" + response.status + " " + response.statusText);
+ }
+
+ let packageInfo = await response.json();
+ 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
+ if (isTouchDevice) {
+ // Remove the address bar
+ setTimeout(() => window.scrollTo(0, 1), 100);
+ }
+
+ // Restore control bar position
+ if (WebUtil.readSetting('controlbar_pos') === 'right') {
+ UI.toggleControlbarSide();
+ }
+
+ UI.initFullscreen();
+
+ // Setup event handlers
+ UI.addControlbarHandlers();
+ UI.addTouchSpecificHandlers();
+ UI.addExtraKeysHandlers();
+ UI.addMachineHandlers();
+ UI.addConnectionControlHandlers();
+ UI.addClipboardHandlers();
+ UI.addSettingsHandlers();
+ document.getElementById("noVNC_status")
+ .addEventListener('click', UI.hideStatus);
+
+ // Bootstrap fallback input handler
+ UI.keyboardinputReset();
+
+ UI.openControlbar();
+
+ UI.updateVisualState('init');
+
+ document.documentElement.classList.remove("noVNC_loading");
+
+ let autoconnect = UI.getSetting('autoconnect');
+ if (autoconnect === 'true' || autoconnect == '1') {
+ autoconnect = true;
+ UI.connect();
+ } else {
+ autoconnect = false;
+ // Show the connect panel on first load unless autoconnecting
+ UI.openConnectPanel();
+ }
+ },
+
+ initFullscreen() {
+ // Only show the button if fullscreen is properly supported
+ // * Safari doesn't support alphanumerical input while in fullscreen
+ if (!isSafari() &&
+ (document.documentElement.requestFullscreen ||
+ document.documentElement.mozRequestFullScreen ||
+ document.documentElement.webkitRequestFullscreen ||
+ document.body.msRequestFullscreen)) {
+ document.getElementById('noVNC_fullscreen_button')
+ .classList.remove("noVNC_hidden");
+ UI.addFullscreenHandlers();
+ }
+ },
+
+ initSettings() {
+ // Logging selection dropdown
+ const llevels = ['error', 'warn', 'info', 'debug'];
+ for (let i = 0; i < llevels.length; i += 1) {
+ UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]);
+ }
+
+ // Settings with immediate effects
+ UI.initSetting('logging', 'warn');
+ UI.updateLogging();
+
+ UI.setupSettingLabels();
+
+ /* Populate the controls if defaults are provided in the URL */
+ UI.initSetting('host', '');
+ UI.initSetting('port', 0);
+ UI.initSetting('encrypt', (window.location.protocol === "https:"));
+ UI.initSetting('password');
+ UI.initSetting('autoconnect', false);
+ UI.initSetting('view_clip', false);
+ UI.initSetting('resize', 'off');
+ UI.initSetting('quality', 6);
+ UI.initSetting('compression', 2);
+ UI.initSetting('shared', true);
+ UI.initSetting('bell', 'on');
+ UI.initSetting('view_only', false);
+ UI.initSetting('show_dot', false);
+ UI.initSetting('path', 'websockify');
+ UI.initSetting('repeaterID', '');
+ UI.initSetting('reconnect', false);
+ UI.initSetting('reconnect_delay', 5000);
+ UI.initSetting('keep_device_awake', false);
+ },
+ // Adds a link to the label elements on the corresponding input elements
+ setupSettingLabels() {
+ const labels = document.getElementsByTagName('LABEL');
+ for (let i = 0; i < labels.length; i++) {
+ const htmlFor = labels[i].htmlFor;
+ if (htmlFor != '') {
+ const elem = document.getElementById(htmlFor);
+ if (elem) elem.label = labels[i];
+ } else {
+ // If 'for' isn't set, use the first input element child
+ const children = labels[i].children;
+ for (let j = 0; j < children.length; j++) {
+ if (children[j].form !== undefined) {
+ children[j].label = labels[i];
+ break;
+ }
+ }
+ }
+ }
+ },
+
+/* ------^-------
+* /INIT
+* ==============
+* EVENT HANDLERS
+* ------v------*/
+
+ addControlbarHandlers() {
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('mousemove', UI.activateControlbar);
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('mouseup', UI.activateControlbar);
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('mousedown', UI.activateControlbar);
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('keydown', UI.activateControlbar);
+
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('mousedown', UI.keepControlbar);
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('keydown', UI.keepControlbar);
+
+ document.getElementById("noVNC_view_drag_button")
+ .addEventListener('click', UI.toggleViewDrag);
+
+ document.getElementById("noVNC_control_bar_handle")
+ .addEventListener('mousedown', UI.controlbarHandleMouseDown);
+ document.getElementById("noVNC_control_bar_handle")
+ .addEventListener('mouseup', UI.controlbarHandleMouseUp);
+ document.getElementById("noVNC_control_bar_handle")
+ .addEventListener('mousemove', UI.dragControlbarHandle);
+ // resize events aren't available for elements
+ window.addEventListener('resize', UI.updateControlbarHandle);
+
+ const exps = document.getElementsByClassName("noVNC_expander");
+ for (let i = 0;i < exps.length;i++) {
+ exps[i].addEventListener('click', UI.toggleExpander);
+ }
+ },
+
+ addTouchSpecificHandlers() {
+ document.getElementById("noVNC_keyboard_button")
+ .addEventListener('click', UI.toggleVirtualKeyboard);
+
+ UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput'));
+ UI.touchKeyboard.onkeyevent = UI.keyEvent;
+ UI.touchKeyboard.grab();
+ document.getElementById("noVNC_keyboardinput")
+ .addEventListener('input', UI.keyInput);
+ document.getElementById("noVNC_keyboardinput")
+ .addEventListener('focus', UI.onfocusVirtualKeyboard);
+ document.getElementById("noVNC_keyboardinput")
+ .addEventListener('blur', UI.onblurVirtualKeyboard);
+ document.getElementById("noVNC_keyboardinput")
+ .addEventListener('submit', () => false);
+
+ document.documentElement
+ .addEventListener('mousedown', UI.keepVirtualKeyboard, true);
+
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('touchstart', UI.activateControlbar);
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('touchmove', UI.activateControlbar);
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('touchend', UI.activateControlbar);
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('input', UI.activateControlbar);
+
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('touchstart', UI.keepControlbar);
+ document.getElementById("noVNC_control_bar")
+ .addEventListener('input', UI.keepControlbar);
+
+ document.getElementById("noVNC_control_bar_handle")
+ .addEventListener('touchstart', UI.controlbarHandleMouseDown);
+ document.getElementById("noVNC_control_bar_handle")
+ .addEventListener('touchend', UI.controlbarHandleMouseUp);
+ document.getElementById("noVNC_control_bar_handle")
+ .addEventListener('touchmove', UI.dragControlbarHandle);
+ },
+
+ addExtraKeysHandlers() {
+ document.getElementById("noVNC_toggle_extra_keys_button")
+ .addEventListener('click', UI.toggleExtraKeys);
+ document.getElementById("noVNC_toggle_ctrl_button")
+ .addEventListener('click', UI.toggleCtrl);
+ document.getElementById("noVNC_toggle_windows_button")
+ .addEventListener('click', UI.toggleWindows);
+ document.getElementById("noVNC_toggle_alt_button")
+ .addEventListener('click', UI.toggleAlt);
+ document.getElementById("noVNC_send_tab_button")
+ .addEventListener('click', UI.sendTab);
+ document.getElementById("noVNC_send_esc_button")
+ .addEventListener('click', UI.sendEsc);
+ document.getElementById("noVNC_send_ctrl_alt_del_button")
+ .addEventListener('click', UI.sendCtrlAltDel);
+ },
+
+ addMachineHandlers() {
+ document.getElementById("noVNC_shutdown_button")
+ .addEventListener('click', () => UI.rfb.machineShutdown());
+ document.getElementById("noVNC_reboot_button")
+ .addEventListener('click', () => UI.rfb.machineReboot());
+ document.getElementById("noVNC_reset_button")
+ .addEventListener('click', () => UI.rfb.machineReset());
+ document.getElementById("noVNC_power_button")
+ .addEventListener('click', UI.togglePowerPanel);
+ },
+
+ addConnectionControlHandlers() {
+ document.getElementById("noVNC_disconnect_button")
+ .addEventListener('click', UI.disconnect);
+ document.getElementById("noVNC_connect_button")
+ .addEventListener('click', UI.connect);
+ document.getElementById("noVNC_cancel_reconnect_button")
+ .addEventListener('click', UI.cancelReconnect);
+
+ document.getElementById("noVNC_approve_server_button")
+ .addEventListener('click', UI.approveServer);
+ document.getElementById("noVNC_reject_server_button")
+ .addEventListener('click', UI.rejectServer);
+ document.getElementById("noVNC_credentials_button")
+ .addEventListener('click', UI.setCredentials);
+ },
+
+ addClipboardHandlers() {
+ document.getElementById("noVNC_clipboard_button")
+ .addEventListener('click', UI.toggleClipboardPanel);
+ document.getElementById("noVNC_clipboard_text")
+ .addEventListener('change', UI.clipboardSend);
+ },
+
+ // Add a call to save settings when the element changes,
+ // unless the optional parameter changeFunc is used instead.
+ addSettingChangeHandler(name, changeFunc) {
+ const settingElem = document.getElementById("noVNC_setting_" + name);
+ if (changeFunc === undefined) {
+ changeFunc = () => UI.saveSetting(name);
+ }
+ settingElem.addEventListener('change', changeFunc);
+ },
+
+ addSettingsHandlers() {
+ document.getElementById("noVNC_settings_button")
+ .addEventListener('click', UI.toggleSettingsPanel);
+
+ UI.addSettingChangeHandler('encrypt');
+ UI.addSettingChangeHandler('resize');
+ UI.addSettingChangeHandler('resize', UI.applyResizeMode);
+ 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.updateViewClip);
+ UI.addSettingChangeHandler('shared');
+ UI.addSettingChangeHandler('view_only');
+ UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
+ UI.addSettingChangeHandler('show_dot');
+ UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
+ UI.addSettingChangeHandler('keep_device_awake');
+ UI.addSettingChangeHandler('keep_device_awake', UI.updateRequestWakelock);
+ UI.addSettingChangeHandler('host');
+ UI.addSettingChangeHandler('port');
+ UI.addSettingChangeHandler('path');
+ UI.addSettingChangeHandler('repeaterID');
+ UI.addSettingChangeHandler('logging');
+ UI.addSettingChangeHandler('logging', UI.updateLogging);
+ UI.addSettingChangeHandler('reconnect');
+ UI.addSettingChangeHandler('reconnect_delay');
+ },
+
+ addFullscreenHandlers() {
+ document.getElementById("noVNC_fullscreen_button")
+ .addEventListener('click', UI.toggleFullscreen);
+
+ window.addEventListener('fullscreenchange', UI.updateFullscreenButton);
+ window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton);
+ window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton);
+ window.addEventListener('msfullscreenchange', UI.updateFullscreenButton);
+ },
+
+/* ------^-------
+ * /EVENT HANDLERS
+ * ==============
+ * VISUAL
+ * ------v------*/
+
+ // Disable/enable controls depending on connection state
+ updateVisualState(state) {
+
+ document.documentElement.classList.remove("noVNC_connecting");
+ document.documentElement.classList.remove("noVNC_connected");
+ document.documentElement.classList.remove("noVNC_disconnecting");
+ document.documentElement.classList.remove("noVNC_reconnecting");
+
+ const transitionElem = document.getElementById("noVNC_transition_text");
+ switch (state) {
+ case 'init':
+ break;
+ case 'connecting':
+ transitionElem.textContent = _("Connecting...");
+ document.documentElement.classList.add("noVNC_connecting");
+ break;
+ case 'connected':
+ document.documentElement.classList.add("noVNC_connected");
+ break;
+ case 'disconnecting':
+ transitionElem.textContent = _("Disconnecting...");
+ document.documentElement.classList.add("noVNC_disconnecting");
+ break;
+ case 'disconnected':
+ break;
+ case 'reconnecting':
+ transitionElem.textContent = _("Reconnecting...");
+ document.documentElement.classList.add("noVNC_reconnecting");
+ break;
+ default:
+ Log.Error("Invalid visual state: " + state);
+ UI.showStatus(_("Internal error"), 'error');
+ return;
+ }
+
+ if (UI.connected) {
+ UI.updateViewClip();
+
+ UI.disableSetting('encrypt');
+ UI.disableSetting('shared');
+ UI.disableSetting('host');
+ UI.disableSetting('port');
+ UI.disableSetting('path');
+ UI.disableSetting('repeaterID');
+
+ // Hide the controlbar after 2 seconds
+ UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
+ } else {
+ UI.enableSetting('encrypt');
+ UI.enableSetting('shared');
+ UI.enableSetting('host');
+ UI.enableSetting('port');
+ UI.enableSetting('path');
+ UI.enableSetting('repeaterID');
+ UI.updatePowerButton();
+ UI.keepControlbar();
+ }
+
+ // State change closes dialogs as they may not be relevant
+ // anymore
+ UI.closeAllPanels();
+ document.getElementById('noVNC_verify_server_dlg')
+ .classList.remove('noVNC_open');
+ document.getElementById('noVNC_credentials_dlg')
+ .classList.remove('noVNC_open');
+ },
+
+ showStatus(text, statusType, time) {
+ const statusElem = document.getElementById('noVNC_status');
+
+ if (typeof statusType === 'undefined') {
+ statusType = 'normal';
+ }
+
+ // Don't overwrite more severe visible statuses and never
+ // errors. Only shows the first error.
+ if (statusElem.classList.contains("noVNC_open")) {
+ if (statusElem.classList.contains("noVNC_status_error")) {
+ return;
+ }
+ if (statusElem.classList.contains("noVNC_status_warn") &&
+ statusType === 'normal') {
+ return;
+ }
+ }
+
+ clearTimeout(UI.statusTimeout);
+
+ switch (statusType) {
+ case 'error':
+ statusElem.classList.remove("noVNC_status_warn");
+ statusElem.classList.remove("noVNC_status_normal");
+ statusElem.classList.add("noVNC_status_error");
+ break;
+ case 'warning':
+ case 'warn':
+ statusElem.classList.remove("noVNC_status_error");
+ statusElem.classList.remove("noVNC_status_normal");
+ statusElem.classList.add("noVNC_status_warn");
+ break;
+ case 'normal':
+ case 'info':
+ default:
+ statusElem.classList.remove("noVNC_status_error");
+ statusElem.classList.remove("noVNC_status_warn");
+ statusElem.classList.add("noVNC_status_normal");
+ break;
+ }
+
+ statusElem.textContent = text;
+ statusElem.classList.add("noVNC_open");
+
+ // If no time was specified, show the status for 1.5 seconds
+ if (typeof time === 'undefined') {
+ time = 1500;
+ }
+
+ // Error messages do not timeout
+ if (statusType !== 'error') {
+ UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
+ }
+ },
+
+ hideStatus() {
+ clearTimeout(UI.statusTimeout);
+ document.getElementById('noVNC_status').classList.remove("noVNC_open");
+ },
+
+ activateControlbar(event) {
+ clearTimeout(UI.idleControlbarTimeout);
+ // We manipulate the anchor instead of the actual control
+ // bar in order to avoid creating new a stacking group
+ document.getElementById('noVNC_control_bar_anchor')
+ .classList.remove("noVNC_idle");
+ UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000);
+ },
+
+ 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')
+ .classList.add("noVNC_idle");
+ },
+
+ keepControlbar() {
+ clearTimeout(UI.closeControlbarTimeout);
+ },
+
+ openControlbar() {
+ document.getElementById('noVNC_control_bar')
+ .classList.add("noVNC_open");
+ },
+
+ closeControlbar() {
+ UI.closeAllPanels();
+ document.getElementById('noVNC_control_bar')
+ .classList.remove("noVNC_open");
+ UI.rfb.focus();
+ },
+
+ toggleControlbar() {
+ if (document.getElementById('noVNC_control_bar')
+ .classList.contains("noVNC_open")) {
+ UI.closeControlbar();
+ } else {
+ UI.openControlbar();
+ }
+ },
+
+ toggleControlbarSide() {
+ // Temporarily disable animation, if bar is displayed, to avoid weird
+ // movement. The transitionend-event will not fire when display=none.
+ const bar = document.getElementById('noVNC_control_bar');
+ const barDisplayStyle = window.getComputedStyle(bar).display;
+ if (barDisplayStyle !== 'none') {
+ bar.style.transitionDuration = '0s';
+ bar.addEventListener('transitionend', () => bar.style.transitionDuration = '');
+ }
+
+ const anchor = document.getElementById('noVNC_control_bar_anchor');
+ if (anchor.classList.contains("noVNC_right")) {
+ WebUtil.writeSetting('controlbar_pos', 'left');
+ anchor.classList.remove("noVNC_right");
+ } else {
+ WebUtil.writeSetting('controlbar_pos', 'right');
+ anchor.classList.add("noVNC_right");
+ }
+
+ // Consider this a movement of the handle
+ UI.controlbarDrag = true;
+
+ // The user has "followed" hint, let's hide it until the next drag
+ UI.showControlbarHint(false, false);
+ },
+
+ showControlbarHint(show, animate=true) {
+ const hint = document.getElementById('noVNC_control_bar_hint');
+
+ if (animate) {
+ hint.classList.remove("noVNC_notransition");
+ } else {
+ hint.classList.add("noVNC_notransition");
+ }
+
+ if (show) {
+ hint.classList.add("noVNC_active");
+ } else {
+ hint.classList.remove("noVNC_active");
+ }
+ },
+
+ dragControlbarHandle(e) {
+ if (!UI.controlbarGrabbed) return;
+
+ const ptr = getPointerEvent(e);
+
+ const anchor = document.getElementById('noVNC_control_bar_anchor');
+ if (ptr.clientX < (window.innerWidth * 0.1)) {
+ if (anchor.classList.contains("noVNC_right")) {
+ UI.toggleControlbarSide();
+ }
+ } else if (ptr.clientX > (window.innerWidth * 0.9)) {
+ if (!anchor.classList.contains("noVNC_right")) {
+ UI.toggleControlbarSide();
+ }
+ }
+
+ if (!UI.controlbarDrag) {
+ const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
+
+ if (dragDistance < dragThreshold) return;
+
+ UI.controlbarDrag = true;
+ }
+
+ const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
+
+ UI.moveControlbarHandle(eventY);
+
+ e.preventDefault();
+ e.stopPropagation();
+ UI.keepControlbar();
+ UI.activateControlbar();
+ },
+
+ // Move the handle but don't allow any position outside the bounds
+ moveControlbarHandle(viewportRelativeY) {
+ const handle = document.getElementById("noVNC_control_bar_handle");
+ const handleHeight = handle.getBoundingClientRect().height;
+ const controlbarBounds = document.getElementById("noVNC_control_bar")
+ .getBoundingClientRect();
+ const margin = 10;
+
+ // These heights need to be non-zero for the below logic to work
+ if (handleHeight === 0 || controlbarBounds.height === 0) {
+ return;
+ }
+
+ let newY = viewportRelativeY;
+
+ // Check if the coordinates are outside the control bar
+ if (newY < controlbarBounds.top + margin) {
+ // Force coordinates to be below the top of the control bar
+ newY = controlbarBounds.top + margin;
+
+ } else if (newY > controlbarBounds.top +
+ controlbarBounds.height - handleHeight - margin) {
+ // Force coordinates to be above the bottom of the control bar
+ newY = controlbarBounds.top +
+ controlbarBounds.height - handleHeight - margin;
+ }
+
+ // Corner case: control bar too small for stable position
+ if (controlbarBounds.height < (handleHeight + margin * 2)) {
+ newY = controlbarBounds.top +
+ (controlbarBounds.height - handleHeight) / 2;
+ }
+
+ // The transform needs coordinates that are relative to the parent
+ const parentRelativeY = newY - controlbarBounds.top;
+ handle.style.transform = "translateY(" + parentRelativeY + "px)";
+ },
+
+ updateControlbarHandle() {
+ // Since the control bar is fixed on the viewport and not the page,
+ // the move function expects coordinates relative the the viewport.
+ const handle = document.getElementById("noVNC_control_bar_handle");
+ const handleBounds = handle.getBoundingClientRect();
+ UI.moveControlbarHandle(handleBounds.top);
+ },
+
+ controlbarHandleMouseUp(e) {
+ if ((e.type == "mouseup") && (e.button != 0)) return;
+
+ // mouseup and mousedown on the same place toggles the controlbar
+ if (UI.controlbarGrabbed && !UI.controlbarDrag) {
+ UI.toggleControlbar();
+ e.preventDefault();
+ e.stopPropagation();
+ UI.keepControlbar();
+ UI.activateControlbar();
+ }
+ UI.controlbarGrabbed = false;
+ UI.showControlbarHint(false);
+ },
+
+ controlbarHandleMouseDown(e) {
+ if ((e.type == "mousedown") && (e.button != 0)) return;
+
+ const ptr = getPointerEvent(e);
+
+ const handle = document.getElementById("noVNC_control_bar_handle");
+ const bounds = handle.getBoundingClientRect();
+
+ // Touch events have implicit capture
+ if (e.type === "mousedown") {
+ setCapture(handle);
+ }
+
+ UI.controlbarGrabbed = true;
+ UI.controlbarDrag = false;
+
+ UI.showControlbarHint(true);
+
+ UI.controlbarMouseDownClientY = ptr.clientY;
+ UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
+ e.preventDefault();
+ e.stopPropagation();
+ UI.keepControlbar();
+ UI.activateControlbar();
+ },
+
+ toggleExpander(e) {
+ if (this.classList.contains("noVNC_open")) {
+ this.classList.remove("noVNC_open");
+ } else {
+ this.classList.add("noVNC_open");
+ }
+ },
+
+/* ------^-------
+ * /VISUAL
+ * ==============
+ * SETTINGS
+ * ------v------*/
+
+ // Initial page load read/initialization of settings
+ initSetting(name, defVal) {
+ // Has the user overridden the default value?
+ if (name in UI.customSettings.defaults) {
+ defVal = UI.customSettings.defaults[name];
+ }
+ // Check Query string followed by cookie
+ let val = WebUtil.getConfigVar(name);
+ if (val === null) {
+ val = WebUtil.readSetting(name, defVal);
+ }
+ WebUtil.setSetting(name, val);
+ UI.updateSetting(name);
+ // Has the user forced a value?
+ if (name in UI.customSettings.mandatory) {
+ val = UI.customSettings.mandatory[name];
+ UI.forceSetting(name, val);
+ }
+ return val;
+ },
+
+ // Set the new value, update and disable form control setting
+ forceSetting(name, val) {
+ WebUtil.setSetting(name, val);
+ UI.updateSetting(name);
+ UI.disableSetting(name);
+ },
+
+ // Update cookie and form control setting. If value is not set, then
+ // updates from control to current cookie setting.
+ updateSetting(name) {
+
+ // Update the settings control
+ let value = UI.getSetting(name);
+
+ const ctrl = document.getElementById('noVNC_setting_' + name);
+ if (ctrl === null) {
+ return;
+ }
+
+ if (ctrl.type === 'checkbox') {
+ ctrl.checked = value;
+ } else if (typeof ctrl.options !== 'undefined') {
+ for (let i = 0; i < ctrl.options.length; i += 1) {
+ if (ctrl.options[i].value === value) {
+ ctrl.selectedIndex = i;
+ break;
+ }
+ }
+ } else {
+ ctrl.value = value;
+ }
+ },
+
+ // Save control setting to cookie
+ saveSetting(name) {
+ const ctrl = document.getElementById('noVNC_setting_' + name);
+ let val;
+ if (ctrl.type === 'checkbox') {
+ val = ctrl.checked;
+ } else if (typeof ctrl.options !== 'undefined') {
+ val = ctrl.options[ctrl.selectedIndex].value;
+ } else {
+ val = ctrl.value;
+ }
+ WebUtil.writeSetting(name, val);
+ //Log.Debug("Setting saved '" + name + "=" + val + "'");
+ return val;
+ },
+
+ // Read form control compatible setting from cookie
+ getSetting(name) {
+ const ctrl = document.getElementById('noVNC_setting_' + name);
+ let val = WebUtil.readSetting(name);
+ if (typeof val !== 'undefined' && val !== null &&
+ ctrl !== null && ctrl.type === 'checkbox') {
+ if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) {
+ val = false;
+ } else {
+ val = true;
+ }
+ }
+ return val;
+ },
+
+ // These helpers compensate for the lack of parent-selectors and
+ // previous-sibling-selectors in CSS which are needed when we want to
+ // disable the labels that belong to disabled input elements.
+ disableSetting(name) {
+ const ctrl = document.getElementById('noVNC_setting_' + name);
+ if (ctrl !== null) {
+ ctrl.disabled = true;
+ if (ctrl.label !== undefined) {
+ ctrl.label.classList.add('noVNC_disabled');
+ }
+ }
+ },
+
+ enableSetting(name) {
+ const ctrl = document.getElementById('noVNC_setting_' + name);
+ if (ctrl !== null) {
+ ctrl.disabled = false;
+ if (ctrl.label !== undefined) {
+ ctrl.label.classList.remove('noVNC_disabled');
+ }
+ }
+ },
+
+/* ------^-------
+ * /SETTINGS
+ * ==============
+ * PANELS
+ * ------v------*/
+
+ closeAllPanels() {
+ UI.closeSettingsPanel();
+ UI.closePowerPanel();
+ UI.closeClipboardPanel();
+ UI.closeExtraKeys();
+ },
+
+/* ------^-------
+ * /PANELS
+ * ==============
+ * SETTINGS (panel)
+ * ------v------*/
+
+ openSettingsPanel() {
+ UI.closeAllPanels();
+ UI.openControlbar();
+
+ // Refresh UI elements from saved cookies
+ UI.updateSetting('encrypt');
+ UI.updateSetting('view_clip');
+ UI.updateSetting('resize');
+ UI.updateSetting('quality');
+ UI.updateSetting('compression');
+ UI.updateSetting('shared');
+ UI.updateSetting('view_only');
+ UI.updateSetting('path');
+ UI.updateSetting('repeaterID');
+ UI.updateSetting('logging');
+ UI.updateSetting('reconnect');
+ UI.updateSetting('reconnect_delay');
+
+ document.getElementById('noVNC_settings')
+ .classList.add("noVNC_open");
+ document.getElementById('noVNC_settings_button')
+ .classList.add("noVNC_selected");
+ },
+
+ closeSettingsPanel() {
+ document.getElementById('noVNC_settings')
+ .classList.remove("noVNC_open");
+ document.getElementById('noVNC_settings_button')
+ .classList.remove("noVNC_selected");
+ },
+
+ toggleSettingsPanel() {
+ if (document.getElementById('noVNC_settings')
+ .classList.contains("noVNC_open")) {
+ UI.closeSettingsPanel();
+ } else {
+ UI.openSettingsPanel();
+ }
+ },
+
+/* ------^-------
+ * /SETTINGS
+ * ==============
+ * POWER
+ * ------v------*/
+
+ openPowerPanel() {
+ UI.closeAllPanels();
+ UI.openControlbar();
+
+ document.getElementById('noVNC_power')
+ .classList.add("noVNC_open");
+ document.getElementById('noVNC_power_button')
+ .classList.add("noVNC_selected");
+ },
+
+ closePowerPanel() {
+ document.getElementById('noVNC_power')
+ .classList.remove("noVNC_open");
+ document.getElementById('noVNC_power_button')
+ .classList.remove("noVNC_selected");
+ },
+
+ togglePowerPanel() {
+ if (document.getElementById('noVNC_power')
+ .classList.contains("noVNC_open")) {
+ UI.closePowerPanel();
+ } else {
+ UI.openPowerPanel();
+ }
+ },
+
+ // Disable/enable power button
+ updatePowerButton() {
+ if (UI.connected &&
+ UI.rfb.capabilities.power &&
+ !UI.rfb.viewOnly) {
+ document.getElementById('noVNC_power_button')
+ .classList.remove("noVNC_hidden");
+ } else {
+ document.getElementById('noVNC_power_button')
+ .classList.add("noVNC_hidden");
+ // Close power panel if open
+ UI.closePowerPanel();
+ }
+ },
+
+/* ------^-------
+ * /POWER
+ * ==============
+ * CLIPBOARD
+ * ------v------*/
+
+ openClipboardPanel() {
+ UI.closeAllPanels();
+ UI.openControlbar();
+
+ document.getElementById('noVNC_clipboard')
+ .classList.add("noVNC_open");
+ document.getElementById('noVNC_clipboard_button')
+ .classList.add("noVNC_selected");
+ },
+
+ closeClipboardPanel() {
+ document.getElementById('noVNC_clipboard')
+ .classList.remove("noVNC_open");
+ document.getElementById('noVNC_clipboard_button')
+ .classList.remove("noVNC_selected");
+ },
+
+ toggleClipboardPanel() {
+ if (document.getElementById('noVNC_clipboard')
+ .classList.contains("noVNC_open")) {
+ UI.closeClipboardPanel();
+ } else {
+ UI.openClipboardPanel();
+ }
+ },
+
+ clipboardReceive(e) {
+ Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0, 40) + "...");
+ document.getElementById('noVNC_clipboard_text').value = e.detail.text;
+ Log.Debug("<< UI.clipboardReceive");
+ },
+
+ clipboardSend() {
+ const text = document.getElementById('noVNC_clipboard_text').value;
+ Log.Debug(">> UI.clipboardSend: " + text.substr(0, 40) + "...");
+ UI.rfb.clipboardPasteFrom(text);
+ Log.Debug("<< UI.clipboardSend");
+ },
+
+/* ------^-------
+ * /CLIPBOARD
+ * ==============
+ * CONNECTION
+ * ------v------*/
+
+ openConnectPanel() {
+ document.getElementById('noVNC_connect_dlg')
+ .classList.add("noVNC_open");
+ },
+
+ closeConnectPanel() {
+ document.getElementById('noVNC_connect_dlg')
+ .classList.remove("noVNC_open");
+ },
+
+ connect(event, password) {
+
+ // Ignore when rfb already exists
+ if (typeof UI.rfb !== 'undefined') {
+ return;
+ }
+
+ const host = UI.getSetting('host');
+ const port = UI.getSetting('port');
+ const path = UI.getSetting('path');
+
+ if (typeof password === 'undefined') {
+ password = UI.getSetting('password');
+ UI.reconnectPassword = password;
+ }
+
+ if (password === null) {
+ password = undefined;
+ }
+
+ UI.hideStatus();
+
+ UI.closeConnectPanel();
+
+ UI.updateVisualState('connecting');
+
+ let url;
+
+ if (host) {
+ url = new URL("https://" + host);
+
+ url.protocol = UI.getSetting('encrypt') ? 'wss:' : 'ws:';
+ if (port) {
+ url.port = port;
+ }
+
+ // "./" is needed to force URL() to interpret the path-variable as
+ // a path and not as an URL. This is relevant if for example path
+ // starts with more than one "/", in which case it would be
+ // interpreted as a host name instead.
+ url = new URL("./" + path, url);
+ } else {
+ // Current (May 2024) browsers support relative WebSocket
+ // URLs natively, but we need to support older browsers for
+ // some time.
+ url = new URL(path, location.href);
+ url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:';
+ }
+
+ if (UI.getSetting('keep_device_awake')) {
+ UI.wakeLockManager.acquire();
+ }
+
+ try {
+ UI.rfb = new RFB(document.getElementById('noVNC_container'),
+ url.href,
+ { shared: UI.getSetting('shared'),
+ repeaterID: UI.getSetting('repeaterID'),
+ credentials: { password: password } });
+ } catch (exc) {
+ Log.Error("Failed to connect to server: " + exc);
+ UI.updateVisualState('disconnected');
+ UI.showStatus(_("Failed to connect to server: ") + exc, 'error');
+ return;
+ }
+
+ UI.rfb.addEventListener("connect", UI.connectFinished);
+ UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
+ UI.rfb.addEventListener("serververification", UI.serverVerify);
+ UI.rfb.addEventListener("credentialsrequired", UI.credentials);
+ UI.rfb.addEventListener("securityfailure", UI.securityFailed);
+ UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag);
+ UI.rfb.addEventListener("capabilities", UI.updatePowerButton);
+ UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
+ UI.rfb.addEventListener("bell", UI.bell);
+ UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
+ UI.rfb.clipViewport = UI.getSetting('view_clip');
+ UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
+ 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.updateClipboard();
+ },
+
+ disconnect() {
+ UI.rfb.disconnect();
+
+ UI.connected = false;
+
+ // Disable automatic reconnecting
+ UI.inhibitReconnect = true;
+
+ UI.updateVisualState('disconnecting');
+
+ // Don't display the connection settings until we're actually disconnected
+ },
+
+ reconnect() {
+ UI.reconnectCallback = null;
+
+ // if reconnect has been disabled in the meantime, do nothing.
+ if (UI.inhibitReconnect) {
+ return;
+ }
+
+ UI.connect(null, UI.reconnectPassword);
+ },
+
+ cancelReconnect() {
+ if (UI.reconnectCallback !== null) {
+ clearTimeout(UI.reconnectCallback);
+ UI.reconnectCallback = null;
+ }
+
+ UI.updateVisualState('disconnected');
+
+ UI.openControlbar();
+ UI.openConnectPanel();
+ },
+
+ connectFinished(e) {
+ UI.connected = true;
+ UI.inhibitReconnect = false;
+
+ let msg;
+ if (UI.getSetting('encrypt')) {
+ msg = _("Connected (encrypted) to ") + UI.desktopName;
+ } else {
+ msg = _("Connected (unencrypted) to ") + UI.desktopName;
+ }
+ UI.showStatus(msg);
+ UI.updateVisualState('connected');
+
+ UI.updateBeforeUnload();
+
+ // Do this last because it can only be used on rendered elements
+ UI.rfb.focus();
+ },
+
+ disconnectFinished(e) {
+ const wasConnected = UI.connected;
+
+ // This variable is ideally set when disconnection starts, but
+ // when the disconnection isn't clean or if it is initiated by
+ // the server, we need to do it here as well since
+ // UI.disconnect() won't be used in those cases.
+ UI.connected = false;
+
+ UI.rfb = undefined;
+ UI.wakeLockManager.release();
+
+ if (!e.detail.clean) {
+ UI.updateVisualState('disconnected');
+ if (wasConnected) {
+ UI.showStatus(_("Something went wrong, connection is closed"),
+ 'error');
+ } else {
+ UI.showStatus(_("Failed to connect to server"), 'error');
+ }
+ }
+ // If reconnecting is allowed process it now
+ if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
+ UI.updateVisualState('reconnecting');
+
+ const delay = parseInt(UI.getSetting('reconnect_delay'));
+ UI.reconnectCallback = setTimeout(UI.reconnect, delay);
+ return;
+ } else {
+ UI.updateVisualState('disconnected');
+ UI.showStatus(_("Disconnected"), 'normal');
+ }
+
+ UI.updateBeforeUnload();
+
+ document.title = PAGE_TITLE;
+
+ UI.openControlbar();
+ UI.openConnectPanel();
+ },
+
+ securityFailed(e) {
+ let msg = "";
+ // On security failures we might get a string with a reason
+ // directly from the server. Note that we can't control if
+ // this string is translated or not.
+ if ('reason' in e.detail) {
+ msg = _("New connection has been rejected with reason: ") +
+ e.detail.reason;
+ } else {
+ msg = _("New connection has been rejected");
+ }
+ UI.showStatus(msg, 'error');
+ },
+
+ handleBeforeUnload(e) {
+ // Trigger a "Leave site?" warning prompt before closing the
+ // page. Modern browsers (Oct 2025) accept either (or both)
+ // preventDefault() or a nonempty returnValue, though the latter is
+ // considered legacy. The custom string is ignored by modern browsers,
+ // which display a native message, but older browsers will show it.
+ e.preventDefault();
+ e.returnValue = _("Are you sure you want to disconnect the session?");
+ },
+
+ updateBeforeUnload() {
+ // Remove first to avoid adding duplicates
+ window.removeEventListener("beforeunload", UI.handleBeforeUnload);
+ if (!UI.rfb?.viewOnly && UI.connected) {
+ window.addEventListener("beforeunload", UI.handleBeforeUnload);
+ }
+ },
+
+/* ------^-------
+ * /CONNECTION
+ * ==============
+ * SERVER VERIFY
+ * ------v------*/
+
+ async serverVerify(e) {
+ const type = e.detail.type;
+ if (type === 'RSA') {
+ const publickey = e.detail.publickey;
+ let fingerprint = await window.crypto.subtle.digest("SHA-1", publickey);
+ // The same fingerprint format as RealVNC
+ fingerprint = Array.from(new Uint8Array(fingerprint).slice(0, 8)).map(
+ x => x.toString(16).padStart(2, '0')).join('-');
+ document.getElementById('noVNC_verify_server_dlg').classList.add('noVNC_open');
+ document.getElementById('noVNC_fingerprint').innerHTML = fingerprint;
+ }
+ },
+
+ approveServer(e) {
+ e.preventDefault();
+ document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');
+ UI.rfb.approveServer();
+ },
+
+ rejectServer(e) {
+ e.preventDefault();
+ document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');
+ UI.disconnect();
+ },
+
+/* ------^-------
+ * /SERVER VERIFY
+ * ==============
+ * PASSWORD
+ * ------v------*/
+
+ credentials(e) {
+ // FIXME: handle more types
+
+ 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');
+
+ setTimeout(() => document
+ .getElementById(inputFocus).focus(), 100);
+
+ Log.Warn("Server asked for credentials");
+ UI.showStatus(_("Credentials are required"), "warning");
+ },
+
+ setCredentials(e) {
+ // Prevent actually submitting the form
+ e.preventDefault();
+
+ let inputElemUsername = document.getElementById('noVNC_username_input');
+ const username = inputElemUsername.value;
+
+ let inputElemPassword = document.getElementById('noVNC_password_input');
+ const password = inputElemPassword.value;
+ // Clear the input after reading the password
+ inputElemPassword.value = "";
+
+ UI.rfb.sendCredentials({ username: username, password: password });
+ UI.reconnectPassword = password;
+ document.getElementById('noVNC_credentials_dlg')
+ .classList.remove('noVNC_open');
+ },
+
+/* ------^-------
+ * /PASSWORD
+ * ==============
+ * FULLSCREEN
+ * ------v------*/
+
+ toggleFullscreen() {
+ if (document.fullscreenElement || // alternative standard method
+ document.mozFullScreenElement || // currently working methods
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement) {
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ } else if (document.mozCancelFullScreen) {
+ document.mozCancelFullScreen();
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen();
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen();
+ }
+ } else {
+ if (document.documentElement.requestFullscreen) {
+ document.documentElement.requestFullscreen();
+ } else if (document.documentElement.mozRequestFullScreen) {
+ document.documentElement.mozRequestFullScreen();
+ } else if (document.documentElement.webkitRequestFullscreen) {
+ document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+ } else if (document.body.msRequestFullscreen) {
+ document.body.msRequestFullscreen();
+ }
+ }
+ UI.updateFullscreenButton();
+ },
+
+ updateFullscreenButton() {
+ if (document.fullscreenElement || // alternative standard method
+ document.mozFullScreenElement || // currently working methods
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement ) {
+ document.getElementById('noVNC_fullscreen_button')
+ .classList.add("noVNC_selected");
+ } else {
+ document.getElementById('noVNC_fullscreen_button')
+ .classList.remove("noVNC_selected");
+ }
+ },
+
+/* ------^-------
+ * /FULLSCREEN
+ * ==============
+ * RESIZE
+ * ------v------*/
+
+ // Apply remote resizing or local scaling
+ applyResizeMode() {
+ if (!UI.rfb) return;
+
+ UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
+ UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
+ },
+
+/* ------^-------
+ * /RESIZE
+ * ==============
+ * VIEW CLIPPING
+ * ------v------*/
+
+ // Update viewport clipping property for the connection. The normal
+ // case is to get the value from the setting. There are special cases
+ // for when the viewport is scaled or when a touch device is used.
+ updateViewClip() {
+ if (!UI.rfb) return;
+
+ const scaling = UI.getSetting('resize') === 'scale';
+
+ // Some platforms have overlay scrollbars that are difficult
+ // to use in our case, which means we have to force panning
+ // FIXME: Working scrollbars can still be annoying to use with
+ // touch, so we should ideally be able to have both
+ // panning and scrollbars at the same time
+
+ let brokenScrollbars = false;
+
+ if (!hasScrollbarGutter) {
+ if (isIOS() || isAndroid() || isMac() || isChromeOS()) {
+ brokenScrollbars = true;
+ }
+ }
+
+ if (scaling) {
+ // Can't be clipping if viewport is scaled to fit
+ UI.forceSetting('view_clip', false);
+ UI.rfb.clipViewport = false;
+ } else if (brokenScrollbars) {
+ UI.forceSetting('view_clip', true);
+ UI.rfb.clipViewport = true;
+ } else {
+ UI.enableSetting('view_clip');
+ UI.rfb.clipViewport = UI.getSetting('view_clip');
+ }
+
+ // Changing the viewport may change the state of
+ // the dragging button
+ UI.updateViewDrag();
+ },
+
+/* ------^-------
+ * /VIEW CLIPPING
+ * ==============
+ * VIEWDRAG
+ * ------v------*/
+
+ toggleViewDrag() {
+ if (!UI.rfb) return;
+
+ UI.rfb.dragViewport = !UI.rfb.dragViewport;
+ UI.updateViewDrag();
+ },
+
+ updateViewDrag() {
+ if (!UI.connected) return;
+
+ const viewDragButton = document.getElementById('noVNC_view_drag_button');
+
+ if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) &&
+ UI.rfb.dragViewport) {
+ // We are no longer clipping the viewport. Make sure
+ // viewport drag isn't active when it can't be used.
+ UI.rfb.dragViewport = false;
+ }
+
+ if (UI.rfb.dragViewport) {
+ viewDragButton.classList.add("noVNC_selected");
+ } else {
+ viewDragButton.classList.remove("noVNC_selected");
+ }
+
+ if (UI.rfb.clipViewport) {
+ viewDragButton.classList.remove("noVNC_hidden");
+ } else {
+ viewDragButton.classList.add("noVNC_hidden");
+ }
+
+ viewDragButton.disabled = !UI.rfb.clippingViewport;
+ },
+
+/* ------^-------
+ * /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
+ * ------v------*/
+
+ showVirtualKeyboard() {
+ if (!isTouchDevice) return;
+
+ const input = document.getElementById('noVNC_keyboardinput');
+
+ if (document.activeElement == input) return;
+
+ input.focus();
+
+ try {
+ const l = input.value.length;
+ // Move the caret to the end
+ input.setSelectionRange(l, l);
+ } catch (err) {
+ // setSelectionRange is undefined in Google Chrome
+ }
+ },
+
+ hideVirtualKeyboard() {
+ if (!isTouchDevice) return;
+
+ const input = document.getElementById('noVNC_keyboardinput');
+
+ if (document.activeElement != input) return;
+
+ input.blur();
+ },
+
+ toggleVirtualKeyboard() {
+ if (document.getElementById('noVNC_keyboard_button')
+ .classList.contains("noVNC_selected")) {
+ UI.hideVirtualKeyboard();
+ } else {
+ UI.showVirtualKeyboard();
+ }
+ },
+
+ onfocusVirtualKeyboard(event) {
+ document.getElementById('noVNC_keyboard_button')
+ .classList.add("noVNC_selected");
+ if (UI.rfb) {
+ UI.rfb.focusOnClick = false;
+ }
+ },
+
+ onblurVirtualKeyboard(event) {
+ document.getElementById('noVNC_keyboard_button')
+ .classList.remove("noVNC_selected");
+ if (UI.rfb) {
+ UI.rfb.focusOnClick = true;
+ }
+ },
+
+ keepVirtualKeyboard(event) {
+ const input = document.getElementById('noVNC_keyboardinput');
+
+ // Only prevent focus change if the virtual keyboard is active
+ if (document.activeElement != input) {
+ return;
+ }
+
+ // Only allow focus to move to other elements that need
+ // focus to function properly
+ if (event.target.form !== undefined) {
+ switch (event.target.type) {
+ case 'text':
+ case 'email':
+ case 'search':
+ case 'password':
+ case 'tel':
+ case 'url':
+ case 'textarea':
+ case 'select-one':
+ case 'select-multiple':
+ return;
+ }
+ }
+
+ event.preventDefault();
+ },
+
+ keyboardinputReset() {
+ const kbi = document.getElementById('noVNC_keyboardinput');
+ kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
+ UI.lastKeyboardinput = kbi.value;
+ },
+
+ keyEvent(keysym, code, down) {
+ if (!UI.rfb) return;
+
+ UI.rfb.sendKey(keysym, code, down);
+ },
+
+ // When normal keyboard events are left uncought, use the input events from
+ // the keyboardinput element instead and generate the corresponding key events.
+ // This code is required since some browsers on Android are inconsistent in
+ // sending keyCodes in the normal keyboard events when using on screen keyboards.
+ keyInput(event) {
+
+ if (!UI.rfb) return;
+
+ const newValue = event.target.value;
+
+ if (!UI.lastKeyboardinput) {
+ UI.keyboardinputReset();
+ }
+ const oldValue = UI.lastKeyboardinput;
+
+ let newLen;
+ try {
+ // Try to check caret position since whitespace at the end
+ // will not be considered by value.length in some browsers
+ newLen = Math.max(event.target.selectionStart, newValue.length);
+ } catch (err) {
+ // selectionStart is undefined in Google Chrome
+ newLen = newValue.length;
+ }
+ const oldLen = oldValue.length;
+
+ let inputs = newLen - oldLen;
+ let backspaces = inputs < 0 ? -inputs : 0;
+
+ // Compare the old string with the new to account for
+ // text-corrections or other input that modify existing text
+ for (let i = 0; i < Math.min(oldLen, newLen); i++) {
+ if (newValue.charAt(i) != oldValue.charAt(i)) {
+ inputs = newLen - i;
+ backspaces = oldLen - i;
+ break;
+ }
+ }
+
+ // Send the key events
+ for (let i = 0; i < backspaces; i++) {
+ UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
+ }
+ for (let i = newLen - inputs; i < newLen; i++) {
+ UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));
+ }
+
+ // Control the text content length in the keyboardinput element
+ if (newLen > 2 * UI.defaultKeyboardinputLen) {
+ UI.keyboardinputReset();
+ } else if (newLen < 1) {
+ // There always have to be some text in the keyboardinput
+ // element with which backspace can interact.
+ UI.keyboardinputReset();
+ // This sometimes causes the keyboard to disappear for a second
+ // but it is required for the android keyboard to recognize that
+ // text has been added to the field
+ event.target.blur();
+ // This has to be ran outside of the input handler in order to work
+ setTimeout(event.target.focus.bind(event.target), 0);
+ } else {
+ UI.lastKeyboardinput = newValue;
+ }
+ },
+
+/* ------^-------
+ * /KEYBOARD
+ * ==============
+ * EXTRA KEYS
+ * ------v------*/
+
+ openExtraKeys() {
+ UI.closeAllPanels();
+ UI.openControlbar();
+
+ document.getElementById('noVNC_modifiers')
+ .classList.add("noVNC_open");
+ document.getElementById('noVNC_toggle_extra_keys_button')
+ .classList.add("noVNC_selected");
+ },
+
+ closeExtraKeys() {
+ document.getElementById('noVNC_modifiers')
+ .classList.remove("noVNC_open");
+ document.getElementById('noVNC_toggle_extra_keys_button')
+ .classList.remove("noVNC_selected");
+ },
+
+ toggleExtraKeys() {
+ if (document.getElementById('noVNC_modifiers')
+ .classList.contains("noVNC_open")) {
+ UI.closeExtraKeys();
+ } else {
+ UI.openExtraKeys();
+ }
+ },
+
+ sendEsc() {
+ UI.sendKey(KeyTable.XK_Escape, "Escape");
+ },
+
+ sendTab() {
+ UI.sendKey(KeyTable.XK_Tab, "Tab");
+ },
+
+ toggleCtrl() {
+ const btn = document.getElementById('noVNC_toggle_ctrl_button');
+ if (btn.classList.contains("noVNC_selected")) {
+ UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
+ btn.classList.remove("noVNC_selected");
+ } else {
+ UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
+ btn.classList.add("noVNC_selected");
+ }
+ },
+
+ toggleWindows() {
+ const btn = document.getElementById('noVNC_toggle_windows_button');
+ if (btn.classList.contains("noVNC_selected")) {
+ UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", false);
+ btn.classList.remove("noVNC_selected");
+ } else {
+ UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", true);
+ btn.classList.add("noVNC_selected");
+ }
+ },
+
+ toggleAlt() {
+ const btn = document.getElementById('noVNC_toggle_alt_button');
+ if (btn.classList.contains("noVNC_selected")) {
+ UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
+ btn.classList.remove("noVNC_selected");
+ } else {
+ UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
+ btn.classList.add("noVNC_selected");
+ }
+ },
+
+ sendCtrlAltDel() {
+ UI.rfb.sendCtrlAltDel();
+ // See below
+ UI.rfb.focus();
+ UI.idleControlbar();
+ },
+
+ sendKey(keysym, code, down) {
+ 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();
+ },
+
+/* ------^-------
+ * /EXTRA KEYS
+ * ==============
+ * MISC
+ * ------v------*/
+
+ updateViewOnly() {
+ if (!UI.rfb) return;
+ UI.rfb.viewOnly = UI.getSetting('view_only');
+
+ UI.updateBeforeUnload();
+
+ // Hide input related buttons in view only mode
+ if (UI.rfb.viewOnly) {
+ document.getElementById('noVNC_keyboard_button')
+ .classList.add('noVNC_hidden');
+ document.getElementById('noVNC_toggle_extra_keys_button')
+ .classList.add('noVNC_hidden');
+ document.getElementById('noVNC_clipboard_button')
+ .classList.add('noVNC_hidden');
+ } else {
+ document.getElementById('noVNC_keyboard_button')
+ .classList.remove('noVNC_hidden');
+ document.getElementById('noVNC_toggle_extra_keys_button')
+ .classList.remove('noVNC_hidden');
+ document.getElementById('noVNC_clipboard_button')
+ .classList.remove('noVNC_hidden');
+ }
+ },
+
+ updateClipboard() {
+ browserAsyncClipboardSupport()
+ .then((support) => {
+ if (support === 'unsupported') {
+ // Use fallback clipboard panel
+ return;
+ }
+ if (support === 'denied' || support === 'available') {
+ UI.closeClipboardPanel();
+ document.getElementById('noVNC_clipboard_button')
+ .classList.add('noVNC_hidden');
+ document.getElementById('noVNC_clipboard_button')
+ .removeEventListener('click', UI.toggleClipboardPanel);
+ document.getElementById('noVNC_clipboard_text')
+ .removeEventListener('change', UI.clipboardSend);
+ if (UI.rfb) {
+ UI.rfb.removeEventListener('clipboard', UI.clipboardReceive);
+ }
+ }
+ })
+ .catch(() => {
+ // Treat as unsupported
+ });
+ },
+
+ updateShowDotCursor() {
+ if (!UI.rfb) return;
+ UI.rfb.showDotCursor = UI.getSetting('show_dot');
+ },
+
+ updateLogging() {
+ WebUtil.initLogging(UI.getSetting('logging'));
+ },
+
+ updateDesktopName(e) {
+ UI.desktopName = e.detail.name;
+ // Display the desktop name in the document title
+ document.title = e.detail.name + " - " + PAGE_TITLE;
+ },
+
+ updateRequestWakelock() {
+ if (!UI.rfb) return;
+ if (UI.getSetting('keep_device_awake')) {
+ UI.wakeLockManager.acquire();
+ } else {
+ UI.wakeLockManager.release();
+ }
+ },
+
+
+ bell(e) {
+ if (UI.getSetting('bell') === 'on') {
+ const promise = document.getElementById('noVNC_bell').play();
+ // The standards disagree on the return value here
+ if (promise) {
+ promise.catch((e) => {
+ if (e.name === "NotAllowedError") {
+ // Ignore when the browser doesn't let us play audio.
+ // It is common that the browsers require audio to be
+ // initiated from a user action.
+ } else {
+ Log.Error("Unable to play bell: " + e);
+ }
+ });
+ }
+ }
+ },
+
+ //Helper to add options to dropdown.
+ addOption(selectbox, text, value) {
+ const optn = document.createElement("OPTION");
+ optn.text = text;
+ optn.value = value;
+ selectbox.options.add(optn);
+ },
+
+/* ------^-------
+ * /MISC
+ * ==============
+ */
+};
+
+export default UI;
pkg/web/noVNC/app/wakelock.js
@@ -0,0 +1,199 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2025 The noVNC authors
+ * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
+ *
+ * Wrapper around the `navigator.wakeLock` api that handles reacquiring the
+ * lock on visiblility changes.
+ *
+ * The `acquire` and `release` methods may be called any number of times. The
+ * most recent call dictates the desired end-state (if `acquire` was most
+ * recently called, then we will try to acquire and hold the wake lock).
+ */
+
+import * as Log from '../core/util/logging.js';
+
+const _STATES = {
+ /* No wake lock.
+ *
+ * Can transition to:
+ * - AWAITING_VISIBLE: `acquire` called when document is hidden.
+ * - ACQUIRING: `acquire` called.
+ * - ERROR: `acquired` called when the api is not available.
+ */
+ RELEASED: 'released',
+ /* Wake lock requested, waiting for browser.
+ *
+ * Can transition to:
+ * - ACQUIRED: success
+ * - ACQUIRING_WANT_RELEASE: `release` called while waiting
+ * - ERROR
+ */
+ ACQUIRING: 'acquiring',
+ /* Wake lock requested, release called, still waiting for browser.
+ *
+ * Can transition to:
+ * - ACQUIRING: `acquire` called (but promise has not resolved yet)
+ * - RELEASED: success
+ */
+ ACQUIRING_WANT_RELEASE: 'releasing',
+ /* Wake lock held.
+ *
+ * Can transition to:
+ * - AWAITING_VISIBLE: wakelock lost due to visibility change
+ * - RELEASED: success
+ */
+ ACQUIRED: 'acquired',
+ /* Caller wants wakelock, but we can not get it due to visibility.
+ *
+ * Can transition to:
+ * - ACQUIRING: document is now visible, attempting to get wakelock.
+ * - RELEASED: when release is called.
+ */
+ AWAITING_VISIBLE: 'awaiting_visible',
+ /* An error has occurred.
+ *
+ * Can transition to:
+ * - RELEASED: will happen immediately.
+ */
+ ERROR: 'error',
+};
+
+class TestOnlyWakeLockManagerStateChangeEvent extends Event {
+ constructor(oldState, newState) {
+ super("testOnlyStateChange");
+ this.oldState = oldState;
+ this.newState = newState;
+ }
+}
+
+export default class WakeLockManager extends EventTarget {
+ constructor() {
+ super();
+
+ this._state = _STATES.RELEASED;
+ this._wakelock = null;
+
+ this._eventHandlers = {
+ wakelockAcquired: this._wakelockAcquired.bind(this),
+ wakelockReleased: this._wakelockReleased.bind(this),
+ documentVisibilityChange: this._documentVisibilityChange.bind(this),
+ };
+ }
+
+ acquire() {
+ switch (this._state) {
+ case _STATES.ACQUIRING_WANT_RELEASE:
+ // We are currently waiting to acquire the wakelock. While
+ // waiting, `release()` was called. By transitioning back to
+ // ACQUIRING, we will keep the lock after we receive it.
+ this._transitionTo(_STATES.ACQUIRING);
+ break;
+ case _STATES.AWAITING_VISIBLE:
+ case _STATES.ACQUIRING:
+ case _STATES.ACQUIRED:
+ break;
+ case _STATES.ERROR:
+ case _STATES.RELEASED:
+ if (document.hidden) {
+ // We can not acquire the wakelock while the document is
+ // hidden (eg, not the active tab). Wait until it is
+ // visible, then acquire the wakelock.
+ this._awaitVisible();
+ break;
+ }
+ this._acquireWakelockNow();
+ break;
+ }
+ }
+
+ release() {
+ switch (this._state) {
+ case _STATES.ERROR:
+ case _STATES.RELEASED:
+ case _STATES.ACQUIRING_WANT_RELEASE:
+ break;
+ case _STATES.ACQUIRING:
+ // We are have requested (but not yet received) the wakelock.
+ // Give it up as soon as we acquire it.
+ this._transitionTo(_STATES.ACQUIRING_WANT_RELEASE);
+ break;
+ case _STATES.ACQUIRED:
+ // We remove the event listener first, as we don't want to be
+ // notified about this release (it is expected).
+ this._wakelock.removeEventListener("release", this._eventHandlers.wakelockReleased);
+ this._wakelock.release();
+ this._wakelock = null;
+ this._transitionTo(_STATES.RELEASED);
+ break;
+ case _STATES.AWAITING_VISIBLE:
+ // We don't currently have the lock, but are waiting for the
+ // document to become visible. By removing the event listener,
+ // we will not attempt to get the wakelock in the future.
+ document.removeEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
+ this._transitionTo(_STATES.RELEASED);
+ break;
+ }
+ }
+
+ _transitionTo(newState) {
+ let oldState = this._state;
+ Log.Debug(`WakelockManager transitioning ${oldState} -> ${newState}`);
+ this._state = newState;
+ this.dispatchEvent(new TestOnlyWakeLockManagerStateChangeEvent(oldState, newState));
+ }
+
+ _awaitVisible() {
+ document.addEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
+ this._transitionTo(_STATES.AWAITING_VISIBLE);
+ }
+
+ _acquireWakelockNow() {
+ if (!("wakeLock" in navigator)) {
+ Log.Warn("Unable to request wakeLock, Browser does not have wakeLock api");
+ this._transitionTo(_STATES.ERROR);
+ this._transitionTo(_STATES.RELEASED);
+ return;
+ }
+ navigator.wakeLock.request("screen")
+ .then(this._eventHandlers.wakelockAcquired)
+ .catch((err) => {
+ Log.Warn("Error occurred while acquiring wakelock: " + err);
+ this._transitionTo(_STATES.ERROR);
+ this._transitionTo(_STATES.RELEASED);
+ });
+ this._transitionTo(_STATES.ACQUIRING);
+ }
+
+
+ _wakelockAcquired(wakelock) {
+ if (this._state === _STATES.ACQUIRING_WANT_RELEASE) {
+ // We were requested to release the wakelock while we were trying to
+ // acquire it. Now that we have acquired it, immediately release it.
+ wakelock.release();
+ this._transitionTo(_STATES.RELEASED);
+ return;
+ }
+ this._wakelock = wakelock;
+ this._wakelock.addEventListener("release", this._eventHandlers.wakelockReleased);
+ this._transitionTo(_STATES.ACQUIRED);
+ }
+
+ _wakelockReleased(event) {
+ this._wakelock = null;
+ if (document.visibilityState === "visible") {
+ Log.Warn("Lost wakelock, but document is still visible. Not reacquiring");
+ this._transitionTo(_STATES.RELEASED);
+ return;
+ }
+ this._awaitVisible();
+ }
+
+ _documentVisibilityChange(event) {
+ if (document.visibilityState !== "visible") {
+ return;
+ }
+ document.removeEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
+ this._acquireWakelockNow();
+ }
+}
pkg/web/noVNC/app/webutil.js
@@ -0,0 +1,250 @@
+/*
+ * 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.
+ */
+
+import * as Log from '../core/util/logging.js';
+
+// init log level reading the logging HTTP param
+export function initLogging(level) {
+ "use strict";
+ if (typeof level !== "undefined") {
+ Log.initLogging(level);
+ } else {
+ const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);
+ Log.initLogging(param || undefined);
+ }
+}
+
+// Read a query string variable
+// A URL with a query parameter can look like this (But will most probably get logged on the http server):
+// https://www.example.com?myqueryparam=myvalue
+//
+// For privacy (Using a hastag #, the parameters will not be sent to the server)
+// the url can be requested in the following way:
+// https://www.example.com#myqueryparam=myvalue&password=secretvalue
+//
+// Even mixing public and non public parameters will work:
+// https://www.example.com?nonsecretparam=example.com#password=secretvalue
+export function getQueryVar(name, defVal) {
+ "use strict";
+ const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
+ match = document.location.href.match(re);
+ if (typeof defVal === 'undefined') { defVal = null; }
+
+ if (match) {
+ return decodeURIComponent(match[1]);
+ }
+
+ return defVal;
+}
+
+// Read a hash fragment variable
+export function getHashVar(name, defVal) {
+ "use strict";
+ const re = new RegExp('.*[&#]' + name + '=([^&]*)'),
+ match = document.location.hash.match(re);
+ if (typeof defVal === 'undefined') { defVal = null; }
+
+ if (match) {
+ return decodeURIComponent(match[1]);
+ }
+
+ return defVal;
+}
+
+// Read a variable from the fragment or the query string
+// Fragment takes precedence
+export function getConfigVar(name, defVal) {
+ "use strict";
+ const val = getHashVar(name);
+
+ if (val === null) {
+ return getQueryVar(name, defVal);
+ }
+
+ return val;
+}
+
+/*
+ * Cookie handling. Dervied from: http://www.quirksmode.org/js/cookies.html
+ */
+
+// No days means only for this browser session
+export function createCookie(name, value, days) {
+ "use strict";
+ let date, expires;
+ if (days) {
+ date = new Date();
+ date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
+ expires = "; expires=" + date.toGMTString();
+ } else {
+ expires = "";
+ }
+
+ let secure;
+ if (document.location.protocol === "https:") {
+ secure = "; secure";
+ } else {
+ secure = "";
+ }
+ document.cookie = name + "=" + value + expires + "; path=/" + secure;
+}
+
+export function readCookie(name, defaultValue) {
+ "use strict";
+ const nameEQ = name + "=";
+ const ca = document.cookie.split(';');
+
+ for (let i = 0; i < ca.length; i += 1) {
+ let c = ca[i];
+ while (c.charAt(0) === ' ') {
+ c = c.substring(1, c.length);
+ }
+ if (c.indexOf(nameEQ) === 0) {
+ return c.substring(nameEQ.length, c.length);
+ }
+ }
+
+ return (typeof defaultValue !== 'undefined') ? defaultValue : null;
+}
+
+export function eraseCookie(name) {
+ "use strict";
+ createCookie(name, "", -1);
+}
+
+/*
+ * Setting handling.
+ */
+
+let settings = {};
+
+export function initSettings() {
+ if (!window.chrome || !window.chrome.storage) {
+ settings = {};
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => window.chrome.storage.sync.get(resolve))
+ .then((cfg) => { settings = cfg; });
+}
+
+// Update the settings cache, but do not write to permanent storage
+export function setSetting(name, value) {
+ settings[name] = value;
+}
+
+// No days means only for this browser session
+export function writeSetting(name, value) {
+ "use strict";
+ if (settings[name] === value) return;
+ settings[name] = value;
+ if (window.chrome && window.chrome.storage) {
+ window.chrome.storage.sync.set(settings);
+ } else {
+ localStorageSet(name, value);
+ }
+}
+
+export function readSetting(name, defaultValue) {
+ "use strict";
+ let value;
+ if ((name in settings) || (window.chrome && window.chrome.storage)) {
+ value = settings[name];
+ } else {
+ value = localStorageGet(name);
+ settings[name] = value;
+ }
+ if (typeof value === "undefined") {
+ value = null;
+ }
+
+ if (value === null && typeof defaultValue !== "undefined") {
+ return defaultValue;
+ }
+
+ return value;
+}
+
+export function eraseSetting(name) {
+ "use strict";
+ // Deleting here means that next time the setting is read when using local
+ // storage, it will be pulled from local storage again.
+ // If the setting in local storage is changed (e.g. in another tab)
+ // between this delete and the next read, it could lead to an unexpected
+ // value change.
+ delete settings[name];
+ if (window.chrome && window.chrome.storage) {
+ window.chrome.storage.sync.remove(name);
+ } else {
+ localStorageRemove(name);
+ }
+}
+
+let loggedMsgs = [];
+function logOnce(msg, level = "warn") {
+ if (!loggedMsgs.includes(msg)) {
+ switch (level) {
+ case "error":
+ Log.Error(msg);
+ break;
+ case "warn":
+ Log.Warn(msg);
+ break;
+ case "debug":
+ Log.Debug(msg);
+ break;
+ default:
+ Log.Info(msg);
+ }
+ loggedMsgs.push(msg);
+ }
+}
+
+let cookiesMsg = "Couldn't access noVNC settings, are cookies disabled?";
+
+function localStorageGet(name) {
+ let r;
+ try {
+ r = localStorage.getItem(name);
+ } catch (e) {
+ if (e instanceof DOMException) {
+ logOnce(cookiesMsg);
+ logOnce("'localStorage.getItem(" + name + ")' failed: " + e,
+ "debug");
+ } else {
+ throw e;
+ }
+ }
+ return r;
+}
+function localStorageSet(name, value) {
+ try {
+ localStorage.setItem(name, value);
+ } catch (e) {
+ if (e instanceof DOMException) {
+ logOnce(cookiesMsg);
+ logOnce("'localStorage.setItem(" + name + "," + value +
+ ")' failed: " + e, "debug");
+ } else {
+ throw e;
+ }
+ }
+}
+function localStorageRemove(name) {
+ try {
+ localStorage.removeItem(name);
+ } catch (e) {
+ if (e instanceof DOMException) {
+ logOnce(cookiesMsg);
+ logOnce("'localStorage.removeItem(" + name + ")' failed: " + e,
+ "debug");
+ } else {
+ throw e;
+ }
+ }
+}
pkg/web/noVNC/core/crypto/aes.js
@@ -0,0 +1,178 @@
+export class AESECBCipher {
+ constructor() {
+ this._key = null;
+ }
+
+ get algorithm() {
+ return { name: "AES-ECB" };
+ }
+
+ static async importKey(key, _algorithm, extractable, keyUsages) {
+ const cipher = new AESECBCipher;
+ await cipher._importKey(key, extractable, keyUsages);
+ return cipher;
+ }
+
+ async _importKey(key, extractable, keyUsages) {
+ this._key = await window.crypto.subtle.importKey(
+ "raw", key, {name: "AES-CBC"}, extractable, keyUsages);
+ }
+
+ async encrypt(_algorithm, plaintext) {
+ const x = new Uint8Array(plaintext);
+ if (x.length % 16 !== 0 || this._key === null) {
+ return null;
+ }
+ const n = x.length / 16;
+ for (let i = 0; i < n; i++) {
+ const y = new Uint8Array(await window.crypto.subtle.encrypt({
+ name: "AES-CBC",
+ iv: new Uint8Array(16),
+ }, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16);
+ x.set(y, i * 16);
+ }
+ return x;
+ }
+}
+
+export class AESEAXCipher {
+ constructor() {
+ this._rawKey = null;
+ this._ctrKey = null;
+ this._cbcKey = null;
+ this._zeroBlock = new Uint8Array(16);
+ this._prefixBlock0 = this._zeroBlock;
+ this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
+ this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
+ }
+
+ get algorithm() {
+ return { name: "AES-EAX" };
+ }
+
+ async _encryptBlock(block) {
+ const encrypted = await window.crypto.subtle.encrypt({
+ name: "AES-CBC",
+ iv: this._zeroBlock,
+ }, this._cbcKey, block);
+ return new Uint8Array(encrypted).slice(0, 16);
+ }
+
+ async _initCMAC() {
+ const k1 = await this._encryptBlock(this._zeroBlock);
+ const k2 = new Uint8Array(16);
+ const v = k1[0] >>> 6;
+ for (let i = 0; i < 15; i++) {
+ k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
+ k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
+ }
+ const lut = [0x0, 0x87, 0x0e, 0x89];
+ k2[14] ^= v >>> 1;
+ k2[15] = (k1[15] << 2) ^ lut[v];
+ k1[15] = (k1[15] << 1) ^ lut[v >> 1];
+ this._k1 = k1;
+ this._k2 = k2;
+ }
+
+ async _encryptCTR(data, counter) {
+ const encrypted = await window.crypto.subtle.encrypt({
+ name: "AES-CTR",
+ counter: counter,
+ length: 128
+ }, this._ctrKey, data);
+ return new Uint8Array(encrypted);
+ }
+
+ async _decryptCTR(data, counter) {
+ const decrypted = await window.crypto.subtle.decrypt({
+ name: "AES-CTR",
+ counter: counter,
+ length: 128
+ }, this._ctrKey, data);
+ return new Uint8Array(decrypted);
+ }
+
+ async _computeCMAC(data, prefixBlock) {
+ if (prefixBlock.length !== 16) {
+ return null;
+ }
+ const n = Math.floor(data.length / 16);
+ const m = Math.ceil(data.length / 16);
+ const r = data.length - n * 16;
+ const cbcData = new Uint8Array((m + 1) * 16);
+ cbcData.set(prefixBlock);
+ cbcData.set(data, 16);
+ if (r === 0) {
+ for (let i = 0; i < 16; i++) {
+ cbcData[n * 16 + i] ^= this._k1[i];
+ }
+ } else {
+ cbcData[(n + 1) * 16 + r] = 0x80;
+ for (let i = 0; i < 16; i++) {
+ cbcData[(n + 1) * 16 + i] ^= this._k2[i];
+ }
+ }
+ let cbcEncrypted = await window.crypto.subtle.encrypt({
+ name: "AES-CBC",
+ iv: this._zeroBlock,
+ }, this._cbcKey, cbcData);
+
+ cbcEncrypted = new Uint8Array(cbcEncrypted);
+ const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
+ return mac;
+ }
+
+ static async importKey(key, _algorithm, _extractable, _keyUsages) {
+ const cipher = new AESEAXCipher;
+ await cipher._importKey(key);
+ return cipher;
+ }
+
+ async _importKey(key) {
+ this._rawKey = key;
+ this._ctrKey = await window.crypto.subtle.importKey(
+ "raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]);
+ this._cbcKey = await window.crypto.subtle.importKey(
+ "raw", key, {name: "AES-CBC"}, false, ["encrypt"]);
+ await this._initCMAC();
+ }
+
+ async encrypt(algorithm, message) {
+ const ad = algorithm.additionalData;
+ const nonce = algorithm.iv;
+ const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
+ const encrypted = await this._encryptCTR(message, nCMAC);
+ const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
+ const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
+ for (let i = 0; i < 16; i++) {
+ mac[i] ^= nCMAC[i] ^ adCMAC[i];
+ }
+ const res = new Uint8Array(16 + encrypted.length);
+ res.set(encrypted);
+ res.set(mac, encrypted.length);
+ return res;
+ }
+
+ async decrypt(algorithm, data) {
+ const encrypted = data.slice(0, data.length - 16);
+ const ad = algorithm.additionalData;
+ const nonce = algorithm.iv;
+ const mac = data.slice(data.length - 16);
+ const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
+ const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
+ const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
+ for (let i = 0; i < 16; i++) {
+ computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
+ }
+ if (computedMac.length !== mac.length) {
+ return null;
+ }
+ for (let i = 0; i < mac.length; i++) {
+ if (computedMac[i] !== mac[i]) {
+ return null;
+ }
+ }
+ const res = await this._decryptCTR(encrypted, nCMAC);
+ return res;
+ }
+}
pkg/web/noVNC/core/crypto/bigint.js
@@ -0,0 +1,34 @@
+export function modPow(b, e, m) {
+ let r = 1n;
+ b = b % m;
+ while (e > 0n) {
+ if ((e & 1n) === 1n) {
+ r = (r * b) % m;
+ }
+ e = e >> 1n;
+ b = (b * b) % m;
+ }
+ return r;
+}
+
+export function bigIntToU8Array(bigint, padLength=0) {
+ let hex = bigint.toString(16);
+ if (padLength === 0) {
+ padLength = Math.ceil(hex.length / 2);
+ }
+ hex = hex.padStart(padLength * 2, '0');
+ const length = hex.length / 2;
+ const arr = new Uint8Array(length);
+ for (let i = 0; i < length; i++) {
+ arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
+ }
+ return arr;
+}
+
+export function u8ArrayToBigInt(arr) {
+ let hex = '0x';
+ for (let i = 0; i < arr.length; i++) {
+ hex += arr[i].toString(16).padStart(2, '0');
+ }
+ return BigInt(hex);
+}
pkg/web/noVNC/core/crypto/crypto.js
@@ -0,0 +1,90 @@
+import { AESECBCipher, AESEAXCipher } from "./aes.js";
+import { DESCBCCipher, DESECBCipher } from "./des.js";
+import { RSACipher } from "./rsa.js";
+import { DHCipher } from "./dh.js";
+import { MD5 } from "./md5.js";
+
+// A single interface for the cryptographic algorithms not supported by SubtleCrypto.
+// Both synchronous and asynchronous implmentations are allowed.
+class LegacyCrypto {
+ constructor() {
+ this._algorithms = {
+ "AES-ECB": AESECBCipher,
+ "AES-EAX": AESEAXCipher,
+ "DES-ECB": DESECBCipher,
+ "DES-CBC": DESCBCCipher,
+ "RSA-PKCS1-v1_5": RSACipher,
+ "DH": DHCipher,
+ "MD5": MD5,
+ };
+ }
+
+ encrypt(algorithm, key, data) {
+ if (key.algorithm.name !== algorithm.name) {
+ throw new Error("algorithm does not match");
+ }
+ if (typeof key.encrypt !== "function") {
+ throw new Error("key does not support encryption");
+ }
+ return key.encrypt(algorithm, data);
+ }
+
+ decrypt(algorithm, key, data) {
+ if (key.algorithm.name !== algorithm.name) {
+ throw new Error("algorithm does not match");
+ }
+ if (typeof key.decrypt !== "function") {
+ throw new Error("key does not support encryption");
+ }
+ return key.decrypt(algorithm, data);
+ }
+
+ importKey(format, keyData, algorithm, extractable, keyUsages) {
+ if (format !== "raw") {
+ throw new Error("key format is not supported");
+ }
+ const alg = this._algorithms[algorithm.name];
+ if (typeof alg === "undefined" || typeof alg.importKey !== "function") {
+ throw new Error("algorithm is not supported");
+ }
+ return alg.importKey(keyData, algorithm, extractable, keyUsages);
+ }
+
+ generateKey(algorithm, extractable, keyUsages) {
+ const alg = this._algorithms[algorithm.name];
+ if (typeof alg === "undefined" || typeof alg.generateKey !== "function") {
+ throw new Error("algorithm is not supported");
+ }
+ return alg.generateKey(algorithm, extractable, keyUsages);
+ }
+
+ exportKey(format, key) {
+ if (format !== "raw") {
+ throw new Error("key format is not supported");
+ }
+ if (typeof key.exportKey !== "function") {
+ throw new Error("key does not support exportKey");
+ }
+ return key.exportKey();
+ }
+
+ digest(algorithm, data) {
+ const alg = this._algorithms[algorithm];
+ if (typeof alg !== "function") {
+ throw new Error("algorithm is not supported");
+ }
+ return alg(data);
+ }
+
+ deriveBits(algorithm, key, length) {
+ if (key.algorithm.name !== algorithm.name) {
+ throw new Error("algorithm does not match");
+ }
+ if (typeof key.deriveBits !== "function") {
+ throw new Error("key does not support deriveBits");
+ }
+ return key.deriveBits(algorithm, length);
+ }
+}
+
+export default new LegacyCrypto;
pkg/web/noVNC/core/crypto/des.js
@@ -0,0 +1,330 @@
+/*
+ * Ported from Flashlight VNC ActionScript implementation:
+ * http://www.wizhelp.com/flashlight-vnc/
+ *
+ * Full attribution follows:
+ *
+ * -------------------------------------------------------------------------
+ *
+ * This DES class has been extracted from package Acme.Crypto for use in VNC.
+ * The unnecessary odd parity code has been removed.
+ *
+ * These changes are:
+ * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+
+ * DesCipher - the DES encryption method
+ *
+ * The meat of this code is by Dave Zimmerman <dzimm@widget.com>, and is:
+ *
+ * Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software
+ * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and
+ * without fee is hereby granted, provided that this copyright notice is kept
+ * intact.
+ *
+ * WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY
+ * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+ * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE
+ * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
+ *
+ * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE
+ * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE
+ * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT
+ * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE
+ * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE
+ * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE
+ * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP
+ * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR
+ * HIGH RISK ACTIVITIES.
+ *
+ *
+ * The rest is:
+ *
+ * Copyright (C) 1996 by Jef Poskanzer <jef@acme.com>. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * Visit the ACME Labs Java page for up-to-date versions of this and other
+ * fine Java utilities: http://www.acme.com/java/
+ */
+
+/* eslint-disable comma-spacing */
+
+// Tables, permutations, S-boxes, etc.
+const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
+ 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
+ 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
+ totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28];
+
+const z = 0x0;
+let a,b,c,d,e,f;
+a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e;
+const SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d,
+ z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z,
+ a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f,
+ c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d];
+a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e;
+const SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d,
+ a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f,
+ z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z,
+ z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e];
+a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e;
+const SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f,
+ b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z,
+ c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d,
+ b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e];
+a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e;
+const SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d,
+ z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f,
+ b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e,
+ c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e];
+a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e;
+const SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z,
+ a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f,
+ z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e,
+ c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d];
+a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e;
+const SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f,
+ z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z,
+ b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z,
+ a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f];
+a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e;
+const SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f,
+ b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e,
+ b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e,
+ z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d];
+a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e;
+const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d,
+ c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z,
+ a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f,
+ z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e];
+
+/* eslint-enable comma-spacing */
+
+class DES {
+ constructor(password) {
+ this.keys = [];
+
+ // Set the key.
+ const pc1m = [], pcr = [], kn = [];
+
+ for (let j = 0, l = 56; j < 56; ++j, l -= 8) {
+ l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1
+ const m = l & 0x7;
+ pc1m[j] = ((password[l >>> 3] & (1<<m)) !== 0) ? 1: 0;
+ }
+
+ for (let i = 0; i < 16; ++i) {
+ const m = i << 1;
+ const n = m + 1;
+ kn[m] = kn[n] = 0;
+ for (let o = 28; o < 59; o += 28) {
+ for (let j = o - 28; j < o; ++j) {
+ const l = j + totrot[i];
+ pcr[j] = l < o ? pc1m[l] : pc1m[l - 28];
+ }
+ }
+ for (let j = 0; j < 24; ++j) {
+ if (pcr[PC2[j]] !== 0) {
+ kn[m] |= 1 << (23 - j);
+ }
+ if (pcr[PC2[j + 24]] !== 0) {
+ kn[n] |= 1 << (23 - j);
+ }
+ }
+ }
+
+ // cookey
+ for (let i = 0, rawi = 0, KnLi = 0; i < 16; ++i) {
+ const raw0 = kn[rawi++];
+ const raw1 = kn[rawi++];
+ this.keys[KnLi] = (raw0 & 0x00fc0000) << 6;
+ this.keys[KnLi] |= (raw0 & 0x00000fc0) << 10;
+ this.keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10;
+ this.keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6;
+ ++KnLi;
+ this.keys[KnLi] = (raw0 & 0x0003f000) << 12;
+ this.keys[KnLi] |= (raw0 & 0x0000003f) << 16;
+ this.keys[KnLi] |= (raw1 & 0x0003f000) >>> 4;
+ this.keys[KnLi] |= (raw1 & 0x0000003f);
+ ++KnLi;
+ }
+ }
+
+ // Encrypt 8 bytes of text
+ enc8(text) {
+ const b = text.slice();
+ let i = 0, l, r, x; // left, right, accumulator
+
+ // Squash 8 bytes to 2 ints
+ l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
+ r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
+
+ x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
+ r ^= x;
+ l ^= (x << 4);
+ x = ((l >>> 16) ^ r) & 0x0000ffff;
+ r ^= x;
+ l ^= (x << 16);
+ x = ((r >>> 2) ^ l) & 0x33333333;
+ l ^= x;
+ r ^= (x << 2);
+ x = ((r >>> 8) ^ l) & 0x00ff00ff;
+ l ^= x;
+ r ^= (x << 8);
+ r = (r << 1) | ((r >>> 31) & 1);
+ x = (l ^ r) & 0xaaaaaaaa;
+ l ^= x;
+ r ^= x;
+ l = (l << 1) | ((l >>> 31) & 1);
+
+ for (let i = 0, keysi = 0; i < 8; ++i) {
+ x = (r << 28) | (r >>> 4);
+ x ^= this.keys[keysi++];
+ let fval = SP7[x & 0x3f];
+ fval |= SP5[(x >>> 8) & 0x3f];
+ fval |= SP3[(x >>> 16) & 0x3f];
+ fval |= SP1[(x >>> 24) & 0x3f];
+ x = r ^ this.keys[keysi++];
+ fval |= SP8[x & 0x3f];
+ fval |= SP6[(x >>> 8) & 0x3f];
+ fval |= SP4[(x >>> 16) & 0x3f];
+ fval |= SP2[(x >>> 24) & 0x3f];
+ l ^= fval;
+ x = (l << 28) | (l >>> 4);
+ x ^= this.keys[keysi++];
+ fval = SP7[x & 0x3f];
+ fval |= SP5[(x >>> 8) & 0x3f];
+ fval |= SP3[(x >>> 16) & 0x3f];
+ fval |= SP1[(x >>> 24) & 0x3f];
+ x = l ^ this.keys[keysi++];
+ fval |= SP8[x & 0x0000003f];
+ fval |= SP6[(x >>> 8) & 0x3f];
+ fval |= SP4[(x >>> 16) & 0x3f];
+ fval |= SP2[(x >>> 24) & 0x3f];
+ r ^= fval;
+ }
+
+ r = (r << 31) | (r >>> 1);
+ x = (l ^ r) & 0xaaaaaaaa;
+ l ^= x;
+ r ^= x;
+ l = (l << 31) | (l >>> 1);
+ x = ((l >>> 8) ^ r) & 0x00ff00ff;
+ r ^= x;
+ l ^= (x << 8);
+ x = ((l >>> 2) ^ r) & 0x33333333;
+ r ^= x;
+ l ^= (x << 2);
+ x = ((r >>> 16) ^ l) & 0x0000ffff;
+ l ^= x;
+ r ^= (x << 16);
+ x = ((r >>> 4) ^ l) & 0x0f0f0f0f;
+ l ^= x;
+ r ^= (x << 4);
+
+ // Spread ints to bytes
+ x = [r, l];
+ for (i = 0; i < 8; i++) {
+ b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
+ if (b[i] < 0) { b[i] += 256; } // unsigned
+ }
+ return b;
+ }
+}
+
+export class DESECBCipher {
+ constructor() {
+ this._cipher = null;
+ }
+
+ get algorithm() {
+ return { name: "DES-ECB" };
+ }
+
+ static importKey(key, _algorithm, _extractable, _keyUsages) {
+ const cipher = new DESECBCipher;
+ cipher._importKey(key);
+ return cipher;
+ }
+
+ _importKey(key, _extractable, _keyUsages) {
+ this._cipher = new DES(key);
+ }
+
+ encrypt(_algorithm, plaintext) {
+ const x = new Uint8Array(plaintext);
+ if (x.length % 8 !== 0 || this._cipher === null) {
+ return null;
+ }
+ const n = x.length / 8;
+ for (let i = 0; i < n; i++) {
+ x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8);
+ }
+ return x;
+ }
+}
+
+export class DESCBCCipher {
+ constructor() {
+ this._cipher = null;
+ }
+
+ get algorithm() {
+ return { name: "DES-CBC" };
+ }
+
+ static importKey(key, _algorithm, _extractable, _keyUsages) {
+ const cipher = new DESCBCCipher;
+ cipher._importKey(key);
+ return cipher;
+ }
+
+ _importKey(key) {
+ this._cipher = new DES(key);
+ }
+
+ encrypt(algorithm, plaintext) {
+ const x = new Uint8Array(plaintext);
+ let y = new Uint8Array(algorithm.iv);
+ if (x.length % 8 !== 0 || this._cipher === null) {
+ return null;
+ }
+ const n = x.length / 8;
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < 8; j++) {
+ y[j] ^= plaintext[i * 8 + j];
+ }
+ y = this._cipher.enc8(y);
+ x.set(y, i * 8);
+ }
+ return x;
+ }
+}
pkg/web/noVNC/core/crypto/dh.js
@@ -0,0 +1,55 @@
+import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
+
+class DHPublicKey {
+ constructor(key) {
+ this._key = key;
+ }
+
+ get algorithm() {
+ return { name: "DH" };
+ }
+
+ exportKey() {
+ return this._key;
+ }
+}
+
+export class DHCipher {
+ constructor() {
+ this._g = null;
+ this._p = null;
+ this._gBigInt = null;
+ this._pBigInt = null;
+ this._privateKey = null;
+ }
+
+ get algorithm() {
+ return { name: "DH" };
+ }
+
+ static generateKey(algorithm, _extractable) {
+ const cipher = new DHCipher;
+ cipher._generateKey(algorithm);
+ return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) };
+ }
+
+ _generateKey(algorithm) {
+ const g = algorithm.g;
+ const p = algorithm.p;
+ this._keyBytes = p.length;
+ this._gBigInt = u8ArrayToBigInt(g);
+ this._pBigInt = u8ArrayToBigInt(p);
+ this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes));
+ this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey);
+ this._publicKey = bigIntToU8Array(modPow(
+ this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes);
+ }
+
+ deriveBits(algorithm, length) {
+ const bytes = Math.ceil(length / 8);
+ const pkey = new Uint8Array(algorithm.public);
+ const len = bytes > this._keyBytes ? bytes : this._keyBytes;
+ const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt);
+ return bigIntToU8Array(secret, len).slice(0, len);
+ }
+}
pkg/web/noVNC/core/crypto/md5.js
@@ -0,0 +1,82 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2021 The noVNC authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ */
+
+/*
+ * Performs MD5 hashing on an array of bytes, returns an array of bytes
+ */
+
+export async function MD5(d) {
+ let s = "";
+ for (let i = 0; i < d.length; i++) {
+ s += String.fromCharCode(d[i]);
+ }
+ return M(V(Y(X(s), 8 * s.length)));
+}
+
+function M(d) {
+ let f = new Uint8Array(d.length);
+ for (let i=0;i<d.length;i++) {
+ f[i] = d.charCodeAt(i);
+ }
+ return f;
+}
+
+function X(d) {
+ let r = Array(d.length >> 2);
+ for (let m = 0; m < r.length; m++) r[m] = 0;
+ for (let m = 0; m < 8 * d.length; m += 8) r[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32;
+ return r;
+}
+
+function V(d) {
+ let r = "";
+ for (let m = 0; m < 32 * d.length; m += 8) r += String.fromCharCode(d[m >> 5] >>> m % 32 & 255);
+ return r;
+}
+
+function Y(d, g) {
+ d[g >> 5] |= 128 << g % 32, d[14 + (g + 64 >>> 9 << 4)] = g;
+ let m = 1732584193, f = -271733879, r = -1732584194, i = 271733878;
+ for (let n = 0; n < d.length; n += 16) {
+ let h = m,
+ t = f,
+ g = r,
+ e = i;
+ f = ii(f = ii(f = ii(f = ii(f = hh(f = hh(f = hh(f = hh(f = gg(f = gg(f = gg(f = gg(f = ff(f = ff(f = ff(f = ff(f, r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = add(m, h), f = add(f, t), r = add(r, g), i = add(i, e);
+ }
+ return Array(m, f, r, i);
+}
+
+function cmn(d, g, m, f, r, i) {
+ return add(rol(add(add(g, d), add(f, i)), r), m);
+}
+
+function ff(d, g, m, f, r, i, n) {
+ return cmn(g & m | ~g & f, d, g, r, i, n);
+}
+
+function gg(d, g, m, f, r, i, n) {
+ return cmn(g & f | m & ~f, d, g, r, i, n);
+}
+
+function hh(d, g, m, f, r, i, n) {
+ return cmn(g ^ m ^ f, d, g, r, i, n);
+}
+
+function ii(d, g, m, f, r, i, n) {
+ return cmn(m ^ (g | ~f), d, g, r, i, n);
+}
+
+function add(d, g) {
+ let m = (65535 & d) + (65535 & g);
+ return (d >> 16) + (g >> 16) + (m >> 16) << 16 | 65535 & m;
+}
+
+function rol(d, g) {
+ return d << g | d >>> 32 - g;
+}
pkg/web/noVNC/core/crypto/rsa.js
@@ -0,0 +1,132 @@
+import Base64 from "../base64.js";
+import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
+
+export class RSACipher {
+ constructor() {
+ this._keyLength = 0;
+ this._keyBytes = 0;
+ this._n = null;
+ this._e = null;
+ this._d = null;
+ this._nBigInt = null;
+ this._eBigInt = null;
+ this._dBigInt = null;
+ this._extractable = false;
+ }
+
+ get algorithm() {
+ return { name: "RSA-PKCS1-v1_5" };
+ }
+
+ _base64urlDecode(data) {
+ data = data.replace(/-/g, "+").replace(/_/g, "/");
+ data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
+ return Base64.decode(data);
+ }
+
+ _padArray(arr, length) {
+ const res = new Uint8Array(length);
+ res.set(arr, length - arr.length);
+ return res;
+ }
+
+ static async generateKey(algorithm, extractable, _keyUsages) {
+ const cipher = new RSACipher;
+ await cipher._generateKey(algorithm, extractable);
+ return { privateKey: cipher };
+ }
+
+ async _generateKey(algorithm, extractable) {
+ this._keyLength = algorithm.modulusLength;
+ this._keyBytes = Math.ceil(this._keyLength / 8);
+ const key = await window.crypto.subtle.generateKey(
+ {
+ name: "RSA-OAEP",
+ modulusLength: algorithm.modulusLength,
+ publicExponent: algorithm.publicExponent,
+ hash: {name: "SHA-256"},
+ },
+ true, ["encrypt", "decrypt"]);
+ const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey);
+ this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
+ this._nBigInt = u8ArrayToBigInt(this._n);
+ this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
+ this._eBigInt = u8ArrayToBigInt(this._e);
+ this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
+ this._dBigInt = u8ArrayToBigInt(this._d);
+ this._extractable = extractable;
+ }
+
+ static async importKey(key, _algorithm, extractable, keyUsages) {
+ if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") {
+ throw new Error("only support importing RSA public key");
+ }
+ const cipher = new RSACipher;
+ await cipher._importKey(key, extractable);
+ return cipher;
+ }
+
+ async _importKey(key, extractable) {
+ const n = key.n;
+ const e = key.e;
+ if (n.length !== e.length) {
+ throw new Error("the sizes of modulus and public exponent do not match");
+ }
+ this._keyBytes = n.length;
+ this._keyLength = this._keyBytes * 8;
+ this._n = new Uint8Array(this._keyBytes);
+ this._e = new Uint8Array(this._keyBytes);
+ this._n.set(n);
+ this._e.set(e);
+ this._nBigInt = u8ArrayToBigInt(this._n);
+ this._eBigInt = u8ArrayToBigInt(this._e);
+ this._extractable = extractable;
+ }
+
+ async encrypt(_algorithm, message) {
+ if (message.length > this._keyBytes - 11) {
+ return null;
+ }
+ const ps = new Uint8Array(this._keyBytes - message.length - 3);
+ window.crypto.getRandomValues(ps);
+ for (let i = 0; i < ps.length; i++) {
+ ps[i] = Math.floor(ps[i] * 254 / 255 + 1);
+ }
+ const em = new Uint8Array(this._keyBytes);
+ em[1] = 0x02;
+ em.set(ps, 2);
+ em.set(message, ps.length + 3);
+ const emBigInt = u8ArrayToBigInt(em);
+ const c = modPow(emBigInt, this._eBigInt, this._nBigInt);
+ return bigIntToU8Array(c, this._keyBytes);
+ }
+
+ async decrypt(_algorithm, message) {
+ if (message.length !== this._keyBytes) {
+ return null;
+ }
+ const msgBigInt = u8ArrayToBigInt(message);
+ const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt);
+ const em = bigIntToU8Array(emBigInt, this._keyBytes);
+ if (em[0] !== 0x00 || em[1] !== 0x02) {
+ return null;
+ }
+ let i = 2;
+ for (; i < em.length; i++) {
+ if (em[i] === 0x00) {
+ break;
+ }
+ }
+ if (i === em.length) {
+ return null;
+ }
+ return em.slice(i + 1, em.length);
+ }
+
+ async exportKey() {
+ if (!this._extractable) {
+ throw new Error("key is not extractable");
+ }
+ return { n: this._n, e: this._e, d: this._d };
+ }
+}
pkg/web/noVNC/core/decoders/copyrect.js
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ *
+ */
+
+export default class CopyRectDecoder {
+ decodeRect(x, y, width, height, sock, display, depth) {
+ if (sock.rQwait("COPYRECT", 4)) {
+ return false;
+ }
+
+ let deltaX = sock.rQshift16();
+ let deltaY = sock.rQshift16();
+
+ if ((width === 0) || (height === 0)) {
+ return true;
+ }
+
+ display.copyImage(deltaX, deltaY, x, y, width, height);
+
+ return true;
+ }
+}
pkg/web/noVNC/core/decoders/h264.js
@@ -0,0 +1,321 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2024 The noVNC authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ *
+ */
+
+import * as Log from '../util/logging.js';
+
+export class H264Parser {
+ constructor(data) {
+ this._data = data;
+ this._index = 0;
+ this.profileIdc = null;
+ this.constraintSet = null;
+ this.levelIdc = null;
+ }
+
+ _getStartSequenceLen(index) {
+ let data = this._data;
+ if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 0 && data[index + 3] == 1) {
+ return 4;
+ }
+ if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 1) {
+ return 3;
+ }
+ return 0;
+ }
+
+ _indexOfNextNalUnit(index) {
+ let data = this._data;
+ for (let i = index; i < data.length; ++i) {
+ if (this._getStartSequenceLen(i) != 0) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ _parseSps(index) {
+ this.profileIdc = this._data[index];
+ this.constraintSet = this._data[index + 1];
+ this.levelIdc = this._data[index + 2];
+ }
+
+ _parseNalUnit(index) {
+ const firstByte = this._data[index];
+ if (firstByte & 0x80) {
+ throw new Error('H264 parsing sanity check failed, forbidden zero bit is set');
+ }
+ const unitType = firstByte & 0x1f;
+
+ switch (unitType) {
+ case 1: // coded slice, non-idr
+ return { slice: true };
+ case 5: // coded slice, idr
+ return { slice: true, key: true };
+ case 6: // sei
+ return {};
+ case 7: // sps
+ this._parseSps(index + 1);
+ return {};
+ case 8: // pps
+ return {};
+ default:
+ Log.Warn("Unhandled unit type: ", unitType);
+ break;
+ }
+ return {};
+ }
+
+ parse() {
+ const startIndex = this._index;
+ let isKey = false;
+
+ while (this._index < this._data.length) {
+ const startSequenceLen = this._getStartSequenceLen(this._index);
+ if (startSequenceLen == 0) {
+ throw new Error('Invalid start sequence in bit stream');
+ }
+
+ const { slice, key } = this._parseNalUnit(this._index + startSequenceLen);
+
+ let nextIndex = this._indexOfNextNalUnit(this._index + startSequenceLen);
+ if (nextIndex == -1) {
+ this._index = this._data.length;
+ } else {
+ this._index = nextIndex;
+ }
+
+ if (key) {
+ isKey = true;
+ }
+ if (slice) {
+ break;
+ }
+ }
+
+ if (startIndex === this._index) {
+ return null;
+ }
+
+ return {
+ frame: this._data.subarray(startIndex, this._index),
+ key: isKey,
+ };
+ }
+}
+
+export class H264Context {
+ constructor(width, height) {
+ this.lastUsed = 0;
+ this._width = width;
+ this._height = height;
+ this._profileIdc = null;
+ this._constraintSet = null;
+ this._levelIdc = null;
+ this._decoder = null;
+ this._pendingFrames = [];
+ }
+
+ _handleFrame(frame) {
+ let pending = this._pendingFrames.shift();
+ if (pending === undefined) {
+ throw new Error("Pending frame queue empty when receiving frame from decoder");
+ }
+
+ if (pending.timestamp != frame.timestamp) {
+ throw new Error("Video frame timestamp mismatch. Expected " +
+ frame.timestamp + " but but got " + pending.timestamp);
+ }
+
+ pending.frame = frame;
+ pending.ready = true;
+ pending.resolve();
+
+ if (!pending.keep) {
+ frame.close();
+ }
+ }
+
+ _handleError(e) {
+ throw new Error("Failed to decode frame: " + e.message);
+ }
+
+ _configureDecoder(profileIdc, constraintSet, levelIdc) {
+ if (this._decoder === null || this._decoder.state === 'closed') {
+ this._decoder = new VideoDecoder({
+ output: frame => this._handleFrame(frame),
+ error: e => this._handleError(e),
+ });
+ }
+ const codec = 'avc1.' +
+ profileIdc.toString(16).padStart(2, '0') +
+ constraintSet.toString(16).padStart(2, '0') +
+ levelIdc.toString(16).padStart(2, '0');
+ this._decoder.configure({
+ codec: codec,
+ codedWidth: this._width,
+ codedHeight: this._height,
+ optimizeForLatency: true,
+ });
+ }
+
+ _preparePendingFrame(timestamp) {
+ let pending = {
+ timestamp: timestamp,
+ promise: null,
+ resolve: null,
+ frame: null,
+ ready: false,
+ keep: false,
+ };
+ pending.promise = new Promise((resolve) => {
+ pending.resolve = resolve;
+ });
+ this._pendingFrames.push(pending);
+
+ return pending;
+ }
+
+ decode(payload) {
+ let parser = new H264Parser(payload);
+ let result = null;
+
+ // Ideally, this timestamp should come from the server, but we'll just
+ // approximate it instead.
+ let timestamp = Math.round(window.performance.now() * 1e3);
+
+ while (true) {
+ let encodedFrame = parser.parse();
+ if (encodedFrame === null) {
+ break;
+ }
+
+ if (parser.profileIdc !== null) {
+ self._profileIdc = parser.profileIdc;
+ self._constraintSet = parser.constraintSet;
+ self._levelIdc = parser.levelIdc;
+ }
+
+ if (this._decoder === null || this._decoder.state !== 'configured') {
+ if (!encodedFrame.key) {
+ Log.Warn("Missing key frame. Can't decode until one arrives");
+ continue;
+ }
+ if (self._profileIdc === null) {
+ Log.Warn('Cannot config decoder. Have not received SPS and PPS yet.');
+ continue;
+ }
+ this._configureDecoder(self._profileIdc, self._constraintSet,
+ self._levelIdc);
+ }
+
+ result = this._preparePendingFrame(timestamp);
+
+ const chunk = new EncodedVideoChunk({
+ timestamp: timestamp,
+ type: encodedFrame.key ? 'key' : 'delta',
+ data: encodedFrame.frame,
+ });
+
+ try {
+ this._decoder.decode(chunk);
+ } catch (e) {
+ Log.Warn("Failed to decode:", e);
+ }
+ }
+
+ // We only keep last frame of each payload
+ if (result !== null) {
+ result.keep = true;
+ }
+
+ return result;
+ }
+}
+
+export default class H264Decoder {
+ constructor() {
+ this._tick = 0;
+ this._contexts = {};
+ }
+
+ _contextId(x, y, width, height) {
+ return [x, y, width, height].join(',');
+ }
+
+ _findOldestContextId() {
+ let oldestTick = Number.MAX_VALUE;
+ let oldestKey = undefined;
+ for (const [key, value] of Object.entries(this._contexts)) {
+ if (value.lastUsed < oldestTick) {
+ oldestTick = value.lastUsed;
+ oldestKey = key;
+ }
+ }
+ return oldestKey;
+ }
+
+ _createContext(x, y, width, height) {
+ const maxContexts = 64;
+ if (Object.keys(this._contexts).length >= maxContexts) {
+ let oldestContextId = this._findOldestContextId();
+ delete this._contexts[oldestContextId];
+ }
+ let context = new H264Context(width, height);
+ this._contexts[this._contextId(x, y, width, height)] = context;
+ return context;
+ }
+
+ _getContext(x, y, width, height) {
+ let context = this._contexts[this._contextId(x, y, width, height)];
+ return context !== undefined ? context : this._createContext(x, y, width, height);
+ }
+
+ _resetContext(x, y, width, height) {
+ delete this._contexts[this._contextId(x, y, width, height)];
+ }
+
+ _resetAllContexts() {
+ this._contexts = {};
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ const resetContextFlag = 1;
+ const resetAllContextsFlag = 2;
+
+ if (sock.rQwait("h264 header", 8)) {
+ return false;
+ }
+
+ const length = sock.rQshift32();
+ const flags = sock.rQshift32();
+
+ if (sock.rQwait("h264 payload", length, 8)) {
+ return false;
+ }
+
+ if (flags & resetAllContextsFlag) {
+ this._resetAllContexts();
+ } else if (flags & resetContextFlag) {
+ this._resetContext(x, y, width, height);
+ }
+
+ let context = this._getContext(x, y, width, height);
+ context.lastUsed = this._tick++;
+
+ if (length !== 0) {
+ let payload = sock.rQshiftBytes(length, false);
+ let frame = context.decode(payload);
+ if (frame !== null) {
+ display.videoFrame(x, y, width, height, frame);
+ }
+ }
+
+ return true;
+ }
+}
pkg/web/noVNC/core/decoders/hextile.js
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ *
+ */
+
+import * as Log from '../util/logging.js';
+
+export default class HextileDecoder {
+ constructor() {
+ this._tiles = 0;
+ this._lastsubencoding = 0;
+ this._tileBuffer = new Uint8Array(16 * 16 * 4);
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ if (this._tiles === 0) {
+ this._tilesX = Math.ceil(width / 16);
+ this._tilesY = Math.ceil(height / 16);
+ this._totalTiles = this._tilesX * this._tilesY;
+ this._tiles = this._totalTiles;
+ }
+
+ while (this._tiles > 0) {
+ let bytes = 1;
+
+ if (sock.rQwait("HEXTILE", bytes)) {
+ return false;
+ }
+
+ let subencoding = sock.rQpeek8();
+ if (subencoding > 30) { // Raw
+ throw new Error("Illegal hextile subencoding (subencoding: " +
+ subencoding + ")");
+ }
+
+ const currTile = this._totalTiles - this._tiles;
+ const tileX = currTile % this._tilesX;
+ const tileY = Math.floor(currTile / this._tilesX);
+ const tx = x + tileX * 16;
+ const ty = y + tileY * 16;
+ const tw = Math.min(16, (x + width) - tx);
+ const th = Math.min(16, (y + height) - ty);
+
+ // Figure out how much we are expecting
+ if (subencoding & 0x01) { // Raw
+ bytes += tw * th * 4;
+ } else {
+ if (subencoding & 0x02) { // Background
+ bytes += 4;
+ }
+ if (subencoding & 0x04) { // Foreground
+ bytes += 4;
+ }
+ if (subencoding & 0x08) { // AnySubrects
+ bytes++; // Since we aren't shifting it off
+
+ if (sock.rQwait("HEXTILE", bytes)) {
+ return false;
+ }
+
+ let subrects = sock.rQpeekBytes(bytes).at(-1);
+ if (subencoding & 0x10) { // SubrectsColoured
+ bytes += subrects * (4 + 2);
+ } else {
+ bytes += subrects * 2;
+ }
+ }
+ }
+
+ if (sock.rQwait("HEXTILE", bytes)) {
+ return false;
+ }
+
+ // We know the encoding and have a whole tile
+ sock.rQshift8();
+ if (subencoding === 0) {
+ if (this._lastsubencoding & 0x01) {
+ // Weird: ignore blanks are RAW
+ Log.Debug(" Ignoring blank after RAW");
+ } else {
+ display.fillRect(tx, ty, tw, th, this._background);
+ }
+ } else if (subencoding & 0x01) { // Raw
+ let pixels = tw * th;
+ let data = sock.rQshiftBytes(pixels * 4, false);
+ // Max sure the image is fully opaque
+ for (let i = 0;i < pixels;i++) {
+ data[i * 4 + 3] = 255;
+ }
+ display.blitImage(tx, ty, tw, th, data, 0);
+ } else {
+ if (subencoding & 0x02) { // Background
+ this._background = new Uint8Array(sock.rQshiftBytes(4));
+ }
+ if (subencoding & 0x04) { // Foreground
+ this._foreground = new Uint8Array(sock.rQshiftBytes(4));
+ }
+
+ this._startTile(tx, ty, tw, th, this._background);
+ if (subencoding & 0x08) { // AnySubrects
+ let subrects = sock.rQshift8();
+
+ for (let s = 0; s < subrects; s++) {
+ let color;
+ if (subencoding & 0x10) { // SubrectsColoured
+ color = sock.rQshiftBytes(4);
+ } else {
+ color = this._foreground;
+ }
+ const xy = sock.rQshift8();
+ const sx = (xy >> 4);
+ const sy = (xy & 0x0f);
+
+ const wh = sock.rQshift8();
+ const sw = (wh >> 4) + 1;
+ const sh = (wh & 0x0f) + 1;
+
+ this._subTile(sx, sy, sw, sh, color);
+ }
+ }
+ this._finishTile(display);
+ }
+ this._lastsubencoding = subencoding;
+ this._tiles--;
+ }
+
+ return true;
+ }
+
+ // start updating a tile
+ _startTile(x, y, width, height, color) {
+ this._tileX = x;
+ this._tileY = y;
+ this._tileW = width;
+ this._tileH = height;
+
+ const red = color[0];
+ const green = color[1];
+ const blue = color[2];
+
+ const data = this._tileBuffer;
+ for (let i = 0; i < width * height * 4; i += 4) {
+ data[i] = red;
+ data[i + 1] = green;
+ data[i + 2] = blue;
+ data[i + 3] = 255;
+ }
+ }
+
+ // update sub-rectangle of the current tile
+ _subTile(x, y, w, h, color) {
+ const red = color[0];
+ const green = color[1];
+ const blue = color[2];
+ const xend = x + w;
+ const yend = y + h;
+
+ const data = this._tileBuffer;
+ const width = this._tileW;
+ for (let j = y; j < yend; j++) {
+ for (let i = x; i < xend; i++) {
+ const p = (i + (j * width)) * 4;
+ data[p] = red;
+ data[p + 1] = green;
+ data[p + 2] = blue;
+ data[p + 3] = 255;
+ }
+ }
+ }
+
+ // draw the current tile to the screen
+ _finishTile(display) {
+ display.blitImage(this._tileX, this._tileY,
+ this._tileW, this._tileH,
+ this._tileBuffer, 0);
+ }
+}
pkg/web/noVNC/core/decoders/jpeg.js
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ *
+ */
+
+export default class JPEGDecoder {
+ constructor() {
+ // RealVNC will reuse the quantization tables
+ // and Huffman tables, so we need to cache them.
+ this._cachedQuantTables = [];
+ this._cachedHuffmanTables = [];
+
+ this._segments = [];
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ // A rect of JPEG encodings is simply a JPEG file
+ while (true) {
+ let segment = this._readSegment(sock);
+ if (segment === null) {
+ return false;
+ }
+ this._segments.push(segment);
+ // End of image?
+ if (segment[1] === 0xD9) {
+ break;
+ }
+ }
+
+ let huffmanTables = [];
+ let quantTables = [];
+ for (let segment of this._segments) {
+ let type = segment[1];
+ if (type === 0xC4) {
+ // Huffman tables
+ huffmanTables.push(segment);
+ } else if (type === 0xDB) {
+ // Quantization tables
+ quantTables.push(segment);
+ }
+ }
+
+ const sofIndex = this._segments.findIndex(
+ x => x[1] == 0xC0 || x[1] == 0xC2
+ );
+ if (sofIndex == -1) {
+ throw new Error("Illegal JPEG image without SOF");
+ }
+
+ if (quantTables.length === 0) {
+ this._segments.splice(sofIndex+1, 0,
+ ...this._cachedQuantTables);
+ }
+ if (huffmanTables.length === 0) {
+ this._segments.splice(sofIndex+1, 0,
+ ...this._cachedHuffmanTables);
+ }
+
+ let length = 0;
+ for (let segment of this._segments) {
+ length += segment.length;
+ }
+
+ let data = new Uint8Array(length);
+ length = 0;
+ for (let segment of this._segments) {
+ data.set(segment, length);
+ length += segment.length;
+ }
+
+ display.imageRect(x, y, width, height, "image/jpeg", data);
+
+ if (huffmanTables.length !== 0) {
+ this._cachedHuffmanTables = huffmanTables;
+ }
+ if (quantTables.length !== 0) {
+ this._cachedQuantTables = quantTables;
+ }
+
+ this._segments = [];
+
+ return true;
+ }
+
+ _readSegment(sock) {
+ if (sock.rQwait("JPEG", 2)) {
+ return null;
+ }
+
+ let marker = sock.rQshift8();
+ if (marker != 0xFF) {
+ throw new Error("Illegal JPEG marker received (byte: " +
+ marker + ")");
+ }
+ let type = sock.rQshift8();
+ if (type >= 0xD0 && type <= 0xD9 || type == 0x01) {
+ // No length after marker
+ return new Uint8Array([marker, type]);
+ }
+
+ if (sock.rQwait("JPEG", 2, 2)) {
+ return null;
+ }
+
+ let length = sock.rQshift16();
+ if (length < 2) {
+ throw new Error("Illegal JPEG length received (length: " +
+ length + ")");
+ }
+
+ if (sock.rQwait("JPEG", length-2, 4)) {
+ return null;
+ }
+
+ let extra = 0;
+ if (type === 0xDA) {
+ // start of scan
+ if (sock.rQwait("JPEG", length-2 + 2, 4)) {
+ return null;
+ }
+
+ let len = sock.rQlen();
+ let data = sock.rQpeekBytes(len, false);
+
+ while (true) {
+ let idx = data.indexOf(0xFF, length-2+extra);
+ if (idx === -1) {
+ sock.rQwait("JPEG", Infinity, 4);
+ return null;
+ }
+
+ if (idx === len-1) {
+ sock.rQwait("JPEG", Infinity, 4);
+ return null;
+ }
+
+ if (data.at(idx+1) === 0x00 ||
+ (data.at(idx+1) >= 0xD0 && data.at(idx+1) <= 0xD7)) {
+ extra = idx+2 - (length-2);
+ continue;
+ }
+
+ extra = idx - (length-2);
+ break;
+ }
+ }
+
+ let segment = new Uint8Array(2 + length + extra);
+ segment[0] = marker;
+ segment[1] = type;
+ segment[2] = length >> 8;
+ segment[3] = length;
+ segment.set(sock.rQshiftBytes(length-2+extra, false), 4);
+
+ return segment;
+ }
+}
pkg/web/noVNC/core/decoders/raw.js
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ *
+ */
+
+export default class RawDecoder {
+ constructor() {
+ this._lines = 0;
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ if ((width === 0) || (height === 0)) {
+ return true;
+ }
+
+ if (this._lines === 0) {
+ this._lines = height;
+ }
+
+ const pixelSize = depth == 8 ? 1 : 4;
+ const bytesPerLine = width * pixelSize;
+
+ while (this._lines > 0) {
+ if (sock.rQwait("RAW", bytesPerLine)) {
+ return false;
+ }
+
+ const curY = y + (height - this._lines);
+
+ let data = sock.rQshiftBytes(bytesPerLine, false);
+
+ // Convert data if needed
+ if (depth == 8) {
+ const newdata = new Uint8Array(width * 4);
+ for (let i = 0; i < width; i++) {
+ newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3;
+ newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3;
+ newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3;
+ newdata[i * 4 + 3] = 255;
+ }
+ data = newdata;
+ }
+
+ // Max sure the image is fully opaque
+ for (let i = 0; i < width; i++) {
+ data[i * 4 + 3] = 255;
+ }
+
+ display.blitImage(x, curY, width, 1, data, 0);
+ this._lines--;
+ }
+
+ return true;
+ }
+}
pkg/web/noVNC/core/decoders/rre.js
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ *
+ */
+
+export default class RREDecoder {
+ constructor() {
+ this._subrects = 0;
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ if (this._subrects === 0) {
+ if (sock.rQwait("RRE", 4 + 4)) {
+ return false;
+ }
+
+ this._subrects = sock.rQshift32();
+
+ let color = sock.rQshiftBytes(4); // Background
+ display.fillRect(x, y, width, height, color);
+ }
+
+ while (this._subrects > 0) {
+ if (sock.rQwait("RRE", 4 + 8)) {
+ return false;
+ }
+
+ let color = sock.rQshiftBytes(4);
+ let sx = sock.rQshift16();
+ let sy = sock.rQshift16();
+ let swidth = sock.rQshift16();
+ let sheight = sock.rQshift16();
+ display.fillRect(x + sx, y + sy, swidth, sheight, color);
+
+ this._subrects--;
+ }
+
+ return true;
+ }
+}
pkg/web/noVNC/core/decoders/tight.js
@@ -0,0 +1,393 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2019 The noVNC authors
+ * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ *
+ */
+
+import * as Log from '../util/logging.js';
+import Inflator from "../inflator.js";
+
+export default class TightDecoder {
+ constructor() {
+ this._ctl = null;
+ this._filter = null;
+ this._numColors = 0;
+ this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel)
+ this._len = 0;
+
+ this._zlibs = [];
+ for (let i = 0; i < 4; i++) {
+ this._zlibs[i] = new Inflator();
+ }
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ if (this._ctl === null) {
+ if (sock.rQwait("TIGHT compression-control", 1)) {
+ return false;
+ }
+
+ this._ctl = sock.rQshift8();
+
+ // Reset streams if the server requests it
+ for (let i = 0; i < 4; i++) {
+ if ((this._ctl >> i) & 1) {
+ this._zlibs[i].reset();
+ Log.Info("Reset zlib stream " + i);
+ }
+ }
+
+ // Figure out filter
+ this._ctl = this._ctl >> 4;
+ }
+
+ let ret;
+
+ if (this._ctl === 0x08) {
+ ret = this._fillRect(x, y, width, height,
+ sock, display, depth);
+ } else if (this._ctl === 0x09) {
+ ret = this._jpegRect(x, y, width, height,
+ sock, display, depth);
+ } else if (this._ctl === 0x0A) {
+ ret = this._pngRect(x, y, width, height,
+ sock, display, depth);
+ } else if ((this._ctl & 0x08) == 0) {
+ ret = this._basicRect(this._ctl, x, y, width, height,
+ sock, display, depth);
+ } else {
+ throw new Error("Illegal tight compression received (ctl: " +
+ this._ctl + ")");
+ }
+
+ if (ret) {
+ this._ctl = null;
+ }
+
+ return ret;
+ }
+
+ _fillRect(x, y, width, height, sock, display, depth) {
+ if (sock.rQwait("TIGHT", 3)) {
+ return false;
+ }
+
+ let pixel = sock.rQshiftBytes(3);
+ display.fillRect(x, y, width, height, pixel, false);
+
+ return true;
+ }
+
+ _jpegRect(x, y, width, height, sock, display, depth) {
+ let data = this._readData(sock);
+ if (data === null) {
+ return false;
+ }
+
+ display.imageRect(x, y, width, height, "image/jpeg", data);
+
+ return true;
+ }
+
+ _pngRect(x, y, width, height, sock, display, depth) {
+ throw new Error("PNG received in standard Tight rect");
+ }
+
+ _basicRect(ctl, x, y, width, height, sock, display, depth) {
+ if (this._filter === null) {
+ if (ctl & 0x4) {
+ if (sock.rQwait("TIGHT", 1)) {
+ return false;
+ }
+
+ this._filter = sock.rQshift8();
+ } else {
+ // Implicit CopyFilter
+ this._filter = 0;
+ }
+ }
+
+ let streamId = ctl & 0x3;
+
+ let ret;
+
+ switch (this._filter) {
+ case 0: // CopyFilter
+ ret = this._copyFilter(streamId, x, y, width, height,
+ sock, display, depth);
+ break;
+ case 1: // PaletteFilter
+ ret = this._paletteFilter(streamId, x, y, width, height,
+ sock, display, depth);
+ break;
+ case 2: // GradientFilter
+ ret = this._gradientFilter(streamId, x, y, width, height,
+ sock, display, depth);
+ break;
+ default:
+ throw new Error("Illegal tight filter received (ctl: " +
+ this._filter + ")");
+ }
+
+ if (ret) {
+ this._filter = null;
+ }
+
+ return ret;
+ }
+
+ _copyFilter(streamId, x, y, width, height, sock, display, depth) {
+ const uncompressedSize = width * height * 3;
+ let data;
+
+ if (uncompressedSize === 0) {
+ return true;
+ }
+
+ if (uncompressedSize < 12) {
+ if (sock.rQwait("TIGHT", uncompressedSize)) {
+ return false;
+ }
+
+ data = sock.rQshiftBytes(uncompressedSize);
+ } else {
+ data = this._readData(sock);
+ if (data === null) {
+ return false;
+ }
+
+ this._zlibs[streamId].setInput(data);
+ data = this._zlibs[streamId].inflate(uncompressedSize);
+ this._zlibs[streamId].setInput(null);
+ }
+
+ let rgbx = new Uint8Array(width * height * 4);
+ for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) {
+ rgbx[i] = data[j];
+ rgbx[i + 1] = data[j + 1];
+ rgbx[i + 2] = data[j + 2];
+ rgbx[i + 3] = 255; // Alpha
+ }
+
+ display.blitImage(x, y, width, height, rgbx, 0, false);
+
+ return true;
+ }
+
+ _paletteFilter(streamId, x, y, width, height, sock, display, depth) {
+ if (this._numColors === 0) {
+ if (sock.rQwait("TIGHT palette", 1)) {
+ return false;
+ }
+
+ const numColors = sock.rQpeek8() + 1;
+ const paletteSize = numColors * 3;
+
+ if (sock.rQwait("TIGHT palette", 1 + paletteSize)) {
+ return false;
+ }
+
+ this._numColors = numColors;
+ sock.rQskipBytes(1);
+
+ sock.rQshiftTo(this._palette, paletteSize);
+ }
+
+ const bpp = (this._numColors <= 2) ? 1 : 8;
+ const rowSize = Math.floor((width * bpp + 7) / 8);
+ const uncompressedSize = rowSize * height;
+
+ let data;
+
+ if (uncompressedSize === 0) {
+ return true;
+ }
+
+ if (uncompressedSize < 12) {
+ if (sock.rQwait("TIGHT", uncompressedSize)) {
+ return false;
+ }
+
+ data = sock.rQshiftBytes(uncompressedSize);
+ } else {
+ data = this._readData(sock);
+ if (data === null) {
+ return false;
+ }
+
+ this._zlibs[streamId].setInput(data);
+ data = this._zlibs[streamId].inflate(uncompressedSize);
+ this._zlibs[streamId].setInput(null);
+ }
+
+ // Convert indexed (palette based) image data to RGB
+ if (this._numColors == 2) {
+ this._monoRect(x, y, width, height, data, this._palette, display);
+ } else {
+ this._paletteRect(x, y, width, height, data, this._palette, display);
+ }
+
+ this._numColors = 0;
+
+ return true;
+ }
+
+ _monoRect(x, y, width, height, data, palette, display) {
+ // Convert indexed (palette based) image data to RGB
+ // TODO: reduce number of calculations inside loop
+ const dest = this._getScratchBuffer(width * height * 4);
+ const w = Math.floor((width + 7) / 8);
+ const w1 = Math.floor(width / 8);
+
+ for (let y = 0; y < height; y++) {
+ let dp, sp, x;
+ for (x = 0; x < w1; x++) {
+ for (let b = 7; b >= 0; b--) {
+ dp = (y * width + x * 8 + 7 - b) * 4;
+ sp = (data[y * w + x] >> b & 1) * 3;
+ dest[dp] = palette[sp];
+ dest[dp + 1] = palette[sp + 1];
+ dest[dp + 2] = palette[sp + 2];
+ dest[dp + 3] = 255;
+ }
+ }
+
+ for (let b = 7; b >= 8 - width % 8; b--) {
+ dp = (y * width + x * 8 + 7 - b) * 4;
+ sp = (data[y * w + x] >> b & 1) * 3;
+ dest[dp] = palette[sp];
+ dest[dp + 1] = palette[sp + 1];
+ dest[dp + 2] = palette[sp + 2];
+ dest[dp + 3] = 255;
+ }
+ }
+
+ display.blitImage(x, y, width, height, dest, 0, false);
+ }
+
+ _paletteRect(x, y, width, height, data, palette, display) {
+ // Convert indexed (palette based) image data to RGB
+ const dest = this._getScratchBuffer(width * height * 4);
+ const total = width * height * 4;
+ for (let i = 0, j = 0; i < total; i += 4, j++) {
+ const sp = data[j] * 3;
+ dest[i] = palette[sp];
+ dest[i + 1] = palette[sp + 1];
+ dest[i + 2] = palette[sp + 2];
+ dest[i + 3] = 255;
+ }
+
+ display.blitImage(x, y, width, height, dest, 0, false);
+ }
+
+ _gradientFilter(streamId, x, y, width, height, sock, display, depth) {
+ // assume the TPIXEL is 3 bytes long
+ const uncompressedSize = width * height * 3;
+ let data;
+
+ if (uncompressedSize === 0) {
+ return true;
+ }
+
+ if (uncompressedSize < 12) {
+ if (sock.rQwait("TIGHT", uncompressedSize)) {
+ return false;
+ }
+
+ data = sock.rQshiftBytes(uncompressedSize);
+ } else {
+ data = this._readData(sock);
+ if (data === null) {
+ return false;
+ }
+
+ this._zlibs[streamId].setInput(data);
+ data = this._zlibs[streamId].inflate(uncompressedSize);
+ this._zlibs[streamId].setInput(null);
+ }
+
+ let rgbx = new Uint8Array(4 * width * height);
+
+ let rgbxIndex = 0, dataIndex = 0;
+ let left = new Uint8Array(3);
+ for (let x = 0; x < width; x++) {
+ for (let c = 0; c < 3; c++) {
+ const prediction = left[c];
+ const value = data[dataIndex++] + prediction;
+ rgbx[rgbxIndex++] = value;
+ left[c] = value;
+ }
+ rgbx[rgbxIndex++] = 255;
+ }
+
+ let upperIndex = 0;
+ let upper = new Uint8Array(3),
+ upperleft = new Uint8Array(3);
+ for (let y = 1; y < height; y++) {
+ left.fill(0);
+ upperleft.fill(0);
+ for (let x = 0; x < width; x++) {
+ for (let c = 0; c < 3; c++) {
+ upper[c] = rgbx[upperIndex++];
+ let prediction = left[c] + upper[c] - upperleft[c];
+ if (prediction < 0) {
+ prediction = 0;
+ } else if (prediction > 255) {
+ prediction = 255;
+ }
+ const value = data[dataIndex++] + prediction;
+ rgbx[rgbxIndex++] = value;
+ upperleft[c] = upper[c];
+ left[c] = value;
+ }
+ rgbx[rgbxIndex++] = 255;
+ upperIndex++;
+ }
+ }
+
+ display.blitImage(x, y, width, height, rgbx, 0, false);
+
+ return true;
+ }
+
+ _readData(sock) {
+ if (this._len === 0) {
+ if (sock.rQwait("TIGHT", 3)) {
+ return null;
+ }
+
+ let byte;
+
+ byte = sock.rQshift8();
+ this._len = byte & 0x7f;
+ if (byte & 0x80) {
+ byte = sock.rQshift8();
+ this._len |= (byte & 0x7f) << 7;
+ if (byte & 0x80) {
+ byte = sock.rQshift8();
+ this._len |= byte << 14;
+ }
+ }
+ }
+
+ if (sock.rQwait("TIGHT", this._len)) {
+ return null;
+ }
+
+ let data = sock.rQshiftBytes(this._len, false);
+ this._len = 0;
+
+ return data;
+ }
+
+ _getScratchBuffer(size) {
+ if (!this._scratchBuffer || (this._scratchBuffer.length < size)) {
+ this._scratchBuffer = new Uint8Array(size);
+ }
+ return this._scratchBuffer;
+ }
+}
pkg/web/noVNC/core/decoders/tightpng.js
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ *
+ */
+
+import TightDecoder from './tight.js';
+
+export default class TightPNGDecoder extends TightDecoder {
+ _pngRect(x, y, width, height, sock, display, depth) {
+ let data = this._readData(sock);
+ if (data === null) {
+ return false;
+ }
+
+ display.imageRect(x, y, width, height, "image/png", data);
+
+ return true;
+ }
+
+ _basicRect(ctl, x, y, width, height, sock, display, depth) {
+ throw new Error("BasicCompression received in TightPNG rect");
+ }
+}
pkg/web/noVNC/core/decoders/zlib.js
@@ -0,0 +1,51 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2024 The noVNC authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ *
+ */
+
+import Inflator from "../inflator.js";
+
+export default class ZlibDecoder {
+ constructor() {
+ this._zlib = new Inflator();
+ this._length = 0;
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ if ((width === 0) || (height === 0)) {
+ return true;
+ }
+
+ if (this._length === 0) {
+ if (sock.rQwait("ZLIB", 4)) {
+ return false;
+ }
+
+ this._length = sock.rQshift32();
+ }
+
+ if (sock.rQwait("ZLIB", this._length)) {
+ return false;
+ }
+
+ let data = new Uint8Array(sock.rQshiftBytes(this._length, false));
+ this._length = 0;
+
+ this._zlib.setInput(data);
+ data = this._zlib.inflate(width * height * 4);
+ this._zlib.setInput(null);
+
+ // Max sure the image is fully opaque
+ for (let i = 0; i < width * height; i++) {
+ data[i * 4 + 3] = 255;
+ }
+
+ display.blitImage(x, y, width, height, data, 0);
+
+ return true;
+ }
+}
pkg/web/noVNC/core/decoders/zrle.js
@@ -0,0 +1,185 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2021 The noVNC authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ *
+ */
+
+import Inflate from "../inflator.js";
+
+const ZRLE_TILE_WIDTH = 64;
+const ZRLE_TILE_HEIGHT = 64;
+
+export default class ZRLEDecoder {
+ constructor() {
+ this._length = 0;
+ this._inflator = new Inflate();
+
+ this._pixelBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
+ this._tileBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ if (this._length === 0) {
+ if (sock.rQwait("ZLib data length", 4)) {
+ return false;
+ }
+ this._length = sock.rQshift32();
+ }
+ if (sock.rQwait("Zlib data", this._length)) {
+ return false;
+ }
+
+ const data = sock.rQshiftBytes(this._length, false);
+
+ this._inflator.setInput(data);
+
+ for (let ty = y; ty < y + height; ty += ZRLE_TILE_HEIGHT) {
+ let th = Math.min(ZRLE_TILE_HEIGHT, y + height - ty);
+
+ for (let tx = x; tx < x + width; tx += ZRLE_TILE_WIDTH) {
+ let tw = Math.min(ZRLE_TILE_WIDTH, x + width - tx);
+
+ const tileSize = tw * th;
+ const subencoding = this._inflator.inflate(1)[0];
+ if (subencoding === 0) {
+ // raw data
+ const data = this._readPixels(tileSize);
+ display.blitImage(tx, ty, tw, th, data, 0, false);
+ } else if (subencoding === 1) {
+ // solid
+ const background = this._readPixels(1);
+ display.fillRect(tx, ty, tw, th, [background[0], background[1], background[2]]);
+ } else if (subencoding >= 2 && subencoding <= 16) {
+ const data = this._decodePaletteTile(subencoding, tileSize, tw, th);
+ display.blitImage(tx, ty, tw, th, data, 0, false);
+ } else if (subencoding === 128) {
+ const data = this._decodeRLETile(tileSize);
+ display.blitImage(tx, ty, tw, th, data, 0, false);
+ } else if (subencoding >= 130 && subencoding <= 255) {
+ const data = this._decodeRLEPaletteTile(subencoding - 128, tileSize);
+ display.blitImage(tx, ty, tw, th, data, 0, false);
+ } else {
+ throw new Error('Unknown subencoding: ' + subencoding);
+ }
+ }
+ }
+ this._length = 0;
+ return true;
+ }
+
+ _getBitsPerPixelInPalette(paletteSize) {
+ if (paletteSize <= 2) {
+ return 1;
+ } else if (paletteSize <= 4) {
+ return 2;
+ } else if (paletteSize <= 16) {
+ return 4;
+ }
+ }
+
+ _readPixels(pixels) {
+ let data = this._pixelBuffer;
+ const buffer = this._inflator.inflate(3*pixels);
+ for (let i = 0, j = 0; i < pixels*4; i += 4, j += 3) {
+ data[i] = buffer[j];
+ data[i + 1] = buffer[j + 1];
+ data[i + 2] = buffer[j + 2];
+ data[i + 3] = 255; // Add the Alpha
+ }
+ return data;
+ }
+
+ _decodePaletteTile(paletteSize, tileSize, tilew, tileh) {
+ const data = this._tileBuffer;
+ const palette = this._readPixels(paletteSize);
+ const bitsPerPixel = this._getBitsPerPixelInPalette(paletteSize);
+ const mask = (1 << bitsPerPixel) - 1;
+
+ let offset = 0;
+ let encoded = this._inflator.inflate(1)[0];
+
+ for (let y=0; y<tileh; y++) {
+ let shift = 8-bitsPerPixel;
+ for (let x=0; x<tilew; x++) {
+ if (shift<0) {
+ shift=8-bitsPerPixel;
+ encoded = this._inflator.inflate(1)[0];
+ }
+ let indexInPalette = (encoded>>shift) & mask;
+
+ data[offset] = palette[indexInPalette * 4];
+ data[offset + 1] = palette[indexInPalette * 4 + 1];
+ data[offset + 2] = palette[indexInPalette * 4 + 2];
+ data[offset + 3] = palette[indexInPalette * 4 + 3];
+ offset += 4;
+ shift-=bitsPerPixel;
+ }
+ if (shift<8-bitsPerPixel && y<tileh-1) {
+ encoded = this._inflator.inflate(1)[0];
+ }
+ }
+ return data;
+ }
+
+ _decodeRLETile(tileSize) {
+ const data = this._tileBuffer;
+ let i = 0;
+ while (i < tileSize) {
+ const pixel = this._readPixels(1);
+ const length = this._readRLELength();
+ for (let j = 0; j < length; j++) {
+ data[i * 4] = pixel[0];
+ data[i * 4 + 1] = pixel[1];
+ data[i * 4 + 2] = pixel[2];
+ data[i * 4 + 3] = pixel[3];
+ i++;
+ }
+ }
+ return data;
+ }
+
+ _decodeRLEPaletteTile(paletteSize, tileSize) {
+ const data = this._tileBuffer;
+
+ // palette
+ const palette = this._readPixels(paletteSize);
+
+ let offset = 0;
+ while (offset < tileSize) {
+ let indexInPalette = this._inflator.inflate(1)[0];
+ let length = 1;
+ if (indexInPalette >= 128) {
+ indexInPalette -= 128;
+ length = this._readRLELength();
+ }
+ if (indexInPalette > paletteSize) {
+ throw new Error('Too big index in palette: ' + indexInPalette + ', palette size: ' + paletteSize);
+ }
+ if (offset + length > tileSize) {
+ throw new Error('Too big rle length in palette mode: ' + length + ', allowed length is: ' + (tileSize - offset));
+ }
+
+ for (let j = 0; j < length; j++) {
+ data[offset * 4] = palette[indexInPalette * 4];
+ data[offset * 4 + 1] = palette[indexInPalette * 4 + 1];
+ data[offset * 4 + 2] = palette[indexInPalette * 4 + 2];
+ data[offset * 4 + 3] = palette[indexInPalette * 4 + 3];
+ offset++;
+ }
+ }
+ return data;
+ }
+
+ _readRLELength() {
+ let length = 0;
+ let current = 0;
+ do {
+ current = this._inflator.inflate(1)[0];
+ length += current;
+ } while (current === 255);
+ return length + 1;
+ }
+}
pkg/web/noVNC/core/input/domkeytable.js
@@ -0,0 +1,311 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2018 The noVNC authors
+ * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
+ */
+
+import KeyTable from "./keysym.js";
+
+/*
+ * Mapping between HTML key values and VNC/X11 keysyms for "special"
+ * keys that cannot be handled via their Unicode codepoint.
+ *
+ * See https://www.w3.org/TR/uievents-key/ for possible values.
+ */
+
+const DOMKeyTable = {};
+
+function addStandard(key, standard) {
+ if (standard === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
+ if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\"");
+ DOMKeyTable[key] = [standard, standard, standard, standard];
+}
+
+function addLeftRight(key, left, right) {
+ if (left === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
+ if (right === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
+ if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\"");
+ DOMKeyTable[key] = [left, left, right, left];
+}
+
+function addNumpad(key, standard, numpad) {
+ if (standard === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
+ if (numpad === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
+ if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\"");
+ DOMKeyTable[key] = [standard, standard, standard, numpad];
+}
+
+// 3.2. Modifier Keys
+
+addLeftRight("Alt", KeyTable.XK_Alt_L, KeyTable.XK_Alt_R);
+addStandard("AltGraph", KeyTable.XK_ISO_Level3_Shift);
+addStandard("CapsLock", KeyTable.XK_Caps_Lock);
+addLeftRight("Control", KeyTable.XK_Control_L, KeyTable.XK_Control_R);
+// - Fn
+// - FnLock
+addLeftRight("Meta", KeyTable.XK_Super_L, KeyTable.XK_Super_R);
+addStandard("NumLock", KeyTable.XK_Num_Lock);
+addStandard("ScrollLock", KeyTable.XK_Scroll_Lock);
+addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R);
+// - Symbol
+// - SymbolLock
+// - Hyper
+// - Super
+
+// 3.3. Whitespace Keys
+
+addNumpad("Enter", KeyTable.XK_Return, KeyTable.XK_KP_Enter);
+addStandard("Tab", KeyTable.XK_Tab);
+addNumpad(" ", KeyTable.XK_space, KeyTable.XK_KP_Space);
+
+// 3.4. Navigation Keys
+
+addNumpad("ArrowDown", KeyTable.XK_Down, KeyTable.XK_KP_Down);
+addNumpad("ArrowLeft", KeyTable.XK_Left, KeyTable.XK_KP_Left);
+addNumpad("ArrowRight", KeyTable.XK_Right, KeyTable.XK_KP_Right);
+addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up);
+addNumpad("End", KeyTable.XK_End, KeyTable.XK_KP_End);
+addNumpad("Home", KeyTable.XK_Home, KeyTable.XK_KP_Home);
+addNumpad("PageDown", KeyTable.XK_Next, KeyTable.XK_KP_Next);
+addNumpad("PageUp", KeyTable.XK_Prior, KeyTable.XK_KP_Prior);
+
+// 3.5. Editing Keys
+
+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);
+addStandard("Copy", KeyTable.XF86XK_Copy);
+// - CrSel
+addStandard("Cut", KeyTable.XF86XK_Cut);
+addNumpad("Delete", KeyTable.XK_Delete, KeyTable.XK_KP_Delete);
+// - EraseEof
+// - ExSel
+addNumpad("Insert", KeyTable.XK_Insert, KeyTable.XK_KP_Insert);
+addStandard("Paste", KeyTable.XF86XK_Paste);
+addStandard("Redo", KeyTable.XK_Redo);
+addStandard("Undo", KeyTable.XK_Undo);
+
+// 3.6. UI Keys
+
+// - Accept
+// - Again (could just be XK_Redo)
+// - Attn
+addStandard("Cancel", KeyTable.XK_Cancel);
+addStandard("ContextMenu", KeyTable.XK_Menu);
+addStandard("Escape", KeyTable.XK_Escape);
+addStandard("Execute", KeyTable.XK_Execute);
+addStandard("Find", KeyTable.XK_Find);
+addStandard("Help", KeyTable.XK_Help);
+addStandard("Pause", KeyTable.XK_Pause);
+// - Play
+// - Props
+addStandard("Select", KeyTable.XK_Select);
+addStandard("ZoomIn", KeyTable.XF86XK_ZoomIn);
+addStandard("ZoomOut", KeyTable.XF86XK_ZoomOut);
+
+// 3.7. Device Keys
+
+addStandard("BrightnessDown", KeyTable.XF86XK_MonBrightnessDown);
+addStandard("BrightnessUp", KeyTable.XF86XK_MonBrightnessUp);
+addStandard("Eject", KeyTable.XF86XK_Eject);
+addStandard("LogOff", KeyTable.XF86XK_LogOff);
+addStandard("Power", KeyTable.XF86XK_PowerOff);
+addStandard("PowerOff", KeyTable.XF86XK_PowerDown);
+addStandard("PrintScreen", KeyTable.XK_Print);
+addStandard("Hibernate", KeyTable.XF86XK_Hibernate);
+addStandard("Standby", KeyTable.XF86XK_Standby);
+addStandard("WakeUp", KeyTable.XF86XK_WakeUp);
+
+// 3.8. IME and Composition Keys
+
+addStandard("AllCandidates", KeyTable.XK_MultipleCandidate);
+addStandard("Alphanumeric", KeyTable.XK_Eisu_toggle);
+addStandard("CodeInput", KeyTable.XK_Codeinput);
+addStandard("Compose", KeyTable.XK_Multi_key);
+addStandard("Convert", KeyTable.XK_Henkan);
+// - Dead
+// - FinalMode
+addStandard("GroupFirst", KeyTable.XK_ISO_First_Group);
+addStandard("GroupLast", KeyTable.XK_ISO_Last_Group);
+addStandard("GroupNext", KeyTable.XK_ISO_Next_Group);
+addStandard("GroupPrevious", KeyTable.XK_ISO_Prev_Group);
+// - ModeChange (XK_Mode_switch is often used for AltGr)
+// - NextCandidate
+addStandard("NonConvert", KeyTable.XK_Muhenkan);
+addStandard("PreviousCandidate", KeyTable.XK_PreviousCandidate);
+// - Process
+addStandard("SingleCandidate", KeyTable.XK_SingleCandidate);
+addStandard("HangulMode", KeyTable.XK_Hangul);
+addStandard("HanjaMode", KeyTable.XK_Hangul_Hanja);
+addStandard("JunjaMode", KeyTable.XK_Hangul_Jeonja);
+addStandard("Eisu", KeyTable.XK_Eisu_toggle);
+addStandard("Hankaku", KeyTable.XK_Hankaku);
+addStandard("Hiragana", KeyTable.XK_Hiragana);
+addStandard("HiraganaKatakana", KeyTable.XK_Hiragana_Katakana);
+addStandard("KanaMode", KeyTable.XK_Kana_Shift); // could also be _Kana_Lock
+addStandard("KanjiMode", KeyTable.XK_Kanji);
+addStandard("Katakana", KeyTable.XK_Katakana);
+addStandard("Romaji", KeyTable.XK_Romaji);
+addStandard("Zenkaku", KeyTable.XK_Zenkaku);
+addStandard("ZenkakuHankaku", KeyTable.XK_Zenkaku_Hankaku);
+
+// 3.9. General-Purpose Function Keys
+
+addStandard("F1", KeyTable.XK_F1);
+addStandard("F2", KeyTable.XK_F2);
+addStandard("F3", KeyTable.XK_F3);
+addStandard("F4", KeyTable.XK_F4);
+addStandard("F5", KeyTable.XK_F5);
+addStandard("F6", KeyTable.XK_F6);
+addStandard("F7", KeyTable.XK_F7);
+addStandard("F8", KeyTable.XK_F8);
+addStandard("F9", KeyTable.XK_F9);
+addStandard("F10", KeyTable.XK_F10);
+addStandard("F11", KeyTable.XK_F11);
+addStandard("F12", KeyTable.XK_F12);
+addStandard("F13", KeyTable.XK_F13);
+addStandard("F14", KeyTable.XK_F14);
+addStandard("F15", KeyTable.XK_F15);
+addStandard("F16", KeyTable.XK_F16);
+addStandard("F17", KeyTable.XK_F17);
+addStandard("F18", KeyTable.XK_F18);
+addStandard("F19", KeyTable.XK_F19);
+addStandard("F20", KeyTable.XK_F20);
+addStandard("F21", KeyTable.XK_F21);
+addStandard("F22", KeyTable.XK_F22);
+addStandard("F23", KeyTable.XK_F23);
+addStandard("F24", KeyTable.XK_F24);
+addStandard("F25", KeyTable.XK_F25);
+addStandard("F26", KeyTable.XK_F26);
+addStandard("F27", KeyTable.XK_F27);
+addStandard("F28", KeyTable.XK_F28);
+addStandard("F29", KeyTable.XK_F29);
+addStandard("F30", KeyTable.XK_F30);
+addStandard("F31", KeyTable.XK_F31);
+addStandard("F32", KeyTable.XK_F32);
+addStandard("F33", KeyTable.XK_F33);
+addStandard("F34", KeyTable.XK_F34);
+addStandard("F35", KeyTable.XK_F35);
+// - Soft1...
+
+// 3.10. Multimedia Keys
+
+// - ChannelDown
+// - ChannelUp
+addStandard("Close", KeyTable.XF86XK_Close);
+addStandard("MailForward", KeyTable.XF86XK_MailForward);
+addStandard("MailReply", KeyTable.XF86XK_Reply);
+addStandard("MailSend", KeyTable.XF86XK_Send);
+// - MediaClose
+addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward);
+addStandard("MediaPause", KeyTable.XF86XK_AudioPause);
+addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay);
+// - MediaPlayPause
+addStandard("MediaRecord", KeyTable.XF86XK_AudioRecord);
+addStandard("MediaRewind", KeyTable.XF86XK_AudioRewind);
+addStandard("MediaStop", KeyTable.XF86XK_AudioStop);
+addStandard("MediaTrackNext", KeyTable.XF86XK_AudioNext);
+addStandard("MediaTrackPrevious", KeyTable.XF86XK_AudioPrev);
+addStandard("New", KeyTable.XF86XK_New);
+addStandard("Open", KeyTable.XF86XK_Open);
+addStandard("Print", KeyTable.XK_Print);
+addStandard("Save", KeyTable.XF86XK_Save);
+addStandard("SpellCheck", KeyTable.XF86XK_Spell);
+
+// 3.11. Multimedia Numpad Keys
+
+// - Key11
+// - Key12
+
+// 3.12. Audio Keys
+
+// - AudioBalanceLeft
+// - AudioBalanceRight
+// - AudioBassBoostDown
+// - AudioBassBoostToggle
+// - AudioBassBoostUp
+// - AudioFaderFront
+// - AudioFaderRear
+// - AudioSurroundModeNext
+// - AudioTrebleDown
+// - AudioTrebleUp
+addStandard("AudioVolumeDown", KeyTable.XF86XK_AudioLowerVolume);
+addStandard("AudioVolumeUp", KeyTable.XF86XK_AudioRaiseVolume);
+addStandard("AudioVolumeMute", KeyTable.XF86XK_AudioMute);
+// - MicrophoneToggle
+// - MicrophoneVolumeDown
+// - MicrophoneVolumeUp
+addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute);
+
+// 3.13. Speech Keys
+
+// - SpeechCorrectionList
+// - SpeechInputToggle
+
+// 3.14. Application Keys
+
+addStandard("LaunchApplication1", KeyTable.XF86XK_MyComputer);
+addStandard("LaunchApplication2", KeyTable.XF86XK_Calculator);
+addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar);
+// - LaunchContacts
+addStandard("LaunchMail", KeyTable.XF86XK_Mail);
+addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia);
+addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music);
+addStandard("LaunchPhone", KeyTable.XF86XK_Phone);
+addStandard("LaunchScreenSaver", KeyTable.XF86XK_ScreenSaver);
+addStandard("LaunchSpreadsheet", KeyTable.XF86XK_Excel);
+addStandard("LaunchWebBrowser", KeyTable.XF86XK_WWW);
+addStandard("LaunchWebCam", KeyTable.XF86XK_WebCam);
+addStandard("LaunchWordProcessor", KeyTable.XF86XK_Word);
+
+// 3.15. Browser Keys
+
+addStandard("BrowserBack", KeyTable.XF86XK_Back);
+addStandard("BrowserFavorites", KeyTable.XF86XK_Favorites);
+addStandard("BrowserForward", KeyTable.XF86XK_Forward);
+addStandard("BrowserHome", KeyTable.XF86XK_HomePage);
+addStandard("BrowserRefresh", KeyTable.XF86XK_Refresh);
+addStandard("BrowserSearch", KeyTable.XF86XK_Search);
+addStandard("BrowserStop", KeyTable.XF86XK_Stop);
+
+// 3.16. Mobile Phone Keys
+
+// - A whole bunch...
+
+// 3.17. TV Keys
+
+// - A whole bunch...
+
+// 3.18. Media Controller Keys
+
+// - A whole bunch...
+addStandard("Dimmer", KeyTable.XF86XK_BrightnessAdjust);
+addStandard("MediaAudioTrack", KeyTable.XF86XK_AudioCycleTrack);
+addStandard("RandomToggle", KeyTable.XF86XK_AudioRandomPlay);
+addStandard("SplitScreenToggle", KeyTable.XF86XK_SplitScreen);
+addStandard("Subtitle", KeyTable.XF86XK_Subtitle);
+addStandard("VideoModeNext", KeyTable.XF86XK_Next_VMode);
+
+// Extra: Numpad
+
+addNumpad("=", KeyTable.XK_equal, KeyTable.XK_KP_Equal);
+addNumpad("+", KeyTable.XK_plus, KeyTable.XK_KP_Add);
+addNumpad("-", KeyTable.XK_minus, KeyTable.XK_KP_Subtract);
+addNumpad("*", KeyTable.XK_asterisk, KeyTable.XK_KP_Multiply);
+addNumpad("/", KeyTable.XK_slash, KeyTable.XK_KP_Divide);
+addNumpad(".", KeyTable.XK_period, KeyTable.XK_KP_Decimal);
+addNumpad(",", KeyTable.XK_comma, KeyTable.XK_KP_Separator);
+addNumpad("0", KeyTable.XK_0, KeyTable.XK_KP_0);
+addNumpad("1", KeyTable.XK_1, KeyTable.XK_KP_1);
+addNumpad("2", KeyTable.XK_2, KeyTable.XK_KP_2);
+addNumpad("3", KeyTable.XK_3, KeyTable.XK_KP_3);
+addNumpad("4", KeyTable.XK_4, KeyTable.XK_KP_4);
+addNumpad("5", KeyTable.XK_5, KeyTable.XK_KP_5);
+addNumpad("6", KeyTable.XK_6, KeyTable.XK_KP_6);
+addNumpad("7", KeyTable.XK_7, KeyTable.XK_KP_7);
+addNumpad("8", KeyTable.XK_8, KeyTable.XK_KP_8);
+addNumpad("9", KeyTable.XK_9, KeyTable.XK_KP_9);
+
+export default DOMKeyTable;
pkg/web/noVNC/core/input/fixedkeys.js
@@ -0,0 +1,129 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2018 The noVNC authors
+ * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
+ */
+
+/*
+ * Fallback mapping between HTML key codes (physical keys) and
+ * HTML key values. This only works for keys that don't vary
+ * between layouts. We also omit those who manage fine by mapping the
+ * Unicode representation.
+ *
+ * See https://www.w3.org/TR/uievents-code/ for possible codes.
+ * See https://www.w3.org/TR/uievents-key/ for possible values.
+ */
+
+/* eslint-disable key-spacing */
+
+export default {
+
+// 3.1.1.1. Writing System Keys
+
+ 'Backspace': 'Backspace',
+
+// 3.1.1.2. Functional Keys
+
+ 'AltLeft': 'Alt',
+ 'AltRight': 'Alt', // This could also be 'AltGraph'
+ 'CapsLock': 'CapsLock',
+ 'ContextMenu': 'ContextMenu',
+ 'ControlLeft': 'Control',
+ 'ControlRight': 'Control',
+ 'Enter': 'Enter',
+ 'MetaLeft': 'Meta',
+ 'MetaRight': 'Meta',
+ 'ShiftLeft': 'Shift',
+ 'ShiftRight': 'Shift',
+ 'Tab': 'Tab',
+ // FIXME: Japanese/Korean keys
+
+// 3.1.2. Control Pad Section
+
+ 'Delete': 'Delete',
+ 'End': 'End',
+ 'Help': 'Help',
+ 'Home': 'Home',
+ 'Insert': 'Insert',
+ 'PageDown': 'PageDown',
+ 'PageUp': 'PageUp',
+
+// 3.1.3. Arrow Pad Section
+
+ 'ArrowDown': 'ArrowDown',
+ 'ArrowLeft': 'ArrowLeft',
+ 'ArrowRight': 'ArrowRight',
+ 'ArrowUp': 'ArrowUp',
+
+// 3.1.4. Numpad Section
+
+ 'NumLock': 'NumLock',
+ 'NumpadBackspace': 'Backspace',
+ 'NumpadClear': 'Clear',
+
+// 3.1.5. Function Section
+
+ 'Escape': 'Escape',
+ 'F1': 'F1',
+ 'F2': 'F2',
+ 'F3': 'F3',
+ 'F4': 'F4',
+ 'F5': 'F5',
+ 'F6': 'F6',
+ 'F7': 'F7',
+ 'F8': 'F8',
+ 'F9': 'F9',
+ 'F10': 'F10',
+ 'F11': 'F11',
+ 'F12': 'F12',
+ 'F13': 'F13',
+ 'F14': 'F14',
+ 'F15': 'F15',
+ 'F16': 'F16',
+ 'F17': 'F17',
+ 'F18': 'F18',
+ 'F19': 'F19',
+ 'F20': 'F20',
+ 'F21': 'F21',
+ 'F22': 'F22',
+ 'F23': 'F23',
+ 'F24': 'F24',
+ 'F25': 'F25',
+ 'F26': 'F26',
+ 'F27': 'F27',
+ 'F28': 'F28',
+ 'F29': 'F29',
+ 'F30': 'F30',
+ 'F31': 'F31',
+ 'F32': 'F32',
+ 'F33': 'F33',
+ 'F34': 'F34',
+ 'F35': 'F35',
+ 'PrintScreen': 'PrintScreen',
+ 'ScrollLock': 'ScrollLock',
+ 'Pause': 'Pause',
+
+// 3.1.6. Media Keys
+
+ 'BrowserBack': 'BrowserBack',
+ 'BrowserFavorites': 'BrowserFavorites',
+ 'BrowserForward': 'BrowserForward',
+ 'BrowserHome': 'BrowserHome',
+ 'BrowserRefresh': 'BrowserRefresh',
+ 'BrowserSearch': 'BrowserSearch',
+ 'BrowserStop': 'BrowserStop',
+ 'Eject': 'Eject',
+ 'LaunchApp1': 'LaunchMyComputer',
+ 'LaunchApp2': 'LaunchCalendar',
+ 'LaunchMail': 'LaunchMail',
+ 'MediaPlayPause': 'MediaPlay',
+ 'MediaStop': 'MediaStop',
+ 'MediaTrackNext': 'MediaTrackNext',
+ 'MediaTrackPrevious': 'MediaTrackPrevious',
+ 'Power': 'Power',
+ 'Sleep': 'Sleep',
+ 'AudioVolumeDown': 'AudioVolumeDown',
+ 'AudioVolumeMute': 'AudioVolumeMute',
+ 'AudioVolumeUp': 'AudioVolumeUp',
+ 'WakeUp': 'WakeUp',
+};
pkg/web/noVNC/core/input/gesturehandler.js
@@ -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 } };
+ }
+}
pkg/web/noVNC/core/input/keyboard.js
@@ -0,0 +1,294 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2019 The noVNC authors
+ * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
+ */
+
+import * as Log from '../util/logging.js';
+import { stopEvent } from '../util/events.js';
+import * as KeyboardUtil from "./util.js";
+import KeyTable from "./keysym.js";
+import * as browser from "../util/browser.js";
+
+//
+// Keyboard event handler
+//
+
+export default class Keyboard {
+ constructor(target) {
+ this._target = target || null;
+
+ this._keyDownList = {}; // List of depressed keys
+ // (even if they are happy)
+ this._altGrArmed = false; // Windows AltGr detection
+
+ // keep these here so we can refer to them later
+ this._eventHandlers = {
+ 'keyup': this._handleKeyUp.bind(this),
+ 'keydown': this._handleKeyDown.bind(this),
+ 'blur': this._allKeysUp.bind(this),
+ };
+
+ // ===== EVENT HANDLERS =====
+
+ this.onkeyevent = () => {}; // Handler for key press/release
+ }
+
+ // ===== PRIVATE METHODS =====
+
+ _sendKeyEvent(keysym, code, down, numlock = null, capslock = null) {
+ if (down) {
+ this._keyDownList[code] = keysym;
+ } else {
+ // Do we really think this key is down?
+ if (!(code in this._keyDownList)) {
+ return;
+ }
+ delete this._keyDownList[code];
+ }
+
+ Log.Debug("onkeyevent " + (down ? "down" : "up") +
+ ", keysym: " + keysym, ", code: " + code +
+ ", numlock: " + numlock + ", capslock: " + capslock);
+ this.onkeyevent(keysym, code, down, numlock, capslock);
+ }
+
+ _getKeyCode(e) {
+ const code = KeyboardUtil.getKeycode(e);
+ if (code !== 'Unidentified') {
+ return code;
+ }
+
+ // Unstable, but we don't have anything else to go on
+ if (e.keyCode) {
+ // 229 is used for composition events
+ if (e.keyCode !== 229) {
+ return 'Platform' + e.keyCode;
+ }
+ }
+
+ // A precursor to the final DOM3 standard. Unfortunately it
+ // is not layout independent, so it is as bad as using keyCode
+ if (e.keyIdentifier) {
+ // Non-character key?
+ if (e.keyIdentifier.substr(0, 2) !== 'U+') {
+ return e.keyIdentifier;
+ }
+
+ const codepoint = parseInt(e.keyIdentifier.substr(2), 16);
+ const char = String.fromCharCode(codepoint).toUpperCase();
+
+ return 'Platform' + char.charCodeAt();
+ }
+
+ return 'Unidentified';
+ }
+
+ _handleKeyDown(e) {
+ const code = this._getKeyCode(e);
+ let keysym = KeyboardUtil.getKeysym(e);
+ let numlock = e.getModifierState('NumLock');
+ let capslock = e.getModifierState('CapsLock');
+
+ // getModifierState for NumLock is not supported on mac and ios and always returns false.
+ // Set to null to indicate unknown/unsupported instead.
+ if (browser.isMac() || browser.isIOS()) {
+ numlock = null;
+ }
+
+ // Windows doesn't have a proper AltGr, but handles it using
+ // fake Ctrl+Alt. However the remote end might not be Windows,
+ // so we need to merge those in to a single AltGr event. We
+ // detect this case by seeing the two key events directly after
+ // each other with a very short time between them (<50ms).
+ if (this._altGrArmed) {
+ this._altGrArmed = false;
+ clearTimeout(this._altGrTimeout);
+
+ if ((code === "AltRight") &&
+ ((e.timeStamp - this._altGrCtrlTime) < 50)) {
+ // FIXME: We fail to detect this if either Ctrl key is
+ // first manually pressed as Windows then no
+ // longer sends the fake Ctrl down event. It
+ // does however happily send real Ctrl events
+ // even when AltGr is already down. Some
+ // browsers detect this for us though and set the
+ // key to "AltGraph".
+ keysym = KeyTable.XK_ISO_Level3_Shift;
+ } else {
+ this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock);
+ }
+ }
+
+ // We cannot handle keys we cannot track, but we also need
+ // to deal with virtual keyboards which omit key info
+ if (code === 'Unidentified') {
+ if (keysym) {
+ // If it's a virtual keyboard then it should be
+ // sufficient to just send press and release right
+ // after each other
+ this._sendKeyEvent(keysym, code, true, numlock, capslock);
+ this._sendKeyEvent(keysym, code, false, numlock, capslock);
+ }
+
+ stopEvent(e);
+ return;
+ }
+
+ // Alt behaves more like AltGraph on macOS, so shuffle the
+ // keys around a bit to make things more sane for the remote
+ // server. This method is used by RealVNC and TigerVNC (and
+ // possibly others).
+ if (browser.isMac() || browser.isIOS()) {
+ switch (keysym) {
+ case KeyTable.XK_Super_L:
+ keysym = KeyTable.XK_Alt_L;
+ break;
+ case KeyTable.XK_Super_R:
+ keysym = KeyTable.XK_Super_L;
+ break;
+ case KeyTable.XK_Alt_L:
+ keysym = KeyTable.XK_Mode_switch;
+ break;
+ case KeyTable.XK_Alt_R:
+ keysym = KeyTable.XK_ISO_Level3_Shift;
+ break;
+ }
+ }
+
+ // Is this key already pressed? If so, then we must use the
+ // same keysym or we'll confuse the server
+ if (code in this._keyDownList) {
+ keysym = this._keyDownList[code];
+ }
+
+ // macOS doesn't send proper key releases if a key is pressed
+ // while meta is held down
+ if ((browser.isMac() || browser.isIOS()) &&
+ (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) {
+ this._sendKeyEvent(keysym, code, true, numlock, capslock);
+ this._sendKeyEvent(keysym, code, false, numlock, capslock);
+ stopEvent(e);
+ return;
+ }
+
+ // macOS doesn't send proper key events for modifiers, only
+ // state change events. That gets extra confusing for CapsLock
+ // which toggles on each press, but not on release. So pretend
+ // it was a quick press and release of the button.
+ if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
+ this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock);
+ this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock);
+ stopEvent(e);
+ return;
+ }
+
+ // Windows doesn't send proper key releases for a bunch of
+ // Japanese IM keys so we have to fake the release right away
+ const jpBadKeys = [ KeyTable.XK_Zenkaku_Hankaku,
+ KeyTable.XK_Eisu_toggle,
+ KeyTable.XK_Katakana,
+ KeyTable.XK_Hiragana,
+ KeyTable.XK_Romaji ];
+ if (browser.isWindows() && jpBadKeys.includes(keysym)) {
+ this._sendKeyEvent(keysym, code, true, numlock, capslock);
+ this._sendKeyEvent(keysym, code, false, numlock, capslock);
+ stopEvent(e);
+ return;
+ }
+
+ stopEvent(e);
+
+ // Possible start of AltGr sequence? (see above)
+ if ((code === "ControlLeft") && browser.isWindows() &&
+ !("ControlLeft" in this._keyDownList)) {
+ this._altGrArmed = true;
+ this._altGrTimeout = setTimeout(this._interruptAltGrSequence.bind(this), 100);
+ this._altGrCtrlTime = e.timeStamp;
+ return;
+ }
+
+ this._sendKeyEvent(keysym, code, true, numlock, capslock);
+ }
+
+ _handleKeyUp(e) {
+ stopEvent(e);
+
+ const code = this._getKeyCode(e);
+
+ // We can't get a release in the middle of an AltGr sequence, so
+ // abort that detection
+ this._interruptAltGrSequence();
+
+ // See comment in _handleKeyDown()
+ if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
+ this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
+ this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
+ return;
+ }
+
+ 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);
+ }
+ }
+ }
+
+ _interruptAltGrSequence() {
+ if (this._altGrArmed) {
+ this._altGrArmed = false;
+ clearTimeout(this._altGrTimeout);
+ this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
+ }
+ }
+
+ _allKeysUp() {
+ Log.Debug(">> Keyboard.allKeysUp");
+
+ // Prevent control key being processed after losing focus.
+ this._interruptAltGrSequence();
+
+ for (let code in this._keyDownList) {
+ this._sendKeyEvent(this._keyDownList[code], code, false);
+ }
+ Log.Debug("<< Keyboard.allKeysUp");
+ }
+
+ // ===== PUBLIC METHODS =====
+
+ grab() {
+ //Log.Debug(">> Keyboard.grab");
+
+ this._target.addEventListener('keydown', this._eventHandlers.keydown);
+ this._target.addEventListener('keyup', this._eventHandlers.keyup);
+
+ // Release (key up) if window loses focus
+ window.addEventListener('blur', this._eventHandlers.blur);
+
+ //Log.Debug("<< Keyboard.grab");
+ }
+
+ ungrab() {
+ //Log.Debug(">> Keyboard.ungrab");
+
+ this._target.removeEventListener('keydown', this._eventHandlers.keydown);
+ this._target.removeEventListener('keyup', this._eventHandlers.keyup);
+ window.removeEventListener('blur', this._eventHandlers.blur);
+
+ // Release (key up) all keys that are in a down state
+ this._allKeysUp();
+
+ //Log.Debug(">> Keyboard.ungrab");
+ }
+}
pkg/web/noVNC/core/input/keysym.js
@@ -0,0 +1,616 @@
+/* eslint-disable key-spacing */
+
+export default {
+ XK_VoidSymbol: 0xffffff, /* Void symbol */
+
+ XK_BackSpace: 0xff08, /* Back space, back char */
+ XK_Tab: 0xff09,
+ XK_Linefeed: 0xff0a, /* Linefeed, LF */
+ XK_Clear: 0xff0b,
+ XK_Return: 0xff0d, /* Return, enter */
+ XK_Pause: 0xff13, /* Pause, hold */
+ XK_Scroll_Lock: 0xff14,
+ XK_Sys_Req: 0xff15,
+ XK_Escape: 0xff1b,
+ XK_Delete: 0xffff, /* Delete, rubout */
+
+ /* International & multi-key character composition */
+
+ XK_Multi_key: 0xff20, /* Multi-key character compose */
+ XK_Codeinput: 0xff37,
+ XK_SingleCandidate: 0xff3c,
+ XK_MultipleCandidate: 0xff3d,
+ XK_PreviousCandidate: 0xff3e,
+
+ /* Japanese keyboard support */
+
+ XK_Kanji: 0xff21, /* Kanji, Kanji convert */
+ XK_Muhenkan: 0xff22, /* Cancel Conversion */
+ XK_Henkan_Mode: 0xff23, /* Start/Stop Conversion */
+ XK_Henkan: 0xff23, /* Alias for Henkan_Mode */
+ XK_Romaji: 0xff24, /* to Romaji */
+ XK_Hiragana: 0xff25, /* to Hiragana */
+ XK_Katakana: 0xff26, /* to Katakana */
+ XK_Hiragana_Katakana: 0xff27, /* Hiragana/Katakana toggle */
+ XK_Zenkaku: 0xff28, /* to Zenkaku */
+ XK_Hankaku: 0xff29, /* to Hankaku */
+ XK_Zenkaku_Hankaku: 0xff2a, /* Zenkaku/Hankaku toggle */
+ XK_Touroku: 0xff2b, /* Add to Dictionary */
+ XK_Massyo: 0xff2c, /* Delete from Dictionary */
+ XK_Kana_Lock: 0xff2d, /* Kana Lock */
+ XK_Kana_Shift: 0xff2e, /* Kana Shift */
+ XK_Eisu_Shift: 0xff2f, /* Alphanumeric Shift */
+ XK_Eisu_toggle: 0xff30, /* Alphanumeric toggle */
+ XK_Kanji_Bangou: 0xff37, /* Codeinput */
+ XK_Zen_Koho: 0xff3d, /* Multiple/All Candidate(s) */
+ XK_Mae_Koho: 0xff3e, /* Previous Candidate */
+
+ /* Cursor control & motion */
+
+ XK_Home: 0xff50,
+ XK_Left: 0xff51, /* Move left, left arrow */
+ XK_Up: 0xff52, /* Move up, up arrow */
+ XK_Right: 0xff53, /* Move right, right arrow */
+ XK_Down: 0xff54, /* Move down, down arrow */
+ XK_Prior: 0xff55, /* Prior, previous */
+ XK_Page_Up: 0xff55,
+ XK_Next: 0xff56, /* Next */
+ XK_Page_Down: 0xff56,
+ XK_End: 0xff57, /* EOL */
+ XK_Begin: 0xff58, /* BOL */
+
+
+ /* Misc functions */
+
+ XK_Select: 0xff60, /* Select, mark */
+ XK_Print: 0xff61,
+ XK_Execute: 0xff62, /* Execute, run, do */
+ XK_Insert: 0xff63, /* Insert, insert here */
+ XK_Undo: 0xff65,
+ XK_Redo: 0xff66, /* Redo, again */
+ XK_Menu: 0xff67,
+ XK_Find: 0xff68, /* Find, search */
+ XK_Cancel: 0xff69, /* Cancel, stop, abort, exit */
+ XK_Help: 0xff6a, /* Help */
+ XK_Break: 0xff6b,
+ XK_Mode_switch: 0xff7e, /* Character set switch */
+ XK_script_switch: 0xff7e, /* Alias for mode_switch */
+ XK_Num_Lock: 0xff7f,
+
+ /* Keypad functions, keypad numbers cleverly chosen to map to ASCII */
+
+ XK_KP_Space: 0xff80, /* Space */
+ XK_KP_Tab: 0xff89,
+ XK_KP_Enter: 0xff8d, /* Enter */
+ XK_KP_F1: 0xff91, /* PF1, KP_A, ... */
+ XK_KP_F2: 0xff92,
+ XK_KP_F3: 0xff93,
+ XK_KP_F4: 0xff94,
+ XK_KP_Home: 0xff95,
+ XK_KP_Left: 0xff96,
+ XK_KP_Up: 0xff97,
+ XK_KP_Right: 0xff98,
+ XK_KP_Down: 0xff99,
+ XK_KP_Prior: 0xff9a,
+ XK_KP_Page_Up: 0xff9a,
+ XK_KP_Next: 0xff9b,
+ XK_KP_Page_Down: 0xff9b,
+ XK_KP_End: 0xff9c,
+ XK_KP_Begin: 0xff9d,
+ XK_KP_Insert: 0xff9e,
+ XK_KP_Delete: 0xff9f,
+ XK_KP_Equal: 0xffbd, /* Equals */
+ XK_KP_Multiply: 0xffaa,
+ XK_KP_Add: 0xffab,
+ XK_KP_Separator: 0xffac, /* Separator, often comma */
+ XK_KP_Subtract: 0xffad,
+ XK_KP_Decimal: 0xffae,
+ XK_KP_Divide: 0xffaf,
+
+ XK_KP_0: 0xffb0,
+ XK_KP_1: 0xffb1,
+ XK_KP_2: 0xffb2,
+ XK_KP_3: 0xffb3,
+ XK_KP_4: 0xffb4,
+ XK_KP_5: 0xffb5,
+ XK_KP_6: 0xffb6,
+ XK_KP_7: 0xffb7,
+ XK_KP_8: 0xffb8,
+ XK_KP_9: 0xffb9,
+
+ /*
+ * Auxiliary functions; note the duplicate definitions for left and right
+ * function keys; Sun keyboards and a few other manufacturers have such
+ * function key groups on the left and/or right sides of the keyboard.
+ * We've not found a keyboard with more than 35 function keys total.
+ */
+
+ XK_F1: 0xffbe,
+ XK_F2: 0xffbf,
+ XK_F3: 0xffc0,
+ XK_F4: 0xffc1,
+ XK_F5: 0xffc2,
+ XK_F6: 0xffc3,
+ XK_F7: 0xffc4,
+ XK_F8: 0xffc5,
+ XK_F9: 0xffc6,
+ XK_F10: 0xffc7,
+ XK_F11: 0xffc8,
+ XK_L1: 0xffc8,
+ XK_F12: 0xffc9,
+ XK_L2: 0xffc9,
+ XK_F13: 0xffca,
+ XK_L3: 0xffca,
+ XK_F14: 0xffcb,
+ XK_L4: 0xffcb,
+ XK_F15: 0xffcc,
+ XK_L5: 0xffcc,
+ XK_F16: 0xffcd,
+ XK_L6: 0xffcd,
+ XK_F17: 0xffce,
+ XK_L7: 0xffce,
+ XK_F18: 0xffcf,
+ XK_L8: 0xffcf,
+ XK_F19: 0xffd0,
+ XK_L9: 0xffd0,
+ XK_F20: 0xffd1,
+ XK_L10: 0xffd1,
+ XK_F21: 0xffd2,
+ XK_R1: 0xffd2,
+ XK_F22: 0xffd3,
+ XK_R2: 0xffd3,
+ XK_F23: 0xffd4,
+ XK_R3: 0xffd4,
+ XK_F24: 0xffd5,
+ XK_R4: 0xffd5,
+ XK_F25: 0xffd6,
+ XK_R5: 0xffd6,
+ XK_F26: 0xffd7,
+ XK_R6: 0xffd7,
+ XK_F27: 0xffd8,
+ XK_R7: 0xffd8,
+ XK_F28: 0xffd9,
+ XK_R8: 0xffd9,
+ XK_F29: 0xffda,
+ XK_R9: 0xffda,
+ XK_F30: 0xffdb,
+ XK_R10: 0xffdb,
+ XK_F31: 0xffdc,
+ XK_R11: 0xffdc,
+ XK_F32: 0xffdd,
+ XK_R12: 0xffdd,
+ XK_F33: 0xffde,
+ XK_R13: 0xffde,
+ XK_F34: 0xffdf,
+ XK_R14: 0xffdf,
+ XK_F35: 0xffe0,
+ XK_R15: 0xffe0,
+
+ /* Modifiers */
+
+ XK_Shift_L: 0xffe1, /* Left shift */
+ XK_Shift_R: 0xffe2, /* Right shift */
+ XK_Control_L: 0xffe3, /* Left control */
+ XK_Control_R: 0xffe4, /* Right control */
+ XK_Caps_Lock: 0xffe5, /* Caps lock */
+ XK_Shift_Lock: 0xffe6, /* Shift lock */
+
+ XK_Meta_L: 0xffe7, /* Left meta */
+ XK_Meta_R: 0xffe8, /* Right meta */
+ XK_Alt_L: 0xffe9, /* Left alt */
+ XK_Alt_R: 0xffea, /* Right alt */
+ XK_Super_L: 0xffeb, /* Left super */
+ XK_Super_R: 0xffec, /* Right super */
+ XK_Hyper_L: 0xffed, /* Left hyper */
+ XK_Hyper_R: 0xffee, /* Right hyper */
+
+ /*
+ * Keyboard (XKB) Extension function and modifier keys
+ * (from Appendix C of "The X Keyboard Extension: Protocol Specification")
+ * Byte 3 = 0xfe
+ */
+
+ XK_ISO_Level3_Shift: 0xfe03, /* AltGr */
+ XK_ISO_Next_Group: 0xfe08,
+ XK_ISO_Prev_Group: 0xfe0a,
+ XK_ISO_First_Group: 0xfe0c,
+ XK_ISO_Last_Group: 0xfe0e,
+
+ /*
+ * Latin 1
+ * (ISO/IEC 8859-1: Unicode U+0020..U+00FF)
+ * Byte 3: 0
+ */
+
+ XK_space: 0x0020, /* U+0020 SPACE */
+ XK_exclam: 0x0021, /* U+0021 EXCLAMATION MARK */
+ XK_quotedbl: 0x0022, /* U+0022 QUOTATION MARK */
+ XK_numbersign: 0x0023, /* U+0023 NUMBER SIGN */
+ XK_dollar: 0x0024, /* U+0024 DOLLAR SIGN */
+ XK_percent: 0x0025, /* U+0025 PERCENT SIGN */
+ XK_ampersand: 0x0026, /* U+0026 AMPERSAND */
+ XK_apostrophe: 0x0027, /* U+0027 APOSTROPHE */
+ XK_quoteright: 0x0027, /* deprecated */
+ XK_parenleft: 0x0028, /* U+0028 LEFT PARENTHESIS */
+ XK_parenright: 0x0029, /* U+0029 RIGHT PARENTHESIS */
+ XK_asterisk: 0x002a, /* U+002A ASTERISK */
+ XK_plus: 0x002b, /* U+002B PLUS SIGN */
+ XK_comma: 0x002c, /* U+002C COMMA */
+ XK_minus: 0x002d, /* U+002D HYPHEN-MINUS */
+ XK_period: 0x002e, /* U+002E FULL STOP */
+ XK_slash: 0x002f, /* U+002F SOLIDUS */
+ XK_0: 0x0030, /* U+0030 DIGIT ZERO */
+ XK_1: 0x0031, /* U+0031 DIGIT ONE */
+ XK_2: 0x0032, /* U+0032 DIGIT TWO */
+ XK_3: 0x0033, /* U+0033 DIGIT THREE */
+ XK_4: 0x0034, /* U+0034 DIGIT FOUR */
+ XK_5: 0x0035, /* U+0035 DIGIT FIVE */
+ XK_6: 0x0036, /* U+0036 DIGIT SIX */
+ XK_7: 0x0037, /* U+0037 DIGIT SEVEN */
+ XK_8: 0x0038, /* U+0038 DIGIT EIGHT */
+ XK_9: 0x0039, /* U+0039 DIGIT NINE */
+ XK_colon: 0x003a, /* U+003A COLON */
+ XK_semicolon: 0x003b, /* U+003B SEMICOLON */
+ XK_less: 0x003c, /* U+003C LESS-THAN SIGN */
+ XK_equal: 0x003d, /* U+003D EQUALS SIGN */
+ XK_greater: 0x003e, /* U+003E GREATER-THAN SIGN */
+ XK_question: 0x003f, /* U+003F QUESTION MARK */
+ XK_at: 0x0040, /* U+0040 COMMERCIAL AT */
+ XK_A: 0x0041, /* U+0041 LATIN CAPITAL LETTER A */
+ XK_B: 0x0042, /* U+0042 LATIN CAPITAL LETTER B */
+ XK_C: 0x0043, /* U+0043 LATIN CAPITAL LETTER C */
+ XK_D: 0x0044, /* U+0044 LATIN CAPITAL LETTER D */
+ XK_E: 0x0045, /* U+0045 LATIN CAPITAL LETTER E */
+ XK_F: 0x0046, /* U+0046 LATIN CAPITAL LETTER F */
+ XK_G: 0x0047, /* U+0047 LATIN CAPITAL LETTER G */
+ XK_H: 0x0048, /* U+0048 LATIN CAPITAL LETTER H */
+ XK_I: 0x0049, /* U+0049 LATIN CAPITAL LETTER I */
+ XK_J: 0x004a, /* U+004A LATIN CAPITAL LETTER J */
+ XK_K: 0x004b, /* U+004B LATIN CAPITAL LETTER K */
+ XK_L: 0x004c, /* U+004C LATIN CAPITAL LETTER L */
+ XK_M: 0x004d, /* U+004D LATIN CAPITAL LETTER M */
+ XK_N: 0x004e, /* U+004E LATIN CAPITAL LETTER N */
+ XK_O: 0x004f, /* U+004F LATIN CAPITAL LETTER O */
+ XK_P: 0x0050, /* U+0050 LATIN CAPITAL LETTER P */
+ XK_Q: 0x0051, /* U+0051 LATIN CAPITAL LETTER Q */
+ XK_R: 0x0052, /* U+0052 LATIN CAPITAL LETTER R */
+ XK_S: 0x0053, /* U+0053 LATIN CAPITAL LETTER S */
+ XK_T: 0x0054, /* U+0054 LATIN CAPITAL LETTER T */
+ XK_U: 0x0055, /* U+0055 LATIN CAPITAL LETTER U */
+ XK_V: 0x0056, /* U+0056 LATIN CAPITAL LETTER V */
+ XK_W: 0x0057, /* U+0057 LATIN CAPITAL LETTER W */
+ XK_X: 0x0058, /* U+0058 LATIN CAPITAL LETTER X */
+ XK_Y: 0x0059, /* U+0059 LATIN CAPITAL LETTER Y */
+ XK_Z: 0x005a, /* U+005A LATIN CAPITAL LETTER Z */
+ XK_bracketleft: 0x005b, /* U+005B LEFT SQUARE BRACKET */
+ XK_backslash: 0x005c, /* U+005C REVERSE SOLIDUS */
+ XK_bracketright: 0x005d, /* U+005D RIGHT SQUARE BRACKET */
+ XK_asciicircum: 0x005e, /* U+005E CIRCUMFLEX ACCENT */
+ XK_underscore: 0x005f, /* U+005F LOW LINE */
+ XK_grave: 0x0060, /* U+0060 GRAVE ACCENT */
+ XK_quoteleft: 0x0060, /* deprecated */
+ XK_a: 0x0061, /* U+0061 LATIN SMALL LETTER A */
+ XK_b: 0x0062, /* U+0062 LATIN SMALL LETTER B */
+ XK_c: 0x0063, /* U+0063 LATIN SMALL LETTER C */
+ XK_d: 0x0064, /* U+0064 LATIN SMALL LETTER D */
+ XK_e: 0x0065, /* U+0065 LATIN SMALL LETTER E */
+ XK_f: 0x0066, /* U+0066 LATIN SMALL LETTER F */
+ XK_g: 0x0067, /* U+0067 LATIN SMALL LETTER G */
+ XK_h: 0x0068, /* U+0068 LATIN SMALL LETTER H */
+ XK_i: 0x0069, /* U+0069 LATIN SMALL LETTER I */
+ XK_j: 0x006a, /* U+006A LATIN SMALL LETTER J */
+ XK_k: 0x006b, /* U+006B LATIN SMALL LETTER K */
+ XK_l: 0x006c, /* U+006C LATIN SMALL LETTER L */
+ XK_m: 0x006d, /* U+006D LATIN SMALL LETTER M */
+ XK_n: 0x006e, /* U+006E LATIN SMALL LETTER N */
+ XK_o: 0x006f, /* U+006F LATIN SMALL LETTER O */
+ XK_p: 0x0070, /* U+0070 LATIN SMALL LETTER P */
+ XK_q: 0x0071, /* U+0071 LATIN SMALL LETTER Q */
+ XK_r: 0x0072, /* U+0072 LATIN SMALL LETTER R */
+ XK_s: 0x0073, /* U+0073 LATIN SMALL LETTER S */
+ XK_t: 0x0074, /* U+0074 LATIN SMALL LETTER T */
+ XK_u: 0x0075, /* U+0075 LATIN SMALL LETTER U */
+ XK_v: 0x0076, /* U+0076 LATIN SMALL LETTER V */
+ XK_w: 0x0077, /* U+0077 LATIN SMALL LETTER W */
+ XK_x: 0x0078, /* U+0078 LATIN SMALL LETTER X */
+ XK_y: 0x0079, /* U+0079 LATIN SMALL LETTER Y */
+ XK_z: 0x007a, /* U+007A LATIN SMALL LETTER Z */
+ XK_braceleft: 0x007b, /* U+007B LEFT CURLY BRACKET */
+ XK_bar: 0x007c, /* U+007C VERTICAL LINE */
+ XK_braceright: 0x007d, /* U+007D RIGHT CURLY BRACKET */
+ XK_asciitilde: 0x007e, /* U+007E TILDE */
+
+ XK_nobreakspace: 0x00a0, /* U+00A0 NO-BREAK SPACE */
+ XK_exclamdown: 0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */
+ XK_cent: 0x00a2, /* U+00A2 CENT SIGN */
+ XK_sterling: 0x00a3, /* U+00A3 POUND SIGN */
+ XK_currency: 0x00a4, /* U+00A4 CURRENCY SIGN */
+ XK_yen: 0x00a5, /* U+00A5 YEN SIGN */
+ XK_brokenbar: 0x00a6, /* U+00A6 BROKEN BAR */
+ XK_section: 0x00a7, /* U+00A7 SECTION SIGN */
+ XK_diaeresis: 0x00a8, /* U+00A8 DIAERESIS */
+ XK_copyright: 0x00a9, /* U+00A9 COPYRIGHT SIGN */
+ XK_ordfeminine: 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */
+ XK_guillemotleft: 0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */
+ XK_notsign: 0x00ac, /* U+00AC NOT SIGN */
+ XK_hyphen: 0x00ad, /* U+00AD SOFT HYPHEN */
+ XK_registered: 0x00ae, /* U+00AE REGISTERED SIGN */
+ XK_macron: 0x00af, /* U+00AF MACRON */
+ XK_degree: 0x00b0, /* U+00B0 DEGREE SIGN */
+ XK_plusminus: 0x00b1, /* U+00B1 PLUS-MINUS SIGN */
+ XK_twosuperior: 0x00b2, /* U+00B2 SUPERSCRIPT TWO */
+ XK_threesuperior: 0x00b3, /* U+00B3 SUPERSCRIPT THREE */
+ XK_acute: 0x00b4, /* U+00B4 ACUTE ACCENT */
+ XK_mu: 0x00b5, /* U+00B5 MICRO SIGN */
+ XK_paragraph: 0x00b6, /* U+00B6 PILCROW SIGN */
+ XK_periodcentered: 0x00b7, /* U+00B7 MIDDLE DOT */
+ XK_cedilla: 0x00b8, /* U+00B8 CEDILLA */
+ XK_onesuperior: 0x00b9, /* U+00B9 SUPERSCRIPT ONE */
+ XK_masculine: 0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */
+ XK_guillemotright: 0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */
+ XK_onequarter: 0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */
+ XK_onehalf: 0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */
+ XK_threequarters: 0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */
+ XK_questiondown: 0x00bf, /* U+00BF INVERTED QUESTION MARK */
+ XK_Agrave: 0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */
+ XK_Aacute: 0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */
+ XK_Acircumflex: 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */
+ XK_Atilde: 0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */
+ XK_Adiaeresis: 0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */
+ XK_Aring: 0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */
+ XK_AE: 0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */
+ XK_Ccedilla: 0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */
+ XK_Egrave: 0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */
+ XK_Eacute: 0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */
+ XK_Ecircumflex: 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */
+ XK_Ediaeresis: 0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */
+ XK_Igrave: 0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */
+ XK_Iacute: 0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */
+ XK_Icircumflex: 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */
+ XK_Idiaeresis: 0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */
+ XK_ETH: 0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */
+ XK_Eth: 0x00d0, /* deprecated */
+ XK_Ntilde: 0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */
+ XK_Ograve: 0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */
+ XK_Oacute: 0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */
+ XK_Ocircumflex: 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */
+ XK_Otilde: 0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */
+ XK_Odiaeresis: 0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */
+ XK_multiply: 0x00d7, /* U+00D7 MULTIPLICATION SIGN */
+ XK_Oslash: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */
+ XK_Ooblique: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */
+ XK_Ugrave: 0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */
+ XK_Uacute: 0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */
+ XK_Ucircumflex: 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */
+ XK_Udiaeresis: 0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */
+ XK_Yacute: 0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */
+ XK_THORN: 0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */
+ XK_Thorn: 0x00de, /* deprecated */
+ XK_ssharp: 0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */
+ XK_agrave: 0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */
+ XK_aacute: 0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
+ XK_acircumflex: 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */
+ XK_atilde: 0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */
+ XK_adiaeresis: 0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */
+ XK_aring: 0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */
+ XK_ae: 0x00e6, /* U+00E6 LATIN SMALL LETTER AE */
+ XK_ccedilla: 0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */
+ XK_egrave: 0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */
+ XK_eacute: 0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */
+ XK_ecircumflex: 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */
+ XK_ediaeresis: 0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */
+ XK_igrave: 0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */
+ XK_iacute: 0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */
+ XK_icircumflex: 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */
+ XK_idiaeresis: 0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */
+ XK_eth: 0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */
+ XK_ntilde: 0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */
+ XK_ograve: 0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */
+ XK_oacute: 0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */
+ XK_ocircumflex: 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */
+ XK_otilde: 0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */
+ XK_odiaeresis: 0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */
+ XK_division: 0x00f7, /* U+00F7 DIVISION SIGN */
+ XK_oslash: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */
+ XK_ooblique: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */
+ XK_ugrave: 0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */
+ XK_uacute: 0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */
+ XK_ucircumflex: 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */
+ XK_udiaeresis: 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */
+ XK_yacute: 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */
+ XK_thorn: 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */
+ XK_ydiaeresis: 0x00ff, /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */
+
+ /*
+ * Korean
+ * Byte 3 = 0x0e
+ */
+
+ XK_Hangul: 0xff31, /* Hangul start/stop(toggle) */
+ XK_Hangul_Hanja: 0xff34, /* Start Hangul->Hanja Conversion */
+ XK_Hangul_Jeonja: 0xff38, /* Jeonja mode */
+
+ /*
+ * XFree86 vendor specific keysyms.
+ *
+ * The XFree86 keysym range is 0x10080001 - 0x1008FFFF.
+ */
+
+ XF86XK_ModeLock: 0x1008FF01,
+ XF86XK_MonBrightnessUp: 0x1008FF02,
+ XF86XK_MonBrightnessDown: 0x1008FF03,
+ XF86XK_KbdLightOnOff: 0x1008FF04,
+ XF86XK_KbdBrightnessUp: 0x1008FF05,
+ XF86XK_KbdBrightnessDown: 0x1008FF06,
+ XF86XK_Standby: 0x1008FF10,
+ XF86XK_AudioLowerVolume: 0x1008FF11,
+ XF86XK_AudioMute: 0x1008FF12,
+ XF86XK_AudioRaiseVolume: 0x1008FF13,
+ XF86XK_AudioPlay: 0x1008FF14,
+ XF86XK_AudioStop: 0x1008FF15,
+ XF86XK_AudioPrev: 0x1008FF16,
+ XF86XK_AudioNext: 0x1008FF17,
+ XF86XK_HomePage: 0x1008FF18,
+ XF86XK_Mail: 0x1008FF19,
+ XF86XK_Start: 0x1008FF1A,
+ XF86XK_Search: 0x1008FF1B,
+ XF86XK_AudioRecord: 0x1008FF1C,
+ XF86XK_Calculator: 0x1008FF1D,
+ XF86XK_Memo: 0x1008FF1E,
+ XF86XK_ToDoList: 0x1008FF1F,
+ XF86XK_Calendar: 0x1008FF20,
+ XF86XK_PowerDown: 0x1008FF21,
+ XF86XK_ContrastAdjust: 0x1008FF22,
+ XF86XK_RockerUp: 0x1008FF23,
+ XF86XK_RockerDown: 0x1008FF24,
+ XF86XK_RockerEnter: 0x1008FF25,
+ XF86XK_Back: 0x1008FF26,
+ XF86XK_Forward: 0x1008FF27,
+ XF86XK_Stop: 0x1008FF28,
+ XF86XK_Refresh: 0x1008FF29,
+ XF86XK_PowerOff: 0x1008FF2A,
+ XF86XK_WakeUp: 0x1008FF2B,
+ XF86XK_Eject: 0x1008FF2C,
+ XF86XK_ScreenSaver: 0x1008FF2D,
+ XF86XK_WWW: 0x1008FF2E,
+ XF86XK_Sleep: 0x1008FF2F,
+ XF86XK_Favorites: 0x1008FF30,
+ XF86XK_AudioPause: 0x1008FF31,
+ XF86XK_AudioMedia: 0x1008FF32,
+ XF86XK_MyComputer: 0x1008FF33,
+ XF86XK_VendorHome: 0x1008FF34,
+ XF86XK_LightBulb: 0x1008FF35,
+ XF86XK_Shop: 0x1008FF36,
+ XF86XK_History: 0x1008FF37,
+ XF86XK_OpenURL: 0x1008FF38,
+ XF86XK_AddFavorite: 0x1008FF39,
+ XF86XK_HotLinks: 0x1008FF3A,
+ XF86XK_BrightnessAdjust: 0x1008FF3B,
+ XF86XK_Finance: 0x1008FF3C,
+ XF86XK_Community: 0x1008FF3D,
+ XF86XK_AudioRewind: 0x1008FF3E,
+ XF86XK_BackForward: 0x1008FF3F,
+ XF86XK_Launch0: 0x1008FF40,
+ XF86XK_Launch1: 0x1008FF41,
+ XF86XK_Launch2: 0x1008FF42,
+ XF86XK_Launch3: 0x1008FF43,
+ XF86XK_Launch4: 0x1008FF44,
+ XF86XK_Launch5: 0x1008FF45,
+ XF86XK_Launch6: 0x1008FF46,
+ XF86XK_Launch7: 0x1008FF47,
+ XF86XK_Launch8: 0x1008FF48,
+ XF86XK_Launch9: 0x1008FF49,
+ XF86XK_LaunchA: 0x1008FF4A,
+ XF86XK_LaunchB: 0x1008FF4B,
+ XF86XK_LaunchC: 0x1008FF4C,
+ XF86XK_LaunchD: 0x1008FF4D,
+ XF86XK_LaunchE: 0x1008FF4E,
+ XF86XK_LaunchF: 0x1008FF4F,
+ XF86XK_ApplicationLeft: 0x1008FF50,
+ XF86XK_ApplicationRight: 0x1008FF51,
+ XF86XK_Book: 0x1008FF52,
+ XF86XK_CD: 0x1008FF53,
+ XF86XK_Calculater: 0x1008FF54,
+ XF86XK_Clear: 0x1008FF55,
+ XF86XK_Close: 0x1008FF56,
+ XF86XK_Copy: 0x1008FF57,
+ XF86XK_Cut: 0x1008FF58,
+ XF86XK_Display: 0x1008FF59,
+ XF86XK_DOS: 0x1008FF5A,
+ XF86XK_Documents: 0x1008FF5B,
+ XF86XK_Excel: 0x1008FF5C,
+ XF86XK_Explorer: 0x1008FF5D,
+ XF86XK_Game: 0x1008FF5E,
+ XF86XK_Go: 0x1008FF5F,
+ XF86XK_iTouch: 0x1008FF60,
+ XF86XK_LogOff: 0x1008FF61,
+ XF86XK_Market: 0x1008FF62,
+ XF86XK_Meeting: 0x1008FF63,
+ XF86XK_MenuKB: 0x1008FF65,
+ XF86XK_MenuPB: 0x1008FF66,
+ XF86XK_MySites: 0x1008FF67,
+ XF86XK_New: 0x1008FF68,
+ XF86XK_News: 0x1008FF69,
+ XF86XK_OfficeHome: 0x1008FF6A,
+ XF86XK_Open: 0x1008FF6B,
+ XF86XK_Option: 0x1008FF6C,
+ XF86XK_Paste: 0x1008FF6D,
+ XF86XK_Phone: 0x1008FF6E,
+ XF86XK_Q: 0x1008FF70,
+ XF86XK_Reply: 0x1008FF72,
+ XF86XK_Reload: 0x1008FF73,
+ XF86XK_RotateWindows: 0x1008FF74,
+ XF86XK_RotationPB: 0x1008FF75,
+ XF86XK_RotationKB: 0x1008FF76,
+ XF86XK_Save: 0x1008FF77,
+ XF86XK_ScrollUp: 0x1008FF78,
+ XF86XK_ScrollDown: 0x1008FF79,
+ XF86XK_ScrollClick: 0x1008FF7A,
+ XF86XK_Send: 0x1008FF7B,
+ XF86XK_Spell: 0x1008FF7C,
+ XF86XK_SplitScreen: 0x1008FF7D,
+ XF86XK_Support: 0x1008FF7E,
+ XF86XK_TaskPane: 0x1008FF7F,
+ XF86XK_Terminal: 0x1008FF80,
+ XF86XK_Tools: 0x1008FF81,
+ XF86XK_Travel: 0x1008FF82,
+ XF86XK_UserPB: 0x1008FF84,
+ XF86XK_User1KB: 0x1008FF85,
+ XF86XK_User2KB: 0x1008FF86,
+ XF86XK_Video: 0x1008FF87,
+ XF86XK_WheelButton: 0x1008FF88,
+ XF86XK_Word: 0x1008FF89,
+ XF86XK_Xfer: 0x1008FF8A,
+ XF86XK_ZoomIn: 0x1008FF8B,
+ XF86XK_ZoomOut: 0x1008FF8C,
+ XF86XK_Away: 0x1008FF8D,
+ XF86XK_Messenger: 0x1008FF8E,
+ XF86XK_WebCam: 0x1008FF8F,
+ XF86XK_MailForward: 0x1008FF90,
+ XF86XK_Pictures: 0x1008FF91,
+ XF86XK_Music: 0x1008FF92,
+ XF86XK_Battery: 0x1008FF93,
+ XF86XK_Bluetooth: 0x1008FF94,
+ XF86XK_WLAN: 0x1008FF95,
+ XF86XK_UWB: 0x1008FF96,
+ XF86XK_AudioForward: 0x1008FF97,
+ XF86XK_AudioRepeat: 0x1008FF98,
+ XF86XK_AudioRandomPlay: 0x1008FF99,
+ XF86XK_Subtitle: 0x1008FF9A,
+ XF86XK_AudioCycleTrack: 0x1008FF9B,
+ XF86XK_CycleAngle: 0x1008FF9C,
+ XF86XK_FrameBack: 0x1008FF9D,
+ XF86XK_FrameForward: 0x1008FF9E,
+ XF86XK_Time: 0x1008FF9F,
+ XF86XK_Select: 0x1008FFA0,
+ XF86XK_View: 0x1008FFA1,
+ XF86XK_TopMenu: 0x1008FFA2,
+ XF86XK_Red: 0x1008FFA3,
+ XF86XK_Green: 0x1008FFA4,
+ XF86XK_Yellow: 0x1008FFA5,
+ XF86XK_Blue: 0x1008FFA6,
+ XF86XK_Suspend: 0x1008FFA7,
+ XF86XK_Hibernate: 0x1008FFA8,
+ XF86XK_TouchpadToggle: 0x1008FFA9,
+ XF86XK_TouchpadOn: 0x1008FFB0,
+ XF86XK_TouchpadOff: 0x1008FFB1,
+ XF86XK_AudioMicMute: 0x1008FFB2,
+ XF86XK_Switch_VT_1: 0x1008FE01,
+ XF86XK_Switch_VT_2: 0x1008FE02,
+ XF86XK_Switch_VT_3: 0x1008FE03,
+ XF86XK_Switch_VT_4: 0x1008FE04,
+ XF86XK_Switch_VT_5: 0x1008FE05,
+ XF86XK_Switch_VT_6: 0x1008FE06,
+ XF86XK_Switch_VT_7: 0x1008FE07,
+ XF86XK_Switch_VT_8: 0x1008FE08,
+ XF86XK_Switch_VT_9: 0x1008FE09,
+ XF86XK_Switch_VT_10: 0x1008FE0A,
+ XF86XK_Switch_VT_11: 0x1008FE0B,
+ XF86XK_Switch_VT_12: 0x1008FE0C,
+ XF86XK_Ungrab: 0x1008FE20,
+ XF86XK_ClearGrab: 0x1008FE21,
+ XF86XK_Next_VMode: 0x1008FE22,
+ XF86XK_Prev_VMode: 0x1008FE23,
+ XF86XK_LogWindowTree: 0x1008FE24,
+ XF86XK_LogGrabInfo: 0x1008FE25,
+};
pkg/web/noVNC/core/input/keysymdef.js
@@ -0,0 +1,688 @@
+/*
+ * Mapping from Unicode codepoints to X11/RFB keysyms
+ *
+ * This file was automatically generated from keysymdef.h
+ * DO NOT EDIT!
+ */
+
+/* Functions at the bottom */
+
+const codepoints = {
+ 0x0100: 0x03c0, // XK_Amacron
+ 0x0101: 0x03e0, // XK_amacron
+ 0x0102: 0x01c3, // XK_Abreve
+ 0x0103: 0x01e3, // XK_abreve
+ 0x0104: 0x01a1, // XK_Aogonek
+ 0x0105: 0x01b1, // XK_aogonek
+ 0x0106: 0x01c6, // XK_Cacute
+ 0x0107: 0x01e6, // XK_cacute
+ 0x0108: 0x02c6, // XK_Ccircumflex
+ 0x0109: 0x02e6, // XK_ccircumflex
+ 0x010a: 0x02c5, // XK_Cabovedot
+ 0x010b: 0x02e5, // XK_cabovedot
+ 0x010c: 0x01c8, // XK_Ccaron
+ 0x010d: 0x01e8, // XK_ccaron
+ 0x010e: 0x01cf, // XK_Dcaron
+ 0x010f: 0x01ef, // XK_dcaron
+ 0x0110: 0x01d0, // XK_Dstroke
+ 0x0111: 0x01f0, // XK_dstroke
+ 0x0112: 0x03aa, // XK_Emacron
+ 0x0113: 0x03ba, // XK_emacron
+ 0x0116: 0x03cc, // XK_Eabovedot
+ 0x0117: 0x03ec, // XK_eabovedot
+ 0x0118: 0x01ca, // XK_Eogonek
+ 0x0119: 0x01ea, // XK_eogonek
+ 0x011a: 0x01cc, // XK_Ecaron
+ 0x011b: 0x01ec, // XK_ecaron
+ 0x011c: 0x02d8, // XK_Gcircumflex
+ 0x011d: 0x02f8, // XK_gcircumflex
+ 0x011e: 0x02ab, // XK_Gbreve
+ 0x011f: 0x02bb, // XK_gbreve
+ 0x0120: 0x02d5, // XK_Gabovedot
+ 0x0121: 0x02f5, // XK_gabovedot
+ 0x0122: 0x03ab, // XK_Gcedilla
+ 0x0123: 0x03bb, // XK_gcedilla
+ 0x0124: 0x02a6, // XK_Hcircumflex
+ 0x0125: 0x02b6, // XK_hcircumflex
+ 0x0126: 0x02a1, // XK_Hstroke
+ 0x0127: 0x02b1, // XK_hstroke
+ 0x0128: 0x03a5, // XK_Itilde
+ 0x0129: 0x03b5, // XK_itilde
+ 0x012a: 0x03cf, // XK_Imacron
+ 0x012b: 0x03ef, // XK_imacron
+ 0x012e: 0x03c7, // XK_Iogonek
+ 0x012f: 0x03e7, // XK_iogonek
+ 0x0130: 0x02a9, // XK_Iabovedot
+ 0x0131: 0x02b9, // XK_idotless
+ 0x0134: 0x02ac, // XK_Jcircumflex
+ 0x0135: 0x02bc, // XK_jcircumflex
+ 0x0136: 0x03d3, // XK_Kcedilla
+ 0x0137: 0x03f3, // XK_kcedilla
+ 0x0138: 0x03a2, // XK_kra
+ 0x0139: 0x01c5, // XK_Lacute
+ 0x013a: 0x01e5, // XK_lacute
+ 0x013b: 0x03a6, // XK_Lcedilla
+ 0x013c: 0x03b6, // XK_lcedilla
+ 0x013d: 0x01a5, // XK_Lcaron
+ 0x013e: 0x01b5, // XK_lcaron
+ 0x0141: 0x01a3, // XK_Lstroke
+ 0x0142: 0x01b3, // XK_lstroke
+ 0x0143: 0x01d1, // XK_Nacute
+ 0x0144: 0x01f1, // XK_nacute
+ 0x0145: 0x03d1, // XK_Ncedilla
+ 0x0146: 0x03f1, // XK_ncedilla
+ 0x0147: 0x01d2, // XK_Ncaron
+ 0x0148: 0x01f2, // XK_ncaron
+ 0x014a: 0x03bd, // XK_ENG
+ 0x014b: 0x03bf, // XK_eng
+ 0x014c: 0x03d2, // XK_Omacron
+ 0x014d: 0x03f2, // XK_omacron
+ 0x0150: 0x01d5, // XK_Odoubleacute
+ 0x0151: 0x01f5, // XK_odoubleacute
+ 0x0152: 0x13bc, // XK_OE
+ 0x0153: 0x13bd, // XK_oe
+ 0x0154: 0x01c0, // XK_Racute
+ 0x0155: 0x01e0, // XK_racute
+ 0x0156: 0x03a3, // XK_Rcedilla
+ 0x0157: 0x03b3, // XK_rcedilla
+ 0x0158: 0x01d8, // XK_Rcaron
+ 0x0159: 0x01f8, // XK_rcaron
+ 0x015a: 0x01a6, // XK_Sacute
+ 0x015b: 0x01b6, // XK_sacute
+ 0x015c: 0x02de, // XK_Scircumflex
+ 0x015d: 0x02fe, // XK_scircumflex
+ 0x015e: 0x01aa, // XK_Scedilla
+ 0x015f: 0x01ba, // XK_scedilla
+ 0x0160: 0x01a9, // XK_Scaron
+ 0x0161: 0x01b9, // XK_scaron
+ 0x0162: 0x01de, // XK_Tcedilla
+ 0x0163: 0x01fe, // XK_tcedilla
+ 0x0164: 0x01ab, // XK_Tcaron
+ 0x0165: 0x01bb, // XK_tcaron
+ 0x0166: 0x03ac, // XK_Tslash
+ 0x0167: 0x03bc, // XK_tslash
+ 0x0168: 0x03dd, // XK_Utilde
+ 0x0169: 0x03fd, // XK_utilde
+ 0x016a: 0x03de, // XK_Umacron
+ 0x016b: 0x03fe, // XK_umacron
+ 0x016c: 0x02dd, // XK_Ubreve
+ 0x016d: 0x02fd, // XK_ubreve
+ 0x016e: 0x01d9, // XK_Uring
+ 0x016f: 0x01f9, // XK_uring
+ 0x0170: 0x01db, // XK_Udoubleacute
+ 0x0171: 0x01fb, // XK_udoubleacute
+ 0x0172: 0x03d9, // XK_Uogonek
+ 0x0173: 0x03f9, // XK_uogonek
+ 0x0178: 0x13be, // XK_Ydiaeresis
+ 0x0179: 0x01ac, // XK_Zacute
+ 0x017a: 0x01bc, // XK_zacute
+ 0x017b: 0x01af, // XK_Zabovedot
+ 0x017c: 0x01bf, // XK_zabovedot
+ 0x017d: 0x01ae, // XK_Zcaron
+ 0x017e: 0x01be, // XK_zcaron
+ 0x0192: 0x08f6, // XK_function
+ 0x01d2: 0x10001d1, // XK_Ocaron
+ 0x02c7: 0x01b7, // XK_caron
+ 0x02d8: 0x01a2, // XK_breve
+ 0x02d9: 0x01ff, // XK_abovedot
+ 0x02db: 0x01b2, // XK_ogonek
+ 0x02dd: 0x01bd, // XK_doubleacute
+ 0x0385: 0x07ae, // XK_Greek_accentdieresis
+ 0x0386: 0x07a1, // XK_Greek_ALPHAaccent
+ 0x0388: 0x07a2, // XK_Greek_EPSILONaccent
+ 0x0389: 0x07a3, // XK_Greek_ETAaccent
+ 0x038a: 0x07a4, // XK_Greek_IOTAaccent
+ 0x038c: 0x07a7, // XK_Greek_OMICRONaccent
+ 0x038e: 0x07a8, // XK_Greek_UPSILONaccent
+ 0x038f: 0x07ab, // XK_Greek_OMEGAaccent
+ 0x0390: 0x07b6, // XK_Greek_iotaaccentdieresis
+ 0x0391: 0x07c1, // XK_Greek_ALPHA
+ 0x0392: 0x07c2, // XK_Greek_BETA
+ 0x0393: 0x07c3, // XK_Greek_GAMMA
+ 0x0394: 0x07c4, // XK_Greek_DELTA
+ 0x0395: 0x07c5, // XK_Greek_EPSILON
+ 0x0396: 0x07c6, // XK_Greek_ZETA
+ 0x0397: 0x07c7, // XK_Greek_ETA
+ 0x0398: 0x07c8, // XK_Greek_THETA
+ 0x0399: 0x07c9, // XK_Greek_IOTA
+ 0x039a: 0x07ca, // XK_Greek_KAPPA
+ 0x039b: 0x07cb, // XK_Greek_LAMDA
+ 0x039c: 0x07cc, // XK_Greek_MU
+ 0x039d: 0x07cd, // XK_Greek_NU
+ 0x039e: 0x07ce, // XK_Greek_XI
+ 0x039f: 0x07cf, // XK_Greek_OMICRON
+ 0x03a0: 0x07d0, // XK_Greek_PI
+ 0x03a1: 0x07d1, // XK_Greek_RHO
+ 0x03a3: 0x07d2, // XK_Greek_SIGMA
+ 0x03a4: 0x07d4, // XK_Greek_TAU
+ 0x03a5: 0x07d5, // XK_Greek_UPSILON
+ 0x03a6: 0x07d6, // XK_Greek_PHI
+ 0x03a7: 0x07d7, // XK_Greek_CHI
+ 0x03a8: 0x07d8, // XK_Greek_PSI
+ 0x03a9: 0x07d9, // XK_Greek_OMEGA
+ 0x03aa: 0x07a5, // XK_Greek_IOTAdieresis
+ 0x03ab: 0x07a9, // XK_Greek_UPSILONdieresis
+ 0x03ac: 0x07b1, // XK_Greek_alphaaccent
+ 0x03ad: 0x07b2, // XK_Greek_epsilonaccent
+ 0x03ae: 0x07b3, // XK_Greek_etaaccent
+ 0x03af: 0x07b4, // XK_Greek_iotaaccent
+ 0x03b0: 0x07ba, // XK_Greek_upsilonaccentdieresis
+ 0x03b1: 0x07e1, // XK_Greek_alpha
+ 0x03b2: 0x07e2, // XK_Greek_beta
+ 0x03b3: 0x07e3, // XK_Greek_gamma
+ 0x03b4: 0x07e4, // XK_Greek_delta
+ 0x03b5: 0x07e5, // XK_Greek_epsilon
+ 0x03b6: 0x07e6, // XK_Greek_zeta
+ 0x03b7: 0x07e7, // XK_Greek_eta
+ 0x03b8: 0x07e8, // XK_Greek_theta
+ 0x03b9: 0x07e9, // XK_Greek_iota
+ 0x03ba: 0x07ea, // XK_Greek_kappa
+ 0x03bb: 0x07eb, // XK_Greek_lamda
+ 0x03bc: 0x07ec, // XK_Greek_mu
+ 0x03bd: 0x07ed, // XK_Greek_nu
+ 0x03be: 0x07ee, // XK_Greek_xi
+ 0x03bf: 0x07ef, // XK_Greek_omicron
+ 0x03c0: 0x07f0, // XK_Greek_pi
+ 0x03c1: 0x07f1, // XK_Greek_rho
+ 0x03c2: 0x07f3, // XK_Greek_finalsmallsigma
+ 0x03c3: 0x07f2, // XK_Greek_sigma
+ 0x03c4: 0x07f4, // XK_Greek_tau
+ 0x03c5: 0x07f5, // XK_Greek_upsilon
+ 0x03c6: 0x07f6, // XK_Greek_phi
+ 0x03c7: 0x07f7, // XK_Greek_chi
+ 0x03c8: 0x07f8, // XK_Greek_psi
+ 0x03c9: 0x07f9, // XK_Greek_omega
+ 0x03ca: 0x07b5, // XK_Greek_iotadieresis
+ 0x03cb: 0x07b9, // XK_Greek_upsilondieresis
+ 0x03cc: 0x07b7, // XK_Greek_omicronaccent
+ 0x03cd: 0x07b8, // XK_Greek_upsilonaccent
+ 0x03ce: 0x07bb, // XK_Greek_omegaaccent
+ 0x0401: 0x06b3, // XK_Cyrillic_IO
+ 0x0402: 0x06b1, // XK_Serbian_DJE
+ 0x0403: 0x06b2, // XK_Macedonia_GJE
+ 0x0404: 0x06b4, // XK_Ukrainian_IE
+ 0x0405: 0x06b5, // XK_Macedonia_DSE
+ 0x0406: 0x06b6, // XK_Ukrainian_I
+ 0x0407: 0x06b7, // XK_Ukrainian_YI
+ 0x0408: 0x06b8, // XK_Cyrillic_JE
+ 0x0409: 0x06b9, // XK_Cyrillic_LJE
+ 0x040a: 0x06ba, // XK_Cyrillic_NJE
+ 0x040b: 0x06bb, // XK_Serbian_TSHE
+ 0x040c: 0x06bc, // XK_Macedonia_KJE
+ 0x040e: 0x06be, // XK_Byelorussian_SHORTU
+ 0x040f: 0x06bf, // XK_Cyrillic_DZHE
+ 0x0410: 0x06e1, // XK_Cyrillic_A
+ 0x0411: 0x06e2, // XK_Cyrillic_BE
+ 0x0412: 0x06f7, // XK_Cyrillic_VE
+ 0x0413: 0x06e7, // XK_Cyrillic_GHE
+ 0x0414: 0x06e4, // XK_Cyrillic_DE
+ 0x0415: 0x06e5, // XK_Cyrillic_IE
+ 0x0416: 0x06f6, // XK_Cyrillic_ZHE
+ 0x0417: 0x06fa, // XK_Cyrillic_ZE
+ 0x0418: 0x06e9, // XK_Cyrillic_I
+ 0x0419: 0x06ea, // XK_Cyrillic_SHORTI
+ 0x041a: 0x06eb, // XK_Cyrillic_KA
+ 0x041b: 0x06ec, // XK_Cyrillic_EL
+ 0x041c: 0x06ed, // XK_Cyrillic_EM
+ 0x041d: 0x06ee, // XK_Cyrillic_EN
+ 0x041e: 0x06ef, // XK_Cyrillic_O
+ 0x041f: 0x06f0, // XK_Cyrillic_PE
+ 0x0420: 0x06f2, // XK_Cyrillic_ER
+ 0x0421: 0x06f3, // XK_Cyrillic_ES
+ 0x0422: 0x06f4, // XK_Cyrillic_TE
+ 0x0423: 0x06f5, // XK_Cyrillic_U
+ 0x0424: 0x06e6, // XK_Cyrillic_EF
+ 0x0425: 0x06e8, // XK_Cyrillic_HA
+ 0x0426: 0x06e3, // XK_Cyrillic_TSE
+ 0x0427: 0x06fe, // XK_Cyrillic_CHE
+ 0x0428: 0x06fb, // XK_Cyrillic_SHA
+ 0x0429: 0x06fd, // XK_Cyrillic_SHCHA
+ 0x042a: 0x06ff, // XK_Cyrillic_HARDSIGN
+ 0x042b: 0x06f9, // XK_Cyrillic_YERU
+ 0x042c: 0x06f8, // XK_Cyrillic_SOFTSIGN
+ 0x042d: 0x06fc, // XK_Cyrillic_E
+ 0x042e: 0x06e0, // XK_Cyrillic_YU
+ 0x042f: 0x06f1, // XK_Cyrillic_YA
+ 0x0430: 0x06c1, // XK_Cyrillic_a
+ 0x0431: 0x06c2, // XK_Cyrillic_be
+ 0x0432: 0x06d7, // XK_Cyrillic_ve
+ 0x0433: 0x06c7, // XK_Cyrillic_ghe
+ 0x0434: 0x06c4, // XK_Cyrillic_de
+ 0x0435: 0x06c5, // XK_Cyrillic_ie
+ 0x0436: 0x06d6, // XK_Cyrillic_zhe
+ 0x0437: 0x06da, // XK_Cyrillic_ze
+ 0x0438: 0x06c9, // XK_Cyrillic_i
+ 0x0439: 0x06ca, // XK_Cyrillic_shorti
+ 0x043a: 0x06cb, // XK_Cyrillic_ka
+ 0x043b: 0x06cc, // XK_Cyrillic_el
+ 0x043c: 0x06cd, // XK_Cyrillic_em
+ 0x043d: 0x06ce, // XK_Cyrillic_en
+ 0x043e: 0x06cf, // XK_Cyrillic_o
+ 0x043f: 0x06d0, // XK_Cyrillic_pe
+ 0x0440: 0x06d2, // XK_Cyrillic_er
+ 0x0441: 0x06d3, // XK_Cyrillic_es
+ 0x0442: 0x06d4, // XK_Cyrillic_te
+ 0x0443: 0x06d5, // XK_Cyrillic_u
+ 0x0444: 0x06c6, // XK_Cyrillic_ef
+ 0x0445: 0x06c8, // XK_Cyrillic_ha
+ 0x0446: 0x06c3, // XK_Cyrillic_tse
+ 0x0447: 0x06de, // XK_Cyrillic_che
+ 0x0448: 0x06db, // XK_Cyrillic_sha
+ 0x0449: 0x06dd, // XK_Cyrillic_shcha
+ 0x044a: 0x06df, // XK_Cyrillic_hardsign
+ 0x044b: 0x06d9, // XK_Cyrillic_yeru
+ 0x044c: 0x06d8, // XK_Cyrillic_softsign
+ 0x044d: 0x06dc, // XK_Cyrillic_e
+ 0x044e: 0x06c0, // XK_Cyrillic_yu
+ 0x044f: 0x06d1, // XK_Cyrillic_ya
+ 0x0451: 0x06a3, // XK_Cyrillic_io
+ 0x0452: 0x06a1, // XK_Serbian_dje
+ 0x0453: 0x06a2, // XK_Macedonia_gje
+ 0x0454: 0x06a4, // XK_Ukrainian_ie
+ 0x0455: 0x06a5, // XK_Macedonia_dse
+ 0x0456: 0x06a6, // XK_Ukrainian_i
+ 0x0457: 0x06a7, // XK_Ukrainian_yi
+ 0x0458: 0x06a8, // XK_Cyrillic_je
+ 0x0459: 0x06a9, // XK_Cyrillic_lje
+ 0x045a: 0x06aa, // XK_Cyrillic_nje
+ 0x045b: 0x06ab, // XK_Serbian_tshe
+ 0x045c: 0x06ac, // XK_Macedonia_kje
+ 0x045e: 0x06ae, // XK_Byelorussian_shortu
+ 0x045f: 0x06af, // XK_Cyrillic_dzhe
+ 0x0490: 0x06bd, // XK_Ukrainian_GHE_WITH_UPTURN
+ 0x0491: 0x06ad, // XK_Ukrainian_ghe_with_upturn
+ 0x05d0: 0x0ce0, // XK_hebrew_aleph
+ 0x05d1: 0x0ce1, // XK_hebrew_bet
+ 0x05d2: 0x0ce2, // XK_hebrew_gimel
+ 0x05d3: 0x0ce3, // XK_hebrew_dalet
+ 0x05d4: 0x0ce4, // XK_hebrew_he
+ 0x05d5: 0x0ce5, // XK_hebrew_waw
+ 0x05d6: 0x0ce6, // XK_hebrew_zain
+ 0x05d7: 0x0ce7, // XK_hebrew_chet
+ 0x05d8: 0x0ce8, // XK_hebrew_tet
+ 0x05d9: 0x0ce9, // XK_hebrew_yod
+ 0x05da: 0x0cea, // XK_hebrew_finalkaph
+ 0x05db: 0x0ceb, // XK_hebrew_kaph
+ 0x05dc: 0x0cec, // XK_hebrew_lamed
+ 0x05dd: 0x0ced, // XK_hebrew_finalmem
+ 0x05de: 0x0cee, // XK_hebrew_mem
+ 0x05df: 0x0cef, // XK_hebrew_finalnun
+ 0x05e0: 0x0cf0, // XK_hebrew_nun
+ 0x05e1: 0x0cf1, // XK_hebrew_samech
+ 0x05e2: 0x0cf2, // XK_hebrew_ayin
+ 0x05e3: 0x0cf3, // XK_hebrew_finalpe
+ 0x05e4: 0x0cf4, // XK_hebrew_pe
+ 0x05e5: 0x0cf5, // XK_hebrew_finalzade
+ 0x05e6: 0x0cf6, // XK_hebrew_zade
+ 0x05e7: 0x0cf7, // XK_hebrew_qoph
+ 0x05e8: 0x0cf8, // XK_hebrew_resh
+ 0x05e9: 0x0cf9, // XK_hebrew_shin
+ 0x05ea: 0x0cfa, // XK_hebrew_taw
+ 0x060c: 0x05ac, // XK_Arabic_comma
+ 0x061b: 0x05bb, // XK_Arabic_semicolon
+ 0x061f: 0x05bf, // XK_Arabic_question_mark
+ 0x0621: 0x05c1, // XK_Arabic_hamza
+ 0x0622: 0x05c2, // XK_Arabic_maddaonalef
+ 0x0623: 0x05c3, // XK_Arabic_hamzaonalef
+ 0x0624: 0x05c4, // XK_Arabic_hamzaonwaw
+ 0x0625: 0x05c5, // XK_Arabic_hamzaunderalef
+ 0x0626: 0x05c6, // XK_Arabic_hamzaonyeh
+ 0x0627: 0x05c7, // XK_Arabic_alef
+ 0x0628: 0x05c8, // XK_Arabic_beh
+ 0x0629: 0x05c9, // XK_Arabic_tehmarbuta
+ 0x062a: 0x05ca, // XK_Arabic_teh
+ 0x062b: 0x05cb, // XK_Arabic_theh
+ 0x062c: 0x05cc, // XK_Arabic_jeem
+ 0x062d: 0x05cd, // XK_Arabic_hah
+ 0x062e: 0x05ce, // XK_Arabic_khah
+ 0x062f: 0x05cf, // XK_Arabic_dal
+ 0x0630: 0x05d0, // XK_Arabic_thal
+ 0x0631: 0x05d1, // XK_Arabic_ra
+ 0x0632: 0x05d2, // XK_Arabic_zain
+ 0x0633: 0x05d3, // XK_Arabic_seen
+ 0x0634: 0x05d4, // XK_Arabic_sheen
+ 0x0635: 0x05d5, // XK_Arabic_sad
+ 0x0636: 0x05d6, // XK_Arabic_dad
+ 0x0637: 0x05d7, // XK_Arabic_tah
+ 0x0638: 0x05d8, // XK_Arabic_zah
+ 0x0639: 0x05d9, // XK_Arabic_ain
+ 0x063a: 0x05da, // XK_Arabic_ghain
+ 0x0640: 0x05e0, // XK_Arabic_tatweel
+ 0x0641: 0x05e1, // XK_Arabic_feh
+ 0x0642: 0x05e2, // XK_Arabic_qaf
+ 0x0643: 0x05e3, // XK_Arabic_kaf
+ 0x0644: 0x05e4, // XK_Arabic_lam
+ 0x0645: 0x05e5, // XK_Arabic_meem
+ 0x0646: 0x05e6, // XK_Arabic_noon
+ 0x0647: 0x05e7, // XK_Arabic_ha
+ 0x0648: 0x05e8, // XK_Arabic_waw
+ 0x0649: 0x05e9, // XK_Arabic_alefmaksura
+ 0x064a: 0x05ea, // XK_Arabic_yeh
+ 0x064b: 0x05eb, // XK_Arabic_fathatan
+ 0x064c: 0x05ec, // XK_Arabic_dammatan
+ 0x064d: 0x05ed, // XK_Arabic_kasratan
+ 0x064e: 0x05ee, // XK_Arabic_fatha
+ 0x064f: 0x05ef, // XK_Arabic_damma
+ 0x0650: 0x05f0, // XK_Arabic_kasra
+ 0x0651: 0x05f1, // XK_Arabic_shadda
+ 0x0652: 0x05f2, // XK_Arabic_sukun
+ 0x0e01: 0x0da1, // XK_Thai_kokai
+ 0x0e02: 0x0da2, // XK_Thai_khokhai
+ 0x0e03: 0x0da3, // XK_Thai_khokhuat
+ 0x0e04: 0x0da4, // XK_Thai_khokhwai
+ 0x0e05: 0x0da5, // XK_Thai_khokhon
+ 0x0e06: 0x0da6, // XK_Thai_khorakhang
+ 0x0e07: 0x0da7, // XK_Thai_ngongu
+ 0x0e08: 0x0da8, // XK_Thai_chochan
+ 0x0e09: 0x0da9, // XK_Thai_choching
+ 0x0e0a: 0x0daa, // XK_Thai_chochang
+ 0x0e0b: 0x0dab, // XK_Thai_soso
+ 0x0e0c: 0x0dac, // XK_Thai_chochoe
+ 0x0e0d: 0x0dad, // XK_Thai_yoying
+ 0x0e0e: 0x0dae, // XK_Thai_dochada
+ 0x0e0f: 0x0daf, // XK_Thai_topatak
+ 0x0e10: 0x0db0, // XK_Thai_thothan
+ 0x0e11: 0x0db1, // XK_Thai_thonangmontho
+ 0x0e12: 0x0db2, // XK_Thai_thophuthao
+ 0x0e13: 0x0db3, // XK_Thai_nonen
+ 0x0e14: 0x0db4, // XK_Thai_dodek
+ 0x0e15: 0x0db5, // XK_Thai_totao
+ 0x0e16: 0x0db6, // XK_Thai_thothung
+ 0x0e17: 0x0db7, // XK_Thai_thothahan
+ 0x0e18: 0x0db8, // XK_Thai_thothong
+ 0x0e19: 0x0db9, // XK_Thai_nonu
+ 0x0e1a: 0x0dba, // XK_Thai_bobaimai
+ 0x0e1b: 0x0dbb, // XK_Thai_popla
+ 0x0e1c: 0x0dbc, // XK_Thai_phophung
+ 0x0e1d: 0x0dbd, // XK_Thai_fofa
+ 0x0e1e: 0x0dbe, // XK_Thai_phophan
+ 0x0e1f: 0x0dbf, // XK_Thai_fofan
+ 0x0e20: 0x0dc0, // XK_Thai_phosamphao
+ 0x0e21: 0x0dc1, // XK_Thai_moma
+ 0x0e22: 0x0dc2, // XK_Thai_yoyak
+ 0x0e23: 0x0dc3, // XK_Thai_rorua
+ 0x0e24: 0x0dc4, // XK_Thai_ru
+ 0x0e25: 0x0dc5, // XK_Thai_loling
+ 0x0e26: 0x0dc6, // XK_Thai_lu
+ 0x0e27: 0x0dc7, // XK_Thai_wowaen
+ 0x0e28: 0x0dc8, // XK_Thai_sosala
+ 0x0e29: 0x0dc9, // XK_Thai_sorusi
+ 0x0e2a: 0x0dca, // XK_Thai_sosua
+ 0x0e2b: 0x0dcb, // XK_Thai_hohip
+ 0x0e2c: 0x0dcc, // XK_Thai_lochula
+ 0x0e2d: 0x0dcd, // XK_Thai_oang
+ 0x0e2e: 0x0dce, // XK_Thai_honokhuk
+ 0x0e2f: 0x0dcf, // XK_Thai_paiyannoi
+ 0x0e30: 0x0dd0, // XK_Thai_saraa
+ 0x0e31: 0x0dd1, // XK_Thai_maihanakat
+ 0x0e32: 0x0dd2, // XK_Thai_saraaa
+ 0x0e33: 0x0dd3, // XK_Thai_saraam
+ 0x0e34: 0x0dd4, // XK_Thai_sarai
+ 0x0e35: 0x0dd5, // XK_Thai_saraii
+ 0x0e36: 0x0dd6, // XK_Thai_saraue
+ 0x0e37: 0x0dd7, // XK_Thai_sarauee
+ 0x0e38: 0x0dd8, // XK_Thai_sarau
+ 0x0e39: 0x0dd9, // XK_Thai_sarauu
+ 0x0e3a: 0x0dda, // XK_Thai_phinthu
+ 0x0e3f: 0x0ddf, // XK_Thai_baht
+ 0x0e40: 0x0de0, // XK_Thai_sarae
+ 0x0e41: 0x0de1, // XK_Thai_saraae
+ 0x0e42: 0x0de2, // XK_Thai_sarao
+ 0x0e43: 0x0de3, // XK_Thai_saraaimaimuan
+ 0x0e44: 0x0de4, // XK_Thai_saraaimaimalai
+ 0x0e45: 0x0de5, // XK_Thai_lakkhangyao
+ 0x0e46: 0x0de6, // XK_Thai_maiyamok
+ 0x0e47: 0x0de7, // XK_Thai_maitaikhu
+ 0x0e48: 0x0de8, // XK_Thai_maiek
+ 0x0e49: 0x0de9, // XK_Thai_maitho
+ 0x0e4a: 0x0dea, // XK_Thai_maitri
+ 0x0e4b: 0x0deb, // XK_Thai_maichattawa
+ 0x0e4c: 0x0dec, // XK_Thai_thanthakhat
+ 0x0e4d: 0x0ded, // XK_Thai_nikhahit
+ 0x0e50: 0x0df0, // XK_Thai_leksun
+ 0x0e51: 0x0df1, // XK_Thai_leknung
+ 0x0e52: 0x0df2, // XK_Thai_leksong
+ 0x0e53: 0x0df3, // XK_Thai_leksam
+ 0x0e54: 0x0df4, // XK_Thai_leksi
+ 0x0e55: 0x0df5, // XK_Thai_lekha
+ 0x0e56: 0x0df6, // XK_Thai_lekhok
+ 0x0e57: 0x0df7, // XK_Thai_lekchet
+ 0x0e58: 0x0df8, // XK_Thai_lekpaet
+ 0x0e59: 0x0df9, // XK_Thai_lekkao
+ 0x2002: 0x0aa2, // XK_enspace
+ 0x2003: 0x0aa1, // XK_emspace
+ 0x2004: 0x0aa3, // XK_em3space
+ 0x2005: 0x0aa4, // XK_em4space
+ 0x2007: 0x0aa5, // XK_digitspace
+ 0x2008: 0x0aa6, // XK_punctspace
+ 0x2009: 0x0aa7, // XK_thinspace
+ 0x200a: 0x0aa8, // XK_hairspace
+ 0x2012: 0x0abb, // XK_figdash
+ 0x2013: 0x0aaa, // XK_endash
+ 0x2014: 0x0aa9, // XK_emdash
+ 0x2015: 0x07af, // XK_Greek_horizbar
+ 0x2017: 0x0cdf, // XK_hebrew_doublelowline
+ 0x2018: 0x0ad0, // XK_leftsinglequotemark
+ 0x2019: 0x0ad1, // XK_rightsinglequotemark
+ 0x201a: 0x0afd, // XK_singlelowquotemark
+ 0x201c: 0x0ad2, // XK_leftdoublequotemark
+ 0x201d: 0x0ad3, // XK_rightdoublequotemark
+ 0x201e: 0x0afe, // XK_doublelowquotemark
+ 0x2020: 0x0af1, // XK_dagger
+ 0x2021: 0x0af2, // XK_doubledagger
+ 0x2022: 0x0ae6, // XK_enfilledcircbullet
+ 0x2025: 0x0aaf, // XK_doubbaselinedot
+ 0x2026: 0x0aae, // XK_ellipsis
+ 0x2030: 0x0ad5, // XK_permille
+ 0x2032: 0x0ad6, // XK_minutes
+ 0x2033: 0x0ad7, // XK_seconds
+ 0x2038: 0x0afc, // XK_caret
+ 0x203e: 0x047e, // XK_overline
+ 0x20a9: 0x0eff, // XK_Korean_Won
+ 0x20ac: 0x20ac, // XK_EuroSign
+ 0x2105: 0x0ab8, // XK_careof
+ 0x2116: 0x06b0, // XK_numerosign
+ 0x2117: 0x0afb, // XK_phonographcopyright
+ 0x211e: 0x0ad4, // XK_prescription
+ 0x2122: 0x0ac9, // XK_trademark
+ 0x2153: 0x0ab0, // XK_onethird
+ 0x2154: 0x0ab1, // XK_twothirds
+ 0x2155: 0x0ab2, // XK_onefifth
+ 0x2156: 0x0ab3, // XK_twofifths
+ 0x2157: 0x0ab4, // XK_threefifths
+ 0x2158: 0x0ab5, // XK_fourfifths
+ 0x2159: 0x0ab6, // XK_onesixth
+ 0x215a: 0x0ab7, // XK_fivesixths
+ 0x215b: 0x0ac3, // XK_oneeighth
+ 0x215c: 0x0ac4, // XK_threeeighths
+ 0x215d: 0x0ac5, // XK_fiveeighths
+ 0x215e: 0x0ac6, // XK_seveneighths
+ 0x2190: 0x08fb, // XK_leftarrow
+ 0x2191: 0x08fc, // XK_uparrow
+ 0x2192: 0x08fd, // XK_rightarrow
+ 0x2193: 0x08fe, // XK_downarrow
+ 0x21d2: 0x08ce, // XK_implies
+ 0x21d4: 0x08cd, // XK_ifonlyif
+ 0x2202: 0x08ef, // XK_partialderivative
+ 0x2207: 0x08c5, // XK_nabla
+ 0x2218: 0x0bca, // XK_jot
+ 0x221a: 0x08d6, // XK_radical
+ 0x221d: 0x08c1, // XK_variation
+ 0x221e: 0x08c2, // XK_infinity
+ 0x2227: 0x08de, // XK_logicaland
+ 0x2228: 0x08df, // XK_logicalor
+ 0x2229: 0x08dc, // XK_intersection
+ 0x222a: 0x08dd, // XK_union
+ 0x222b: 0x08bf, // XK_integral
+ 0x2234: 0x08c0, // XK_therefore
+ 0x223c: 0x08c8, // XK_approximate
+ 0x2243: 0x08c9, // XK_similarequal
+ 0x2245: 0x1002248, // XK_approxeq
+ 0x2260: 0x08bd, // XK_notequal
+ 0x2261: 0x08cf, // XK_identical
+ 0x2264: 0x08bc, // XK_lessthanequal
+ 0x2265: 0x08be, // XK_greaterthanequal
+ 0x2282: 0x08da, // XK_includedin
+ 0x2283: 0x08db, // XK_includes
+ 0x22a2: 0x0bfc, // XK_righttack
+ 0x22a3: 0x0bdc, // XK_lefttack
+ 0x22a4: 0x0bc2, // XK_downtack
+ 0x22a5: 0x0bce, // XK_uptack
+ 0x2308: 0x0bd3, // XK_upstile
+ 0x230a: 0x0bc4, // XK_downstile
+ 0x2315: 0x0afa, // XK_telephonerecorder
+ 0x2320: 0x08a4, // XK_topintegral
+ 0x2321: 0x08a5, // XK_botintegral
+ 0x2395: 0x0bcc, // XK_quad
+ 0x239b: 0x08ab, // XK_topleftparens
+ 0x239d: 0x08ac, // XK_botleftparens
+ 0x239e: 0x08ad, // XK_toprightparens
+ 0x23a0: 0x08ae, // XK_botrightparens
+ 0x23a1: 0x08a7, // XK_topleftsqbracket
+ 0x23a3: 0x08a8, // XK_botleftsqbracket
+ 0x23a4: 0x08a9, // XK_toprightsqbracket
+ 0x23a6: 0x08aa, // XK_botrightsqbracket
+ 0x23a8: 0x08af, // XK_leftmiddlecurlybrace
+ 0x23ac: 0x08b0, // XK_rightmiddlecurlybrace
+ 0x23b7: 0x08a1, // XK_leftradical
+ 0x23ba: 0x09ef, // XK_horizlinescan1
+ 0x23bb: 0x09f0, // XK_horizlinescan3
+ 0x23bc: 0x09f2, // XK_horizlinescan7
+ 0x23bd: 0x09f3, // XK_horizlinescan9
+ 0x2409: 0x09e2, // XK_ht
+ 0x240a: 0x09e5, // XK_lf
+ 0x240b: 0x09e9, // XK_vt
+ 0x240c: 0x09e3, // XK_ff
+ 0x240d: 0x09e4, // XK_cr
+ 0x2423: 0x0aac, // XK_signifblank
+ 0x2424: 0x09e8, // XK_nl
+ 0x2500: 0x08a3, // XK_horizconnector
+ 0x2502: 0x08a6, // XK_vertconnector
+ 0x250c: 0x08a2, // XK_topleftradical
+ 0x2510: 0x09eb, // XK_uprightcorner
+ 0x2514: 0x09ed, // XK_lowleftcorner
+ 0x2518: 0x09ea, // XK_lowrightcorner
+ 0x251c: 0x09f4, // XK_leftt
+ 0x2524: 0x09f5, // XK_rightt
+ 0x252c: 0x09f7, // XK_topt
+ 0x2534: 0x09f6, // XK_bott
+ 0x253c: 0x09ee, // XK_crossinglines
+ 0x2592: 0x09e1, // XK_checkerboard
+ 0x25aa: 0x0ae7, // XK_enfilledsqbullet
+ 0x25ab: 0x0ae1, // XK_enopensquarebullet
+ 0x25ac: 0x0adb, // XK_filledrectbullet
+ 0x25ad: 0x0ae2, // XK_openrectbullet
+ 0x25ae: 0x0adf, // XK_emfilledrect
+ 0x25af: 0x0acf, // XK_emopenrectangle
+ 0x25b2: 0x0ae8, // XK_filledtribulletup
+ 0x25b3: 0x0ae3, // XK_opentribulletup
+ 0x25b6: 0x0add, // XK_filledrighttribullet
+ 0x25b7: 0x0acd, // XK_rightopentriangle
+ 0x25bc: 0x0ae9, // XK_filledtribulletdown
+ 0x25bd: 0x0ae4, // XK_opentribulletdown
+ 0x25c0: 0x0adc, // XK_filledlefttribullet
+ 0x25c1: 0x0acc, // XK_leftopentriangle
+ 0x25c6: 0x09e0, // XK_soliddiamond
+ 0x25cb: 0x0ace, // XK_emopencircle
+ 0x25cf: 0x0ade, // XK_emfilledcircle
+ 0x25e6: 0x0ae0, // XK_enopencircbullet
+ 0x2606: 0x0ae5, // XK_openstar
+ 0x260e: 0x0af9, // XK_telephone
+ 0x2613: 0x0aca, // XK_signaturemark
+ 0x261c: 0x0aea, // XK_leftpointer
+ 0x261e: 0x0aeb, // XK_rightpointer
+ 0x2640: 0x0af8, // XK_femalesymbol
+ 0x2642: 0x0af7, // XK_malesymbol
+ 0x2663: 0x0aec, // XK_club
+ 0x2665: 0x0aee, // XK_heart
+ 0x2666: 0x0aed, // XK_diamond
+ 0x266d: 0x0af6, // XK_musicalflat
+ 0x266f: 0x0af5, // XK_musicalsharp
+ 0x2713: 0x0af3, // XK_checkmark
+ 0x2717: 0x0af4, // XK_ballotcross
+ 0x271d: 0x0ad9, // XK_latincross
+ 0x2720: 0x0af0, // XK_maltesecross
+ 0x27e8: 0x0abc, // XK_leftanglebracket
+ 0x27e9: 0x0abe, // XK_rightanglebracket
+ 0x3001: 0x04a4, // XK_kana_comma
+ 0x3002: 0x04a1, // XK_kana_fullstop
+ 0x300c: 0x04a2, // XK_kana_openingbracket
+ 0x300d: 0x04a3, // XK_kana_closingbracket
+ 0x309b: 0x04de, // XK_voicedsound
+ 0x309c: 0x04df, // XK_semivoicedsound
+ 0x30a1: 0x04a7, // XK_kana_a
+ 0x30a2: 0x04b1, // XK_kana_A
+ 0x30a3: 0x04a8, // XK_kana_i
+ 0x30a4: 0x04b2, // XK_kana_I
+ 0x30a5: 0x04a9, // XK_kana_u
+ 0x30a6: 0x04b3, // XK_kana_U
+ 0x30a7: 0x04aa, // XK_kana_e
+ 0x30a8: 0x04b4, // XK_kana_E
+ 0x30a9: 0x04ab, // XK_kana_o
+ 0x30aa: 0x04b5, // XK_kana_O
+ 0x30ab: 0x04b6, // XK_kana_KA
+ 0x30ad: 0x04b7, // XK_kana_KI
+ 0x30af: 0x04b8, // XK_kana_KU
+ 0x30b1: 0x04b9, // XK_kana_KE
+ 0x30b3: 0x04ba, // XK_kana_KO
+ 0x30b5: 0x04bb, // XK_kana_SA
+ 0x30b7: 0x04bc, // XK_kana_SHI
+ 0x30b9: 0x04bd, // XK_kana_SU
+ 0x30bb: 0x04be, // XK_kana_SE
+ 0x30bd: 0x04bf, // XK_kana_SO
+ 0x30bf: 0x04c0, // XK_kana_TA
+ 0x30c1: 0x04c1, // XK_kana_CHI
+ 0x30c3: 0x04af, // XK_kana_tsu
+ 0x30c4: 0x04c2, // XK_kana_TSU
+ 0x30c6: 0x04c3, // XK_kana_TE
+ 0x30c8: 0x04c4, // XK_kana_TO
+ 0x30ca: 0x04c5, // XK_kana_NA
+ 0x30cb: 0x04c6, // XK_kana_NI
+ 0x30cc: 0x04c7, // XK_kana_NU
+ 0x30cd: 0x04c8, // XK_kana_NE
+ 0x30ce: 0x04c9, // XK_kana_NO
+ 0x30cf: 0x04ca, // XK_kana_HA
+ 0x30d2: 0x04cb, // XK_kana_HI
+ 0x30d5: 0x04cc, // XK_kana_FU
+ 0x30d8: 0x04cd, // XK_kana_HE
+ 0x30db: 0x04ce, // XK_kana_HO
+ 0x30de: 0x04cf, // XK_kana_MA
+ 0x30df: 0x04d0, // XK_kana_MI
+ 0x30e0: 0x04d1, // XK_kana_MU
+ 0x30e1: 0x04d2, // XK_kana_ME
+ 0x30e2: 0x04d3, // XK_kana_MO
+ 0x30e3: 0x04ac, // XK_kana_ya
+ 0x30e4: 0x04d4, // XK_kana_YA
+ 0x30e5: 0x04ad, // XK_kana_yu
+ 0x30e6: 0x04d5, // XK_kana_YU
+ 0x30e7: 0x04ae, // XK_kana_yo
+ 0x30e8: 0x04d6, // XK_kana_YO
+ 0x30e9: 0x04d7, // XK_kana_RA
+ 0x30ea: 0x04d8, // XK_kana_RI
+ 0x30eb: 0x04d9, // XK_kana_RU
+ 0x30ec: 0x04da, // XK_kana_RE
+ 0x30ed: 0x04db, // XK_kana_RO
+ 0x30ef: 0x04dc, // XK_kana_WA
+ 0x30f2: 0x04a6, // XK_kana_WO
+ 0x30f3: 0x04dd, // XK_kana_N
+ 0x30fb: 0x04a5, // XK_kana_conjunctive
+ 0x30fc: 0x04b0, // XK_prolongedsound
+};
+
+export default {
+ lookup(u) {
+ // Latin-1 is one-to-one mapping
+ if ((u >= 0x20) && (u <= 0xff)) {
+ return u;
+ }
+
+ // Lookup table (fairly random)
+ const keysym = codepoints[u];
+ if (keysym !== undefined) {
+ return keysym;
+ }
+
+ // General mapping as final fallback
+ return 0x01000000 | u;
+ },
+};
pkg/web/noVNC/core/input/util.js
@@ -0,0 +1,191 @@
+import KeyTable from "./keysym.js";
+import keysyms from "./keysymdef.js";
+import vkeys from "./vkeys.js";
+import fixedkeys from "./fixedkeys.js";
+import DOMKeyTable from "./domkeytable.js";
+import * as browser from "../util/browser.js";
+
+// Get 'KeyboardEvent.code', handling legacy browsers
+export function getKeycode(evt) {
+ // Are we getting proper key identifiers?
+ // (unfortunately Firefox and Chrome are crappy here and gives
+ // us an empty string on some platforms, rather than leaving it
+ // undefined)
+ if (evt.code) {
+ // Mozilla isn't fully in sync with the spec yet
+ switch (evt.code) {
+ case 'OSLeft': return 'MetaLeft';
+ case 'OSRight': return 'MetaRight';
+ }
+
+ return evt.code;
+ }
+
+ // The de-facto standard is to use Windows Virtual-Key codes
+ // in the 'keyCode' field for non-printable characters
+ if (evt.keyCode in vkeys) {
+ let code = vkeys[evt.keyCode];
+
+ // macOS has messed up this code for some reason
+ if (browser.isMac() && (code === 'ContextMenu')) {
+ code = 'MetaRight';
+ }
+
+ // The keyCode doesn't distinguish between left and right
+ // for the standard modifiers
+ if (evt.location === 2) {
+ switch (code) {
+ case 'ShiftLeft': return 'ShiftRight';
+ case 'ControlLeft': return 'ControlRight';
+ case 'AltLeft': return 'AltRight';
+ }
+ }
+
+ // Nor a bunch of the numpad keys
+ if (evt.location === 3) {
+ switch (code) {
+ case 'Delete': return 'NumpadDecimal';
+ case 'Insert': return 'Numpad0';
+ case 'End': return 'Numpad1';
+ case 'ArrowDown': return 'Numpad2';
+ case 'PageDown': return 'Numpad3';
+ case 'ArrowLeft': return 'Numpad4';
+ case 'ArrowRight': return 'Numpad6';
+ case 'Home': return 'Numpad7';
+ case 'ArrowUp': return 'Numpad8';
+ case 'PageUp': return 'Numpad9';
+ case 'Enter': return 'NumpadEnter';
+ }
+ }
+
+ return code;
+ }
+
+ return 'Unidentified';
+}
+
+// Get 'KeyboardEvent.key', handling legacy browsers
+export function getKey(evt) {
+ // Are we getting a proper key value?
+ if ((evt.key !== undefined) && (evt.key !== 'Unidentified')) {
+ // Mozilla isn't fully in sync with the spec yet
+ switch (evt.key) {
+ case 'OS': return 'Meta';
+ case 'LaunchMyComputer': return 'LaunchApplication1';
+ case 'LaunchCalculator': return 'LaunchApplication2';
+ }
+
+ // iOS leaks some OS names
+ switch (evt.key) {
+ case 'UIKeyInputUpArrow': return 'ArrowUp';
+ case 'UIKeyInputDownArrow': return 'ArrowDown';
+ case 'UIKeyInputLeftArrow': return 'ArrowLeft';
+ case 'UIKeyInputRightArrow': return 'ArrowRight';
+ case 'UIKeyInputEscape': return 'Escape';
+ }
+
+ // Broken behaviour in Chrome
+ if ((evt.key === '\x00') && (evt.code === 'NumpadDecimal')) {
+ return 'Delete';
+ }
+
+ return evt.key;
+ }
+
+ // Try to deduce it based on the physical key
+ const code = getKeycode(evt);
+ if (code in fixedkeys) {
+ return fixedkeys[code];
+ }
+
+ // If that failed, then see if we have a printable character
+ if (evt.charCode) {
+ return String.fromCharCode(evt.charCode);
+ }
+
+ // At this point we have nothing left to go on
+ return 'Unidentified';
+}
+
+// Get the most reliable keysym value we can get from a key event
+export function getKeysym(evt) {
+ const key = getKey(evt);
+
+ if (key === 'Unidentified') {
+ return null;
+ }
+
+ // First look up special keys
+ if (key in DOMKeyTable) {
+ let location = evt.location;
+
+ // Safari screws up location for the right cmd key
+ if ((key === 'Meta') && (location === 0)) {
+ location = 2;
+ }
+
+ // And for Clear
+ if ((key === 'Clear') && (location === 3)) {
+ let code = getKeycode(evt);
+ if (code === 'NumLock') {
+ location = 0;
+ }
+ }
+
+ if ((location === undefined) || (location > 3)) {
+ 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;
+ }
+ }
+
+ // Windows sends alternating symbols for some keys when using a
+ // Japanese layout. We have no way of synchronising with the IM
+ // running on the remote system, so we send some combined keysym
+ // instead and hope for the best.
+ if (browser.isWindows()) {
+ switch (key) {
+ case 'Zenkaku':
+ case 'Hankaku':
+ return KeyTable.XK_Zenkaku_Hankaku;
+ case 'Romaji':
+ case 'KanaMode':
+ return KeyTable.XK_Romaji;
+ }
+ }
+
+ return DOMKeyTable[key][location];
+ }
+
+ // Now we need to look at the Unicode symbol instead
+
+ // Special key? (FIXME: Should have been caught earlier)
+ if (key.length !== 1) {
+ return null;
+ }
+
+ const codepoint = key.charCodeAt();
+ if (codepoint) {
+ return keysyms.lookup(codepoint);
+ }
+
+ return null;
+}
pkg/web/noVNC/core/input/vkeys.js
@@ -0,0 +1,116 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2018 The noVNC authors
+ * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
+ */
+
+/*
+ * Mapping between Microsoftยฎ Windowsยฎ Virtual-Key codes and
+ * HTML key codes.
+ */
+
+export default {
+ 0x08: 'Backspace',
+ 0x09: 'Tab',
+ 0x0a: 'NumpadClear',
+ 0x0d: 'Enter',
+ 0x10: 'ShiftLeft',
+ 0x11: 'ControlLeft',
+ 0x12: 'AltLeft',
+ 0x13: 'Pause',
+ 0x14: 'CapsLock',
+ 0x15: 'Lang1',
+ 0x19: 'Lang2',
+ 0x1b: 'Escape',
+ 0x1c: 'Convert',
+ 0x1d: 'NonConvert',
+ 0x20: 'Space',
+ 0x21: 'PageUp',
+ 0x22: 'PageDown',
+ 0x23: 'End',
+ 0x24: 'Home',
+ 0x25: 'ArrowLeft',
+ 0x26: 'ArrowUp',
+ 0x27: 'ArrowRight',
+ 0x28: 'ArrowDown',
+ 0x29: 'Select',
+ 0x2c: 'PrintScreen',
+ 0x2d: 'Insert',
+ 0x2e: 'Delete',
+ 0x2f: 'Help',
+ 0x30: 'Digit0',
+ 0x31: 'Digit1',
+ 0x32: 'Digit2',
+ 0x33: 'Digit3',
+ 0x34: 'Digit4',
+ 0x35: 'Digit5',
+ 0x36: 'Digit6',
+ 0x37: 'Digit7',
+ 0x38: 'Digit8',
+ 0x39: 'Digit9',
+ 0x5b: 'MetaLeft',
+ 0x5c: 'MetaRight',
+ 0x5d: 'ContextMenu',
+ 0x5f: 'Sleep',
+ 0x60: 'Numpad0',
+ 0x61: 'Numpad1',
+ 0x62: 'Numpad2',
+ 0x63: 'Numpad3',
+ 0x64: 'Numpad4',
+ 0x65: 'Numpad5',
+ 0x66: 'Numpad6',
+ 0x67: 'Numpad7',
+ 0x68: 'Numpad8',
+ 0x69: 'Numpad9',
+ 0x6a: 'NumpadMultiply',
+ 0x6b: 'NumpadAdd',
+ 0x6c: 'NumpadDecimal',
+ 0x6d: 'NumpadSubtract',
+ 0x6e: 'NumpadDecimal', // Duplicate, because buggy on Windows
+ 0x6f: 'NumpadDivide',
+ 0x70: 'F1',
+ 0x71: 'F2',
+ 0x72: 'F3',
+ 0x73: 'F4',
+ 0x74: 'F5',
+ 0x75: 'F6',
+ 0x76: 'F7',
+ 0x77: 'F8',
+ 0x78: 'F9',
+ 0x79: 'F10',
+ 0x7a: 'F11',
+ 0x7b: 'F12',
+ 0x7c: 'F13',
+ 0x7d: 'F14',
+ 0x7e: 'F15',
+ 0x7f: 'F16',
+ 0x80: 'F17',
+ 0x81: 'F18',
+ 0x82: 'F19',
+ 0x83: 'F20',
+ 0x84: 'F21',
+ 0x85: 'F22',
+ 0x86: 'F23',
+ 0x87: 'F24',
+ 0x90: 'NumLock',
+ 0x91: 'ScrollLock',
+ 0xa6: 'BrowserBack',
+ 0xa7: 'BrowserForward',
+ 0xa8: 'BrowserRefresh',
+ 0xa9: 'BrowserStop',
+ 0xaa: 'BrowserSearch',
+ 0xab: 'BrowserFavorites',
+ 0xac: 'BrowserHome',
+ 0xad: 'AudioVolumeMute',
+ 0xae: 'AudioVolumeDown',
+ 0xaf: 'AudioVolumeUp',
+ 0xb0: 'MediaTrackNext',
+ 0xb1: 'MediaTrackPrevious',
+ 0xb2: 'MediaStop',
+ 0xb3: 'MediaPlayPause',
+ 0xb4: 'LaunchMail',
+ 0xb5: 'MediaSelect',
+ 0xb6: 'LaunchApp1',
+ 0xb7: 'LaunchApp2',
+ 0xe1: 'AltRight', // Only when it is AltGraph
+};
pkg/web/noVNC/core/input/xtscancodes.js
@@ -0,0 +1,173 @@
+/*
+ * This file is auto-generated from keymaps.csv
+ * Database checksum sha256(76d68c10e97d37fe2ea459e210125ae41796253fb217e900bf2983ade13a7920)
+ * To re-generate, run:
+ * keymap-gen code-map --lang=js keymaps.csv html atset1
+*/
+export default {
+ "Again": 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */
+ "AltLeft": 0x38, /* html:AltLeft (AltLeft) -> linux:56 (KEY_LEFTALT) -> atset1:56 */
+ "AltRight": 0xe038, /* html:AltRight (AltRight) -> linux:100 (KEY_RIGHTALT) -> atset1:57400 */
+ "ArrowDown": 0xe050, /* html:ArrowDown (ArrowDown) -> linux:108 (KEY_DOWN) -> atset1:57424 */
+ "ArrowLeft": 0xe04b, /* html:ArrowLeft (ArrowLeft) -> linux:105 (KEY_LEFT) -> atset1:57419 */
+ "ArrowRight": 0xe04d, /* html:ArrowRight (ArrowRight) -> linux:106 (KEY_RIGHT) -> atset1:57421 */
+ "ArrowUp": 0xe048, /* html:ArrowUp (ArrowUp) -> linux:103 (KEY_UP) -> atset1:57416 */
+ "AudioVolumeDown": 0xe02e, /* html:AudioVolumeDown (AudioVolumeDown) -> linux:114 (KEY_VOLUMEDOWN) -> atset1:57390 */
+ "AudioVolumeMute": 0xe020, /* html:AudioVolumeMute (AudioVolumeMute) -> linux:113 (KEY_MUTE) -> atset1:57376 */
+ "AudioVolumeUp": 0xe030, /* html:AudioVolumeUp (AudioVolumeUp) -> linux:115 (KEY_VOLUMEUP) -> atset1:57392 */
+ "Backquote": 0x29, /* html:Backquote (Backquote) -> linux:41 (KEY_GRAVE) -> atset1:41 */
+ "Backslash": 0x2b, /* html:Backslash (Backslash) -> linux:43 (KEY_BACKSLASH) -> atset1:43 */
+ "Backspace": 0xe, /* html:Backspace (Backspace) -> linux:14 (KEY_BACKSPACE) -> atset1:14 */
+ "BracketLeft": 0x1a, /* html:BracketLeft (BracketLeft) -> linux:26 (KEY_LEFTBRACE) -> atset1:26 */
+ "BracketRight": 0x1b, /* html:BracketRight (BracketRight) -> linux:27 (KEY_RIGHTBRACE) -> atset1:27 */
+ "BrowserBack": 0xe06a, /* html:BrowserBack (BrowserBack) -> linux:158 (KEY_BACK) -> atset1:57450 */
+ "BrowserFavorites": 0xe066, /* html:BrowserFavorites (BrowserFavorites) -> linux:156 (KEY_BOOKMARKS) -> atset1:57446 */
+ "BrowserForward": 0xe069, /* html:BrowserForward (BrowserForward) -> linux:159 (KEY_FORWARD) -> atset1:57449 */
+ "BrowserHome": 0xe032, /* html:BrowserHome (BrowserHome) -> linux:172 (KEY_HOMEPAGE) -> atset1:57394 */
+ "BrowserRefresh": 0xe067, /* html:BrowserRefresh (BrowserRefresh) -> linux:173 (KEY_REFRESH) -> atset1:57447 */
+ "BrowserSearch": 0xe065, /* html:BrowserSearch (BrowserSearch) -> linux:217 (KEY_SEARCH) -> atset1:57445 */
+ "BrowserStop": 0xe068, /* html:BrowserStop (BrowserStop) -> linux:128 (KEY_STOP) -> atset1:57448 */
+ "CapsLock": 0x3a, /* html:CapsLock (CapsLock) -> linux:58 (KEY_CAPSLOCK) -> atset1:58 */
+ "Comma": 0x33, /* html:Comma (Comma) -> linux:51 (KEY_COMMA) -> atset1:51 */
+ "ContextMenu": 0xe05d, /* html:ContextMenu (ContextMenu) -> linux:127 (KEY_COMPOSE) -> atset1:57437 */
+ "ControlLeft": 0x1d, /* html:ControlLeft (ControlLeft) -> linux:29 (KEY_LEFTCTRL) -> atset1:29 */
+ "ControlRight": 0xe01d, /* html:ControlRight (ControlRight) -> linux:97 (KEY_RIGHTCTRL) -> atset1:57373 */
+ "Convert": 0x79, /* html:Convert (Convert) -> linux:92 (KEY_HENKAN) -> atset1:121 */
+ "Copy": 0xe078, /* html:Copy (Copy) -> linux:133 (KEY_COPY) -> atset1:57464 */
+ "Cut": 0xe03c, /* html:Cut (Cut) -> linux:137 (KEY_CUT) -> atset1:57404 */
+ "Delete": 0xe053, /* html:Delete (Delete) -> linux:111 (KEY_DELETE) -> atset1:57427 */
+ "Digit0": 0xb, /* html:Digit0 (Digit0) -> linux:11 (KEY_0) -> atset1:11 */
+ "Digit1": 0x2, /* html:Digit1 (Digit1) -> linux:2 (KEY_1) -> atset1:2 */
+ "Digit2": 0x3, /* html:Digit2 (Digit2) -> linux:3 (KEY_2) -> atset1:3 */
+ "Digit3": 0x4, /* html:Digit3 (Digit3) -> linux:4 (KEY_3) -> atset1:4 */
+ "Digit4": 0x5, /* html:Digit4 (Digit4) -> linux:5 (KEY_4) -> atset1:5 */
+ "Digit5": 0x6, /* html:Digit5 (Digit5) -> linux:6 (KEY_5) -> atset1:6 */
+ "Digit6": 0x7, /* html:Digit6 (Digit6) -> linux:7 (KEY_6) -> atset1:7 */
+ "Digit7": 0x8, /* html:Digit7 (Digit7) -> linux:8 (KEY_7) -> atset1:8 */
+ "Digit8": 0x9, /* html:Digit8 (Digit8) -> linux:9 (KEY_8) -> atset1:9 */
+ "Digit9": 0xa, /* html:Digit9 (Digit9) -> linux:10 (KEY_9) -> atset1:10 */
+ "Eject": 0xe07d, /* html:Eject (Eject) -> linux:162 (KEY_EJECTCLOSECD) -> atset1:57469 */
+ "End": 0xe04f, /* html:End (End) -> linux:107 (KEY_END) -> atset1:57423 */
+ "Enter": 0x1c, /* html:Enter (Enter) -> linux:28 (KEY_ENTER) -> atset1:28 */
+ "Equal": 0xd, /* html:Equal (Equal) -> linux:13 (KEY_EQUAL) -> atset1:13 */
+ "Escape": 0x1, /* html:Escape (Escape) -> linux:1 (KEY_ESC) -> atset1:1 */
+ "F1": 0x3b, /* html:F1 (F1) -> linux:59 (KEY_F1) -> atset1:59 */
+ "F10": 0x44, /* html:F10 (F10) -> linux:68 (KEY_F10) -> atset1:68 */
+ "F11": 0x57, /* html:F11 (F11) -> linux:87 (KEY_F11) -> atset1:87 */
+ "F12": 0x58, /* html:F12 (F12) -> linux:88 (KEY_F12) -> atset1:88 */
+ "F13": 0x5d, /* html:F13 (F13) -> linux:183 (KEY_F13) -> atset1:93 */
+ "F14": 0x5e, /* html:F14 (F14) -> linux:184 (KEY_F14) -> atset1:94 */
+ "F15": 0x5f, /* html:F15 (F15) -> linux:185 (KEY_F15) -> atset1:95 */
+ "F16": 0x55, /* html:F16 (F16) -> linux:186 (KEY_F16) -> atset1:85 */
+ "F17": 0xe003, /* html:F17 (F17) -> linux:187 (KEY_F17) -> atset1:57347 */
+ "F18": 0xe077, /* html:F18 (F18) -> linux:188 (KEY_F18) -> atset1:57463 */
+ "F19": 0xe004, /* html:F19 (F19) -> linux:189 (KEY_F19) -> atset1:57348 */
+ "F2": 0x3c, /* html:F2 (F2) -> linux:60 (KEY_F2) -> atset1:60 */
+ "F20": 0x5a, /* html:F20 (F20) -> linux:190 (KEY_F20) -> atset1:90 */
+ "F21": 0x74, /* html:F21 (F21) -> linux:191 (KEY_F21) -> atset1:116 */
+ "F22": 0xe079, /* html:F22 (F22) -> linux:192 (KEY_F22) -> atset1:57465 */
+ "F23": 0x6d, /* html:F23 (F23) -> linux:193 (KEY_F23) -> atset1:109 */
+ "F24": 0x6f, /* html:F24 (F24) -> linux:194 (KEY_F24) -> atset1:111 */
+ "F3": 0x3d, /* html:F3 (F3) -> linux:61 (KEY_F3) -> atset1:61 */
+ "F4": 0x3e, /* html:F4 (F4) -> linux:62 (KEY_F4) -> atset1:62 */
+ "F5": 0x3f, /* html:F5 (F5) -> linux:63 (KEY_F5) -> atset1:63 */
+ "F6": 0x40, /* html:F6 (F6) -> linux:64 (KEY_F6) -> atset1:64 */
+ "F7": 0x41, /* html:F7 (F7) -> linux:65 (KEY_F7) -> atset1:65 */
+ "F8": 0x42, /* html:F8 (F8) -> linux:66 (KEY_F8) -> atset1:66 */
+ "F9": 0x43, /* html:F9 (F9) -> linux:67 (KEY_F9) -> atset1:67 */
+ "Find": 0xe041, /* html:Find (Find) -> linux:136 (KEY_FIND) -> atset1:57409 */
+ "Help": 0xe075, /* html:Help (Help) -> linux:138 (KEY_HELP) -> atset1:57461 */
+ "Hiragana": 0x77, /* html:Hiragana (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */
+ "Home": 0xe047, /* html:Home (Home) -> linux:102 (KEY_HOME) -> atset1:57415 */
+ "Insert": 0xe052, /* html:Insert (Insert) -> linux:110 (KEY_INSERT) -> atset1:57426 */
+ "IntlBackslash": 0x56, /* html:IntlBackslash (IntlBackslash) -> linux:86 (KEY_102ND) -> atset1:86 */
+ "IntlRo": 0x73, /* html:IntlRo (IntlRo) -> linux:89 (KEY_RO) -> atset1:115 */
+ "IntlYen": 0x7d, /* html:IntlYen (IntlYen) -> linux:124 (KEY_YEN) -> atset1:125 */
+ "KanaMode": 0x70, /* html:KanaMode (KanaMode) -> linux:93 (KEY_KATAKANAHIRAGANA) -> atset1:112 */
+ "Katakana": 0x78, /* html:Katakana (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */
+ "KeyA": 0x1e, /* html:KeyA (KeyA) -> linux:30 (KEY_A) -> atset1:30 */
+ "KeyB": 0x30, /* html:KeyB (KeyB) -> linux:48 (KEY_B) -> atset1:48 */
+ "KeyC": 0x2e, /* html:KeyC (KeyC) -> linux:46 (KEY_C) -> atset1:46 */
+ "KeyD": 0x20, /* html:KeyD (KeyD) -> linux:32 (KEY_D) -> atset1:32 */
+ "KeyE": 0x12, /* html:KeyE (KeyE) -> linux:18 (KEY_E) -> atset1:18 */
+ "KeyF": 0x21, /* html:KeyF (KeyF) -> linux:33 (KEY_F) -> atset1:33 */
+ "KeyG": 0x22, /* html:KeyG (KeyG) -> linux:34 (KEY_G) -> atset1:34 */
+ "KeyH": 0x23, /* html:KeyH (KeyH) -> linux:35 (KEY_H) -> atset1:35 */
+ "KeyI": 0x17, /* html:KeyI (KeyI) -> linux:23 (KEY_I) -> atset1:23 */
+ "KeyJ": 0x24, /* html:KeyJ (KeyJ) -> linux:36 (KEY_J) -> atset1:36 */
+ "KeyK": 0x25, /* html:KeyK (KeyK) -> linux:37 (KEY_K) -> atset1:37 */
+ "KeyL": 0x26, /* html:KeyL (KeyL) -> linux:38 (KEY_L) -> atset1:38 */
+ "KeyM": 0x32, /* html:KeyM (KeyM) -> linux:50 (KEY_M) -> atset1:50 */
+ "KeyN": 0x31, /* html:KeyN (KeyN) -> linux:49 (KEY_N) -> atset1:49 */
+ "KeyO": 0x18, /* html:KeyO (KeyO) -> linux:24 (KEY_O) -> atset1:24 */
+ "KeyP": 0x19, /* html:KeyP (KeyP) -> linux:25 (KEY_P) -> atset1:25 */
+ "KeyQ": 0x10, /* html:KeyQ (KeyQ) -> linux:16 (KEY_Q) -> atset1:16 */
+ "KeyR": 0x13, /* html:KeyR (KeyR) -> linux:19 (KEY_R) -> atset1:19 */
+ "KeyS": 0x1f, /* html:KeyS (KeyS) -> linux:31 (KEY_S) -> atset1:31 */
+ "KeyT": 0x14, /* html:KeyT (KeyT) -> linux:20 (KEY_T) -> atset1:20 */
+ "KeyU": 0x16, /* html:KeyU (KeyU) -> linux:22 (KEY_U) -> atset1:22 */
+ "KeyV": 0x2f, /* html:KeyV (KeyV) -> linux:47 (KEY_V) -> atset1:47 */
+ "KeyW": 0x11, /* html:KeyW (KeyW) -> linux:17 (KEY_W) -> atset1:17 */
+ "KeyX": 0x2d, /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */
+ "KeyY": 0x15, /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */
+ "KeyZ": 0x2c, /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */
+ "Lang1": 0x72, /* html:Lang1 (Lang1) -> linux:122 (KEY_HANGEUL) -> atset1:114 */
+ "Lang2": 0x71, /* html:Lang2 (Lang2) -> linux:123 (KEY_HANJA) -> atset1:113 */
+ "Lang3": 0x78, /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */
+ "Lang4": 0x77, /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */
+ "Lang5": 0x76, /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */
+ "LaunchApp1": 0xe06b, /* html:LaunchApp1 (LaunchApp1) -> linux:157 (KEY_COMPUTER) -> atset1:57451 */
+ "LaunchApp2": 0xe021, /* html:LaunchApp2 (LaunchApp2) -> linux:140 (KEY_CALC) -> atset1:57377 */
+ "LaunchMail": 0xe06c, /* html:LaunchMail (LaunchMail) -> linux:155 (KEY_MAIL) -> atset1:57452 */
+ "MediaPlayPause": 0xe022, /* html:MediaPlayPause (MediaPlayPause) -> linux:164 (KEY_PLAYPAUSE) -> atset1:57378 */
+ "MediaSelect": 0xe06d, /* html:MediaSelect (MediaSelect) -> linux:226 (KEY_MEDIA) -> atset1:57453 */
+ "MediaStop": 0xe024, /* html:MediaStop (MediaStop) -> linux:166 (KEY_STOPCD) -> atset1:57380 */
+ "MediaTrackNext": 0xe019, /* html:MediaTrackNext (MediaTrackNext) -> linux:163 (KEY_NEXTSONG) -> atset1:57369 */
+ "MediaTrackPrevious": 0xe010, /* html:MediaTrackPrevious (MediaTrackPrevious) -> linux:165 (KEY_PREVIOUSSONG) -> atset1:57360 */
+ "MetaLeft": 0xe05b, /* html:MetaLeft (MetaLeft) -> linux:125 (KEY_LEFTMETA) -> atset1:57435 */
+ "MetaRight": 0xe05c, /* html:MetaRight (MetaRight) -> linux:126 (KEY_RIGHTMETA) -> atset1:57436 */
+ "Minus": 0xc, /* html:Minus (Minus) -> linux:12 (KEY_MINUS) -> atset1:12 */
+ "NonConvert": 0x7b, /* html:NonConvert (NonConvert) -> linux:94 (KEY_MUHENKAN) -> atset1:123 */
+ "NumLock": 0x45, /* html:NumLock (NumLock) -> linux:69 (KEY_NUMLOCK) -> atset1:69 */
+ "Numpad0": 0x52, /* html:Numpad0 (Numpad0) -> linux:82 (KEY_KP0) -> atset1:82 */
+ "Numpad1": 0x4f, /* html:Numpad1 (Numpad1) -> linux:79 (KEY_KP1) -> atset1:79 */
+ "Numpad2": 0x50, /* html:Numpad2 (Numpad2) -> linux:80 (KEY_KP2) -> atset1:80 */
+ "Numpad3": 0x51, /* html:Numpad3 (Numpad3) -> linux:81 (KEY_KP3) -> atset1:81 */
+ "Numpad4": 0x4b, /* html:Numpad4 (Numpad4) -> linux:75 (KEY_KP4) -> atset1:75 */
+ "Numpad5": 0x4c, /* html:Numpad5 (Numpad5) -> linux:76 (KEY_KP5) -> atset1:76 */
+ "Numpad6": 0x4d, /* html:Numpad6 (Numpad6) -> linux:77 (KEY_KP6) -> atset1:77 */
+ "Numpad7": 0x47, /* html:Numpad7 (Numpad7) -> linux:71 (KEY_KP7) -> atset1:71 */
+ "Numpad8": 0x48, /* html:Numpad8 (Numpad8) -> linux:72 (KEY_KP8) -> atset1:72 */
+ "Numpad9": 0x49, /* html:Numpad9 (Numpad9) -> linux:73 (KEY_KP9) -> atset1:73 */
+ "NumpadAdd": 0x4e, /* html:NumpadAdd (NumpadAdd) -> linux:78 (KEY_KPPLUS) -> atset1:78 */
+ "NumpadComma": 0x7e, /* html:NumpadComma (NumpadComma) -> linux:121 (KEY_KPCOMMA) -> atset1:126 */
+ "NumpadDecimal": 0x53, /* html:NumpadDecimal (NumpadDecimal) -> linux:83 (KEY_KPDOT) -> atset1:83 */
+ "NumpadDivide": 0xe035, /* html:NumpadDivide (NumpadDivide) -> linux:98 (KEY_KPSLASH) -> atset1:57397 */
+ "NumpadEnter": 0xe01c, /* html:NumpadEnter (NumpadEnter) -> linux:96 (KEY_KPENTER) -> atset1:57372 */
+ "NumpadEqual": 0x59, /* html:NumpadEqual (NumpadEqual) -> linux:117 (KEY_KPEQUAL) -> atset1:89 */
+ "NumpadMultiply": 0x37, /* html:NumpadMultiply (NumpadMultiply) -> linux:55 (KEY_KPASTERISK) -> atset1:55 */
+ "NumpadParenLeft": 0xe076, /* html:NumpadParenLeft (NumpadParenLeft) -> linux:179 (KEY_KPLEFTPAREN) -> atset1:57462 */
+ "NumpadParenRight": 0xe07b, /* html:NumpadParenRight (NumpadParenRight) -> linux:180 (KEY_KPRIGHTPAREN) -> atset1:57467 */
+ "NumpadSubtract": 0x4a, /* html:NumpadSubtract (NumpadSubtract) -> linux:74 (KEY_KPMINUS) -> atset1:74 */
+ "Open": 0x64, /* html:Open (Open) -> linux:134 (KEY_OPEN) -> atset1:100 */
+ "PageDown": 0xe051, /* html:PageDown (PageDown) -> linux:109 (KEY_PAGEDOWN) -> atset1:57425 */
+ "PageUp": 0xe049, /* html:PageUp (PageUp) -> linux:104 (KEY_PAGEUP) -> atset1:57417 */
+ "Paste": 0x65, /* html:Paste (Paste) -> linux:135 (KEY_PASTE) -> atset1:101 */
+ "Pause": 0xe046, /* html:Pause (Pause) -> linux:119 (KEY_PAUSE) -> atset1:57414 */
+ "Period": 0x34, /* html:Period (Period) -> linux:52 (KEY_DOT) -> atset1:52 */
+ "Power": 0xe05e, /* html:Power (Power) -> linux:116 (KEY_POWER) -> atset1:57438 */
+ "PrintScreen": 0x54, /* html:PrintScreen (PrintScreen) -> linux:99 (KEY_SYSRQ) -> atset1:84 */
+ "Props": 0xe006, /* html:Props (Props) -> linux:130 (KEY_PROPS) -> atset1:57350 */
+ "Quote": 0x28, /* html:Quote (Quote) -> linux:40 (KEY_APOSTROPHE) -> atset1:40 */
+ "ScrollLock": 0x46, /* html:ScrollLock (ScrollLock) -> linux:70 (KEY_SCROLLLOCK) -> atset1:70 */
+ "Semicolon": 0x27, /* html:Semicolon (Semicolon) -> linux:39 (KEY_SEMICOLON) -> atset1:39 */
+ "ShiftLeft": 0x2a, /* html:ShiftLeft (ShiftLeft) -> linux:42 (KEY_LEFTSHIFT) -> atset1:42 */
+ "ShiftRight": 0x36, /* html:ShiftRight (ShiftRight) -> linux:54 (KEY_RIGHTSHIFT) -> atset1:54 */
+ "Slash": 0x35, /* html:Slash (Slash) -> linux:53 (KEY_SLASH) -> atset1:53 */
+ "Sleep": 0xe05f, /* html:Sleep (Sleep) -> linux:142 (KEY_SLEEP) -> atset1:57439 */
+ "Space": 0x39, /* html:Space (Space) -> linux:57 (KEY_SPACE) -> atset1:57 */
+ "Suspend": 0xe025, /* html:Suspend (Suspend) -> linux:205 (KEY_SUSPEND) -> atset1:57381 */
+ "Tab": 0xf, /* html:Tab (Tab) -> linux:15 (KEY_TAB) -> atset1:15 */
+ "Undo": 0xe007, /* html:Undo (Undo) -> linux:131 (KEY_UNDO) -> atset1:57351 */
+ "WakeUp": 0xe063, /* html:WakeUp (WakeUp) -> linux:143 (KEY_WAKEUP) -> atset1:57443 */
+};
pkg/web/noVNC/core/util/browser.js
@@ -0,0 +1,266 @@
+/*
+ * 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.
+ *
+ * Browser feature support detection
+ */
+
+import * as Log from './logging.js';
+import Base64 from '../base64.js';
+
+// Async clipboard detection
+
+/* Evaluates if there is browser support for the async clipboard API and
+ * relevant clipboard permissions. Returns 'unsupported' if permission states
+ * cannot be resolved. On the other hand, detecting 'granted' or 'prompt'
+ * permission states for both read and write indicates full API support with no
+ * imposed native browser paste prompt. Conversely, detecting 'denied' indicates
+ * the user elected to disable clipboard.
+ */
+export async function browserAsyncClipboardSupport() {
+ if (!(navigator?.permissions?.query &&
+ navigator?.clipboard?.writeText &&
+ navigator?.clipboard?.readText)) {
+ return 'unsupported';
+ }
+ try {
+ const writePerm = await navigator.permissions.query(
+ {name: "clipboard-write", allowWithoutGesture: true});
+ const readPerm = await navigator.permissions.query(
+ {name: "clipboard-read", allowWithoutGesture: false});
+ if (writePerm.state === "denied" || readPerm.state === "denied") {
+ return 'denied';
+ }
+ if ((writePerm.state === "granted" || writePerm.state === "prompt") &&
+ (readPerm.state === "granted" || readPerm.state === "prompt")) {
+ return 'available';
+ }
+ } catch {
+ return 'unsupported';
+ }
+ return 'unsupported';
+}
+
+// Touch detection
+export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
+ // required for Chrome debugger
+ (document.ontouchstart !== undefined) ||
+ // required for MS Surface
+ (navigator.maxTouchPoints > 0) ||
+ (navigator.msMaxTouchPoints > 0);
+window.addEventListener('touchstart', function onFirstTouch() {
+ isTouchDevice = true;
+ window.removeEventListener('touchstart', onFirstTouch, false);
+}, false);
+
+
+// The goal is to find a certain physical width, the devicePixelRatio
+// brings us a bit closer but is not optimal.
+export let dragThreshold = 10 * (window.devicePixelRatio || 1);
+
+let _supportsCursorURIs = false;
+
+try {
+ const target = document.createElement('canvas');
+ target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default';
+
+ if (target.style.cursor.indexOf("url") === 0) {
+ Log.Info("Data URI scheme cursor supported");
+ _supportsCursorURIs = true;
+ } else {
+ Log.Warn("Data URI scheme cursor not supported");
+ }
+} catch (exc) {
+ Log.Error("Data URI scheme cursor test exception: " + exc);
+}
+
+export const supportsCursorURIs = _supportsCursorURIs;
+
+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;
+
+export let supportsWebCodecsH264Decode = false;
+
+async function _checkWebCodecsH264DecodeSupport() {
+ if (!('VideoDecoder' in window)) {
+ return false;
+ }
+
+ // We'll need to make do with some placeholders here
+ const config = {
+ codec: 'avc1.42401f',
+ codedWidth: 1920,
+ codedHeight: 1080,
+ optimizeForLatency: true,
+ };
+
+ let support = await VideoDecoder.isConfigSupported(config);
+ if (!support.supported) {
+ return false;
+ }
+
+ // Firefox incorrectly reports supports for H.264 under some
+ // circumstances, so we need to actually test a real frame
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1932392
+
+ const data = new Uint8Array(Base64.decode(
+ 'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4Hc' +
+ 'Rem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5Zjkg' +
+ 'LSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIz' +
+ 'IC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9u' +
+ 'czogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjB4' +
+ 'MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' +
+ 'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4' +
+ 'OGRjdD0wIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJv' +
+ 'bWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0x' +
+ 'IHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9' +
+ 'MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVz' +
+ 'PTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' +
+ 'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9' +
+ 'YWJyIG1idHJlZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAu' +
+ 'NjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFx' +
+ 'PTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS044AA5DRJMnkycJk4TPw=='));
+
+ let gotframe = false;
+ let error = null;
+
+ let decoder = new VideoDecoder({
+ output: (frame) => { gotframe = true; frame.close(); },
+ error: (e) => { error = e; },
+ });
+ let chunk = new EncodedVideoChunk({
+ timestamp: 0,
+ type: 'key',
+ data: data,
+ });
+
+ decoder.configure(config);
+ decoder.decode(chunk);
+ try {
+ await decoder.flush();
+ } catch (e) {
+ // Firefox incorrectly throws an exception here
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1932566
+ error = e;
+ }
+
+ // Firefox fails to deliver the error on Windows, so we need to
+ // check if we got a frame instead
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1932579
+ if (!gotframe) {
+ return false;
+ }
+
+ if (error !== null) {
+ return false;
+ }
+
+ return true;
+}
+supportsWebCodecsH264Decode = await _checkWebCodecsH264DecodeSupport();
+
+/*
+ * 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.
+ */
+
+/* OS */
+
+export function isMac() {
+ return !!(/mac/i).exec(navigator.platform);
+}
+
+export function isWindows() {
+ return !!(/win/i).exec(navigator.platform);
+}
+
+export function isIOS() {
+ return (!!(/ipad/i).exec(navigator.platform) ||
+ !!(/iphone/i).exec(navigator.platform) ||
+ !!(/ipod/i).exec(navigator.platform));
+}
+
+export function isAndroid() {
+ /* Android sets navigator.platform to Linux :/ */
+ return !!navigator.userAgent.match('Android ');
+}
+
+export function isChromeOS() {
+ /* ChromeOS sets navigator.platform to Linux :/ */
+ return !!navigator.userAgent.match(' CrOS ');
+}
+
+/* Browser */
+
+export function isSafari() {
+ return !!navigator.userAgent.match('Safari/...') &&
+ !navigator.userAgent.match('Chrome/...') &&
+ !navigator.userAgent.match('Chromium/...') &&
+ !navigator.userAgent.match('Epiphany/...');
+}
+
+export function isFirefox() {
+ return !!navigator.userAgent.match('Firefox/...') &&
+ !navigator.userAgent.match('Seamonkey/...');
+}
+
+export function isChrome() {
+ return !!navigator.userAgent.match('Chrome/...') &&
+ !navigator.userAgent.match('Chromium/...') &&
+ !navigator.userAgent.match('Edg/...') &&
+ !navigator.userAgent.match('OPR/...');
+}
+
+export function isChromium() {
+ return !!navigator.userAgent.match('Chromium/...');
+}
+
+export function isOpera() {
+ return !!navigator.userAgent.match('OPR/...');
+}
+
+export function isEdge() {
+ return !!navigator.userAgent.match('Edg/...');
+}
+
+/* Engine */
+
+export function isGecko() {
+ return !!navigator.userAgent.match('Gecko/...');
+}
+
+export function isWebKit() {
+ return !!navigator.userAgent.match('AppleWebKit/...') &&
+ !navigator.userAgent.match('Chrome/...');
+}
+
+export function isBlink() {
+ return !!navigator.userAgent.match('Chrome/...');
+}
pkg/web/noVNC/core/util/cursor.js
@@ -0,0 +1,249 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2019 The noVNC authors
+ * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
+ */
+
+import { supportsCursorURIs, isTouchDevice } from './browser.js';
+
+const useFallback = !supportsCursorURIs || isTouchDevice;
+
+export default class Cursor {
+ constructor() {
+ this._target = null;
+
+ this._canvas = document.createElement('canvas');
+
+ if (useFallback) {
+ this._canvas.style.position = 'fixed';
+ this._canvas.style.zIndex = '65535';
+ this._canvas.style.pointerEvents = 'none';
+ // Safari on iOS can select the cursor image
+ // https://bugs.webkit.org/show_bug.cgi?id=249223
+ this._canvas.style.userSelect = 'none';
+ this._canvas.style.WebkitUserSelect = 'none';
+ // Can't use "display" because of Firefox bug #1445997
+ this._canvas.style.visibility = 'hidden';
+ }
+
+ this._position = { x: 0, y: 0 };
+ this._hotSpot = { x: 0, y: 0 };
+
+ this._eventHandlers = {
+ 'mouseover': this._handleMouseOver.bind(this),
+ 'mouseleave': this._handleMouseLeave.bind(this),
+ 'mousemove': this._handleMouseMove.bind(this),
+ 'mouseup': this._handleMouseUp.bind(this),
+ };
+ }
+
+ attach(target) {
+ if (this._target) {
+ this.detach();
+ }
+
+ this._target = target;
+
+ if (useFallback) {
+ document.body.appendChild(this._canvas);
+
+ const options = { capture: true, passive: true };
+ this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
+ this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
+ this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
+ this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
+ }
+
+ this.clear();
+ }
+
+ detach() {
+ if (!this._target) {
+ return;
+ }
+
+ if (useFallback) {
+ const options = { capture: true, passive: true };
+ this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
+ this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
+ this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
+ this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
+
+ if (document.contains(this._canvas)) {
+ document.body.removeChild(this._canvas);
+ }
+ }
+
+ this._target = null;
+ }
+
+ change(rgba, hotx, hoty, w, h) {
+ if ((w === 0) || (h === 0)) {
+ this.clear();
+ return;
+ }
+
+ this._position.x = this._position.x + this._hotSpot.x - hotx;
+ this._position.y = this._position.y + this._hotSpot.y - hoty;
+ this._hotSpot.x = hotx;
+ this._hotSpot.y = hoty;
+
+ let ctx = this._canvas.getContext('2d');
+
+ this._canvas.width = w;
+ this._canvas.height = h;
+
+ let img = new ImageData(new Uint8ClampedArray(rgba), w, h);
+ ctx.clearRect(0, 0, w, h);
+ ctx.putImageData(img, 0, 0);
+
+ if (useFallback) {
+ this._updatePosition();
+ } else {
+ let url = this._canvas.toDataURL();
+ this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
+ }
+ }
+
+ clear() {
+ this._target.style.cursor = 'none';
+ this._canvas.width = 0;
+ this._canvas.height = 0;
+ this._position.x = this._position.x + this._hotSpot.x;
+ this._position.y = this._position.y + this._hotSpot.y;
+ this._hotSpot.x = 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) {
+ // This event could be because we're entering the target, or
+ // moving around amongst its sub elements. Let the move handler
+ // sort things out.
+ this._handleMouseMove(event);
+ }
+
+ _handleMouseLeave(event) {
+ // Check if we should show the cursor on the element we are leaving to
+ this._updateVisibility(event.relatedTarget);
+ }
+
+ _handleMouseMove(event) {
+ this._updateVisibility(event.target);
+
+ this._position.x = event.clientX - this._hotSpot.x;
+ this._position.y = event.clientY - this._hotSpot.y;
+
+ this._updatePosition();
+ }
+
+ _handleMouseUp(event) {
+ // We might get this event because of a drag operation that
+ // moved outside of the target. Check what's under the cursor
+ // now and adjust visibility based on that.
+ let target = document.elementFromPoint(event.clientX, event.clientY);
+ 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
+ // might have altered the DOM
+ target = document.elementFromPoint(event.clientX,
+ event.clientY);
+ this._updateVisibility(target);
+ }, 0);
+ }
+ }
+
+ _showCursor() {
+ if (this._canvas.style.visibility === 'hidden') {
+ this._canvas.style.visibility = '';
+ }
+ }
+
+ _hideCursor() {
+ if (this._canvas.style.visibility !== 'hidden') {
+ this._canvas.style.visibility = 'hidden';
+ }
+ }
+
+ // Should we currently display the cursor?
+ // (i.e. are we over the target, or a child of the target without a
+ // different cursor set)
+ _shouldShowCursor(target) {
+ if (!target) {
+ return false;
+ }
+ // Easy case
+ if (target === this._target) {
+ return true;
+ }
+ // Other part of the DOM?
+ if (!this._target.contains(target)) {
+ return false;
+ }
+ // Has the child its own cursor?
+ // FIXME: How can we tell that a sub element has an
+ // explicit "cursor: none;"?
+ if (window.getComputedStyle(target).cursor !== 'none') {
+ return false;
+ }
+ return true;
+ }
+
+ _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)) {
+ this._showCursor();
+ } else {
+ this._hideCursor();
+ }
+ }
+
+ _updatePosition() {
+ this._canvas.style.left = this._position.x + "px";
+ this._canvas.style.top = this._position.y + "px";
+ }
+
+ _captureIsActive() {
+ return document.captureElement &&
+ document.documentElement.contains(document.captureElement);
+ }
+}
pkg/web/noVNC/core/util/element.js
@@ -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;
+}
pkg/web/noVNC/core/util/events.js
@@ -0,0 +1,138 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2018 The noVNC authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ */
+
+/*
+ * Cross-browser event and position routines
+ */
+
+export function getPointerEvent(e) {
+ return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e;
+}
+
+export function stopEvent(e) {
+ e.stopPropagation();
+ e.preventDefault();
+}
+
+// Emulate Element.setCapture() when not supported
+let _captureRecursion = false;
+let _elementForUnflushedEvents = null;
+document.captureElement = null;
+function _captureProxy(e) {
+ // Recursion protection as we'll see our own event
+ if (_captureRecursion) return;
+
+ // Clone the event as we cannot dispatch an already dispatched event
+ const newEv = new e.constructor(e.type, e);
+
+ _captureRecursion = true;
+ if (document.captureElement) {
+ document.captureElement.dispatchEvent(newEv);
+ } else {
+ _elementForUnflushedEvents.dispatchEvent(newEv);
+ }
+ _captureRecursion = false;
+
+ // Avoid double events
+ e.stopPropagation();
+
+ // Respect the wishes of the redirected event handlers
+ if (newEv.defaultPrevented) {
+ e.preventDefault();
+ }
+
+ // Implicitly release the capture on button release
+ if (e.type === "mouseup") {
+ releaseCapture();
+ }
+}
+
+// Follow cursor style of target element
+function _capturedElemChanged() {
+ const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
+ proxyElem.style.cursor = window.getComputedStyle(document.captureElement).cursor;
+}
+
+const _captureObserver = new MutationObserver(_capturedElemChanged);
+
+export function setCapture(target) {
+ if (target.setCapture) {
+
+ target.setCapture();
+ document.captureElement = target;
+ } else {
+ // Release any existing capture in case this method is
+ // called multiple times without coordination
+ releaseCapture();
+
+ let proxyElem = document.getElementById("noVNC_mouse_capture_elem");
+
+ if (proxyElem === null) {
+ proxyElem = document.createElement("div");
+ proxyElem.id = "noVNC_mouse_capture_elem";
+ proxyElem.style.position = "fixed";
+ proxyElem.style.top = "0px";
+ proxyElem.style.left = "0px";
+ proxyElem.style.width = "100%";
+ proxyElem.style.height = "100%";
+ proxyElem.style.zIndex = 10000;
+ proxyElem.style.display = "none";
+ document.body.appendChild(proxyElem);
+
+ // This is to make sure callers don't get confused by having
+ // our blocking element as the target
+ proxyElem.addEventListener('contextmenu', _captureProxy);
+
+ proxyElem.addEventListener('mousemove', _captureProxy);
+ proxyElem.addEventListener('mouseup', _captureProxy);
+ }
+
+ document.captureElement = target;
+
+ // Track cursor and get initial cursor
+ _captureObserver.observe(target, {attributes: true});
+ _capturedElemChanged();
+
+ proxyElem.style.display = "";
+
+ // We listen to events on window in order to keep tracking if it
+ // happens to leave the viewport
+ window.addEventListener('mousemove', _captureProxy);
+ window.addEventListener('mouseup', _captureProxy);
+ }
+}
+
+export function releaseCapture() {
+ if (document.releaseCapture) {
+
+ document.releaseCapture();
+ document.captureElement = null;
+
+ } else {
+ if (!document.captureElement) {
+ return;
+ }
+
+ // There might be events already queued. The event proxy needs
+ // access to the captured element for these queued events.
+ // E.g. contextmenu (right-click) in Microsoft Edge
+ //
+ // Before removing the capturedElem pointer we save it to a
+ // temporary variable that the unflushed events can use.
+ _elementForUnflushedEvents = document.captureElement;
+ document.captureElement = null;
+
+ _captureObserver.disconnect();
+
+ const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
+ proxyElem.style.display = "none";
+
+ window.removeEventListener('mousemove', _captureProxy);
+ window.removeEventListener('mouseup', _captureProxy);
+ }
+}
pkg/web/noVNC/core/util/eventtarget.js
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+export default class EventTargetMixin {
+ constructor() {
+ this._listeners = new Map();
+ }
+
+ addEventListener(type, callback) {
+ if (!this._listeners.has(type)) {
+ this._listeners.set(type, new Set());
+ }
+ this._listeners.get(type).add(callback);
+ }
+
+ removeEventListener(type, callback) {
+ if (this._listeners.has(type)) {
+ this._listeners.get(type).delete(callback);
+ }
+ }
+
+ dispatchEvent(event) {
+ if (!this._listeners.has(event.type)) {
+ return true;
+ }
+ this._listeners.get(event.type)
+ .forEach(callback => callback.call(this, event));
+ return !event.defaultPrevented;
+ }
+}
pkg/web/noVNC/core/util/int.js
@@ -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;
+}
pkg/web/noVNC/core/util/logging.js
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+/*
+ * Logging/debug routines
+ */
+
+let _logLevel = 'warn';
+
+let Debug = () => {};
+let Info = () => {};
+let Warn = () => {};
+let Error = () => {};
+
+export function initLogging(level) {
+ if (typeof level === 'undefined') {
+ level = _logLevel;
+ } else {
+ _logLevel = level;
+ }
+
+ Debug = Info = Warn = Error = () => {};
+
+ if (typeof window.console !== "undefined") {
+ /* eslint-disable no-console, no-fallthrough */
+ switch (level) {
+ case 'debug':
+ Debug = console.debug.bind(window.console);
+ case 'info':
+ Info = console.info.bind(window.console);
+ case 'warn':
+ Warn = console.warn.bind(window.console);
+ case 'error':
+ Error = console.error.bind(window.console);
+ case 'none':
+ break;
+ default:
+ throw new window.Error("invalid logging type '" + level + "'");
+ }
+ /* eslint-enable no-console, no-fallthrough */
+ }
+}
+
+export function getLogging() {
+ return _logLevel;
+}
+
+export { Debug, Info, Warn, Error };
+
+// Initialize logging level
+initLogging();
pkg/web/noVNC/core/util/strings.js
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+// Decode from UTF-8
+export function decodeUTF8(utf8string, allowLatin1=false) {
+ try {
+ 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));
+}
pkg/web/noVNC/core/base64.js
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js
+
+import * as Log from './util/logging.js';
+
+export default {
+ /* Convert data (an array of integers) to a Base64 string. */
+ toBase64Table: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''),
+ base64Pad: '=',
+
+ encode(data) {
+ "use strict";
+ let result = '';
+ const length = data.length;
+ const lengthpad = (length % 3);
+ // Convert every three bytes to 4 ascii characters.
+
+ for (let i = 0; i < (length - 2); i += 3) {
+ result += this.toBase64Table[data[i] >> 2];
+ result += this.toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)];
+ result += this.toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)];
+ result += this.toBase64Table[data[i + 2] & 0x3f];
+ }
+
+ // Convert the remaining 1 or 2 bytes, pad out to 4 characters.
+ const j = length - lengthpad;
+ if (lengthpad === 2) {
+ result += this.toBase64Table[data[j] >> 2];
+ result += this.toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)];
+ result += this.toBase64Table[(data[j + 1] & 0x0f) << 2];
+ result += this.toBase64Table[64];
+ } else if (lengthpad === 1) {
+ result += this.toBase64Table[data[j] >> 2];
+ result += this.toBase64Table[(data[j] & 0x03) << 4];
+ result += this.toBase64Table[64];
+ result += this.toBase64Table[64];
+ }
+
+ return result;
+ },
+
+ /* Convert Base64 data to a string */
+ /* eslint-disable comma-spacing */
+ toBinaryTable: [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+ ],
+ /* eslint-enable comma-spacing */
+
+ decode(data, offset = 0) {
+ let dataLength = data.indexOf('=') - offset;
+ if (dataLength < 0) { dataLength = data.length - offset; }
+
+ /* Every four characters is 3 resulting numbers */
+ const resultLength = (dataLength >> 2) * 3 + Math.floor((dataLength % 4) / 1.5);
+ const result = new Array(resultLength);
+
+ // Convert one by one.
+
+ let leftbits = 0; // number of bits decoded, but yet to be appended
+ let leftdata = 0; // bits decoded, but yet to be appended
+ for (let idx = 0, i = offset; i < data.length; i++) {
+ const c = this.toBinaryTable[data.charCodeAt(i) & 0x7f];
+ const padding = (data.charAt(i) === this.base64Pad);
+ // Skip illegal characters and whitespace
+ if (c === -1) {
+ Log.Error("Illegal character code " + data.charCodeAt(i) + " at position " + i);
+ continue;
+ }
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding) {
+ result[idx++] = (leftdata >> leftbits) & 0xff;
+ }
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits) {
+ const err = new Error('Corrupted base64 string');
+ err.name = 'Base64-Error';
+ throw err;
+ }
+
+ return result;
+ }
+}; /* End of Base64 namespace */
pkg/web/noVNC/core/clipboard.js
@@ -0,0 +1,72 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (c) 2025 The noVNC authors
+ * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
+ */
+
+import * as Log from './util/logging.js';
+import { browserAsyncClipboardSupport } from './util/browser.js';
+
+export default class AsyncClipboard {
+ constructor(target) {
+ this._target = target || null;
+
+ this._isAvailable = null;
+
+ this._eventHandlers = {
+ 'focus': this._handleFocus.bind(this),
+ };
+
+ // ===== EVENT HANDLERS =====
+
+ this.onpaste = () => {};
+ }
+
+ // ===== PRIVATE METHODS =====
+
+ async _ensureAvailable() {
+ if (this._isAvailable !== null) return this._isAvailable;
+ try {
+ const status = await browserAsyncClipboardSupport();
+ this._isAvailable = (status === 'available');
+ } catch {
+ this._isAvailable = false;
+ }
+ return this._isAvailable;
+ }
+
+ async _handleFocus(event) {
+ if (!(await this._ensureAvailable())) return;
+ try {
+ const text = await navigator.clipboard.readText();
+ this.onpaste(text);
+ } catch (error) {
+ Log.Error("Clipboard read failed: ", error);
+ }
+ }
+
+ // ===== PUBLIC METHODS =====
+
+ writeClipboard(text) {
+ // Can lazily check cached availability
+ if (!this._isAvailable) return false;
+ navigator.clipboard.writeText(text)
+ .catch(error => Log.Error("Clipboard write failed: ", error));
+ return true;
+ }
+
+ grab() {
+ if (!this._target) return;
+ this._ensureAvailable()
+ .then((isAvailable) => {
+ if (isAvailable) {
+ this._target.addEventListener('focus', this._eventHandlers.focus);
+ }
+ });
+ }
+
+ ungrab() {
+ if (!this._target) return;
+ this._target.removeEventListener('focus', this._eventHandlers.focus);
+ }
+}
pkg/web/noVNC/core/deflator.js
@@ -0,0 +1,84 @@
+/*
+ * 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, Z_DEFAULT_COMPRESSION } 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);
+
+ deflateInit(this.strm, Z_DEFAULT_COMPRESSION);
+ }
+
+ 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;
+ }
+
+}
pkg/web/noVNC/core/display.js
@@ -0,0 +1,578 @@
+/*
+ * 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.
+ */
+
+import * as Log from './util/logging.js';
+import Base64 from "./base64.js";
+import { toSigned32bit } from './util/int.js';
+
+export default class Display {
+ constructor(target) {
+ this._drawCtx = null;
+
+ this._renderQ = []; // queue drawing actions for in-order rendering
+ this._flushPromise = null;
+
+ // the full frame buffer (logical canvas) size
+ this._fbWidth = 0;
+ this._fbHeight = 0;
+
+ this._prevDrawStyle = "";
+
+ Log.Debug(">> Display.constructor");
+
+ // The visible canvas
+ this._target = target;
+
+ if (!this._target) {
+ throw new Error("Target must be set");
+ }
+
+ if (typeof this._target === 'string') {
+ throw new Error('target must be a DOM element');
+ }
+
+ if (!this._target.getContext) {
+ throw new Error("no getContext method");
+ }
+
+ this._targetCtx = this._target.getContext('2d');
+
+ // the visible canvas viewport (i.e. what actually gets seen)
+ this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
+
+ // The hidden canvas, where we do the actual rendering
+ this._backbuffer = document.createElement('canvas');
+ this._drawCtx = this._backbuffer.getContext('2d');
+
+ this._damageBounds = { left: 0, top: 0,
+ right: this._backbuffer.width,
+ bottom: this._backbuffer.height };
+
+ Log.Debug("User Agent: " + navigator.userAgent);
+
+ Log.Debug("<< Display.constructor");
+
+ // ===== PROPERTIES =====
+
+ this._scale = 1.0;
+ this._clipViewport = false;
+ }
+
+ // ===== PROPERTIES =====
+
+ get scale() { return this._scale; }
+ set scale(scale) {
+ this._rescale(scale);
+ }
+
+ get clipViewport() { return this._clipViewport; }
+ set clipViewport(viewport) {
+ this._clipViewport = viewport;
+ // May need to readjust the viewport dimensions
+ const vp = this._viewportLoc;
+ this.viewportChangeSize(vp.w, vp.h);
+ this.viewportChangePos(0, 0);
+ }
+
+ get width() {
+ return this._fbWidth;
+ }
+
+ get height() {
+ return this._fbHeight;
+ }
+
+ // ===== PUBLIC METHODS =====
+
+ viewportChangePos(deltaX, deltaY) {
+ const vp = this._viewportLoc;
+ deltaX = Math.floor(deltaX);
+ deltaY = Math.floor(deltaY);
+
+ if (!this._clipViewport) {
+ deltaX = -vp.w; // clamped later of out of bounds
+ deltaY = -vp.h;
+ }
+
+ const vx2 = vp.x + vp.w - 1;
+ const vy2 = vp.y + vp.h - 1;
+
+ // Position change
+
+ if (deltaX < 0 && vp.x + deltaX < 0) {
+ deltaX = -vp.x;
+ }
+ if (vx2 + deltaX >= this._fbWidth) {
+ deltaX -= vx2 + deltaX - this._fbWidth + 1;
+ }
+
+ if (vp.y + deltaY < 0) {
+ deltaY = -vp.y;
+ }
+ if (vy2 + deltaY >= this._fbHeight) {
+ deltaY -= (vy2 + deltaY - this._fbHeight + 1);
+ }
+
+ if (deltaX === 0 && deltaY === 0) {
+ return;
+ }
+ Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
+
+ vp.x += deltaX;
+ vp.y += deltaY;
+
+ this._damage(vp.x, vp.y, vp.w, vp.h);
+
+ this.flip();
+ }
+
+ viewportChangeSize(width, height) {
+
+ if (!this._clipViewport ||
+ typeof(width) === "undefined" ||
+ typeof(height) === "undefined") {
+
+ Log.Debug("Setting viewport to full display region");
+ width = this._fbWidth;
+ height = this._fbHeight;
+ }
+
+ width = Math.floor(width);
+ height = Math.floor(height);
+
+ if (width > this._fbWidth) {
+ width = this._fbWidth;
+ }
+ if (height > this._fbHeight) {
+ height = this._fbHeight;
+ }
+
+ const vp = this._viewportLoc;
+ if (vp.w !== width || vp.h !== height) {
+ vp.w = width;
+ vp.h = height;
+
+ const canvas = this._target;
+ canvas.width = width;
+ canvas.height = height;
+
+ // The position might need to be updated if we've grown
+ this.viewportChangePos(0, 0);
+
+ this._damage(vp.x, vp.y, vp.w, vp.h);
+ this.flip();
+
+ // Update the visible size of the target canvas
+ this._rescale(this._scale);
+ }
+ }
+
+ absX(x) {
+ if (this._scale === 0) {
+ return 0;
+ }
+ return toSigned32bit(x / this._scale + this._viewportLoc.x);
+ }
+
+ absY(y) {
+ if (this._scale === 0) {
+ return 0;
+ }
+ return toSigned32bit(y / this._scale + this._viewportLoc.y);
+ }
+
+ resize(width, height) {
+ this._prevDrawStyle = "";
+
+ this._fbWidth = width;
+ this._fbHeight = height;
+
+ const canvas = this._backbuffer;
+ if (canvas.width !== width || canvas.height !== height) {
+
+ // We have to save the canvas data since changing the size will clear it
+ let saveImg = null;
+ if (canvas.width > 0 && canvas.height > 0) {
+ saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
+ }
+
+ if (canvas.width !== width) {
+ canvas.width = width;
+ }
+ if (canvas.height !== height) {
+ canvas.height = height;
+ }
+
+ if (saveImg) {
+ this._drawCtx.putImageData(saveImg, 0, 0);
+ }
+ }
+
+ // Readjust the viewport as it may be incorrectly sized
+ // and positioned
+ const vp = this._viewportLoc;
+ this.viewportChangeSize(vp.w, vp.h);
+ this.viewportChangePos(0, 0);
+ }
+
+ getImageData() {
+ return this._drawCtx.getImageData(0, 0, this.width, this.height);
+ }
+
+ toDataURL(type, encoderOptions) {
+ return this._backbuffer.toDataURL(type, encoderOptions);
+ }
+
+ toBlob(callback, type, quality) {
+ return this._backbuffer.toBlob(callback, type, quality);
+ }
+
+ // Track what parts of the visible canvas that need updating
+ _damage(x, y, w, h) {
+ if (x < this._damageBounds.left) {
+ this._damageBounds.left = x;
+ }
+ if (y < this._damageBounds.top) {
+ this._damageBounds.top = y;
+ }
+ if ((x + w) > this._damageBounds.right) {
+ this._damageBounds.right = x + w;
+ }
+ if ((y + h) > this._damageBounds.bottom) {
+ this._damageBounds.bottom = y + h;
+ }
+ }
+
+ // Update the visible canvas with the contents of the
+ // rendering canvas
+ flip(fromQueue) {
+ if (this._renderQ.length !== 0 && !fromQueue) {
+ this._renderQPush({
+ 'type': 'flip'
+ });
+ } else {
+ let x = this._damageBounds.left;
+ let y = this._damageBounds.top;
+ let w = this._damageBounds.right - x;
+ let h = this._damageBounds.bottom - y;
+
+ let vx = x - this._viewportLoc.x;
+ let vy = y - this._viewportLoc.y;
+
+ if (vx < 0) {
+ w += vx;
+ x -= vx;
+ vx = 0;
+ }
+ if (vy < 0) {
+ h += vy;
+ y -= vy;
+ vy = 0;
+ }
+
+ if ((vx + w) > this._viewportLoc.w) {
+ w = this._viewportLoc.w - vx;
+ }
+ if ((vy + h) > this._viewportLoc.h) {
+ h = this._viewportLoc.h - vy;
+ }
+
+ if ((w > 0) && (h > 0)) {
+ // FIXME: We may need to disable image smoothing here
+ // as well (see copyImage()), but we haven't
+ // noticed any problem yet.
+ this._targetCtx.drawImage(this._backbuffer,
+ x, y, w, h,
+ vx, vy, w, h);
+ }
+
+ this._damageBounds.left = this._damageBounds.top = 65535;
+ this._damageBounds.right = this._damageBounds.bottom = 0;
+ }
+ }
+
+ pending() {
+ return this._renderQ.length > 0;
+ }
+
+ flush() {
+ if (this._renderQ.length === 0) {
+ return Promise.resolve();
+ } else {
+ if (this._flushPromise === null) {
+ this._flushPromise = new Promise((resolve) => {
+ this._flushResolve = resolve;
+ });
+ }
+ return this._flushPromise;
+ }
+ }
+
+ fillRect(x, y, width, height, color, fromQueue) {
+ if (this._renderQ.length !== 0 && !fromQueue) {
+ this._renderQPush({
+ 'type': 'fill',
+ 'x': x,
+ 'y': y,
+ 'width': width,
+ 'height': height,
+ 'color': color
+ });
+ } else {
+ this._setFillColor(color);
+ this._drawCtx.fillRect(x, y, width, height);
+ this._damage(x, y, width, height);
+ }
+ }
+
+ copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {
+ if (this._renderQ.length !== 0 && !fromQueue) {
+ this._renderQPush({
+ 'type': 'copy',
+ 'oldX': oldX,
+ 'oldY': oldY,
+ 'x': newX,
+ 'y': newY,
+ 'width': w,
+ 'height': h,
+ });
+ } else {
+ // Due to this bug among others [1] we need to disable the image-smoothing to
+ // avoid getting a blur effect when copying data.
+ //
+ // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
+ //
+ // We need to set these every time since all properties are reset
+ // when the the size is changed
+ this._drawCtx.mozImageSmoothingEnabled = false;
+ this._drawCtx.webkitImageSmoothingEnabled = false;
+ this._drawCtx.msImageSmoothingEnabled = false;
+ this._drawCtx.imageSmoothingEnabled = false;
+
+ this._drawCtx.drawImage(this._backbuffer,
+ oldX, oldY, w, h,
+ newX, newY, w, h);
+ this._damage(newX, newY, w, h);
+ }
+ }
+
+ 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();
+ img.src = "data: " + mime + ";base64," + Base64.encode(arr);
+
+ this._renderQPush({
+ 'type': 'img',
+ 'img': img,
+ 'x': x,
+ 'y': y,
+ 'width': width,
+ 'height': height
+ });
+ }
+
+ videoFrame(x, y, width, height, frame) {
+ this._renderQPush({
+ 'type': 'frame',
+ 'frame': frame,
+ 'x': x,
+ 'y': y,
+ 'width': width,
+ 'height': height
+ });
+ }
+
+ blitImage(x, y, width, height, arr, offset, fromQueue) {
+ if (this._renderQ.length !== 0 && !fromQueue) {
+ // 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,
+ // this probably isn't getting called *nearly* as much
+ const newArr = new Uint8Array(width * height * 4);
+ newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
+ this._renderQPush({
+ 'type': 'blit',
+ 'data': newArr,
+ 'x': x,
+ 'y': y,
+ 'width': width,
+ 'height': height,
+ });
+ } else {
+ // NB(directxman12): arr must be an Type Array view
+ let data = new Uint8ClampedArray(arr.buffer,
+ arr.byteOffset + offset,
+ width * height * 4);
+ let img = new ImageData(data, width, height);
+ this._drawCtx.putImageData(img, x, y);
+ this._damage(x, y, width, height);
+ }
+ }
+
+ drawImage(img, ...args) {
+ this._drawCtx.drawImage(img, ...args);
+
+ if (args.length <= 4) {
+ const [x, y] = args;
+ this._damage(x, y, img.width, img.height);
+ } else {
+ const [,, sw, sh, dx, dy] = args;
+ this._damage(dx, dy, sw, sh);
+ }
+ }
+
+ autoscale(containerWidth, containerHeight) {
+ let scaleRatio;
+
+ if (containerWidth === 0 || containerHeight === 0) {
+ scaleRatio = 0;
+
+ } else {
+
+ const vp = this._viewportLoc;
+ const targetAspectRatio = containerWidth / containerHeight;
+ const fbAspectRatio = vp.w / vp.h;
+
+ if (fbAspectRatio >= targetAspectRatio) {
+ scaleRatio = containerWidth / vp.w;
+ } else {
+ scaleRatio = containerHeight / vp.h;
+ }
+ }
+
+ this._rescale(scaleRatio);
+ }
+
+ // ===== PRIVATE METHODS =====
+
+ _rescale(factor) {
+ this._scale = factor;
+ const vp = this._viewportLoc;
+
+ // NB(directxman12): If you set the width directly, or set the
+ // style width to a number, the canvas is cleared.
+ // However, if you set the style width to a string
+ // ('NNNpx'), the canvas is scaled without clearing.
+ const width = factor * vp.w + 'px';
+ const height = factor * vp.h + 'px';
+
+ if ((this._target.style.width !== width) ||
+ (this._target.style.height !== height)) {
+ this._target.style.width = width;
+ this._target.style.height = height;
+ }
+ }
+
+ _setFillColor(color) {
+ const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
+ if (newStyle !== this._prevDrawStyle) {
+ this._drawCtx.fillStyle = newStyle;
+ this._prevDrawStyle = newStyle;
+ }
+ }
+
+ _renderQPush(action) {
+ this._renderQ.push(action);
+ if (this._renderQ.length === 1) {
+ // If this can be rendered immediately it will be, otherwise
+ // the scanner will wait for the relevant event
+ this._scanRenderQ();
+ }
+ }
+
+ _resumeRenderQ() {
+ // "this" is the object that is ready, not the
+ // display object
+ this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);
+ this._noVNCDisplay._scanRenderQ();
+ }
+
+ _scanRenderQ() {
+ let ready = true;
+ while (ready && this._renderQ.length > 0) {
+ const a = this._renderQ[0];
+ switch (a.type) {
+ case 'flip':
+ this.flip(true);
+ break;
+ case 'copy':
+ this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
+ break;
+ case 'fill':
+ this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
+ break;
+ case 'blit':
+ this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
+ break;
+ case 'img':
+ if (a.img.complete) {
+ 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 helps the browser free the memory right
+ // away, rather than ballooning
+ a.img.src = "";
+ } else {
+ a.img._noVNCDisplay = this;
+ a.img.addEventListener('load', this._resumeRenderQ);
+ // We need to wait for this image to 'load'
+ // to keep things in-order
+ ready = false;
+ }
+ break;
+ case 'frame':
+ if (a.frame.ready) {
+ // The encoded frame may be larger than the rect due to
+ // limitations of the encoder, so we need to crop the
+ // frame.
+ let frame = a.frame.frame;
+ if (frame.codedWidth < a.width || frame.codedHeight < a.height) {
+ Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " +
+ a.width + "x" + a.height + " but got " +
+ frame.codedWidth + "x" + frame.codedHeight);
+ }
+ const sx = 0;
+ const sy = 0;
+ const sw = a.width;
+ const sh = a.height;
+ const dx = a.x;
+ const dy = a.y;
+ const dw = sw;
+ const dh = sh;
+ this.drawImage(frame, sx, sy, sw, sh, dx, dy, dw, dh);
+ frame.close();
+ } else {
+ let display = this;
+ a.frame.promise.then(() => {
+ display._scanRenderQ();
+ });
+ ready = false;
+ }
+ break;
+ }
+
+ if (ready) {
+ this._renderQ.shift();
+ }
+ }
+
+ if (this._renderQ.length === 0 &&
+ this._flushPromise !== null) {
+ this._flushResolve();
+ this._flushPromise = null;
+ this._flushResolve = null;
+ }
+ }
+}
pkg/web/noVNC/core/encodings.js
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+export const encodings = {
+ encodingRaw: 0,
+ encodingCopyRect: 1,
+ encodingRRE: 2,
+ encodingHextile: 5,
+ encodingZlib: 6,
+ encodingTight: 7,
+ encodingZRLE: 16,
+ encodingTightPNG: -260,
+ encodingJPEG: 21,
+ encodingH264: 50,
+
+ pseudoEncodingQualityLevel9: -23,
+ pseudoEncodingQualityLevel0: -32,
+ pseudoEncodingDesktopSize: -223,
+ pseudoEncodingLastRect: -224,
+ pseudoEncodingCursor: -239,
+ pseudoEncodingQEMUExtendedKeyEvent: -258,
+ pseudoEncodingQEMULedEvent: -261,
+ pseudoEncodingDesktopName: -307,
+ pseudoEncodingExtendedDesktopSize: -308,
+ pseudoEncodingXvp: -309,
+ pseudoEncodingFence: -312,
+ pseudoEncodingContinuousUpdates: -313,
+ pseudoEncodingExtendedMouseButtons: -316,
+ pseudoEncodingCompressLevel9: -247,
+ pseudoEncodingCompressLevel0: -256,
+ pseudoEncodingVMwareCursor: 0x574d5664,
+ pseudoEncodingExtendedClipboard: 0xc0a1e5ce
+};
+
+export function encodingName(num) {
+ switch (num) {
+ case encodings.encodingRaw: return "Raw";
+ case encodings.encodingCopyRect: return "CopyRect";
+ case encodings.encodingRRE: return "RRE";
+ case encodings.encodingHextile: return "Hextile";
+ case encodings.encodingZlib: return "Zlib";
+ case encodings.encodingTight: return "Tight";
+ case encodings.encodingZRLE: return "ZRLE";
+ case encodings.encodingTightPNG: return "TightPNG";
+ case encodings.encodingJPEG: return "JPEG";
+ case encodings.encodingH264: return "H.264";
+ default: return "[unknown encoding " + num + "]";
+ }
+}
pkg/web/noVNC/core/inflator.js
@@ -0,0 +1,65 @@
+/*
+ * 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 ZStream from "../vendor/pako/lib/zlib/zstream.js";
+
+export default class Inflate {
+ constructor() {
+ this.strm = new ZStream();
+ this.chunkSize = 1024 * 10 * 10;
+ this.strm.output = new Uint8Array(this.chunkSize);
+
+ inflateInit(this.strm);
+ }
+
+ 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.avail_in = this.strm.input.length;
+ this.strm.next_in = 0;
+ /* eslint-enable camelcase */
+ }
+ }
+
+ inflate(expected) {
+ // resize our output buffer if it's too small
+ // (we could just use multiple chunks, but that would cause an extra
+ // allocation each time to flatten the chunks)
+ if (expected > this.chunkSize) {
+ this.chunkSize = expected;
+ this.strm.output = new Uint8Array(this.chunkSize);
+ }
+
+ /* eslint-disable camelcase */
+ this.strm.next_out = 0;
+ this.strm.avail_out = expected;
+ /* eslint-enable camelcase */
+
+ 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);
+ }
+
+ reset() {
+ inflateReset(this.strm);
+ }
+}
pkg/web/noVNC/core/ra2.js
@@ -0,0 +1,312 @@
+import { encodeUTF8 } from './util/strings.js';
+import EventTargetMixin from './util/eventtarget.js';
+import legacyCrypto from './crypto/crypto.js';
+
+class RA2Cipher {
+ constructor() {
+ this._cipher = null;
+ this._counter = new Uint8Array(16);
+ }
+
+ async setKey(key) {
+ this._cipher = await legacyCrypto.importKey(
+ "raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]);
+ }
+
+ async makeMessage(message) {
+ const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
+ const encrypted = await legacyCrypto.encrypt({
+ name: "AES-EAX",
+ iv: this._counter,
+ additionalData: ad,
+ }, this._cipher, message);
+ for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
+ const res = new Uint8Array(message.length + 2 + 16);
+ res.set(ad);
+ res.set(encrypted, 2);
+ return res;
+ }
+
+ async receiveMessage(length, encrypted) {
+ const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
+ const res = await legacyCrypto.decrypt({
+ name: "AES-EAX",
+ iv: this._counter,
+ additionalData: ad,
+ }, this._cipher, encrypted);
+ for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
+ return res;
+ }
+}
+
+export default class RSAAESAuthenticationState extends EventTargetMixin {
+ constructor(sock, getCredentials) {
+ super();
+ this._hasStarted = false;
+ this._checkSock = null;
+ this._checkCredentials = null;
+ this._approveServerResolve = null;
+ this._sockReject = null;
+ this._credentialsReject = null;
+ this._approveServerReject = null;
+ this._sock = sock;
+ this._getCredentials = getCredentials;
+ }
+
+ _waitSockAsync(len) {
+ return new Promise((resolve, reject) => {
+ const hasData = () => !this._sock.rQwait('RA2', len);
+ if (hasData()) {
+ resolve();
+ } else {
+ this._checkSock = () => {
+ if (hasData()) {
+ resolve();
+ this._checkSock = null;
+ this._sockReject = null;
+ }
+ };
+ this._sockReject = reject;
+ }
+ });
+ }
+
+ _waitApproveKeyAsync() {
+ return new Promise((resolve, reject) => {
+ this._approveServerResolve = resolve;
+ this._approveServerReject = reject;
+ });
+ }
+
+ _waitCredentialsAsync(subtype) {
+ const hasCredentials = () => {
+ if (subtype === 1 && this._getCredentials().username !== undefined &&
+ this._getCredentials().password !== undefined) {
+ return true;
+ } else if (subtype === 2 && this._getCredentials().password !== undefined) {
+ return true;
+ }
+ return false;
+ };
+ return new Promise((resolve, reject) => {
+ if (hasCredentials()) {
+ resolve();
+ } else {
+ this._checkCredentials = () => {
+ if (hasCredentials()) {
+ resolve();
+ this._checkCredentials = null;
+ this._credentialsReject = null;
+ }
+ };
+ this._credentialsReject = reject;
+ }
+ });
+ }
+
+ checkInternalEvents() {
+ if (this._checkSock !== null) {
+ this._checkSock();
+ }
+ if (this._checkCredentials !== null) {
+ this._checkCredentials();
+ }
+ }
+
+ approveServer() {
+ if (this._approveServerResolve !== null) {
+ this._approveServerResolve();
+ this._approveServerResolve = null;
+ }
+ }
+
+ disconnect() {
+ if (this._sockReject !== null) {
+ this._sockReject(new Error("disconnect normally"));
+ this._sockReject = null;
+ }
+ if (this._credentialsReject !== null) {
+ this._credentialsReject(new Error("disconnect normally"));
+ this._credentialsReject = null;
+ }
+ if (this._approveServerReject !== null) {
+ this._approveServerReject(new Error("disconnect normally"));
+ this._approveServerReject = null;
+ }
+ }
+
+ async negotiateRA2neAuthAsync() {
+ this._hasStarted = true;
+ // 1: Receive server public key
+ await this._waitSockAsync(4);
+ const serverKeyLengthBuffer = this._sock.rQpeekBytes(4);
+ const serverKeyLength = this._sock.rQshift32();
+ if (serverKeyLength < 1024) {
+ throw new Error("RA2: server public key is too short: " + serverKeyLength);
+ } else if (serverKeyLength > 8192) {
+ throw new Error("RA2: server public key is too long: " + serverKeyLength);
+ }
+ const serverKeyBytes = Math.ceil(serverKeyLength / 8);
+ await this._waitSockAsync(serverKeyBytes * 2);
+ const serverN = this._sock.rQshiftBytes(serverKeyBytes);
+ const serverE = this._sock.rQshiftBytes(serverKeyBytes);
+ const serverRSACipher = await legacyCrypto.importKey(
+ "raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]);
+ const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
+ serverPublickey.set(serverKeyLengthBuffer);
+ serverPublickey.set(serverN, 4);
+ serverPublickey.set(serverE, 4 + serverKeyBytes);
+
+ // verify server public key
+ let approveKey = this._waitApproveKeyAsync();
+ this.dispatchEvent(new CustomEvent("serververification", {
+ detail: { type: "RSA", publickey: serverPublickey }
+ }));
+ await approveKey;
+
+ // 2: Send client public key
+ const clientKeyLength = 2048;
+ const clientKeyBytes = Math.ceil(clientKeyLength / 8);
+ const clientRSACipher = (await legacyCrypto.generateKey({
+ name: "RSA-PKCS1-v1_5",
+ modulusLength: clientKeyLength,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ }, true, ["encrypt"])).privateKey;
+ const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher);
+ const clientN = clientExportedRSAKey.n;
+ const clientE = clientExportedRSAKey.e;
+ const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
+ clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
+ clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
+ clientPublicKey[2] = (clientKeyLength & 0xff00) >>> 8;
+ clientPublicKey[3] = clientKeyLength & 0xff;
+ clientPublicKey.set(clientN, 4);
+ clientPublicKey.set(clientE, 4 + clientKeyBytes);
+ this._sock.sQpushBytes(clientPublicKey);
+ this._sock.flush();
+
+ // 3: Send client random
+ const clientRandom = new Uint8Array(16);
+ window.crypto.getRandomValues(clientRandom);
+ const clientEncryptedRandom = await legacyCrypto.encrypt(
+ { name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom);
+ const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
+ clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
+ clientRandomMessage[1] = serverKeyBytes & 0xff;
+ clientRandomMessage.set(clientEncryptedRandom, 2);
+ this._sock.sQpushBytes(clientRandomMessage);
+ this._sock.flush();
+
+ // 4: Receive server random
+ await this._waitSockAsync(2);
+ if (this._sock.rQshift16() !== clientKeyBytes) {
+ throw new Error("RA2: wrong encrypted message length");
+ }
+ const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
+ const serverRandom = await legacyCrypto.decrypt(
+ { name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom);
+ if (serverRandom === null || serverRandom.length !== 16) {
+ throw new Error("RA2: corrupted server encrypted random");
+ }
+
+ // 5: Compute session keys and set ciphers
+ let clientSessionKey = new Uint8Array(32);
+ let serverSessionKey = new Uint8Array(32);
+ clientSessionKey.set(serverRandom);
+ clientSessionKey.set(clientRandom, 16);
+ serverSessionKey.set(clientRandom);
+ serverSessionKey.set(serverRandom, 16);
+ clientSessionKey = await window.crypto.subtle.digest("SHA-1", clientSessionKey);
+ clientSessionKey = new Uint8Array(clientSessionKey).slice(0, 16);
+ serverSessionKey = await window.crypto.subtle.digest("SHA-1", serverSessionKey);
+ serverSessionKey = new Uint8Array(serverSessionKey).slice(0, 16);
+ const clientCipher = new RA2Cipher();
+ await clientCipher.setKey(clientSessionKey);
+ const serverCipher = new RA2Cipher();
+ await serverCipher.setKey(serverSessionKey);
+
+ // 6: Compute and exchange hashes
+ let serverHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
+ let clientHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
+ serverHash.set(serverPublickey);
+ serverHash.set(clientPublicKey, 4 + serverKeyBytes * 2);
+ clientHash.set(clientPublicKey);
+ clientHash.set(serverPublickey, 4 + clientKeyBytes * 2);
+ serverHash = await window.crypto.subtle.digest("SHA-1", serverHash);
+ clientHash = await window.crypto.subtle.digest("SHA-1", clientHash);
+ serverHash = new Uint8Array(serverHash);
+ clientHash = new Uint8Array(clientHash);
+ this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash));
+ this._sock.flush();
+ await this._waitSockAsync(2 + 20 + 16);
+ if (this._sock.rQshift16() !== 20) {
+ throw new Error("RA2: wrong server hash");
+ }
+ const serverHashReceived = await serverCipher.receiveMessage(
+ 20, this._sock.rQshiftBytes(20 + 16));
+ if (serverHashReceived === null) {
+ throw new Error("RA2: failed to authenticate the message");
+ }
+ for (let i = 0; i < 20; i++) {
+ if (serverHashReceived[i] !== serverHash[i]) {
+ throw new Error("RA2: wrong server hash");
+ }
+ }
+
+ // 7: Receive subtype
+ await this._waitSockAsync(2 + 1 + 16);
+ if (this._sock.rQshift16() !== 1) {
+ throw new Error("RA2: wrong subtype");
+ }
+ let subtype = (await serverCipher.receiveMessage(
+ 1, this._sock.rQshiftBytes(1 + 16)));
+ if (subtype === null) {
+ throw new Error("RA2: failed to authenticate the message");
+ }
+ subtype = subtype[0];
+ let waitCredentials = this._waitCredentialsAsync(subtype);
+ if (subtype === 1) {
+ if (this._getCredentials().username === undefined ||
+ this._getCredentials().password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["username", "password"] } }));
+ }
+ } else if (subtype === 2) {
+ if (this._getCredentials().password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["password"] } }));
+ }
+ } else {
+ throw new Error("RA2: wrong subtype");
+ }
+ await waitCredentials;
+ let username;
+ if (subtype === 1) {
+ username = encodeUTF8(this._getCredentials().username).slice(0, 255);
+ } else {
+ username = "";
+ }
+ const password = encodeUTF8(this._getCredentials().password).slice(0, 255);
+ const credentials = new Uint8Array(username.length + password.length + 2);
+ credentials[0] = username.length;
+ credentials[username.length + 1] = password.length;
+ for (let i = 0; i < username.length; i++) {
+ credentials[i + 1] = username.charCodeAt(i);
+ }
+ for (let i = 0; i < password.length; i++) {
+ credentials[username.length + 2 + i] = password.charCodeAt(i);
+ }
+ this._sock.sQpushBytes(await clientCipher.makeMessage(credentials));
+ this._sock.flush();
+ }
+
+ get hasStarted() {
+ return this._hasStarted;
+ }
+
+ set hasStarted(s) {
+ this._hasStarted = s;
+ }
+}
pkg/web/noVNC/core/rfb.js
@@ -0,0 +1,3426 @@
+/*
+ * 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 { toUnsigned32bit, toSigned32bit } from './util/int.js';
+import * as Log from './util/logging.js';
+import { encodeUTF8, decodeUTF8 } from './util/strings.js';
+import { dragThreshold, supportsWebCodecsH264Decode } from './util/browser.js';
+import { clientToElement } from './util/element.js';
+import { setCapture } from './util/events.js';
+import EventTargetMixin from './util/eventtarget.js';
+import Display from "./display.js";
+import AsyncClipboard from "./clipboard.js";
+import Inflator from "./inflator.js";
+import Deflator from "./deflator.js";
+import Keyboard from "./input/keyboard.js";
+import GestureHandler from "./input/gesturehandler.js";
+import Cursor from "./util/cursor.js";
+import Websock from "./websock.js";
+import KeyTable from "./input/keysym.js";
+import XtScancode from "./input/xtscancodes.js";
+import { encodings } from "./encodings.js";
+import RSAAESAuthenticationState from "./ra2.js";
+import legacyCrypto from "./crypto/crypto.js";
+
+import RawDecoder from "./decoders/raw.js";
+import CopyRectDecoder from "./decoders/copyrect.js";
+import RREDecoder from "./decoders/rre.js";
+import HextileDecoder from "./decoders/hextile.js";
+import ZlibDecoder from './decoders/zlib.js';
+import TightDecoder from "./decoders/tight.js";
+import TightPNGDecoder from "./decoders/tightpng.js";
+import ZRLEDecoder from "./decoders/zrle.js";
+import JPEGDecoder from "./decoders/jpeg.js";
+import H264Decoder from "./decoders/h264.js";
+
+// How many seconds to wait for a disconnect to finish
+const DISCONNECT_TIMEOUT = 3;
+const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
+
+// Minimum wait (ms) between two mouse moves
+const MOUSE_MOVE_DELAY = 17;
+
+// Wheel thresholds
+const WHEEL_STEP = 50; // Pixels needed for one step
+const WHEEL_LINE_HEIGHT = 19; // Assumed pixels for one line step
+
+// Gesture thresholds
+const GESTURE_ZOOMSENS = 75;
+const GESTURE_SCRLSENS = 50;
+const DOUBLE_TAP_TIMEOUT = 1000;
+const DOUBLE_TAP_THRESHOLD = 50;
+
+// Security types
+const securityTypeNone = 1;
+const securityTypeVNCAuth = 2;
+const securityTypeRA2ne = 6;
+const securityTypeTight = 16;
+const securityTypeVeNCrypt = 19;
+const securityTypeXVP = 22;
+const securityTypeARD = 30;
+const securityTypeMSLogonII = 113;
+
+// Special Tight security types
+const securityTypeUnixLogon = 129;
+
+// VeNCrypt security types
+const securityTypePlain = 256;
+
+// Extended clipboard pseudo-encoding formats
+const extendedClipboardFormatText = 1;
+/*eslint-disable no-unused-vars */
+const extendedClipboardFormatRtf = 1 << 1;
+const extendedClipboardFormatHtml = 1 << 2;
+const extendedClipboardFormatDib = 1 << 3;
+const extendedClipboardFormatFiles = 1 << 4;
+/*eslint-enable */
+
+// Extended clipboard pseudo-encoding actions
+const extendedClipboardActionCaps = 1 << 24;
+const extendedClipboardActionRequest = 1 << 25;
+const extendedClipboardActionPeek = 1 << 26;
+const extendedClipboardActionNotify = 1 << 27;
+const extendedClipboardActionProvide = 1 << 28;
+
+export default class RFB extends EventTargetMixin {
+ constructor(target, urlOrChannel, options) {
+ if (!target) {
+ throw new Error("Must specify target");
+ }
+ if (!urlOrChannel) {
+ throw new Error("Must specify URL, WebSocket or RTCDataChannel");
+ }
+
+ // We rely on modern APIs which might not be available in an
+ // insecure context
+ if (!window.isSecureContext) {
+ Log.Error("noVNC requires a secure context (TLS). Expect crashes!");
+ }
+
+ super();
+
+ this._target = target;
+
+ if (typeof urlOrChannel === "string") {
+ this._url = urlOrChannel;
+ } else {
+ this._url = null;
+ this._rawChannel = urlOrChannel;
+ }
+
+ // Connection details
+ options = options || {};
+ this._rfbCredentials = options.credentials || {};
+ this._shared = 'shared' in options ? !!options.shared : true;
+ this._repeaterID = options.repeaterID || '';
+ this._wsProtocols = options.wsProtocols || [];
+
+ // Internal state
+ this._rfbConnectionState = '';
+ this._rfbInitState = '';
+ this._rfbAuthScheme = -1;
+ this._rfbCleanDisconnect = true;
+ this._rfbRSAAESAuthenticationState = null;
+
+ // Server capabilities
+ this._rfbVersion = 0;
+ this._rfbMaxVersion = 3.8;
+ this._rfbTightVNC = false;
+ this._rfbVeNCryptState = 0;
+ this._rfbXvpVer = 0;
+
+ this._fbWidth = 0;
+ this._fbHeight = 0;
+
+ this._fbName = "";
+
+ this._capabilities = { power: false };
+
+ this._supportsFence = false;
+
+ this._supportsContinuousUpdates = false;
+ this._enabledContinuousUpdates = false;
+
+ this._supportsSetDesktopSize = false;
+ this._screenID = 0;
+ this._screenFlags = 0;
+ this._pendingRemoteResize = false;
+ this._lastResize = 0;
+
+ this._qemuExtKeyEventSupported = false;
+
+ this._extendedPointerEventSupported = false;
+
+ this._clipboardText = null;
+ this._clipboardServerCapabilitiesActions = {};
+ this._clipboardServerCapabilitiesFormats = {};
+
+ // Internal objects
+ this._sock = null; // Websock object
+ this._display = null; // Display object
+ this._flushing = false; // Display flushing state
+ this._asyncClipboard = null; // Async clipboard object
+ this._keyboard = null; // Keyboard input handler object
+ this._gestures = null; // Gesture input handler object
+ this._resizeObserver = null; // Resize observer object
+
+ // Timers
+ this._disconnTimer = null; // disconnection timer
+ this._resizeTimeout = null; // resize rate limiting
+ this._mouseMoveTimer = null;
+
+ // Decoder states
+ this._decoders = {};
+
+ this._FBU = {
+ rects: 0,
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ encoding: null,
+ };
+
+ // Mouse state
+ this._mousePos = {};
+ this._mouseButtonMask = 0;
+ this._mouseLastMoveTime = 0;
+ this._viewportDragging = false;
+ this._viewportDragPos = {};
+ this._viewportHasMoved = false;
+ this._accumulatedWheelDeltaX = 0;
+ this._accumulatedWheelDeltaY = 0;
+
+ // Gesture state
+ this._gestureLastTapTime = null;
+ this._gestureFirstDoubleTapEv = null;
+ this._gestureLastMagnitudeX = 0;
+ this._gestureLastMagnitudeY = 0;
+
+ // Bound event handlers
+ this._eventHandlers = {
+ focusCanvas: this._focusCanvas.bind(this),
+ handleResize: this._handleResize.bind(this),
+ handleMouse: this._handleMouse.bind(this),
+ handleWheel: this._handleWheel.bind(this),
+ handleGesture: this._handleGesture.bind(this),
+ handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),
+ handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this),
+ };
+
+ // main setup
+ Log.Debug(">> RFB.constructor");
+
+ // Create DOM elements
+ this._screen = document.createElement('div');
+ this._screen.style.display = 'flex';
+ this._screen.style.width = '100%';
+ this._screen.style.height = '100%';
+ this._screen.style.overflow = 'auto';
+ this._screen.style.background = DEFAULT_BACKGROUND;
+ this._canvas = document.createElement('canvas');
+ this._canvas.style.margin = 'auto';
+ // Some browsers add an outline on focus
+ this._canvas.style.outline = 'none';
+ this._canvas.width = 0;
+ this._canvas.height = 0;
+ this._canvas.tabIndex = -1;
+ this._screen.appendChild(this._canvas);
+
+ // Cursor
+ this._cursor = new Cursor();
+
+ // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
+ // it. Result: no cursor at all until a window border or an edit field
+ // is hit blindly. But there are also VNC servers that draw the cursor
+ // in the framebuffer and don't send the empty local cursor. There is
+ // no way to satisfy both sides.
+ //
+ // The spec is unclear on this "initial cursor" issue. Many other
+ // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
+ // initial cursor instead.
+ this._cursorImage = RFB.cursors.none;
+
+ // populate decoder array with objects
+ this._decoders[encodings.encodingRaw] = new RawDecoder();
+ this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
+ this._decoders[encodings.encodingRRE] = new RREDecoder();
+ this._decoders[encodings.encodingHextile] = new HextileDecoder();
+ this._decoders[encodings.encodingZlib] = new ZlibDecoder();
+ this._decoders[encodings.encodingTight] = new TightDecoder();
+ this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
+ this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
+ this._decoders[encodings.encodingJPEG] = new JPEGDecoder();
+ this._decoders[encodings.encodingH264] = new H264Decoder();
+
+ // NB: nothing that needs explicit teardown should be done
+ // before this point, since this can throw an exception
+ try {
+ this._display = new Display(this._canvas);
+ } catch (exc) {
+ Log.Error("Display exception: " + exc);
+ throw exc;
+ }
+
+ this._asyncClipboard = new AsyncClipboard(this._canvas);
+ this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);
+
+ this._keyboard = new Keyboard(this._canvas);
+ this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
+ this._remoteCapsLock = null; // Null indicates unknown or irrelevant
+ this._remoteNumLock = null;
+
+ this._gestures = new GestureHandler();
+
+ this._sock = new Websock();
+ this._sock.on('open', this._socketOpen.bind(this));
+ this._sock.on('close', this._socketClose.bind(this));
+ this._sock.on('message', this._handleMessage.bind(this));
+ this._sock.on('error', this._socketError.bind(this));
+
+ this._expectedClientWidth = null;
+ this._expectedClientHeight = null;
+ this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize);
+
+ // All prepared, kick off the connection
+ this._updateConnectionState('connecting');
+
+ Log.Debug("<< RFB.constructor");
+
+ // ===== PROPERTIES =====
+
+ this.dragViewport = false;
+ this.focusOnClick = true;
+
+ this._viewOnly = false;
+ this._clipViewport = false;
+ this._clippingViewport = false;
+ this._scaleViewport = false;
+ this._resizeSession = false;
+
+ this._showDotCursor = false;
+
+ this._qualityLevel = 6;
+ this._compressionLevel = 2;
+ }
+
+ // ===== PROPERTIES =====
+
+ get viewOnly() { return this._viewOnly; }
+ set viewOnly(viewOnly) {
+ this._viewOnly = viewOnly;
+
+ if (this._rfbConnectionState === "connecting" ||
+ this._rfbConnectionState === "connected") {
+ if (viewOnly) {
+ this._keyboard.ungrab();
+ this._asyncClipboard.ungrab();
+ } else {
+ this._keyboard.grab();
+ this._asyncClipboard.grab();
+ }
+ }
+ }
+
+ get capabilities() { return this._capabilities; }
+
+ get clippingViewport() { return this._clippingViewport; }
+ _setClippingViewport(on) {
+ if (on === this._clippingViewport) {
+ return;
+ }
+ this._clippingViewport = on;
+ this.dispatchEvent(new CustomEvent("clippingviewport",
+ { detail: this._clippingViewport }));
+ }
+
+ get touchButton() { return 0; }
+ set touchButton(button) { Log.Warn("Using old API!"); }
+
+ get clipViewport() { return this._clipViewport; }
+ set clipViewport(viewport) {
+ this._clipViewport = viewport;
+ this._updateClip();
+ }
+
+ get scaleViewport() { return this._scaleViewport; }
+ set scaleViewport(scale) {
+ this._scaleViewport = scale;
+ // Scaling trumps clipping, so we may need to adjust
+ // clipping when enabling or disabling scaling
+ if (scale && this._clipViewport) {
+ this._updateClip();
+ }
+ this._updateScale();
+ if (!scale && this._clipViewport) {
+ this._updateClip();
+ }
+ }
+
+ get resizeSession() { return this._resizeSession; }
+ set resizeSession(resize) {
+ this._resizeSession = resize;
+ if (resize) {
+ this._requestRemoteResize();
+ }
+ }
+
+ get showDotCursor() { return this._showDotCursor; }
+ set showDotCursor(show) {
+ this._showDotCursor = show;
+ this._refreshCursor();
+ }
+
+ get background() { return this._screen.style.background; }
+ set background(cssValue) { this._screen.style.background = cssValue; }
+
+ get qualityLevel() {
+ return this._qualityLevel;
+ }
+ set qualityLevel(qualityLevel) {
+ if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) {
+ Log.Error("qualityLevel must be an integer between 0 and 9");
+ return;
+ }
+
+ if (this._qualityLevel === qualityLevel) {
+ return;
+ }
+
+ this._qualityLevel = qualityLevel;
+
+ if (this._rfbConnectionState === 'connected') {
+ this._sendEncodings();
+ }
+ }
+
+ get compressionLevel() {
+ return this._compressionLevel;
+ }
+ set compressionLevel(compressionLevel) {
+ if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {
+ Log.Error("compressionLevel must be an integer between 0 and 9");
+ return;
+ }
+
+ if (this._compressionLevel === compressionLevel) {
+ return;
+ }
+
+ this._compressionLevel = compressionLevel;
+
+ if (this._rfbConnectionState === 'connected') {
+ this._sendEncodings();
+ }
+ }
+
+ // ===== PUBLIC METHODS =====
+
+ disconnect() {
+ this._updateConnectionState('disconnecting');
+ this._sock.off('error');
+ this._sock.off('message');
+ this._sock.off('open');
+ if (this._rfbRSAAESAuthenticationState !== null) {
+ this._rfbRSAAESAuthenticationState.disconnect();
+ }
+ }
+
+ approveServer() {
+ if (this._rfbRSAAESAuthenticationState !== null) {
+ this._rfbRSAAESAuthenticationState.approveServer();
+ }
+ }
+
+ sendCredentials(creds) {
+ this._rfbCredentials = creds;
+ this._resumeAuthentication();
+ }
+
+ sendCtrlAltDel() {
+ if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
+ Log.Info("Sending Ctrl-Alt-Del");
+
+ this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
+ this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
+ this.sendKey(KeyTable.XK_Delete, "Delete", true);
+ this.sendKey(KeyTable.XK_Delete, "Delete", false);
+ this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
+ this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
+ }
+
+ machineShutdown() {
+ this._xvpOp(1, 2);
+ }
+
+ machineReboot() {
+ this._xvpOp(1, 3);
+ }
+
+ machineReset() {
+ this._xvpOp(1, 4);
+ }
+
+ // Send a key press. If 'down' is not specified then send a down key
+ // followed by an up key.
+ sendKey(keysym, code, down) {
+ if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
+
+ if (down === undefined) {
+ this.sendKey(keysym, code, true);
+ this.sendKey(keysym, code, false);
+ return;
+ }
+
+ const scancode = XtScancode[code];
+
+ if (this._qemuExtKeyEventSupported && scancode) {
+ // 0 is NoSymbol
+ keysym = keysym || 0;
+
+ Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
+
+ RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
+ } else {
+ if (!keysym) {
+ return;
+ }
+ Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
+ RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
+ }
+ }
+
+ focus(options) {
+ this._canvas.focus(options);
+ }
+
+ blur() {
+ this._canvas.blur();
+ }
+
+ clipboardPasteFrom(text) {
+ if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
+
+ if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] &&
+ this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
+
+ this._clipboardText = text;
+ RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
+ } else {
+ let length, i;
+ let data;
+
+ length = 0;
+ // eslint-disable-next-line no-unused-vars
+ for (let codePoint of text) {
+ length++;
+ }
+
+ data = new Uint8Array(length);
+
+ i = 0;
+ for (let codePoint of text) {
+ let code = codePoint.codePointAt(0);
+
+ /* Only ISO 8859-1 is supported */
+ if (code > 0xff) {
+ code = 0x3f; // '?'
+ }
+
+ data[i++] = code;
+ }
+
+ RFB.messages.clientCutText(this._sock, data);
+ }
+ }
+
+ getImageData() {
+ return this._display.getImageData();
+ }
+
+ toDataURL(type, encoderOptions) {
+ return this._display.toDataURL(type, encoderOptions);
+ }
+
+ toBlob(callback, type, quality) {
+ return this._display.toBlob(callback, type, quality);
+ }
+
+ // ===== PRIVATE METHODS =====
+
+ _connect() {
+ Log.Debug(">> RFB.connect");
+
+ if (this._url) {
+ Log.Info(`connecting to ${this._url}`);
+ this._sock.open(this._url, this._wsProtocols);
+ } else {
+ Log.Info(`attaching ${this._rawChannel} to Websock`);
+ this._sock.attach(this._rawChannel);
+
+ if (this._sock.readyState === 'closed') {
+ throw Error("Cannot use already closed WebSocket/RTCDataChannel");
+ }
+
+ if (this._sock.readyState === 'open') {
+ // FIXME: _socketOpen() can in theory call _fail(), which
+ // isn't allowed this early, but I'm not sure that can
+ // happen without a bug messing up our state variables
+ this._socketOpen();
+ }
+ }
+
+ // Make our elements part of the page
+ this._target.appendChild(this._screen);
+
+ this._gestures.attach(this._canvas);
+
+ this._cursor.attach(this._canvas);
+ this._refreshCursor();
+
+ // Monitor size changes of the screen element
+ this._resizeObserver.observe(this._screen);
+
+ // Always grab focus on some kind of click event
+ this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
+ this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
+
+ // Mouse events
+ this._canvas.addEventListener('mousedown', this._eventHandlers.handleMouse);
+ this._canvas.addEventListener('mouseup', this._eventHandlers.handleMouse);
+ this._canvas.addEventListener('mousemove', this._eventHandlers.handleMouse);
+ // Prevent middle-click pasting (see handler for why we bind to document)
+ this._canvas.addEventListener('click', this._eventHandlers.handleMouse);
+ // preventDefault() on mousedown doesn't stop this event for some
+ // reason so we have to explicitly block it
+ this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);
+
+ // Wheel events
+ this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
+
+ // Gesture events
+ this._canvas.addEventListener("gesturestart", this._eventHandlers.handleGesture);
+ this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture);
+ this._canvas.addEventListener("gestureend", this._eventHandlers.handleGesture);
+
+ Log.Debug("<< RFB.connect");
+ }
+
+ _disconnect() {
+ Log.Debug(">> RFB.disconnect");
+ this._cursor.detach();
+ this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture);
+ this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture);
+ this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture);
+ this._canvas.removeEventListener("wheel", this._eventHandlers.handleWheel);
+ this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse);
+ this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse);
+ this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
+ this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);
+ this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
+ this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
+ this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
+ this._resizeObserver.disconnect();
+ this._keyboard.ungrab();
+ this._gestures.detach();
+ this._sock.close();
+ try {
+ this._target.removeChild(this._screen);
+ } catch (e) {
+ if (e.name === 'NotFoundError') {
+ // Some cases where the initial connection fails
+ // can disconnect before the _screen is created
+ } else {
+ throw e;
+ }
+ }
+ clearTimeout(this._resizeTimeout);
+ clearTimeout(this._mouseMoveTimer);
+ Log.Debug("<< RFB.disconnect");
+ }
+
+ _socketOpen() {
+ if ((this._rfbConnectionState === 'connecting') &&
+ (this._rfbInitState === '')) {
+ this._rfbInitState = 'ProtocolVersion';
+ Log.Debug("Starting VNC handshake");
+ } else {
+ this._fail("Unexpected server connection while " +
+ this._rfbConnectionState);
+ }
+ }
+
+ _socketClose(e) {
+ Log.Debug("WebSocket on-close event");
+ let msg = "";
+ if (e.code) {
+ msg = "(code: " + e.code;
+ if (e.reason) {
+ msg += ", reason: " + e.reason;
+ }
+ msg += ")";
+ }
+ switch (this._rfbConnectionState) {
+ case 'connecting':
+ this._fail("Connection closed " + msg);
+ break;
+ case 'connected':
+ // Handle disconnects that were initiated server-side
+ this._updateConnectionState('disconnecting');
+ this._updateConnectionState('disconnected');
+ break;
+ case 'disconnecting':
+ // Normal disconnection path
+ this._updateConnectionState('disconnected');
+ break;
+ case 'disconnected':
+ this._fail("Unexpected server disconnect " +
+ "when already disconnected " + msg);
+ break;
+ default:
+ this._fail("Unexpected server disconnect before connecting " +
+ msg);
+ break;
+ }
+ this._sock.off('close');
+ // Delete reference to raw channel to allow cleanup.
+ this._rawChannel = null;
+ }
+
+ _socketError(e) {
+ Log.Warn("WebSocket on-error event");
+ }
+
+ _focusCanvas(event) {
+ if (!this.focusOnClick) {
+ return;
+ }
+
+ this.focus({ preventScroll: true });
+ }
+
+ _setDesktopName(name) {
+ this._fbName = name;
+ this.dispatchEvent(new CustomEvent(
+ "desktopname",
+ { detail: { name: this._fbName } }));
+ }
+
+ _saveExpectedClientSize() {
+ this._expectedClientWidth = this._screen.clientWidth;
+ this._expectedClientHeight = this._screen.clientHeight;
+ }
+
+ _currentClientSize() {
+ return [this._screen.clientWidth, this._screen.clientHeight];
+ }
+
+ _clientHasExpectedSize() {
+ const [currentWidth, currentHeight] = this._currentClientSize();
+ return currentWidth == this._expectedClientWidth &&
+ currentHeight == this._expectedClientHeight;
+ }
+
+ // Handle browser window resizes
+ _handleResize() {
+ // Don't change anything if the client size is already as expected
+ if (this._clientHasExpectedSize()) {
+ return;
+ }
+ // If the window resized then our screen element might have
+ // as well. Update the viewport dimensions.
+ window.requestAnimationFrame(() => {
+ this._updateClip();
+ this._updateScale();
+ this._saveExpectedClientSize();
+ });
+
+ // Request changing the resolution of the remote display to
+ // the size of the local browser viewport.
+ this._requestRemoteResize();
+ }
+
+ // Update state of clipping in Display object, and make sure the
+ // configured viewport matches the current screen size
+ _updateClip() {
+ const curClip = this._display.clipViewport;
+ let newClip = this._clipViewport;
+
+ if (this._scaleViewport) {
+ // Disable viewport clipping if we are scaling
+ newClip = false;
+ }
+
+ if (curClip !== newClip) {
+ this._display.clipViewport = newClip;
+ }
+
+ if (newClip) {
+ // When clipping is enabled, the screen is limited to
+ // the size of the container.
+ const size = this._screenSize();
+ this._display.viewportChangeSize(size.w, size.h);
+ this._fixScrollbars();
+ this._setClippingViewport(size.w < this._display.width ||
+ size.h < this._display.height);
+ } else {
+ this._setClippingViewport(false);
+ }
+
+ // When changing clipping we might show or hide scrollbars.
+ // This causes the expected client dimensions to change.
+ if (curClip !== newClip) {
+ this._saveExpectedClientSize();
+ }
+ }
+
+ _updateScale() {
+ if (!this._scaleViewport) {
+ this._display.scale = 1.0;
+ } else {
+ const size = this._screenSize();
+ this._display.autoscale(size.w, size.h);
+ }
+ this._fixScrollbars();
+ }
+
+ // Requests a change of remote desktop size. This message is an extension
+ // and may only be sent if we have received an ExtendedDesktopSize message
+ _requestRemoteResize() {
+ if (!this._resizeSession) {
+ return;
+ }
+ if (this._viewOnly) {
+ return;
+ }
+ if (!this._supportsSetDesktopSize) {
+ return;
+ }
+
+ // Rate limit to one pending resize at a time
+ if (this._pendingRemoteResize) {
+ return;
+ }
+
+ // And no more than once every 100ms
+ if ((Date.now() - this._lastResize) < 100) {
+ clearTimeout(this._resizeTimeout);
+ this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this),
+ 100 - (Date.now() - this._lastResize));
+ return;
+ }
+ this._resizeTimeout = null;
+
+ const size = this._screenSize();
+
+ // Do we actually change anything?
+ if (size.w === this._fbWidth && size.h === this._fbHeight) {
+ return;
+ }
+
+ this._pendingRemoteResize = true;
+ this._lastResize = Date.now();
+ RFB.messages.setDesktopSize(this._sock,
+ Math.floor(size.w), Math.floor(size.h),
+ this._screenID, this._screenFlags);
+
+ Log.Debug('Requested new desktop size: ' +
+ size.w + 'x' + size.h);
+ }
+
+ // Gets the the size of the available screen
+ _screenSize() {
+ let r = this._screen.getBoundingClientRect();
+ return { w: r.width, h: r.height };
+ }
+
+ _fixScrollbars() {
+ // This is a hack because Safari on macOS screws up the calculation
+ // for when scrollbars are needed. We get scrollbars when making the
+ // browser smaller, despite remote resize being enabled. So to fix it
+ // we temporarily toggle them off and on.
+ const orig = this._screen.style.overflow;
+ this._screen.style.overflow = 'hidden';
+ // Force Safari to recalculate the layout by asking for
+ // an element's dimensions
+ this._screen.getBoundingClientRect();
+ this._screen.style.overflow = orig;
+ }
+
+ /*
+ * Connection states:
+ * connecting
+ * connected
+ * disconnecting
+ * disconnected - permanent state
+ */
+ _updateConnectionState(state) {
+ const oldstate = this._rfbConnectionState;
+
+ if (state === oldstate) {
+ Log.Debug("Already in state '" + state + "', ignoring");
+ return;
+ }
+
+ // The 'disconnected' state is permanent for each RFB object
+ if (oldstate === 'disconnected') {
+ Log.Error("Tried changing state of a disconnected RFB object");
+ return;
+ }
+
+ // Ensure proper transitions before doing anything
+ switch (state) {
+ case 'connected':
+ if (oldstate !== 'connecting') {
+ Log.Error("Bad transition to connected state, " +
+ "previous connection state: " + oldstate);
+ return;
+ }
+ break;
+
+ case 'disconnected':
+ if (oldstate !== 'disconnecting') {
+ Log.Error("Bad transition to disconnected state, " +
+ "previous connection state: " + oldstate);
+ return;
+ }
+ break;
+
+ case 'connecting':
+ if (oldstate !== '') {
+ Log.Error("Bad transition to connecting state, " +
+ "previous connection state: " + oldstate);
+ return;
+ }
+ break;
+
+ case 'disconnecting':
+ if (oldstate !== 'connected' && oldstate !== 'connecting') {
+ Log.Error("Bad transition to disconnecting state, " +
+ "previous connection state: " + oldstate);
+ return;
+ }
+ break;
+
+ default:
+ Log.Error("Unknown connection state: " + state);
+ return;
+ }
+
+ // State change actions
+
+ this._rfbConnectionState = state;
+
+ Log.Debug("New state '" + state + "', was '" + oldstate + "'.");
+
+ if (this._disconnTimer && state !== 'disconnecting') {
+ Log.Debug("Clearing disconnect timer");
+ clearTimeout(this._disconnTimer);
+ this._disconnTimer = null;
+
+ // make sure we don't get a double event
+ this._sock.off('close');
+ }
+
+ switch (state) {
+ case 'connecting':
+ this._connect();
+ break;
+
+ case 'connected':
+ this.dispatchEvent(new CustomEvent("connect", { detail: {} }));
+ break;
+
+ case 'disconnecting':
+ this._disconnect();
+
+ this._disconnTimer = setTimeout(() => {
+ Log.Error("Disconnection timed out.");
+ this._updateConnectionState('disconnected');
+ }, DISCONNECT_TIMEOUT * 1000);
+ break;
+
+ case 'disconnected':
+ this.dispatchEvent(new CustomEvent(
+ "disconnect", { detail:
+ { clean: this._rfbCleanDisconnect } }));
+ break;
+ }
+ }
+
+ /* Print errors and disconnect
+ *
+ * The parameter 'details' is used for information that
+ * should be logged but not sent to the user interface.
+ */
+ _fail(details) {
+ switch (this._rfbConnectionState) {
+ case 'disconnecting':
+ Log.Error("Failed when disconnecting: " + details);
+ break;
+ case 'connected':
+ Log.Error("Failed while connected: " + details);
+ break;
+ case 'connecting':
+ Log.Error("Failed when connecting: " + details);
+ break;
+ default:
+ Log.Error("RFB failure: " + details);
+ break;
+ }
+ this._rfbCleanDisconnect = false; //This is sent to the UI
+
+ // Transition to disconnected without waiting for socket to close
+ this._updateConnectionState('disconnecting');
+ this._updateConnectionState('disconnected');
+
+ return false;
+ }
+
+ _setCapability(cap, val) {
+ this._capabilities[cap] = val;
+ this.dispatchEvent(new CustomEvent("capabilities",
+ { detail: { capabilities: this._capabilities } }));
+ }
+
+ _handleMessage() {
+ if (this._sock.rQwait("message", 1)) {
+ Log.Warn("handleMessage called on an empty receive queue");
+ return;
+ }
+
+ switch (this._rfbConnectionState) {
+ case 'disconnected':
+ Log.Error("Got data while disconnected");
+ break;
+ case 'connected':
+ while (true) {
+ if (this._flushing) {
+ break;
+ }
+ if (!this._normalMsg()) {
+ break;
+ }
+ if (this._sock.rQwait("message", 1)) {
+ break;
+ }
+ }
+ break;
+ case 'connecting':
+ while (this._rfbConnectionState === 'connecting') {
+ if (!this._initMsg()) {
+ break;
+ }
+ }
+ break;
+ default:
+ Log.Error("Got data while in an invalid state");
+ break;
+ }
+ }
+
+ _handleKeyEvent(keysym, code, down, numlock, capslock) {
+ // If remote state of capslock is known, and it doesn't match the local led state of
+ // the keyboard, we send a capslock keypress first to bring it into sync.
+ // If we just pressed CapsLock, or we toggled it remotely due to it being out of sync
+ // we clear the remote state so that we don't send duplicate or spurious fixes,
+ // since it may take some time to receive the new remote CapsLock state.
+ if (code == 'CapsLock' && down) {
+ this._remoteCapsLock = null;
+ }
+ if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) {
+ Log.Debug("Fixing remote caps lock");
+
+ this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true);
+ this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false);
+ // We clear the remote capsLock state when we do this to prevent issues with doing this twice
+ // before we receive an update of the the remote state.
+ this._remoteCapsLock = null;
+ }
+
+ // Logic for numlock is exactly the same.
+ if (code == 'NumLock' && down) {
+ this._remoteNumLock = null;
+ }
+ if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) {
+ Log.Debug("Fixing remote num lock");
+ this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true);
+ this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false);
+ this._remoteNumLock = null;
+ }
+ this.sendKey(keysym, code, down);
+ }
+
+ static _convertButtonMask(buttons) {
+ /* The bits in MouseEvent.buttons property correspond
+ * to the following mouse buttons:
+ * 0: Left
+ * 1: Right
+ * 2: Middle
+ * 3: Back
+ * 4: Forward
+ *
+ * These bits needs to be converted to what they are defined as
+ * in the RFB protocol.
+ */
+
+ const buttonMaskMap = {
+ 0: 1 << 0, // Left
+ 1: 1 << 2, // Right
+ 2: 1 << 1, // Middle
+ 3: 1 << 7, // Back
+ 4: 1 << 8, // Forward
+ };
+
+ let bmask = 0;
+ for (let i = 0; i < 5; i++) {
+ if (buttons & (1 << i)) {
+ bmask |= buttonMaskMap[i];
+ }
+ }
+ return bmask;
+ }
+
+ _handleMouse(ev) {
+ /*
+ * We don't check connection status or viewOnly here as the
+ * mouse events might be used to control the viewport
+ */
+
+ if (ev.type === 'click') {
+ /*
+ * 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 (ev.target !== this._canvas) {
+ return;
+ }
+ }
+
+ // FIXME: if we're in view-only and not dragging,
+ // should we stop events?
+ ev.stopPropagation();
+ ev.preventDefault();
+
+ if ((ev.type === 'click') || (ev.type === 'contextmenu')) {
+ return;
+ }
+
+ let pos = clientToElement(ev.clientX, ev.clientY,
+ this._canvas);
+
+ let bmask = RFB._convertButtonMask(ev.buttons);
+
+ let down = ev.type == 'mousedown';
+ switch (ev.type) {
+ case 'mousedown':
+ case 'mouseup':
+ if (this.dragViewport) {
+ if (down && !this._viewportDragging) {
+ this._viewportDragging = true;
+ this._viewportDragPos = {'x': pos.x, 'y': pos.y};
+ this._viewportHasMoved = false;
+
+ this._flushMouseMoveTimer(pos.x, pos.y);
+
+ // Skip sending mouse events, instead save the current
+ // mouse mask so we can send it later.
+ this._mouseButtonMask = bmask;
+ break;
+ } else {
+ this._viewportDragging = false;
+
+ // If we actually performed a drag then we are done
+ // here and should not send any mouse events
+ if (this._viewportHasMoved) {
+ this._mouseButtonMask = bmask;
+ break;
+ }
+ // Otherwise we treat this as a mouse click event.
+ // Send the previously saved button mask, followed
+ // by the current button mask at the end of this
+ // function.
+ this._sendMouse(pos.x, pos.y, this._mouseButtonMask);
+ }
+ }
+ if (down) {
+ setCapture(this._canvas);
+ }
+ this._handleMouseButton(pos.x, pos.y, bmask);
+ break;
+ case 'mousemove':
+ if (this._viewportDragging) {
+ const deltaX = this._viewportDragPos.x - pos.x;
+ const deltaY = this._viewportDragPos.y - pos.y;
+
+ if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
+ Math.abs(deltaY) > dragThreshold)) {
+ this._viewportHasMoved = true;
+
+ this._viewportDragPos = {'x': pos.x, 'y': pos.y};
+ this._display.viewportChangePos(deltaX, deltaY);
+ }
+
+ // Skip sending mouse events
+ break;
+ }
+ this._handleMouseMove(pos.x, pos.y);
+ break;
+ }
+ }
+
+ _handleMouseButton(x, y, bmask) {
+ // Flush waiting move event first
+ this._flushMouseMoveTimer(x, y);
+
+ this._mouseButtonMask = bmask;
+ this._sendMouse(x, y, this._mouseButtonMask);
+ }
+
+ _handleMouseMove(x, y) {
+ this._mousePos = { 'x': x, 'y': y };
+
+ // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
+ if (this._mouseMoveTimer == null) {
+
+ const timeSinceLastMove = Date.now() - this._mouseLastMoveTime;
+ if (timeSinceLastMove > MOUSE_MOVE_DELAY) {
+ this._sendMouse(x, y, this._mouseButtonMask);
+ this._mouseLastMoveTime = Date.now();
+ } else {
+ // Too soon since the latest move, wait the remaining time
+ this._mouseMoveTimer = setTimeout(() => {
+ this._handleDelayedMouseMove();
+ }, MOUSE_MOVE_DELAY - timeSinceLastMove);
+ }
+ }
+ }
+
+ _handleDelayedMouseMove() {
+ this._mouseMoveTimer = null;
+ this._sendMouse(this._mousePos.x, this._mousePos.y,
+ this._mouseButtonMask);
+ this._mouseLastMoveTime = Date.now();
+ }
+
+ _sendMouse(x, y, mask) {
+ if (this._rfbConnectionState !== 'connected') { return; }
+ if (this._viewOnly) { return; } // View only, skip mouse events
+
+ // Highest bit in mask is never sent to the server
+ if (mask & 0x8000) {
+ throw new Error("Illegal mouse button mask (mask: " + mask + ")");
+ }
+
+ let extendedMouseButtons = mask & 0x7f80;
+
+ if (this._extendedPointerEventSupported && extendedMouseButtons) {
+ RFB.messages.extendedPointerEvent(this._sock, this._display.absX(x),
+ this._display.absY(y), mask);
+ } else {
+ RFB.messages.pointerEvent(this._sock, this._display.absX(x),
+ this._display.absY(y), mask);
+ }
+ }
+
+ _handleWheel(ev) {
+ if (this._rfbConnectionState !== 'connected') { return; }
+ if (this._viewOnly) { return; } // View only, skip mouse events
+
+ ev.stopPropagation();
+ ev.preventDefault();
+
+ let pos = clientToElement(ev.clientX, ev.clientY,
+ this._canvas);
+
+ let bmask = RFB._convertButtonMask(ev.buttons);
+ let dX = ev.deltaX;
+ let dY = ev.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 (ev.deltaMode !== 0) {
+ dX *= WHEEL_LINE_HEIGHT;
+ dY *= WHEEL_LINE_HEIGHT;
+ }
+
+ // 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.
+ this._accumulatedWheelDeltaX += dX;
+ this._accumulatedWheelDeltaY += dY;
+
+
+ // Generate a mouse wheel step event when the accumulated delta
+ // for one of the axes is large enough.
+ if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) {
+ if (this._accumulatedWheelDeltaX < 0) {
+ this._handleMouseButton(pos.x, pos.y, bmask | 1 << 5);
+ this._handleMouseButton(pos.x, pos.y, bmask);
+ } else if (this._accumulatedWheelDeltaX > 0) {
+ this._handleMouseButton(pos.x, pos.y, bmask | 1 << 6);
+ this._handleMouseButton(pos.x, pos.y, bmask);
+ }
+
+ this._accumulatedWheelDeltaX = 0;
+ }
+ if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) {
+ if (this._accumulatedWheelDeltaY < 0) {
+ this._handleMouseButton(pos.x, pos.y, bmask | 1 << 3);
+ this._handleMouseButton(pos.x, pos.y, bmask);
+ } else if (this._accumulatedWheelDeltaY > 0) {
+ this._handleMouseButton(pos.x, pos.y, bmask | 1 << 4);
+ this._handleMouseButton(pos.x, pos.y, bmask);
+ }
+
+ this._accumulatedWheelDeltaY = 0;
+ }
+ }
+
+ _fakeMouseMove(ev, elementX, elementY) {
+ this._handleMouseMove(elementX, elementY);
+ this._cursor.move(ev.detail.clientX, ev.detail.clientY);
+ }
+
+ _handleTapEvent(ev, bmask) {
+ let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
+ this._canvas);
+
+ // If the user quickly taps multiple times we assume they meant to
+ // hit the same spot, so slightly adjust coordinates
+
+ if ((this._gestureLastTapTime !== null) &&
+ ((Date.now() - this._gestureLastTapTime) < DOUBLE_TAP_TIMEOUT) &&
+ (this._gestureFirstDoubleTapEv.detail.type === ev.detail.type)) {
+ let dx = this._gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX;
+ let dy = this._gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY;
+ let distance = Math.hypot(dx, dy);
+
+ if (distance < DOUBLE_TAP_THRESHOLD) {
+ pos = clientToElement(this._gestureFirstDoubleTapEv.detail.clientX,
+ this._gestureFirstDoubleTapEv.detail.clientY,
+ this._canvas);
+ } else {
+ this._gestureFirstDoubleTapEv = ev;
+ }
+ } else {
+ this._gestureFirstDoubleTapEv = ev;
+ }
+ this._gestureLastTapTime = Date.now();
+
+ this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y);
+ this._handleMouseButton(pos.x, pos.y, bmask);
+ this._handleMouseButton(pos.x, pos.y, 0x0);
+ }
+
+ _handleGesture(ev) {
+ let magnitude;
+
+ let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
+ this._canvas);
+ switch (ev.type) {
+ case 'gesturestart':
+ switch (ev.detail.type) {
+ case 'onetap':
+ this._handleTapEvent(ev, 0x1);
+ break;
+ case 'twotap':
+ this._handleTapEvent(ev, 0x4);
+ break;
+ case 'threetap':
+ this._handleTapEvent(ev, 0x2);
+ break;
+ case 'drag':
+ if (this.dragViewport) {
+ this._viewportHasMoved = false;
+ this._viewportDragging = true;
+ this._viewportDragPos = {'x': pos.x, 'y': pos.y};
+ } else {
+ this._fakeMouseMove(ev, pos.x, pos.y);
+ this._handleMouseButton(pos.x, pos.y, 0x1);
+ }
+ break;
+ case 'longpress':
+ if (this.dragViewport) {
+ // If dragViewport is true, we need to wait to see
+ // if we have dragged outside the threshold before
+ // sending any events to the server.
+ this._viewportHasMoved = false;
+ this._viewportDragPos = {'x': pos.x, 'y': pos.y};
+ } else {
+ this._fakeMouseMove(ev, pos.x, pos.y);
+ this._handleMouseButton(pos.x, pos.y, 0x4);
+ }
+ break;
+ case 'twodrag':
+ this._gestureLastMagnitudeX = ev.detail.magnitudeX;
+ this._gestureLastMagnitudeY = ev.detail.magnitudeY;
+ this._fakeMouseMove(ev, pos.x, pos.y);
+ break;
+ case 'pinch':
+ this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX,
+ ev.detail.magnitudeY);
+ this._fakeMouseMove(ev, pos.x, pos.y);
+ break;
+ }
+ break;
+
+ case 'gesturemove':
+ switch (ev.detail.type) {
+ case 'onetap':
+ case 'twotap':
+ case 'threetap':
+ break;
+ case 'drag':
+ case 'longpress':
+ if (this.dragViewport) {
+ this._viewportDragging = true;
+ const deltaX = this._viewportDragPos.x - pos.x;
+ const deltaY = this._viewportDragPos.y - pos.y;
+
+ if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
+ Math.abs(deltaY) > dragThreshold)) {
+ this._viewportHasMoved = true;
+
+ this._viewportDragPos = {'x': pos.x, 'y': pos.y};
+ this._display.viewportChangePos(deltaX, deltaY);
+ }
+ } else {
+ this._fakeMouseMove(ev, pos.x, pos.y);
+ }
+ break;
+ case 'twodrag':
+ // Always scroll in the same position.
+ // We don't know if the mouse was moved so we need to move it
+ // every update.
+ this._fakeMouseMove(ev, pos.x, pos.y);
+ while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {
+ this._handleMouseButton(pos.x, pos.y, 0x8);
+ this._handleMouseButton(pos.x, pos.y, 0x0);
+ this._gestureLastMagnitudeY += GESTURE_SCRLSENS;
+ }
+ while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) {
+ this._handleMouseButton(pos.x, pos.y, 0x10);
+ this._handleMouseButton(pos.x, pos.y, 0x0);
+ this._gestureLastMagnitudeY -= GESTURE_SCRLSENS;
+ }
+ while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) {
+ this._handleMouseButton(pos.x, pos.y, 0x20);
+ this._handleMouseButton(pos.x, pos.y, 0x0);
+ this._gestureLastMagnitudeX += GESTURE_SCRLSENS;
+ }
+ while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) {
+ this._handleMouseButton(pos.x, pos.y, 0x40);
+ this._handleMouseButton(pos.x, pos.y, 0x0);
+ this._gestureLastMagnitudeX -= GESTURE_SCRLSENS;
+ }
+ break;
+ case 'pinch':
+ // Always scroll in the same position.
+ // We don't know if the mouse was moved so we need to move it
+ // every update.
+ this._fakeMouseMove(ev, pos.x, pos.y);
+ magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);
+ if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
+ this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
+ while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
+ this._handleMouseButton(pos.x, pos.y, 0x8);
+ this._handleMouseButton(pos.x, pos.y, 0x0);
+ this._gestureLastMagnitudeX += GESTURE_ZOOMSENS;
+ }
+ while ((magnitude - this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) {
+ this._handleMouseButton(pos.x, pos.y, 0x10);
+ this._handleMouseButton(pos.x, pos.y, 0x0);
+ this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS;
+ }
+ }
+ this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", false);
+ break;
+ }
+ break;
+
+ case 'gestureend':
+ switch (ev.detail.type) {
+ case 'onetap':
+ case 'twotap':
+ case 'threetap':
+ case 'pinch':
+ case 'twodrag':
+ break;
+ case 'drag':
+ if (this.dragViewport) {
+ this._viewportDragging = false;
+ } else {
+ this._fakeMouseMove(ev, pos.x, pos.y);
+ this._handleMouseButton(pos.x, pos.y, 0x0);
+ }
+ break;
+ case 'longpress':
+ if (this._viewportHasMoved) {
+ // We don't want to send any events if we have moved
+ // our viewport
+ break;
+ }
+
+ if (this.dragViewport && !this._viewportHasMoved) {
+ this._fakeMouseMove(ev, pos.x, pos.y);
+ // If dragViewport is true, we need to wait to see
+ // if we have dragged outside the threshold before
+ // sending any events to the server.
+ this._handleMouseButton(pos.x, pos.y, 0x4);
+ this._handleMouseButton(pos.x, pos.y, 0x0);
+ this._viewportDragging = false;
+ } else {
+ this._fakeMouseMove(ev, pos.x, pos.y);
+ this._handleMouseButton(pos.x, pos.y, 0x0);
+ }
+ break;
+ }
+ break;
+ }
+ }
+
+ _flushMouseMoveTimer(x, y) {
+ if (this._mouseMoveTimer !== null) {
+ clearTimeout(this._mouseMoveTimer);
+ this._mouseMoveTimer = null;
+ this._sendMouse(x, y, this._mouseButtonMask);
+ }
+ }
+
+ // Message handlers
+
+ _negotiateProtocolVersion() {
+ if (this._sock.rQwait("version", 12)) {
+ return false;
+ }
+
+ const sversion = this._sock.rQshiftStr(12).substr(4, 7);
+ Log.Info("Server ProtocolVersion: " + sversion);
+ let isRepeater = 0;
+ switch (sversion) {
+ case "000.000": // UltraVNC repeater
+ isRepeater = 1;
+ break;
+ case "003.003":
+ case "003.006": // UltraVNC
+ this._rfbVersion = 3.3;
+ break;
+ case "003.007":
+ this._rfbVersion = 3.7;
+ break;
+ case "003.008":
+ case "003.889": // Apple Remote Desktop
+ case "004.000": // Intel AMT KVM
+ case "004.001": // RealVNC 4.6
+ case "005.000": // RealVNC 5.3
+ this._rfbVersion = 3.8;
+ break;
+ default:
+ return this._fail("Invalid server version " + sversion);
+ }
+
+ if (isRepeater) {
+ let repeaterID = "ID:" + this._repeaterID;
+ while (repeaterID.length < 250) {
+ repeaterID += "\0";
+ }
+ this._sock.sQpushString(repeaterID);
+ this._sock.flush();
+ return true;
+ }
+
+ if (this._rfbVersion > this._rfbMaxVersion) {
+ this._rfbVersion = this._rfbMaxVersion;
+ }
+
+ const cversion = "00" + parseInt(this._rfbVersion, 10) +
+ ".00" + ((this._rfbVersion * 10) % 10);
+ this._sock.sQpushString("RFB " + cversion + "\n");
+ this._sock.flush();
+ Log.Debug('Sent ProtocolVersion: ' + cversion);
+
+ this._rfbInitState = 'Security';
+ }
+
+ _isSupportedSecurityType(type) {
+ const clientTypes = [
+ securityTypeNone,
+ securityTypeVNCAuth,
+ securityTypeRA2ne,
+ securityTypeTight,
+ securityTypeVeNCrypt,
+ securityTypeXVP,
+ securityTypeARD,
+ securityTypeMSLogonII,
+ securityTypePlain,
+ ];
+
+ return clientTypes.includes(type);
+ }
+
+ _negotiateSecurity() {
+ if (this._rfbVersion >= 3.7) {
+ // Server sends supported list, client decides
+ const numTypes = this._sock.rQshift8();
+ if (this._sock.rQwait("security type", numTypes, 1)) { return false; }
+
+ if (numTypes === 0) {
+ this._rfbInitState = "SecurityReason";
+ this._securityContext = "no security types";
+ this._securityStatus = 1;
+ return true;
+ }
+
+ const types = this._sock.rQshiftBytes(numTypes);
+ Log.Debug("Server security types: " + types);
+
+ // Look for a matching security type in the order that the
+ // server prefers
+ this._rfbAuthScheme = -1;
+ for (let type of types) {
+ if (this._isSupportedSecurityType(type)) {
+ this._rfbAuthScheme = type;
+ break;
+ }
+ }
+
+ if (this._rfbAuthScheme === -1) {
+ return this._fail("Unsupported security types (types: " + types + ")");
+ }
+
+ this._sock.sQpush8(this._rfbAuthScheme);
+ this._sock.flush();
+ } else {
+ // Server decides
+ if (this._sock.rQwait("security scheme", 4)) { return false; }
+ this._rfbAuthScheme = this._sock.rQshift32();
+
+ if (this._rfbAuthScheme == 0) {
+ this._rfbInitState = "SecurityReason";
+ this._securityContext = "authentication scheme";
+ this._securityStatus = 1;
+ return true;
+ }
+ }
+
+ this._rfbInitState = 'Authentication';
+ Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
+
+ return true;
+ }
+
+ _handleSecurityReason() {
+ if (this._sock.rQwait("reason length", 4)) {
+ return false;
+ }
+ const strlen = this._sock.rQshift32();
+ let reason = "";
+
+ if (strlen > 0) {
+ if (this._sock.rQwait("reason", strlen, 4)) { return false; }
+ reason = this._sock.rQshiftStr(strlen);
+ }
+
+ if (reason !== "") {
+ this.dispatchEvent(new CustomEvent(
+ "securityfailure",
+ { detail: { status: this._securityStatus,
+ reason: reason } }));
+
+ return this._fail("Security negotiation failed on " +
+ this._securityContext +
+ " (reason: " + reason + ")");
+ } else {
+ this.dispatchEvent(new CustomEvent(
+ "securityfailure",
+ { detail: { status: this._securityStatus } }));
+
+ return this._fail("Security negotiation failed on " +
+ this._securityContext);
+ }
+ }
+
+ // authentication
+ _negotiateXvpAuth() {
+ if (this._rfbCredentials.username === undefined ||
+ this._rfbCredentials.password === undefined ||
+ this._rfbCredentials.target === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["username", "password", "target"] } }));
+ return false;
+ }
+
+ this._sock.sQpush8(this._rfbCredentials.username.length);
+ this._sock.sQpush8(this._rfbCredentials.target.length);
+ this._sock.sQpushString(this._rfbCredentials.username);
+ this._sock.sQpushString(this._rfbCredentials.target);
+
+ this._sock.flush();
+
+ this._rfbAuthScheme = securityTypeVNCAuth;
+
+ return this._negotiateAuthentication();
+ }
+
+ // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
+ _negotiateVeNCryptAuth() {
+
+ // waiting for VeNCrypt version
+ if (this._rfbVeNCryptState == 0) {
+ if (this._sock.rQwait("vencrypt version", 2)) { return false; }
+
+ const major = this._sock.rQshift8();
+ const minor = this._sock.rQshift8();
+
+ if (!(major == 0 && minor == 2)) {
+ return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
+ }
+
+ this._sock.sQpush8(0);
+ this._sock.sQpush8(2);
+ this._sock.flush();
+ this._rfbVeNCryptState = 1;
+ }
+
+ // waiting for ACK
+ if (this._rfbVeNCryptState == 1) {
+ if (this._sock.rQwait("vencrypt ack", 1)) { return false; }
+
+ const res = this._sock.rQshift8();
+
+ if (res != 0) {
+ return this._fail("VeNCrypt failure " + res);
+ }
+
+ this._rfbVeNCryptState = 2;
+ }
+ // must fall through here (i.e. no "else if"), beacause we may have already received
+ // the subtypes length and won't be called again
+
+ if (this._rfbVeNCryptState == 2) { // waiting for subtypes length
+ if (this._sock.rQwait("vencrypt subtypes length", 1)) { return false; }
+
+ const subtypesLength = this._sock.rQshift8();
+ if (subtypesLength < 1) {
+ return this._fail("VeNCrypt subtypes empty");
+ }
+
+ this._rfbVeNCryptSubtypesLength = subtypesLength;
+ this._rfbVeNCryptState = 3;
+ }
+
+ // waiting for subtypes list
+ if (this._rfbVeNCryptState == 3) {
+ if (this._sock.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength)) { return false; }
+
+ const subtypes = [];
+ for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) {
+ subtypes.push(this._sock.rQshift32());
+ }
+
+ // Look for a matching security type in the order that the
+ // server prefers
+ this._rfbAuthScheme = -1;
+ for (let type of subtypes) {
+ // Avoid getting in to a loop
+ if (type === securityTypeVeNCrypt) {
+ continue;
+ }
+
+ if (this._isSupportedSecurityType(type)) {
+ this._rfbAuthScheme = type;
+ break;
+ }
+ }
+
+ if (this._rfbAuthScheme === -1) {
+ return this._fail("Unsupported security types (types: " + subtypes + ")");
+ }
+
+ this._sock.sQpush32(this._rfbAuthScheme);
+ this._sock.flush();
+
+ this._rfbVeNCryptState = 4;
+ return true;
+ }
+ }
+
+ _negotiatePlainAuth() {
+ if (this._rfbCredentials.username === undefined ||
+ this._rfbCredentials.password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["username", "password"] } }));
+ return false;
+ }
+
+ const user = encodeUTF8(this._rfbCredentials.username);
+ const pass = encodeUTF8(this._rfbCredentials.password);
+
+ this._sock.sQpush32(user.length);
+ this._sock.sQpush32(pass.length);
+ this._sock.sQpushString(user);
+ this._sock.sQpushString(pass);
+ this._sock.flush();
+
+ this._rfbInitState = "SecurityResult";
+ return true;
+ }
+
+ _negotiateStdVNCAuth() {
+ if (this._sock.rQwait("auth challenge", 16)) { return false; }
+
+ if (this._rfbCredentials.password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["password"] } }));
+ return false;
+ }
+
+ // TODO(directxman12): make genDES not require an Array
+ const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
+ const response = RFB.genDES(this._rfbCredentials.password, challenge);
+ this._sock.sQpushBytes(response);
+ this._sock.flush();
+ this._rfbInitState = "SecurityResult";
+ return true;
+ }
+
+ _negotiateARDAuth() {
+
+ if (this._rfbCredentials.username === undefined ||
+ this._rfbCredentials.password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["username", "password"] } }));
+ return false;
+ }
+
+ if (this._rfbCredentials.ardPublicKey != undefined &&
+ this._rfbCredentials.ardCredentials != undefined) {
+ // if the async web crypto is done return the results
+ this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);
+ this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);
+ this._sock.flush();
+ this._rfbCredentials.ardCredentials = null;
+ this._rfbCredentials.ardPublicKey = null;
+ this._rfbInitState = "SecurityResult";
+ return true;
+ }
+
+ if (this._sock.rQwait("read ard", 4)) { return false; }
+
+ let generator = this._sock.rQshiftBytes(2); // DH base generator value
+
+ let keyLength = this._sock.rQshift16();
+
+ if (this._sock.rQwait("read ard keylength", keyLength*2, 4)) { return false; }
+
+ // read the server values
+ let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
+ let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
+
+ let clientKey = legacyCrypto.generateKey(
+ { name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
+ this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);
+
+ return false;
+ }
+
+ async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {
+ const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
+ const sharedKey = legacyCrypto.deriveBits(
+ { name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);
+
+ const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
+ const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
+
+ const credentials = window.crypto.getRandomValues(new Uint8Array(128));
+ for (let i = 0; i < username.length; i++) {
+ credentials[i] = username.charCodeAt(i);
+ }
+ credentials[username.length] = 0;
+ for (let i = 0; i < password.length; i++) {
+ credentials[64 + i] = password.charCodeAt(i);
+ }
+ credentials[64 + password.length] = 0;
+
+ const key = await legacyCrypto.digest("MD5", sharedKey);
+ const cipher = await legacyCrypto.importKey(
+ "raw", key, { name: "AES-ECB" }, false, ["encrypt"]);
+ const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);
+
+ this._rfbCredentials.ardCredentials = encrypted;
+ this._rfbCredentials.ardPublicKey = clientPublicKey;
+
+ this._resumeAuthentication();
+ }
+
+ _negotiateTightUnixAuth() {
+ if (this._rfbCredentials.username === undefined ||
+ this._rfbCredentials.password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["username", "password"] } }));
+ return false;
+ }
+
+ this._sock.sQpush32(this._rfbCredentials.username.length);
+ this._sock.sQpush32(this._rfbCredentials.password.length);
+ this._sock.sQpushString(this._rfbCredentials.username);
+ this._sock.sQpushString(this._rfbCredentials.password);
+ this._sock.flush();
+
+ this._rfbInitState = "SecurityResult";
+ return true;
+ }
+
+ _negotiateTightTunnels(numTunnels) {
+ const clientSupportedTunnelTypes = {
+ 0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
+ };
+ const serverSupportedTunnelTypes = {};
+ // receive tunnel capabilities
+ for (let i = 0; i < numTunnels; i++) {
+ const capCode = this._sock.rQshift32();
+ const capVendor = this._sock.rQshiftStr(4);
+ const capSignature = this._sock.rQshiftStr(8);
+ serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature };
+ }
+
+ Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);
+
+ // Siemens touch panels have a VNC server that supports NOTUNNEL,
+ // but forgets to advertise it. Try to detect such servers by
+ // looking for their custom tunnel type.
+ if (serverSupportedTunnelTypes[1] &&
+ (serverSupportedTunnelTypes[1].vendor === "SICR") &&
+ (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {
+ Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
+ serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };
+ }
+
+ // choose the notunnel type
+ if (serverSupportedTunnelTypes[0]) {
+ if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
+ serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
+ return this._fail("Client's tunnel type had the incorrect " +
+ "vendor or signature");
+ }
+ Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
+ this._sock.sQpush32(0); // use NOTUNNEL
+ this._sock.flush();
+ return false; // wait until we receive the sub auth count to continue
+ } else {
+ return this._fail("Server wanted tunnels, but doesn't support " +
+ "the notunnel type");
+ }
+ }
+
+ _negotiateTightAuth() {
+ if (!this._rfbTightVNC) { // first pass, do the tunnel negotiation
+ if (this._sock.rQwait("num tunnels", 4)) { return false; }
+ const numTunnels = this._sock.rQshift32();
+ if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
+
+ this._rfbTightVNC = true;
+
+ if (numTunnels > 0) {
+ this._negotiateTightTunnels(numTunnels);
+ return false; // wait until we receive the sub auth to continue
+ }
+ }
+
+ // second pass, do the sub-auth negotiation
+ if (this._sock.rQwait("sub auth count", 4)) { return false; }
+ const subAuthCount = this._sock.rQshift32();
+ if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected
+ this._rfbInitState = 'SecurityResult';
+ return true;
+ }
+
+ if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
+
+ const clientSupportedTypes = {
+ 'STDVNOAUTH__': 1,
+ 'STDVVNCAUTH_': 2,
+ 'TGHTULGNAUTH': 129
+ };
+
+ const serverSupportedTypes = [];
+
+ for (let i = 0; i < subAuthCount; i++) {
+ this._sock.rQshift32(); // capNum
+ const capabilities = this._sock.rQshiftStr(12);
+ serverSupportedTypes.push(capabilities);
+ }
+
+ Log.Debug("Server Tight authentication types: " + serverSupportedTypes);
+
+ for (let authType in clientSupportedTypes) {
+ if (serverSupportedTypes.indexOf(authType) != -1) {
+ this._sock.sQpush32(clientSupportedTypes[authType]);
+ this._sock.flush();
+ Log.Debug("Selected authentication type: " + authType);
+
+ switch (authType) {
+ case 'STDVNOAUTH__': // no auth
+ this._rfbInitState = 'SecurityResult';
+ return true;
+ case 'STDVVNCAUTH_':
+ this._rfbAuthScheme = securityTypeVNCAuth;
+ return true;
+ case 'TGHTULGNAUTH':
+ this._rfbAuthScheme = securityTypeUnixLogon;
+ return true;
+ default:
+ return this._fail("Unsupported tiny auth scheme " +
+ "(scheme: " + authType + ")");
+ }
+ }
+ }
+
+ return this._fail("No supported sub-auth types!");
+ }
+
+ _handleRSAAESCredentialsRequired(event) {
+ this.dispatchEvent(event);
+ }
+
+ _handleRSAAESServerVerification(event) {
+ this.dispatchEvent(event);
+ }
+
+ _negotiateRA2neAuth() {
+ if (this._rfbRSAAESAuthenticationState === null) {
+ this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials);
+ this._rfbRSAAESAuthenticationState.addEventListener(
+ "serververification", this._eventHandlers.handleRSAAESServerVerification);
+ this._rfbRSAAESAuthenticationState.addEventListener(
+ "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
+ }
+ this._rfbRSAAESAuthenticationState.checkInternalEvents();
+ if (!this._rfbRSAAESAuthenticationState.hasStarted) {
+ this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync()
+ .catch((e) => {
+ if (e.message !== "disconnect normally") {
+ this._fail(e.message);
+ }
+ })
+ .then(() => {
+ this._rfbInitState = "SecurityResult";
+ return true;
+ }).finally(() => {
+ this._rfbRSAAESAuthenticationState.removeEventListener(
+ "serververification", this._eventHandlers.handleRSAAESServerVerification);
+ this._rfbRSAAESAuthenticationState.removeEventListener(
+ "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
+ this._rfbRSAAESAuthenticationState = null;
+ });
+ }
+ return false;
+ }
+
+ _negotiateMSLogonIIAuth() {
+ if (this._sock.rQwait("mslogonii dh param", 24)) { return false; }
+
+ if (this._rfbCredentials.username === undefined ||
+ this._rfbCredentials.password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["username", "password"] } }));
+ return false;
+ }
+
+ const g = this._sock.rQshiftBytes(8);
+ const p = this._sock.rQshiftBytes(8);
+ const A = this._sock.rQshiftBytes(8);
+ const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);
+ const B = legacyCrypto.exportKey("raw", dhKey.publicKey);
+ const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64);
+
+ const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]);
+ const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
+ const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
+ let usernameBytes = new Uint8Array(256);
+ let passwordBytes = new Uint8Array(64);
+ window.crypto.getRandomValues(usernameBytes);
+ window.crypto.getRandomValues(passwordBytes);
+ for (let i = 0; i < username.length; i++) {
+ usernameBytes[i] = username.charCodeAt(i);
+ }
+ usernameBytes[username.length] = 0;
+ for (let i = 0; i < password.length; i++) {
+ passwordBytes[i] = password.charCodeAt(i);
+ }
+ passwordBytes[password.length] = 0;
+ usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);
+ passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);
+ this._sock.sQpushBytes(B);
+ this._sock.sQpushBytes(usernameBytes);
+ this._sock.sQpushBytes(passwordBytes);
+ this._sock.flush();
+ this._rfbInitState = "SecurityResult";
+ return true;
+ }
+
+ _negotiateAuthentication() {
+ switch (this._rfbAuthScheme) {
+ case securityTypeNone:
+ if (this._rfbVersion >= 3.8) {
+ this._rfbInitState = 'SecurityResult';
+ } else {
+ this._rfbInitState = 'ClientInitialisation';
+ }
+ return true;
+
+ case securityTypeXVP:
+ return this._negotiateXvpAuth();
+
+ case securityTypeARD:
+ return this._negotiateARDAuth();
+
+ case securityTypeVNCAuth:
+ return this._negotiateStdVNCAuth();
+
+ case securityTypeTight:
+ return this._negotiateTightAuth();
+
+ case securityTypeVeNCrypt:
+ return this._negotiateVeNCryptAuth();
+
+ case securityTypePlain:
+ return this._negotiatePlainAuth();
+
+ case securityTypeUnixLogon:
+ return this._negotiateTightUnixAuth();
+
+ case securityTypeRA2ne:
+ return this._negotiateRA2neAuth();
+
+ case securityTypeMSLogonII:
+ return this._negotiateMSLogonIIAuth();
+
+ default:
+ return this._fail("Unsupported auth scheme (scheme: " +
+ this._rfbAuthScheme + ")");
+ }
+ }
+
+ _handleSecurityResult() {
+ if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
+
+ const status = this._sock.rQshift32();
+
+ if (status === 0) { // OK
+ this._rfbInitState = 'ClientInitialisation';
+ Log.Debug('Authentication OK');
+ return true;
+ } else {
+ if (this._rfbVersion >= 3.8) {
+ this._rfbInitState = "SecurityReason";
+ this._securityContext = "security result";
+ this._securityStatus = status;
+ return true;
+ } else {
+ this.dispatchEvent(new CustomEvent(
+ "securityfailure",
+ { detail: { status: status } }));
+
+ return this._fail("Security handshake failed");
+ }
+ }
+ }
+
+ _negotiateServerInit() {
+ if (this._sock.rQwait("server initialization", 24)) { return false; }
+
+ /* Screen size */
+ const width = this._sock.rQshift16();
+ const height = this._sock.rQshift16();
+
+ /* PIXEL_FORMAT */
+ const bpp = this._sock.rQshift8();
+ const depth = this._sock.rQshift8();
+ const bigEndian = this._sock.rQshift8();
+ const trueColor = this._sock.rQshift8();
+
+ const redMax = this._sock.rQshift16();
+ const greenMax = this._sock.rQshift16();
+ const blueMax = this._sock.rQshift16();
+ const redShift = this._sock.rQshift8();
+ const greenShift = this._sock.rQshift8();
+ const blueShift = this._sock.rQshift8();
+ this._sock.rQskipBytes(3); // padding
+
+ // NB(directxman12): we don't want to call any callbacks or print messages until
+ // *after* we're past the point where we could backtrack
+
+ /* Connection name/title */
+ const nameLength = this._sock.rQshift32();
+ if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
+ let name = this._sock.rQshiftStr(nameLength);
+ name = decodeUTF8(name, true);
+
+ if (this._rfbTightVNC) {
+ if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
+ // In TightVNC mode, ServerInit message is extended
+ const numServerMessages = this._sock.rQshift16();
+ const numClientMessages = this._sock.rQshift16();
+ const numEncodings = this._sock.rQshift16();
+ this._sock.rQskipBytes(2); // padding
+
+ const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
+ if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
+
+ // we don't actually do anything with the capability information that TIGHT sends,
+ // so we just skip the all of this.
+
+ // TIGHT server message capabilities
+ this._sock.rQskipBytes(16 * numServerMessages);
+
+ // TIGHT client message capabilities
+ this._sock.rQskipBytes(16 * numClientMessages);
+
+ // TIGHT encoding capabilities
+ this._sock.rQskipBytes(16 * numEncodings);
+ }
+
+ // NB(directxman12): these are down here so that we don't run them multiple times
+ // if we backtrack
+ Log.Info("Screen: " + width + "x" + height +
+ ", bpp: " + bpp + ", depth: " + depth +
+ ", bigEndian: " + bigEndian +
+ ", trueColor: " + trueColor +
+ ", redMax: " + redMax +
+ ", greenMax: " + greenMax +
+ ", blueMax: " + blueMax +
+ ", redShift: " + redShift +
+ ", greenShift: " + greenShift +
+ ", blueShift: " + blueShift);
+
+ // we're past the point where we could backtrack, so it's safe to call this
+ this._setDesktopName(name);
+ this._resize(width, height);
+
+ if (!this._viewOnly) {
+ this._keyboard.grab();
+ this._asyncClipboard.grab();
+ }
+
+ this._fbDepth = 24;
+
+ if (this._fbName === "Intel(r) AMT KVM") {
+ Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
+ this._fbDepth = 8;
+ }
+
+ RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
+ this._sendEncodings();
+ RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
+
+ this._updateConnectionState('connected');
+ return true;
+ }
+
+ _sendEncodings() {
+ const encs = [];
+
+ // In preference order
+ encs.push(encodings.encodingCopyRect);
+ // Only supported with full depth support
+ if (this._fbDepth == 24) {
+ if (supportsWebCodecsH264Decode) {
+ encs.push(encodings.encodingH264);
+ }
+ encs.push(encodings.encodingTight);
+ encs.push(encodings.encodingTightPNG);
+ encs.push(encodings.encodingZRLE);
+ encs.push(encodings.encodingJPEG);
+ encs.push(encodings.encodingHextile);
+ encs.push(encodings.encodingRRE);
+ encs.push(encodings.encodingZlib);
+ }
+ encs.push(encodings.encodingRaw);
+
+ // Psuedo-encoding settings
+ encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
+ encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
+
+ encs.push(encodings.pseudoEncodingDesktopSize);
+ encs.push(encodings.pseudoEncodingLastRect);
+ encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
+ encs.push(encodings.pseudoEncodingQEMULedEvent);
+ encs.push(encodings.pseudoEncodingExtendedDesktopSize);
+ encs.push(encodings.pseudoEncodingXvp);
+ encs.push(encodings.pseudoEncodingFence);
+ encs.push(encodings.pseudoEncodingContinuousUpdates);
+ encs.push(encodings.pseudoEncodingDesktopName);
+ encs.push(encodings.pseudoEncodingExtendedClipboard);
+ encs.push(encodings.pseudoEncodingExtendedMouseButtons);
+
+ if (this._fbDepth == 24) {
+ encs.push(encodings.pseudoEncodingVMwareCursor);
+ encs.push(encodings.pseudoEncodingCursor);
+ }
+
+ RFB.messages.clientEncodings(this._sock, encs);
+ }
+
+ /* RFB protocol initialization states:
+ * ProtocolVersion
+ * Security
+ * Authentication
+ * SecurityResult
+ * ClientInitialization - not triggered by server message
+ * ServerInitialization
+ */
+ _initMsg() {
+ switch (this._rfbInitState) {
+ case 'ProtocolVersion':
+ return this._negotiateProtocolVersion();
+
+ case 'Security':
+ return this._negotiateSecurity();
+
+ case 'Authentication':
+ return this._negotiateAuthentication();
+
+ case 'SecurityResult':
+ return this._handleSecurityResult();
+
+ case 'SecurityReason':
+ return this._handleSecurityReason();
+
+ case 'ClientInitialisation':
+ this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation
+ this._sock.flush();
+ this._rfbInitState = 'ServerInitialisation';
+ return true;
+
+ case 'ServerInitialisation':
+ return this._negotiateServerInit();
+
+ default:
+ return this._fail("Unknown init state (state: " +
+ this._rfbInitState + ")");
+ }
+ }
+
+ // Resume authentication handshake after it was paused for some
+ // reason, e.g. waiting for a password from the user
+ _resumeAuthentication() {
+ // We use setTimeout() so it's run in its own context, just like
+ // it originally did via the WebSocket's event handler
+ setTimeout(this._initMsg.bind(this), 0);
+ }
+
+ _handleSetColourMapMsg() {
+ Log.Debug("SetColorMapEntries");
+
+ return this._fail("Unexpected SetColorMapEntries message");
+ }
+
+ _writeClipboard(text) {
+ if (this._viewOnly) return;
+ if (this._asyncClipboard.writeClipboard(text)) return;
+ // Fallback clipboard
+ this.dispatchEvent(
+ new CustomEvent("clipboard", {detail: {text: text}})
+ );
+ }
+
+ _handleServerCutText() {
+ Log.Debug("ServerCutText");
+
+ if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
+
+ this._sock.rQskipBytes(3); // Padding
+
+ let length = this._sock.rQshift32();
+ length = toSigned32bit(length);
+
+ if (this._sock.rQwait("ServerCutText content", Math.abs(length), 8)) { return false; }
+
+ if (length >= 0) {
+ //Standard msg
+ const text = this._sock.rQshiftStr(length);
+ if (this._viewOnly) {
+ return true;
+ }
+
+ this._writeClipboard(text);
+
+ } else {
+ //Extended msg.
+ length = Math.abs(length);
+ const flags = this._sock.rQshift32();
+ let formats = flags & 0x0000FFFF;
+ let actions = flags & 0xFF000000;
+
+ let isCaps = (!!(actions & extendedClipboardActionCaps));
+ if (isCaps) {
+ this._clipboardServerCapabilitiesFormats = {};
+ this._clipboardServerCapabilitiesActions = {};
+
+ // Update our server capabilities for Formats
+ for (let i = 0; i <= 15; i++) {
+ let index = 1 << i;
+
+ // Check if format flag is set.
+ if ((formats & index)) {
+ this._clipboardServerCapabilitiesFormats[index] = true;
+ // We don't send unsolicited clipboard, so we
+ // ignore the size
+ this._sock.rQshift32();
+ }
+ }
+
+ // Update our server capabilities for Actions
+ for (let i = 24; i <= 31; i++) {
+ let index = 1 << i;
+ this._clipboardServerCapabilitiesActions[index] = !!(actions & index);
+ }
+
+ /* Caps handling done, send caps with the clients
+ capabilities set as a response */
+ let clientActions = [
+ extendedClipboardActionCaps,
+ extendedClipboardActionRequest,
+ extendedClipboardActionPeek,
+ extendedClipboardActionNotify,
+ extendedClipboardActionProvide
+ ];
+ RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});
+
+ } else if (actions === extendedClipboardActionRequest) {
+ if (this._viewOnly) {
+ return true;
+ }
+
+ // Check if server has told us it can handle Provide and there is clipboard data to send.
+ if (this._clipboardText != null &&
+ this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {
+
+ if (formats & extendedClipboardFormatText) {
+ RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);
+ }
+ }
+
+ } else if (actions === extendedClipboardActionPeek) {
+ if (this._viewOnly) {
+ return true;
+ }
+
+ if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
+
+ if (this._clipboardText != null) {
+ RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
+ } else {
+ RFB.messages.extendedClipboardNotify(this._sock, []);
+ }
+ }
+
+ } else if (actions === extendedClipboardActionNotify) {
+ if (this._viewOnly) {
+ return true;
+ }
+
+ if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {
+
+ if (formats & extendedClipboardFormatText) {
+ RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);
+ }
+ }
+
+ } else if (actions === extendedClipboardActionProvide) {
+ if (this._viewOnly) {
+ return true;
+ }
+
+ if (!(formats & extendedClipboardFormatText)) {
+ return true;
+ }
+ // Ignore what we had in our clipboard client side.
+ this._clipboardText = null;
+
+ // FIXME: Should probably verify that this data was actually requested
+ let zlibStream = this._sock.rQshiftBytes(length - 4);
+ let streamInflator = new Inflator();
+ let textData = null;
+
+ streamInflator.setInput(zlibStream);
+ for (let i = 0; i <= 15; i++) {
+ let format = 1 << i;
+
+ if (formats & format) {
+
+ let size = 0x00;
+ let sizeArray = streamInflator.inflate(4);
+
+ size |= (sizeArray[0] << 24);
+ size |= (sizeArray[1] << 16);
+ size |= (sizeArray[2] << 8);
+ size |= (sizeArray[3]);
+ let chunk = streamInflator.inflate(size);
+
+ if (format === extendedClipboardFormatText) {
+ textData = chunk;
+ }
+ }
+ }
+ streamInflator.setInput(null);
+
+ if (textData !== null) {
+ let tmpText = "";
+ for (let i = 0; i < textData.length; i++) {
+ tmpText += String.fromCharCode(textData[i]);
+ }
+ textData = tmpText;
+
+ textData = decodeUTF8(textData);
+ if ((textData.length > 0) && "\0" === textData.charAt(textData.length - 1)) {
+ textData = textData.slice(0, -1);
+ }
+
+ textData = textData.replaceAll("\r\n", "\n");
+
+ this._writeClipboard(textData);
+ }
+ } else {
+ return this._fail("Unexpected action in extended clipboard message: " + actions);
+ }
+ }
+ return true;
+ }
+
+ _handleServerFenceMsg() {
+ if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }
+ this._sock.rQskipBytes(3); // Padding
+ let flags = this._sock.rQshift32();
+ let length = this._sock.rQshift8();
+
+ if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }
+
+ if (length > 64) {
+ Log.Warn("Bad payload length (" + length + ") in fence response");
+ length = 64;
+ }
+
+ const payload = this._sock.rQshiftStr(length);
+
+ this._supportsFence = true;
+
+ /*
+ * Fence flags
+ *
+ * (1<<0) - BlockBefore
+ * (1<<1) - BlockAfter
+ * (1<<2) - SyncNext
+ * (1<<31) - Request
+ */
+
+ if (!(flags & (1<<31))) {
+ return this._fail("Unexpected fence response");
+ }
+
+ // Filter out unsupported flags
+ // FIXME: support syncNext
+ flags &= (1<<0) | (1<<1);
+
+ // BlockBefore and BlockAfter are automatically handled by
+ // the fact that we process each incoming message
+ // synchronuosly.
+ RFB.messages.clientFence(this._sock, flags, payload);
+
+ return true;
+ }
+
+ _handleXvpMsg() {
+ if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
+ this._sock.rQskipBytes(1); // Padding
+ const xvpVer = this._sock.rQshift8();
+ const xvpMsg = this._sock.rQshift8();
+
+ switch (xvpMsg) {
+ case 0: // XVP_FAIL
+ Log.Error("XVP operation failed");
+ break;
+ case 1: // XVP_INIT
+ this._rfbXvpVer = xvpVer;
+ Log.Info("XVP extensions enabled (version " + this._rfbXvpVer + ")");
+ this._setCapability("power", true);
+ break;
+ default:
+ this._fail("Illegal server XVP message (msg: " + xvpMsg + ")");
+ break;
+ }
+
+ return true;
+ }
+
+ _normalMsg() {
+ let msgType;
+ if (this._FBU.rects > 0) {
+ msgType = 0;
+ } else {
+ msgType = this._sock.rQshift8();
+ }
+
+ let first, ret;
+ switch (msgType) {
+ case 0: // FramebufferUpdate
+ ret = this._framebufferUpdate();
+ if (ret && !this._enabledContinuousUpdates) {
+ RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
+ this._fbWidth, this._fbHeight);
+ }
+ return ret;
+
+ case 1: // SetColorMapEntries
+ return this._handleSetColourMapMsg();
+
+ case 2: // Bell
+ Log.Debug("Bell");
+ this.dispatchEvent(new CustomEvent(
+ "bell",
+ { detail: {} }));
+ return true;
+
+ case 3: // ServerCutText
+ return this._handleServerCutText();
+
+ case 150: // EndOfContinuousUpdates
+ first = !this._supportsContinuousUpdates;
+ this._supportsContinuousUpdates = true;
+ this._enabledContinuousUpdates = false;
+ if (first) {
+ this._enabledContinuousUpdates = true;
+ this._updateContinuousUpdates();
+ Log.Info("Enabling continuous updates.");
+ } else {
+ // FIXME: We need to send a framebufferupdaterequest here
+ // if we add support for turning off continuous updates
+ }
+ return true;
+
+ case 248: // ServerFence
+ return this._handleServerFenceMsg();
+
+ case 250: // XVP
+ return this._handleXvpMsg();
+
+ default:
+ this._fail("Unexpected server message (type " + msgType + ")");
+ Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30));
+ return true;
+ }
+ }
+
+ _framebufferUpdate() {
+ if (this._FBU.rects === 0) {
+ if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
+ this._sock.rQskipBytes(1); // Padding
+ this._FBU.rects = this._sock.rQshift16();
+
+ // Make sure the previous frame is fully rendered first
+ // to avoid building up an excessive queue
+ if (this._display.pending()) {
+ this._flushing = true;
+ this._display.flush()
+ .then(() => {
+ this._flushing = false;
+ // Resume processing
+ if (!this._sock.rQwait("message", 1)) {
+ this._handleMessage();
+ }
+ });
+ return false;
+ }
+ }
+
+ while (this._FBU.rects > 0) {
+ if (this._FBU.encoding === null) {
+ if (this._sock.rQwait("rect header", 12)) { return false; }
+ /* New FramebufferUpdate */
+
+ this._FBU.x = this._sock.rQshift16();
+ this._FBU.y = this._sock.rQshift16();
+ this._FBU.width = this._sock.rQshift16();
+ this._FBU.height = this._sock.rQshift16();
+ this._FBU.encoding = this._sock.rQshift32();
+ /* Encodings are signed */
+ this._FBU.encoding >>= 0;
+ }
+
+ if (!this._handleRect()) {
+ return false;
+ }
+
+ this._FBU.rects--;
+ this._FBU.encoding = null;
+ }
+
+ this._display.flip();
+
+ return true; // We finished this FBU
+ }
+
+ _handleRect() {
+ switch (this._FBU.encoding) {
+ case encodings.pseudoEncodingLastRect:
+ this._FBU.rects = 1; // Will be decreased when we return
+ return true;
+
+ case encodings.pseudoEncodingVMwareCursor:
+ return this._handleVMwareCursor();
+
+ case encodings.pseudoEncodingCursor:
+ return this._handleCursor();
+
+ case encodings.pseudoEncodingQEMUExtendedKeyEvent:
+ this._qemuExtKeyEventSupported = true;
+ return true;
+
+ case encodings.pseudoEncodingDesktopName:
+ return this._handleDesktopName();
+
+ case encodings.pseudoEncodingDesktopSize:
+ this._resize(this._FBU.width, this._FBU.height);
+ return true;
+
+ case encodings.pseudoEncodingExtendedDesktopSize:
+ return this._handleExtendedDesktopSize();
+
+ case encodings.pseudoEncodingExtendedMouseButtons:
+ this._extendedPointerEventSupported = true;
+ return true;
+
+ case encodings.pseudoEncodingQEMULedEvent:
+ return this._handleLedEvent();
+
+ default:
+ return this._handleDataRect();
+ }
+ }
+
+ _handleVMwareCursor() {
+ const hotx = this._FBU.x; // hotspot-x
+ const hoty = this._FBU.y; // hotspot-y
+ const w = this._FBU.width;
+ const h = this._FBU.height;
+ if (this._sock.rQwait("VMware cursor encoding", 1)) {
+ return false;
+ }
+
+ const cursorType = this._sock.rQshift8();
+
+ this._sock.rQshift8(); //Padding
+
+ let rgba;
+ const bytesPerPixel = 4;
+
+ //Classic cursor
+ if (cursorType == 0) {
+ //Used to filter away unimportant bits.
+ //OR is used for correct conversion in js.
+ const PIXEL_MASK = 0xffffff00 | 0;
+ rgba = new Array(w * h * bytesPerPixel);
+
+ if (this._sock.rQwait("VMware cursor classic encoding",
+ (w * h * bytesPerPixel) * 2, 2)) {
+ return false;
+ }
+
+ let andMask = new Array(w * h);
+ for (let pixel = 0; pixel < (w * h); pixel++) {
+ andMask[pixel] = this._sock.rQshift32();
+ }
+
+ let xorMask = new Array(w * h);
+ for (let pixel = 0; pixel < (w * h); pixel++) {
+ xorMask[pixel] = this._sock.rQshift32();
+ }
+
+ for (let pixel = 0; pixel < (w * h); pixel++) {
+ if (andMask[pixel] == 0) {
+ //Fully opaque pixel
+ let bgr = xorMask[pixel];
+ let r = bgr >> 8 & 0xff;
+ let g = bgr >> 16 & 0xff;
+ let b = bgr >> 24 & 0xff;
+
+ rgba[(pixel * bytesPerPixel) ] = r; //r
+ rgba[(pixel * bytesPerPixel) + 1 ] = g; //g
+ rgba[(pixel * bytesPerPixel) + 2 ] = b; //b
+ rgba[(pixel * bytesPerPixel) + 3 ] = 0xff; //a
+
+ } else if ((andMask[pixel] & PIXEL_MASK) ==
+ PIXEL_MASK) {
+ //Only screen value matters, no mouse colouring
+ if (xorMask[pixel] == 0) {
+ //Transparent pixel
+ rgba[(pixel * bytesPerPixel) ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 3 ] = 0x00;
+
+ } else if ((xorMask[pixel] & PIXEL_MASK) ==
+ PIXEL_MASK) {
+ //Inverted pixel, not supported in browsers.
+ //Fully opaque instead.
+ rgba[(pixel * bytesPerPixel) ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
+
+ } else {
+ //Unhandled xorMask
+ rgba[(pixel * bytesPerPixel) ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
+ }
+
+ } else {
+ //Unhandled andMask
+ rgba[(pixel * bytesPerPixel) ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
+ rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
+ }
+ }
+
+ //Alpha cursor.
+ } else if (cursorType == 1) {
+ if (this._sock.rQwait("VMware cursor alpha encoding",
+ (w * h * 4), 2)) {
+ return false;
+ }
+
+ rgba = new Array(w * h * bytesPerPixel);
+
+ for (let pixel = 0; pixel < (w * h); pixel++) {
+ let data = this._sock.rQshift32();
+
+ rgba[(pixel * 4) ] = data >> 24 & 0xff; //r
+ rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g
+ rgba[(pixel * 4) + 2 ] = data >> 8 & 0xff; //b
+ rgba[(pixel * 4) + 3 ] = data & 0xff; //a
+ }
+
+ } else {
+ Log.Warn("The given cursor type is not supported: "
+ + cursorType + " given.");
+ return false;
+ }
+
+ this._updateCursor(rgba, hotx, hoty, w, h);
+
+ return true;
+ }
+
+ _handleCursor() {
+ const hotx = this._FBU.x; // hotspot-x
+ const hoty = this._FBU.y; // hotspot-y
+ const w = this._FBU.width;
+ const h = this._FBU.height;
+
+ const pixelslength = w * h * 4;
+ const masklength = Math.ceil(w / 8) * h;
+
+ let bytes = pixelslength + masklength;
+ if (this._sock.rQwait("cursor encoding", bytes)) {
+ return false;
+ }
+
+ // Decode from BGRX pixels + bit mask to RGBA
+ const pixels = this._sock.rQshiftBytes(pixelslength);
+ const mask = this._sock.rQshiftBytes(masklength);
+ let rgba = new Uint8Array(w * h * 4);
+
+ let pixIdx = 0;
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8);
+ let alpha = (mask[maskIdx] << (x % 8)) & 0x80 ? 255 : 0;
+ rgba[pixIdx ] = pixels[pixIdx + 2];
+ rgba[pixIdx + 1] = pixels[pixIdx + 1];
+ rgba[pixIdx + 2] = pixels[pixIdx];
+ rgba[pixIdx + 3] = alpha;
+ pixIdx += 4;
+ }
+ }
+
+ this._updateCursor(rgba, hotx, hoty, w, h);
+
+ return true;
+ }
+
+ _handleDesktopName() {
+ if (this._sock.rQwait("DesktopName", 4)) {
+ return false;
+ }
+
+ let length = this._sock.rQshift32();
+
+ if (this._sock.rQwait("DesktopName", length, 4)) {
+ return false;
+ }
+
+ let name = this._sock.rQshiftStr(length);
+ name = decodeUTF8(name, true);
+
+ this._setDesktopName(name);
+
+ return true;
+ }
+
+ _handleLedEvent() {
+ if (this._sock.rQwait("LED status", 1)) {
+ return false;
+ }
+
+ let data = this._sock.rQshift8();
+ // ScrollLock state can be retrieved with data & 1. This is currently not needed.
+ let numLock = data & 2 ? true : false;
+ let capsLock = data & 4 ? true : false;
+ this._remoteCapsLock = capsLock;
+ this._remoteNumLock = numLock;
+
+ return true;
+ }
+
+ _handleExtendedDesktopSize() {
+ if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
+ return false;
+ }
+
+ const numberOfScreens = this._sock.rQpeek8();
+
+ let bytes = 4 + (numberOfScreens * 16);
+ if (this._sock.rQwait("ExtendedDesktopSize", bytes)) {
+ return false;
+ }
+
+ const firstUpdate = !this._supportsSetDesktopSize;
+ this._supportsSetDesktopSize = true;
+
+ this._sock.rQskipBytes(1); // number-of-screens
+ this._sock.rQskipBytes(3); // padding
+
+ for (let i = 0; i < numberOfScreens; i += 1) {
+ // Save the id and flags of the first screen
+ if (i === 0) {
+ this._screenID = this._sock.rQshift32(); // id
+ this._sock.rQskipBytes(2); // x-position
+ this._sock.rQskipBytes(2); // y-position
+ this._sock.rQskipBytes(2); // width
+ this._sock.rQskipBytes(2); // height
+ this._screenFlags = this._sock.rQshift32(); // flags
+ } else {
+ this._sock.rQskipBytes(16);
+ }
+ }
+
+ /*
+ * The x-position indicates the reason for the change:
+ *
+ * 0 - server resized on its own
+ * 1 - this client requested the resize
+ * 2 - another client requested the resize
+ */
+
+ if (this._FBU.x === 1) {
+ this._pendingRemoteResize = false;
+ }
+
+ // We need to handle errors when we requested the resize.
+ if (this._FBU.x === 1 && this._FBU.y !== 0) {
+ let msg = "";
+ // The y-position indicates the status code from the server
+ switch (this._FBU.y) {
+ case 1:
+ msg = "Resize is administratively prohibited";
+ break;
+ case 2:
+ msg = "Out of resources";
+ break;
+ case 3:
+ msg = "Invalid screen layout";
+ break;
+ default:
+ msg = "Unknown reason";
+ break;
+ }
+ Log.Warn("Server did not accept the resize request: "
+ + msg);
+ } else {
+ this._resize(this._FBU.width, this._FBU.height);
+ }
+
+ // Normally we only apply the current resize mode after a
+ // window resize event. However there is no such trigger on the
+ // initial connect. And we don't know if the server supports
+ // resizing until we've gotten here.
+ if (firstUpdate) {
+ this._requestRemoteResize();
+ }
+
+ if (this._FBU.x === 1 && this._FBU.y === 0) {
+ // We might have resized again whilst waiting for the
+ // previous request, so check if we are in sync
+ this._requestRemoteResize();
+ }
+
+ return true;
+ }
+
+ _handleDataRect() {
+ let decoder = this._decoders[this._FBU.encoding];
+ if (!decoder) {
+ this._fail("Unsupported encoding (encoding: " +
+ this._FBU.encoding + ")");
+ return false;
+ }
+
+ try {
+ return decoder.decodeRect(this._FBU.x, this._FBU.y,
+ this._FBU.width, this._FBU.height,
+ this._sock, this._display,
+ this._fbDepth);
+ } catch (err) {
+ this._fail("Error decoding rect: " + err);
+ return false;
+ }
+ }
+
+ _updateContinuousUpdates() {
+ if (!this._enabledContinuousUpdates) { return; }
+
+ RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
+ this._fbWidth, this._fbHeight);
+ }
+
+ // Handle resize-messages from the server
+ _resize(width, height) {
+ this._fbWidth = width;
+ this._fbHeight = height;
+
+ this._display.resize(this._fbWidth, this._fbHeight);
+
+ // Adjust the visible viewport based on the new dimensions
+ this._updateClip();
+ this._updateScale();
+
+ this._updateContinuousUpdates();
+
+ // Keep this size until browser client size changes
+ this._saveExpectedClientSize();
+ }
+
+ _xvpOp(ver, op) {
+ if (this._rfbXvpVer < ver) { return; }
+ Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
+ RFB.messages.xvpOp(this._sock, ver, op);
+ }
+
+ _updateCursor(rgba, hotx, hoty, w, h) {
+ this._cursorImage = {
+ rgbaPixels: rgba,
+ hotx: hotx, hoty: hoty, w: w, h: h,
+ };
+ this._refreshCursor();
+ }
+
+ _shouldShowDotCursor() {
+ // Called when this._cursorImage is updated
+ if (!this._showDotCursor) {
+ // User does not want to see the dot, so...
+ return false;
+ }
+
+ // The dot should not be shown if the cursor is already visible,
+ // i.e. contains at least one not-fully-transparent pixel.
+ // So iterate through all alpha bytes in rgba and stop at the
+ // first non-zero.
+ for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {
+ if (this._cursorImage.rgbaPixels[i]) {
+ return false;
+ }
+ }
+
+ // At this point, we know that the cursor is fully transparent, and
+ // the user wants to see the dot instead of this.
+ return true;
+ }
+
+ _refreshCursor() {
+ if (this._rfbConnectionState !== "connecting" &&
+ this._rfbConnectionState !== "connected") {
+ return;
+ }
+ const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;
+ this._cursor.change(image.rgbaPixels,
+ image.hotx, image.hoty,
+ image.w, image.h
+ );
+ }
+
+ static genDES(password, challenge) {
+ const passwordChars = password.split('').map(c => c.charCodeAt(0));
+ const key = legacyCrypto.importKey(
+ "raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);
+ return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);
+ }
+}
+
+// Class Methods
+RFB.messages = {
+ keyEvent(sock, keysym, down) {
+ sock.sQpush8(4); // msg-type
+ sock.sQpush8(down);
+
+ sock.sQpush16(0);
+
+ sock.sQpush32(keysym);
+
+ sock.flush();
+ },
+
+ QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
+ function getRFBkeycode(xtScanCode) {
+ const upperByte = (keycode >> 8);
+ const lowerByte = (keycode & 0x00ff);
+ if (upperByte === 0xe0 && lowerByte < 0x7f) {
+ return lowerByte | 0x80;
+ }
+ return xtScanCode;
+ }
+
+ sock.sQpush8(255); // msg-type
+ sock.sQpush8(0); // sub msg-type
+
+ sock.sQpush16(down);
+
+ sock.sQpush32(keysym);
+
+ const RFBkeycode = getRFBkeycode(keycode);
+
+ sock.sQpush32(RFBkeycode);
+
+ sock.flush();
+ },
+
+ pointerEvent(sock, x, y, mask) {
+ sock.sQpush8(5); // msg-type
+
+ // Marker bit must be set to 0, otherwise the server might
+ // confuse the marker bit with the highest bit in a normal
+ // PointerEvent message.
+ mask = mask & 0x7f;
+ sock.sQpush8(mask);
+
+ sock.sQpush16(x);
+ sock.sQpush16(y);
+
+ sock.flush();
+ },
+
+ extendedPointerEvent(sock, x, y, mask) {
+ sock.sQpush8(5); // msg-type
+
+ let higherBits = (mask >> 7) & 0xff;
+
+ // Bits 2-7 are reserved
+ if (higherBits & 0xfc) {
+ throw new Error("Invalid mouse button mask: " + mask);
+ }
+
+ let lowerBits = mask & 0x7f;
+ lowerBits |= 0x80; // Set marker bit to 1
+
+ sock.sQpush8(lowerBits);
+ sock.sQpush16(x);
+ sock.sQpush16(y);
+ sock.sQpush8(higherBits);
+
+ sock.flush();
+ },
+
+ // Used to build Notify and Request data.
+ _buildExtendedClipboardFlags(actions, formats) {
+ let data = new Uint8Array(4);
+ let formatFlag = 0x00000000;
+ let actionFlag = 0x00000000;
+
+ for (let i = 0; i < actions.length; i++) {
+ actionFlag |= actions[i];
+ }
+
+ for (let i = 0; i < formats.length; i++) {
+ formatFlag |= formats[i];
+ }
+
+ data[0] = actionFlag >> 24; // Actions
+ data[1] = 0x00; // Reserved
+ data[2] = 0x00; // Reserved
+ data[3] = formatFlag; // Formats
+
+ return data;
+ },
+
+ extendedClipboardProvide(sock, formats, inData) {
+ // Deflate incomming data and their sizes
+ let deflator = new Deflator();
+ let dataToDeflate = [];
+
+ for (let i = 0; i < formats.length; i++) {
+ // We only support the format Text at this time
+ if (formats[i] != extendedClipboardFormatText) {
+ throw new Error("Unsupported extended clipboard format for Provide message.");
+ }
+
+ // Change lone \r or \n into \r\n as defined in rfbproto
+ inData[i] = inData[i].replace(/\r\n|\r|\n/gm, "\r\n");
+
+ // Check if it already has \0
+ let text = encodeUTF8(inData[i] + "\0");
+
+ dataToDeflate.push( (text.length >> 24) & 0xFF,
+ (text.length >> 16) & 0xFF,
+ (text.length >> 8) & 0xFF,
+ (text.length & 0xFF));
+
+ for (let j = 0; j < text.length; j++) {
+ dataToDeflate.push(text.charCodeAt(j));
+ }
+ }
+
+ let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));
+
+ // Build data to send
+ let data = new Uint8Array(4 + deflatedData.length);
+ data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],
+ formats));
+ data.set(deflatedData, 4);
+
+ RFB.messages.clientCutText(sock, data, true);
+ },
+
+ extendedClipboardNotify(sock, formats) {
+ let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],
+ formats);
+ RFB.messages.clientCutText(sock, flags, true);
+ },
+
+ extendedClipboardRequest(sock, formats) {
+ let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],
+ formats);
+ RFB.messages.clientCutText(sock, flags, true);
+ },
+
+ extendedClipboardCaps(sock, actions, formats) {
+ let formatKeys = Object.keys(formats);
+ let data = new Uint8Array(4 + (4 * formatKeys.length));
+
+ formatKeys.map(x => parseInt(x));
+ formatKeys.sort((a, b) => a - b);
+
+ data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));
+
+ let loopOffset = 4;
+ for (let i = 0; i < formatKeys.length; i++) {
+ data[loopOffset] = formats[formatKeys[i]] >> 24;
+ data[loopOffset + 1] = formats[formatKeys[i]] >> 16;
+ data[loopOffset + 2] = formats[formatKeys[i]] >> 8;
+ data[loopOffset + 3] = formats[formatKeys[i]] >> 0;
+
+ loopOffset += 4;
+ data[3] |= (1 << formatKeys[i]); // Update our format flags
+ }
+
+ RFB.messages.clientCutText(sock, data, true);
+ },
+
+ clientCutText(sock, data, extended = false) {
+ sock.sQpush8(6); // msg-type
+
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+
+ let length;
+ if (extended) {
+ length = toUnsigned32bit(-data.length);
+ } else {
+ length = data.length;
+ }
+
+ sock.sQpush32(length);
+ sock.sQpushBytes(data);
+ sock.flush();
+ },
+
+ setDesktopSize(sock, width, height, id, flags) {
+ sock.sQpush8(251); // msg-type
+
+ sock.sQpush8(0); // padding
+
+ sock.sQpush16(width);
+ sock.sQpush16(height);
+
+ sock.sQpush8(1); // number-of-screens
+
+ sock.sQpush8(0); // padding
+
+ // screen array
+ sock.sQpush32(id);
+ sock.sQpush16(0); // x-position
+ sock.sQpush16(0); // y-position
+ sock.sQpush16(width);
+ sock.sQpush16(height);
+ sock.sQpush32(flags);
+
+ sock.flush();
+ },
+
+ clientFence(sock, flags, payload) {
+ sock.sQpush8(248); // msg-type
+
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+
+ sock.sQpush32(flags);
+
+ sock.sQpush8(payload.length);
+ sock.sQpushString(payload);
+
+ sock.flush();
+ },
+
+ enableContinuousUpdates(sock, enable, x, y, width, height) {
+ sock.sQpush8(150); // msg-type
+
+ sock.sQpush8(enable);
+
+ sock.sQpush16(x);
+ sock.sQpush16(y);
+ sock.sQpush16(width);
+ sock.sQpush16(height);
+
+ sock.flush();
+ },
+
+ pixelFormat(sock, depth, trueColor) {
+ let bpp;
+
+ if (depth > 16) {
+ bpp = 32;
+ } else if (depth > 8) {
+ bpp = 16;
+ } else {
+ bpp = 8;
+ }
+
+ const bits = Math.floor(depth/3);
+
+ sock.sQpush8(0); // msg-type
+
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+
+ sock.sQpush8(bpp);
+ sock.sQpush8(depth);
+ sock.sQpush8(0); // little-endian
+ sock.sQpush8(trueColor ? 1 : 0);
+
+ sock.sQpush16((1 << bits) - 1); // red-max
+ sock.sQpush16((1 << bits) - 1); // green-max
+ sock.sQpush16((1 << bits) - 1); // blue-max
+
+ sock.sQpush8(bits * 0); // red-shift
+ sock.sQpush8(bits * 1); // green-shift
+ sock.sQpush8(bits * 2); // blue-shift
+
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+
+ sock.flush();
+ },
+
+ clientEncodings(sock, encodings) {
+ sock.sQpush8(2); // msg-type
+
+ sock.sQpush8(0); // padding
+
+ sock.sQpush16(encodings.length);
+ for (let i = 0; i < encodings.length; i++) {
+ sock.sQpush32(encodings[i]);
+ }
+
+ sock.flush();
+ },
+
+ fbUpdateRequest(sock, incremental, x, y, w, h) {
+ if (typeof(x) === "undefined") { x = 0; }
+ if (typeof(y) === "undefined") { y = 0; }
+
+ sock.sQpush8(3); // msg-type
+
+ sock.sQpush8(incremental ? 1 : 0);
+
+ sock.sQpush16(x);
+ sock.sQpush16(y);
+ sock.sQpush16(w);
+ sock.sQpush16(h);
+
+ sock.flush();
+ },
+
+ xvpOp(sock, ver, op) {
+ sock.sQpush8(250); // msg-type
+
+ sock.sQpush8(0); // padding
+
+ sock.sQpush8(ver);
+ sock.sQpush8(op);
+
+ sock.flush();
+ }
+};
+
+RFB.cursors = {
+ none: {
+ rgbaPixels: new Uint8Array(),
+ w: 0, h: 0,
+ hotx: 0, hoty: 0,
+ },
+
+ dot: {
+ /* eslint-disable indent */
+ rgbaPixels: new Uint8Array([
+ 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
+ 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
+ 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
+ ]),
+ /* eslint-enable indent */
+ w: 3, h: 3,
+ hotx: 1, hoty: 1,
+ }
+};
pkg/web/noVNC/core/websock.js
@@ -0,0 +1,369 @@
+/*
+ * Websock: high-performance buffering wrapper
+ * Copyright (C) 2019 The noVNC authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * Websock is similar to the standard WebSocket / RTCDataChannel object
+ * but with extra buffer handling.
+ *
+ * Websock has built-in receive queue buffering; the message event
+ * does not contain actual data but is simply a notification that
+ * there is new data available. Several rQ* methods are available to
+ * read binary data off of the receive queue.
+ */
+
+import * as Log from './util/logging.js';
+
+// this has performance issues in some versions Chromium, and
+// 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.
+const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB
+
+// Constants pulled from RTCDataChannelState enum
+// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum
+const DataChannel = {
+ CONNECTING: "connecting",
+ OPEN: "open",
+ CLOSING: "closing",
+ CLOSED: "closed"
+};
+
+const ReadyStates = {
+ CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING],
+ OPEN: [WebSocket.OPEN, DataChannel.OPEN],
+ CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING],
+ CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED],
+};
+
+// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples
+const rawChannelProps = [
+ "send",
+ "close",
+ "binaryType",
+ "onerror",
+ "onmessage",
+ "onopen",
+ "protocol",
+ "readyState",
+];
+
+export default class Websock {
+ constructor() {
+ this._websocket = null; // WebSocket or RTCDataChannel object
+
+ this._rQi = 0; // Receive queue index
+ this._rQlen = 0; // Next write position in the receive queue
+ this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB)
+ // called in init: this._rQ = new Uint8Array(this._rQbufferSize);
+ this._rQ = null; // Receive queue
+
+ this._sQbufferSize = 1024 * 10; // 10 KiB
+ // called in init: this._sQ = new Uint8Array(this._sQbufferSize);
+ this._sQlen = 0;
+ this._sQ = null; // Send queue
+
+ this._eventHandlers = {
+ message: () => {},
+ open: () => {},
+ close: () => {},
+ error: () => {}
+ };
+ }
+
+ // Getters and setters
+
+ get readyState() {
+ let subState;
+
+ if (this._websocket === null) {
+ return "unused";
+ }
+
+ subState = this._websocket.readyState;
+
+ if (ReadyStates.CONNECTING.includes(subState)) {
+ return "connecting";
+ } else if (ReadyStates.OPEN.includes(subState)) {
+ return "open";
+ } else if (ReadyStates.CLOSING.includes(subState)) {
+ return "closing";
+ } else if (ReadyStates.CLOSED.includes(subState)) {
+ return "closed";
+ }
+
+ return "unknown";
+ }
+
+ // Receive queue
+ rQpeek8() {
+ return this._rQ[this._rQi];
+ }
+
+ rQskipBytes(bytes) {
+ this._rQi += bytes;
+ }
+
+ rQshift8() {
+ return this._rQshift(1);
+ }
+
+ rQshift16() {
+ return this._rQshift(2);
+ }
+
+ rQshift32() {
+ return this._rQshift(4);
+ }
+
+ // TODO(directxman12): test performance with these vs a DataView
+ _rQshift(bytes) {
+ let res = 0;
+ for (let byte = bytes - 1; byte >= 0; byte--) {
+ res += this._rQ[this._rQi++] << (byte * 8);
+ }
+ return res >>> 0;
+ }
+
+ rQlen() {
+ return this._rQlen - this._rQi;
+ }
+
+ rQshiftStr(len) {
+ let str = "";
+ // Handle large arrays in steps to avoid long strings on the stack
+ for (let i = 0; i < len; i += 4096) {
+ let part = this.rQshiftBytes(Math.min(4096, len - i), false);
+ str += String.fromCharCode.apply(null, part);
+ }
+ return str;
+ }
+
+ rQshiftBytes(len, copy=true) {
+ this._rQi += len;
+ if (copy) {
+ return this._rQ.slice(this._rQi - len, this._rQi);
+ } else {
+ return this._rQ.subarray(this._rQi - len, this._rQi);
+ }
+ }
+
+ rQshiftTo(target, len) {
+ // TODO: make this just use set with views when using a ArrayBuffer to store the rQ
+ target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
+ this._rQi += len;
+ }
+
+ rQpeekBytes(len, copy=true) {
+ if (copy) {
+ return this._rQ.slice(this._rQi, this._rQi + len);
+ } else {
+ return this._rQ.subarray(this._rQi, this._rQi + len);
+ }
+ }
+
+ // Check to see if we must wait for 'num' bytes (default to FBU.bytes)
+ // to be available in the receive queue. Return true if we need to
+ // wait (and possibly print a debug message), otherwise false.
+ rQwait(msg, num, goback) {
+ if (this._rQlen - this._rQi < num) {
+ if (goback) {
+ if (this._rQi < goback) {
+ throw new Error("rQwait cannot backup " + goback + " bytes");
+ }
+ this._rQi -= goback;
+ }
+ return true; // true means need more data
+ }
+ return false;
+ }
+
+ // Send queue
+
+ sQpush8(num) {
+ this._sQensureSpace(1);
+ this._sQ[this._sQlen++] = num;
+ }
+
+ sQpush16(num) {
+ this._sQensureSpace(2);
+ this._sQ[this._sQlen++] = (num >> 8) & 0xff;
+ this._sQ[this._sQlen++] = (num >> 0) & 0xff;
+ }
+
+ sQpush32(num) {
+ this._sQensureSpace(4);
+ this._sQ[this._sQlen++] = (num >> 24) & 0xff;
+ this._sQ[this._sQlen++] = (num >> 16) & 0xff;
+ this._sQ[this._sQlen++] = (num >> 8) & 0xff;
+ this._sQ[this._sQlen++] = (num >> 0) & 0xff;
+ }
+
+ sQpushString(str) {
+ let bytes = str.split('').map(chr => chr.charCodeAt(0));
+ this.sQpushBytes(new Uint8Array(bytes));
+ }
+
+ sQpushBytes(bytes) {
+ for (let offset = 0;offset < bytes.length;) {
+ this._sQensureSpace(1);
+
+ let chunkSize = this._sQbufferSize - this._sQlen;
+ if (chunkSize > bytes.length - offset) {
+ chunkSize = bytes.length - offset;
+ }
+
+ this._sQ.set(bytes.subarray(offset, offset + chunkSize), this._sQlen);
+ this._sQlen += chunkSize;
+ offset += chunkSize;
+ }
+ }
+
+ flush() {
+ if (this._sQlen > 0 && this.readyState === 'open') {
+ this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen));
+ this._sQlen = 0;
+ }
+ }
+
+ _sQensureSpace(bytes) {
+ if (this._sQbufferSize - this._sQlen < bytes) {
+ this.flush();
+ }
+ }
+
+ // Event handlers
+ off(evt) {
+ this._eventHandlers[evt] = () => {};
+ }
+
+ on(evt, handler) {
+ this._eventHandlers[evt] = handler;
+ }
+
+ _allocateBuffers() {
+ this._rQ = new Uint8Array(this._rQbufferSize);
+ this._sQ = new Uint8Array(this._sQbufferSize);
+ }
+
+ init() {
+ this._allocateBuffers();
+ this._rQi = 0;
+ this._websocket = null;
+ }
+
+ open(uri, protocols) {
+ this.attach(new WebSocket(uri, protocols));
+ }
+
+ attach(rawChannel) {
+ this.init();
+
+ // Must get object and class methods to be compatible with the tests.
+ const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))];
+ for (let i = 0; i < rawChannelProps.length; i++) {
+ const prop = rawChannelProps[i];
+ if (channelProps.indexOf(prop) < 0) {
+ throw new Error('Raw channel missing property: ' + prop);
+ }
+ }
+
+ this._websocket = rawChannel;
+ this._websocket.binaryType = "arraybuffer";
+ this._websocket.onmessage = this._recvMessage.bind(this);
+
+ this._websocket.onopen = () => {
+ Log.Debug('>> WebSock.onopen');
+ if (this._websocket.protocol) {
+ Log.Info("Server choose sub-protocol: " + this._websocket.protocol);
+ }
+
+ this._eventHandlers.open();
+ Log.Debug("<< WebSock.onopen");
+ };
+
+ this._websocket.onclose = (e) => {
+ Log.Debug(">> WebSock.onclose");
+ this._eventHandlers.close(e);
+ Log.Debug("<< WebSock.onclose");
+ };
+
+ this._websocket.onerror = (e) => {
+ Log.Debug(">> WebSock.onerror: " + e);
+ this._eventHandlers.error(e);
+ Log.Debug("<< WebSock.onerror: " + e);
+ };
+ }
+
+ close() {
+ if (this._websocket) {
+ if (this.readyState === 'connecting' ||
+ this.readyState === 'open') {
+ Log.Info("Closing WebSocket connection");
+ this._websocket.close();
+ }
+
+ this._websocket.onmessage = () => {};
+ }
+ }
+
+ // private methods
+
+ // We want to move all the unread data to the start of the queue,
+ // e.g. compacting.
+ // The function also expands the receive que if needed, and for
+ // performance reasons we combine these two actions to avoid
+ // unnecessary 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) {
+ // Make sure we always *at least* double the buffer size, and have at least space for 8x
+ // the current amount of data
+ this._rQbufferSize = Math.max(this._rQbufferSize * 2, requiredBufferSize);
+ }
+
+ // we don't want to grow unboundedly
+ if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
+ this._rQbufferSize = MAX_RQ_GROW_SIZE;
+ if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) {
+ throw new Error("Receive queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
+ }
+ }
+
+ if (resizeNeeded) {
+ const oldRQbuffer = this._rQ.buffer;
+ this._rQ = new Uint8Array(this._rQbufferSize);
+ this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi));
+ } else {
+ this._rQ.copyWithin(0, this._rQi, this._rQlen);
+ }
+
+ this._rQlen = this._rQlen - this._rQi;
+ this._rQi = 0;
+ }
+
+ // push arraybuffer values onto the end of the receive que
+ _recvMessage(e) {
+ if (this._rQlen == this._rQi) {
+ // All data has now been processed, this means we
+ // can reset the receive queue.
+ this._rQlen = 0;
+ this._rQi = 0;
+ }
+ const u8 = new Uint8Array(e.data);
+ if (u8.length > this._rQbufferSize - this._rQlen) {
+ this._expandCompactRQ(u8.length);
+ }
+ this._rQ.set(u8, this._rQlen);
+ this._rQlen += u8.length;
+
+ if (this._rQlen - this._rQi > 0) {
+ this._eventHandlers.message();
+ } else {
+ Log.Debug("Ignoring empty message");
+ }
+ }
+}
pkg/web/noVNC/vendor/pako/lib/utils/common.js
@@ -0,0 +1,45 @@
+// reduce buffer size, avoiding mem copy
+export function shrinkBuf (buf, size) {
+ if (buf.length === size) { return buf; }
+ if (buf.subarray) { return buf.subarray(0, size); }
+ buf.length = size;
+ return buf;
+};
+
+
+export function arraySet (dest, src, src_offs, len, dest_offs) {
+ if (src.subarray && dest.subarray) {
+ dest.set(src.subarray(src_offs, src_offs + len), dest_offs);
+ return;
+ }
+ // Fallback to ordinary array
+ for (var i = 0; i < len; i++) {
+ dest[dest_offs + i] = src[src_offs + i];
+ }
+}
+
+// Join array of chunks to single array.
+export function flattenChunks (chunks) {
+ var i, l, len, pos, chunk, result;
+
+ // calculate data length
+ len = 0;
+ for (i = 0, l = chunks.length; i < l; i++) {
+ len += chunks[i].length;
+ }
+
+ // join chunks
+ result = new Uint8Array(len);
+ pos = 0;
+ for (i = 0, l = chunks.length; i < l; i++) {
+ chunk = chunks[i];
+ result.set(chunk, pos);
+ pos += chunk.length;
+ }
+
+ return result;
+}
+
+export var Buf8 = Uint8Array;
+export var Buf16 = Uint16Array;
+export var Buf32 = Int32Array;
pkg/web/noVNC/vendor/pako/lib/zlib/adler32.js
@@ -0,0 +1,27 @@
+// Note: adler32 takes 12% for level 0 and 2% for level 6.
+// It doesn't worth to make additional optimizationa as in original.
+// Small size is preferable.
+
+export default function adler32(adler, buf, len, pos) {
+ var s1 = (adler & 0xffff) |0,
+ s2 = ((adler >>> 16) & 0xffff) |0,
+ n = 0;
+
+ while (len !== 0) {
+ // Set limit ~ twice less than 5552, to keep
+ // s2 in 31-bits, because we force signed ints.
+ // in other case %= will fail.
+ n = len > 2000 ? 2000 : len;
+ len -= n;
+
+ do {
+ s1 = (s1 + buf[pos++]) |0;
+ s2 = (s2 + s1) |0;
+ } while (--n);
+
+ s1 %= 65521;
+ s2 %= 65521;
+ }
+
+ return (s1 | (s2 << 16)) |0;
+}
pkg/web/noVNC/vendor/pako/lib/zlib/constants.js
@@ -0,0 +1,47 @@
+export default {
+
+ /* Allowed flush values; see deflate() and inflate() below for details */
+ Z_NO_FLUSH: 0,
+ Z_PARTIAL_FLUSH: 1,
+ Z_SYNC_FLUSH: 2,
+ Z_FULL_FLUSH: 3,
+ Z_FINISH: 4,
+ Z_BLOCK: 5,
+ Z_TREES: 6,
+
+ /* Return codes for the compression/decompression functions. Negative values
+ * are errors, positive values are used for special but normal events.
+ */
+ Z_OK: 0,
+ Z_STREAM_END: 1,
+ Z_NEED_DICT: 2,
+ Z_ERRNO: -1,
+ Z_STREAM_ERROR: -2,
+ Z_DATA_ERROR: -3,
+ //Z_MEM_ERROR: -4,
+ Z_BUF_ERROR: -5,
+ //Z_VERSION_ERROR: -6,
+
+ /* compression levels */
+ Z_NO_COMPRESSION: 0,
+ Z_BEST_SPEED: 1,
+ Z_BEST_COMPRESSION: 9,
+ Z_DEFAULT_COMPRESSION: -1,
+
+
+ Z_FILTERED: 1,
+ Z_HUFFMAN_ONLY: 2,
+ Z_RLE: 3,
+ Z_FIXED: 4,
+ Z_DEFAULT_STRATEGY: 0,
+
+ /* Possible values of the data_type field (though see inflate()) */
+ Z_BINARY: 0,
+ Z_TEXT: 1,
+ //Z_ASCII: 1, // = Z_TEXT (deprecated)
+ Z_UNKNOWN: 2,
+
+ /* The deflate compression method */
+ Z_DEFLATED: 8
+ //Z_NULL: null // Use -1 or null inline, depending on var type
+};
pkg/web/noVNC/vendor/pako/lib/zlib/crc32.js
@@ -0,0 +1,36 @@
+// Note: we can't get significant speed boost here.
+// So write code to minimize size - no pregenerated tables
+// and array tools dependencies.
+
+
+// Use ordinary array, since untyped makes no boost here
+export default function makeTable() {
+ var c, table = [];
+
+ for (var n = 0; n < 256; n++) {
+ c = n;
+ for (var k = 0; k < 8; k++) {
+ c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
+ }
+ table[n] = c;
+ }
+
+ return table;
+}
+
+// Create table on load. Just 255 signed longs. Not a problem.
+var crcTable = makeTable();
+
+
+function crc32(crc, buf, len, pos) {
+ var t = crcTable,
+ end = pos + len;
+
+ crc ^= -1;
+
+ for (var i = pos; i < end; i++) {
+ crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF];
+ }
+
+ return (crc ^ (-1)); // >>> 0;
+}
pkg/web/noVNC/vendor/pako/lib/zlib/deflate.js
@@ -0,0 +1,1846 @@
+import * as utils from "../utils/common.js";
+import * as trees from "./trees.js";
+import adler32 from "./adler32.js";
+import crc32 from "./crc32.js";
+import msg from "./messages.js";
+
+/* Public constants ==========================================================*/
+/* ===========================================================================*/
+
+
+/* Allowed flush values; see deflate() and inflate() below for details */
+export const Z_NO_FLUSH = 0;
+export const Z_PARTIAL_FLUSH = 1;
+//export const Z_SYNC_FLUSH = 2;
+export const Z_FULL_FLUSH = 3;
+export const Z_FINISH = 4;
+export const Z_BLOCK = 5;
+//export const Z_TREES = 6;
+
+
+/* Return codes for the compression/decompression functions. Negative values
+ * are errors, positive values are used for special but normal events.
+ */
+export const Z_OK = 0;
+export const Z_STREAM_END = 1;
+//export const Z_NEED_DICT = 2;
+//export const Z_ERRNO = -1;
+export const Z_STREAM_ERROR = -2;
+export const Z_DATA_ERROR = -3;
+//export const Z_MEM_ERROR = -4;
+export const Z_BUF_ERROR = -5;
+//export const Z_VERSION_ERROR = -6;
+
+
+/* compression levels */
+//export const Z_NO_COMPRESSION = 0;
+//export const Z_BEST_SPEED = 1;
+//export const Z_BEST_COMPRESSION = 9;
+export const Z_DEFAULT_COMPRESSION = -1;
+
+
+export const Z_FILTERED = 1;
+export const Z_HUFFMAN_ONLY = 2;
+export const Z_RLE = 3;
+export const Z_FIXED = 4;
+export const Z_DEFAULT_STRATEGY = 0;
+
+/* Possible values of the data_type field (though see inflate()) */
+//export const Z_BINARY = 0;
+//export const Z_TEXT = 1;
+//export const Z_ASCII = 1; // = Z_TEXT
+export const Z_UNKNOWN = 2;
+
+
+/* The deflate compression method */
+export const Z_DEFLATED = 8;
+
+/*============================================================================*/
+
+
+var MAX_MEM_LEVEL = 9;
+/* Maximum value for memLevel in deflateInit2 */
+var MAX_WBITS = 15;
+/* 32K LZ77 window */
+var DEF_MEM_LEVEL = 8;
+
+
+var LENGTH_CODES = 29;
+/* number of length codes, not counting the special END_BLOCK code */
+var LITERALS = 256;
+/* number of literal bytes 0..255 */
+var L_CODES = LITERALS + 1 + LENGTH_CODES;
+/* number of Literal or Length codes, including the END_BLOCK code */
+var D_CODES = 30;
+/* number of distance codes */
+var BL_CODES = 19;
+/* number of codes used to transfer the bit lengths */
+var HEAP_SIZE = 2 * L_CODES + 1;
+/* maximum heap size */
+var MAX_BITS = 15;
+/* All codes must not exceed MAX_BITS bits */
+
+var MIN_MATCH = 3;
+var MAX_MATCH = 258;
+var MIN_LOOKAHEAD = (MAX_MATCH + MIN_MATCH + 1);
+
+var PRESET_DICT = 0x20;
+
+var INIT_STATE = 42;
+var EXTRA_STATE = 69;
+var NAME_STATE = 73;
+var COMMENT_STATE = 91;
+var HCRC_STATE = 103;
+var BUSY_STATE = 113;
+var FINISH_STATE = 666;
+
+var BS_NEED_MORE = 1; /* block not completed, need more input or more output */
+var BS_BLOCK_DONE = 2; /* block flush performed */
+var BS_FINISH_STARTED = 3; /* finish started, need only more output at next deflate */
+var BS_FINISH_DONE = 4; /* finish done, accept no more input or output */
+
+var OS_CODE = 0x03; // Unix :) . Don't detect, use this default.
+
+function err(strm, errorCode) {
+ strm.msg = msg[errorCode];
+ return errorCode;
+}
+
+function rank(f) {
+ return ((f) << 1) - ((f) > 4 ? 9 : 0);
+}
+
+function zero(buf) { var len = buf.length; while (--len >= 0) { buf[len] = 0; } }
+
+
+/* =========================================================================
+ * Flush as much pending output as possible. All deflate() output goes
+ * through this function so some applications may wish to modify it
+ * to avoid allocating a large strm->output buffer and copying into it.
+ * (See also read_buf()).
+ */
+function flush_pending(strm) {
+ var s = strm.state;
+
+ //_tr_flush_bits(s);
+ var len = s.pending;
+ if (len > strm.avail_out) {
+ len = strm.avail_out;
+ }
+ if (len === 0) { return; }
+
+ utils.arraySet(strm.output, s.pending_buf, s.pending_out, len, strm.next_out);
+ strm.next_out += len;
+ s.pending_out += len;
+ strm.total_out += len;
+ strm.avail_out -= len;
+ s.pending -= len;
+ if (s.pending === 0) {
+ s.pending_out = 0;
+ }
+}
+
+
+function flush_block_only(s, last) {
+ trees._tr_flush_block(s, (s.block_start >= 0 ? s.block_start : -1), s.strstart - s.block_start, last);
+ s.block_start = s.strstart;
+ flush_pending(s.strm);
+}
+
+
+function put_byte(s, b) {
+ s.pending_buf[s.pending++] = b;
+}
+
+
+/* =========================================================================
+ * Put a short in the pending buffer. The 16-bit value is put in MSB order.
+ * IN assertion: the stream state is correct and there is enough room in
+ * pending_buf.
+ */
+function putShortMSB(s, b) {
+// put_byte(s, (Byte)(b >> 8));
+// put_byte(s, (Byte)(b & 0xff));
+ s.pending_buf[s.pending++] = (b >>> 8) & 0xff;
+ s.pending_buf[s.pending++] = b & 0xff;
+}
+
+
+/* ===========================================================================
+ * Read a new buffer from the current input stream, update the adler32
+ * and total number of bytes read. All deflate() input goes through
+ * this function so some applications may wish to modify it to avoid
+ * allocating a large strm->input buffer and copying from it.
+ * (See also flush_pending()).
+ */
+function read_buf(strm, buf, start, size) {
+ var len = strm.avail_in;
+
+ if (len > size) { len = size; }
+ if (len === 0) { return 0; }
+
+ strm.avail_in -= len;
+
+ // zmemcpy(buf, strm->next_in, len);
+ utils.arraySet(buf, strm.input, strm.next_in, len, start);
+ if (strm.state.wrap === 1) {
+ strm.adler = adler32(strm.adler, buf, len, start);
+ }
+
+ else if (strm.state.wrap === 2) {
+ strm.adler = crc32(strm.adler, buf, len, start);
+ }
+
+ strm.next_in += len;
+ strm.total_in += len;
+
+ return len;
+}
+
+
+/* ===========================================================================
+ * Set match_start to the longest match starting at the given string and
+ * return its length. Matches shorter or equal to prev_length are discarded,
+ * in which case the result is equal to prev_length and match_start is
+ * garbage.
+ * IN assertions: cur_match is the head of the hash chain for the current
+ * string (strstart) and its distance is <= MAX_DIST, and prev_length >= 1
+ * OUT assertion: the match length is not greater than s->lookahead.
+ */
+function longest_match(s, cur_match) {
+ var chain_length = s.max_chain_length; /* max hash chain length */
+ var scan = s.strstart; /* current string */
+ var match; /* matched string */
+ var len; /* length of current match */
+ var best_len = s.prev_length; /* best match length so far */
+ var nice_match = s.nice_match; /* stop if match long enough */
+ var limit = (s.strstart > (s.w_size - MIN_LOOKAHEAD)) ?
+ s.strstart - (s.w_size - MIN_LOOKAHEAD) : 0/*NIL*/;
+
+ var _win = s.window; // shortcut
+
+ var wmask = s.w_mask;
+ var prev = s.prev;
+
+ /* Stop when cur_match becomes <= limit. To simplify the code,
+ * we prevent matches with the string of window index 0.
+ */
+
+ var strend = s.strstart + MAX_MATCH;
+ var scan_end1 = _win[scan + best_len - 1];
+ var scan_end = _win[scan + best_len];
+
+ /* The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16.
+ * It is easy to get rid of this optimization if necessary.
+ */
+ // Assert(s->hash_bits >= 8 && MAX_MATCH == 258, "Code too clever");
+
+ /* Do not waste too much time if we already have a good match: */
+ if (s.prev_length >= s.good_match) {
+ chain_length >>= 2;
+ }
+ /* Do not look for matches beyond the end of the input. This is necessary
+ * to make deflate deterministic.
+ */
+ if (nice_match > s.lookahead) { nice_match = s.lookahead; }
+
+ // Assert((ulg)s->strstart <= s->window_size-MIN_LOOKAHEAD, "need lookahead");
+
+ do {
+ // Assert(cur_match < s->strstart, "no future");
+ match = cur_match;
+
+ /* Skip to next match if the match length cannot increase
+ * or if the match length is less than 2. Note that the checks below
+ * for insufficient lookahead only occur occasionally for performance
+ * reasons. Therefore uninitialized memory will be accessed, and
+ * conditional jumps will be made that depend on those values.
+ * However the length of the match is limited to the lookahead, so
+ * the output of deflate is not affected by the uninitialized values.
+ */
+
+ if (_win[match + best_len] !== scan_end ||
+ _win[match + best_len - 1] !== scan_end1 ||
+ _win[match] !== _win[scan] ||
+ _win[++match] !== _win[scan + 1]) {
+ continue;
+ }
+
+ /* The check at best_len-1 can be removed because it will be made
+ * again later. (This heuristic is not always a win.)
+ * It is not necessary to compare scan[2] and match[2] since they
+ * are always equal when the other bytes match, given that
+ * the hash keys are equal and that HASH_BITS >= 8.
+ */
+ scan += 2;
+ match++;
+ // Assert(*scan == *match, "match[2]?");
+
+ /* We check for insufficient lookahead only every 8th comparison;
+ * the 256th check will be made at strstart+258.
+ */
+ do {
+ // Do nothing
+ } while (_win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
+ _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
+ _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
+ _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
+ scan < strend);
+
+ // Assert(scan <= s->window+(unsigned)(s->window_size-1), "wild scan");
+
+ len = MAX_MATCH - (strend - scan);
+ scan = strend - MAX_MATCH;
+
+ if (len > best_len) {
+ s.match_start = cur_match;
+ best_len = len;
+ if (len >= nice_match) {
+ break;
+ }
+ scan_end1 = _win[scan + best_len - 1];
+ scan_end = _win[scan + best_len];
+ }
+ } while ((cur_match = prev[cur_match & wmask]) > limit && --chain_length !== 0);
+
+ if (best_len <= s.lookahead) {
+ return best_len;
+ }
+ return s.lookahead;
+}
+
+
+/* ===========================================================================
+ * Fill the window when the lookahead becomes insufficient.
+ * Updates strstart and lookahead.
+ *
+ * IN assertion: lookahead < MIN_LOOKAHEAD
+ * OUT assertions: strstart <= window_size-MIN_LOOKAHEAD
+ * At least one byte has been read, or avail_in == 0; reads are
+ * performed for at least two bytes (required for the zip translate_eol
+ * option -- not supported here).
+ */
+function fill_window(s) {
+ var _w_size = s.w_size;
+ var p, n, m, more, str;
+
+ //Assert(s->lookahead < MIN_LOOKAHEAD, "already enough lookahead");
+
+ do {
+ more = s.window_size - s.lookahead - s.strstart;
+
+ // JS ints have 32 bit, block below not needed
+ /* Deal with !@#$% 64K limit: */
+ //if (sizeof(int) <= 2) {
+ // if (more == 0 && s->strstart == 0 && s->lookahead == 0) {
+ // more = wsize;
+ //
+ // } else if (more == (unsigned)(-1)) {
+ // /* Very unlikely, but possible on 16 bit machine if
+ // * strstart == 0 && lookahead == 1 (input done a byte at time)
+ // */
+ // more--;
+ // }
+ //}
+
+
+ /* If the window is almost full and there is insufficient lookahead,
+ * move the upper half to the lower one to make room in the upper half.
+ */
+ if (s.strstart >= _w_size + (_w_size - MIN_LOOKAHEAD)) {
+
+ utils.arraySet(s.window, s.window, _w_size, _w_size, 0);
+ s.match_start -= _w_size;
+ s.strstart -= _w_size;
+ /* we now have strstart >= MAX_DIST */
+ s.block_start -= _w_size;
+
+ /* Slide the hash table (could be avoided with 32 bit values
+ at the expense of memory usage). We slide even when level == 0
+ to keep the hash table consistent if we switch back to level > 0
+ later. (Using level 0 permanently is not an optimal usage of
+ zlib, so we don't care about this pathological case.)
+ */
+
+ n = s.hash_size;
+ p = n;
+ do {
+ m = s.head[--p];
+ s.head[p] = (m >= _w_size ? m - _w_size : 0);
+ } while (--n);
+
+ n = _w_size;
+ p = n;
+ do {
+ m = s.prev[--p];
+ s.prev[p] = (m >= _w_size ? m - _w_size : 0);
+ /* If n is not on any hash chain, prev[n] is garbage but
+ * its value will never be used.
+ */
+ } while (--n);
+
+ more += _w_size;
+ }
+ if (s.strm.avail_in === 0) {
+ break;
+ }
+
+ /* If there was no sliding:
+ * strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 &&
+ * more == window_size - lookahead - strstart
+ * => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1)
+ * => more >= window_size - 2*WSIZE + 2
+ * In the BIG_MEM or MMAP case (not yet supported),
+ * window_size == input_size + MIN_LOOKAHEAD &&
+ * strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD.
+ * Otherwise, window_size == 2*WSIZE so more >= 2.
+ * If there was sliding, more >= WSIZE. So in all cases, more >= 2.
+ */
+ //Assert(more >= 2, "more < 2");
+ n = read_buf(s.strm, s.window, s.strstart + s.lookahead, more);
+ s.lookahead += n;
+
+ /* Initialize the hash value now that we have some input: */
+ if (s.lookahead + s.insert >= MIN_MATCH) {
+ str = s.strstart - s.insert;
+ s.ins_h = s.window[str];
+
+ /* UPDATE_HASH(s, s->ins_h, s->window[str + 1]); */
+ s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + 1]) & s.hash_mask;
+//#if MIN_MATCH != 3
+// Call update_hash() MIN_MATCH-3 more times
+//#endif
+ while (s.insert) {
+ /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */
+ s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + MIN_MATCH - 1]) & s.hash_mask;
+
+ s.prev[str & s.w_mask] = s.head[s.ins_h];
+ s.head[s.ins_h] = str;
+ str++;
+ s.insert--;
+ if (s.lookahead + s.insert < MIN_MATCH) {
+ break;
+ }
+ }
+ }
+ /* If the whole input has less than MIN_MATCH bytes, ins_h is garbage,
+ * but this is not important since only literal bytes will be emitted.
+ */
+
+ } while (s.lookahead < MIN_LOOKAHEAD && s.strm.avail_in !== 0);
+
+ /* If the WIN_INIT bytes after the end of the current data have never been
+ * written, then zero those bytes in order to avoid memory check reports of
+ * the use of uninitialized (or uninitialised as Julian writes) bytes by
+ * the longest match routines. Update the high water mark for the next
+ * time through here. WIN_INIT is set to MAX_MATCH since the longest match
+ * routines allow scanning to strstart + MAX_MATCH, ignoring lookahead.
+ */
+// if (s.high_water < s.window_size) {
+// var curr = s.strstart + s.lookahead;
+// var init = 0;
+//
+// if (s.high_water < curr) {
+// /* Previous high water mark below current data -- zero WIN_INIT
+// * bytes or up to end of window, whichever is less.
+// */
+// init = s.window_size - curr;
+// if (init > WIN_INIT)
+// init = WIN_INIT;
+// zmemzero(s->window + curr, (unsigned)init);
+// s->high_water = curr + init;
+// }
+// else if (s->high_water < (ulg)curr + WIN_INIT) {
+// /* High water mark at or above current data, but below current data
+// * plus WIN_INIT -- zero out to current data plus WIN_INIT, or up
+// * to end of window, whichever is less.
+// */
+// init = (ulg)curr + WIN_INIT - s->high_water;
+// if (init > s->window_size - s->high_water)
+// init = s->window_size - s->high_water;
+// zmemzero(s->window + s->high_water, (unsigned)init);
+// s->high_water += init;
+// }
+// }
+//
+// Assert((ulg)s->strstart <= s->window_size - MIN_LOOKAHEAD,
+// "not enough room for search");
+}
+
+/* ===========================================================================
+ * Copy without compression as much as possible from the input stream, return
+ * the current block state.
+ * This function does not insert new strings in the dictionary since
+ * uncompressible data is probably not useful. This function is used
+ * only for the level=0 compression option.
+ * NOTE: this function should be optimized to avoid extra copying from
+ * window to pending_buf.
+ */
+function deflate_stored(s, flush) {
+ /* Stored blocks are limited to 0xffff bytes, pending_buf is limited
+ * to pending_buf_size, and each stored block has a 5 byte header:
+ */
+ var max_block_size = 0xffff;
+
+ if (max_block_size > s.pending_buf_size - 5) {
+ max_block_size = s.pending_buf_size - 5;
+ }
+
+ /* Copy as much as possible from input to output: */
+ for (;;) {
+ /* Fill the window as much as possible: */
+ if (s.lookahead <= 1) {
+
+ //Assert(s->strstart < s->w_size+MAX_DIST(s) ||
+ // s->block_start >= (long)s->w_size, "slide too late");
+// if (!(s.strstart < s.w_size + (s.w_size - MIN_LOOKAHEAD) ||
+// s.block_start >= s.w_size)) {
+// throw new Error("slide too late");
+// }
+
+ fill_window(s);
+ if (s.lookahead === 0 && flush === Z_NO_FLUSH) {
+ return BS_NEED_MORE;
+ }
+
+ if (s.lookahead === 0) {
+ break;
+ }
+ /* flush the current block */
+ }
+ //Assert(s->block_start >= 0L, "block gone");
+// if (s.block_start < 0) throw new Error("block gone");
+
+ s.strstart += s.lookahead;
+ s.lookahead = 0;
+
+ /* Emit a stored block if pending_buf will be full: */
+ var max_start = s.block_start + max_block_size;
+
+ if (s.strstart === 0 || s.strstart >= max_start) {
+ /* strstart == 0 is possible when wraparound on 16-bit machine */
+ s.lookahead = s.strstart - max_start;
+ s.strstart = max_start;
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+
+
+ }
+ /* Flush if we may have to slide, otherwise block_start may become
+ * negative and the data will be gone:
+ */
+ if (s.strstart - s.block_start >= (s.w_size - MIN_LOOKAHEAD)) {
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+ }
+ }
+
+ s.insert = 0;
+
+ if (flush === Z_FINISH) {
+ /*** FLUSH_BLOCK(s, 1); ***/
+ flush_block_only(s, true);
+ if (s.strm.avail_out === 0) {
+ return BS_FINISH_STARTED;
+ }
+ /***/
+ return BS_FINISH_DONE;
+ }
+
+ if (s.strstart > s.block_start) {
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+ }
+
+ return BS_NEED_MORE;
+}
+
+/* ===========================================================================
+ * Compress as much as possible from the input stream, return the current
+ * block state.
+ * This function does not perform lazy evaluation of matches and inserts
+ * new strings in the dictionary only for unmatched strings or for short
+ * matches. It is used only for the fast compression options.
+ */
+function deflate_fast(s, flush) {
+ var hash_head; /* head of the hash chain */
+ var bflush; /* set if current block must be flushed */
+
+ for (;;) {
+ /* Make sure that we always have enough lookahead, except
+ * at the end of the input file. We need MAX_MATCH bytes
+ * for the next match, plus MIN_MATCH bytes to insert the
+ * string following the next match.
+ */
+ if (s.lookahead < MIN_LOOKAHEAD) {
+ fill_window(s);
+ if (s.lookahead < MIN_LOOKAHEAD && flush === Z_NO_FLUSH) {
+ return BS_NEED_MORE;
+ }
+ if (s.lookahead === 0) {
+ break; /* flush the current block */
+ }
+ }
+
+ /* Insert the string window[strstart .. strstart+2] in the
+ * dictionary, and set hash_head to the head of the hash chain:
+ */
+ hash_head = 0/*NIL*/;
+ if (s.lookahead >= MIN_MATCH) {
+ /*** INSERT_STRING(s, s.strstart, hash_head); ***/
+ s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;
+ hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
+ s.head[s.ins_h] = s.strstart;
+ /***/
+ }
+
+ /* Find the longest match, discarding those <= prev_length.
+ * At this point we have always match_length < MIN_MATCH
+ */
+ if (hash_head !== 0/*NIL*/ && ((s.strstart - hash_head) <= (s.w_size - MIN_LOOKAHEAD))) {
+ /* To simplify the code, we prevent matches with the string
+ * of window index 0 (in particular we have to avoid a match
+ * of the string with itself at the start of the input file).
+ */
+ s.match_length = longest_match(s, hash_head);
+ /* longest_match() sets match_start */
+ }
+ if (s.match_length >= MIN_MATCH) {
+ // check_match(s, s.strstart, s.match_start, s.match_length); // for debug only
+
+ /*** _tr_tally_dist(s, s.strstart - s.match_start,
+ s.match_length - MIN_MATCH, bflush); ***/
+ bflush = trees._tr_tally(s, s.strstart - s.match_start, s.match_length - MIN_MATCH);
+
+ s.lookahead -= s.match_length;
+
+ /* Insert new strings in the hash table only if the match length
+ * is not too large. This saves time but degrades compression.
+ */
+ if (s.match_length <= s.max_lazy_match/*max_insert_length*/ && s.lookahead >= MIN_MATCH) {
+ s.match_length--; /* string at strstart already in table */
+ do {
+ s.strstart++;
+ /*** INSERT_STRING(s, s.strstart, hash_head); ***/
+ s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;
+ hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
+ s.head[s.ins_h] = s.strstart;
+ /***/
+ /* strstart never exceeds WSIZE-MAX_MATCH, so there are
+ * always MIN_MATCH bytes ahead.
+ */
+ } while (--s.match_length !== 0);
+ s.strstart++;
+ } else
+ {
+ s.strstart += s.match_length;
+ s.match_length = 0;
+ s.ins_h = s.window[s.strstart];
+ /* UPDATE_HASH(s, s.ins_h, s.window[s.strstart+1]); */
+ s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + 1]) & s.hash_mask;
+
+//#if MIN_MATCH != 3
+// Call UPDATE_HASH() MIN_MATCH-3 more times
+//#endif
+ /* If lookahead < MIN_MATCH, ins_h is garbage, but it does not
+ * matter since it will be recomputed at next deflate call.
+ */
+ }
+ } else {
+ /* No match, output a literal byte */
+ //Tracevv((stderr,"%c", s.window[s.strstart]));
+ /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
+ bflush = trees._tr_tally(s, 0, s.window[s.strstart]);
+
+ s.lookahead--;
+ s.strstart++;
+ }
+ if (bflush) {
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+ }
+ }
+ s.insert = ((s.strstart < (MIN_MATCH - 1)) ? s.strstart : MIN_MATCH - 1);
+ if (flush === Z_FINISH) {
+ /*** FLUSH_BLOCK(s, 1); ***/
+ flush_block_only(s, true);
+ if (s.strm.avail_out === 0) {
+ return BS_FINISH_STARTED;
+ }
+ /***/
+ return BS_FINISH_DONE;
+ }
+ if (s.last_lit) {
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+ }
+ return BS_BLOCK_DONE;
+}
+
+/* ===========================================================================
+ * Same as above, but achieves better compression. We use a lazy
+ * evaluation for matches: a match is finally adopted only if there is
+ * no better match at the next window position.
+ */
+function deflate_slow(s, flush) {
+ var hash_head; /* head of hash chain */
+ var bflush; /* set if current block must be flushed */
+
+ var max_insert;
+
+ /* Process the input block. */
+ for (;;) {
+ /* Make sure that we always have enough lookahead, except
+ * at the end of the input file. We need MAX_MATCH bytes
+ * for the next match, plus MIN_MATCH bytes to insert the
+ * string following the next match.
+ */
+ if (s.lookahead < MIN_LOOKAHEAD) {
+ fill_window(s);
+ if (s.lookahead < MIN_LOOKAHEAD && flush === Z_NO_FLUSH) {
+ return BS_NEED_MORE;
+ }
+ if (s.lookahead === 0) { break; } /* flush the current block */
+ }
+
+ /* Insert the string window[strstart .. strstart+2] in the
+ * dictionary, and set hash_head to the head of the hash chain:
+ */
+ hash_head = 0/*NIL*/;
+ if (s.lookahead >= MIN_MATCH) {
+ /*** INSERT_STRING(s, s.strstart, hash_head); ***/
+ s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;
+ hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
+ s.head[s.ins_h] = s.strstart;
+ /***/
+ }
+
+ /* Find the longest match, discarding those <= prev_length.
+ */
+ s.prev_length = s.match_length;
+ s.prev_match = s.match_start;
+ s.match_length = MIN_MATCH - 1;
+
+ if (hash_head !== 0/*NIL*/ && s.prev_length < s.max_lazy_match &&
+ s.strstart - hash_head <= (s.w_size - MIN_LOOKAHEAD)/*MAX_DIST(s)*/) {
+ /* To simplify the code, we prevent matches with the string
+ * of window index 0 (in particular we have to avoid a match
+ * of the string with itself at the start of the input file).
+ */
+ s.match_length = longest_match(s, hash_head);
+ /* longest_match() sets match_start */
+
+ if (s.match_length <= 5 &&
+ (s.strategy === Z_FILTERED || (s.match_length === MIN_MATCH && s.strstart - s.match_start > 4096/*TOO_FAR*/))) {
+
+ /* If prev_match is also MIN_MATCH, match_start is garbage
+ * but we will ignore the current match anyway.
+ */
+ s.match_length = MIN_MATCH - 1;
+ }
+ }
+ /* If there was a match at the previous step and the current
+ * match is not better, output the previous match:
+ */
+ if (s.prev_length >= MIN_MATCH && s.match_length <= s.prev_length) {
+ max_insert = s.strstart + s.lookahead - MIN_MATCH;
+ /* Do not insert strings in hash table beyond this. */
+
+ //check_match(s, s.strstart-1, s.prev_match, s.prev_length);
+
+ /***_tr_tally_dist(s, s.strstart - 1 - s.prev_match,
+ s.prev_length - MIN_MATCH, bflush);***/
+ bflush = trees._tr_tally(s, s.strstart - 1 - s.prev_match, s.prev_length - MIN_MATCH);
+ /* Insert in hash table all strings up to the end of the match.
+ * strstart-1 and strstart are already inserted. If there is not
+ * enough lookahead, the last two strings are not inserted in
+ * the hash table.
+ */
+ s.lookahead -= s.prev_length - 1;
+ s.prev_length -= 2;
+ do {
+ if (++s.strstart <= max_insert) {
+ /*** INSERT_STRING(s, s.strstart, hash_head); ***/
+ s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;
+ hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
+ s.head[s.ins_h] = s.strstart;
+ /***/
+ }
+ } while (--s.prev_length !== 0);
+ s.match_available = 0;
+ s.match_length = MIN_MATCH - 1;
+ s.strstart++;
+
+ if (bflush) {
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+ }
+
+ } else if (s.match_available) {
+ /* If there was no match at the previous position, output a
+ * single literal. If there was a match but the current match
+ * is longer, truncate the previous match to a single literal.
+ */
+ //Tracevv((stderr,"%c", s->window[s->strstart-1]));
+ /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/
+ bflush = trees._tr_tally(s, 0, s.window[s.strstart - 1]);
+
+ if (bflush) {
+ /*** FLUSH_BLOCK_ONLY(s, 0) ***/
+ flush_block_only(s, false);
+ /***/
+ }
+ s.strstart++;
+ s.lookahead--;
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ } else {
+ /* There is no previous match to compare with, wait for
+ * the next step to decide.
+ */
+ s.match_available = 1;
+ s.strstart++;
+ s.lookahead--;
+ }
+ }
+ //Assert (flush != Z_NO_FLUSH, "no flush?");
+ if (s.match_available) {
+ //Tracevv((stderr,"%c", s->window[s->strstart-1]));
+ /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/
+ bflush = trees._tr_tally(s, 0, s.window[s.strstart - 1]);
+
+ s.match_available = 0;
+ }
+ s.insert = s.strstart < MIN_MATCH - 1 ? s.strstart : MIN_MATCH - 1;
+ if (flush === Z_FINISH) {
+ /*** FLUSH_BLOCK(s, 1); ***/
+ flush_block_only(s, true);
+ if (s.strm.avail_out === 0) {
+ return BS_FINISH_STARTED;
+ }
+ /***/
+ return BS_FINISH_DONE;
+ }
+ if (s.last_lit) {
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+ }
+
+ return BS_BLOCK_DONE;
+}
+
+
+/* ===========================================================================
+ * For Z_RLE, simply look for runs of bytes, generate matches only of distance
+ * one. Do not maintain a hash table. (It will be regenerated if this run of
+ * deflate switches away from Z_RLE.)
+ */
+function deflate_rle(s, flush) {
+ var bflush; /* set if current block must be flushed */
+ var prev; /* byte at distance one to match */
+ var scan, strend; /* scan goes up to strend for length of run */
+
+ var _win = s.window;
+
+ for (;;) {
+ /* Make sure that we always have enough lookahead, except
+ * at the end of the input file. We need MAX_MATCH bytes
+ * for the longest run, plus one for the unrolled loop.
+ */
+ if (s.lookahead <= MAX_MATCH) {
+ fill_window(s);
+ if (s.lookahead <= MAX_MATCH && flush === Z_NO_FLUSH) {
+ return BS_NEED_MORE;
+ }
+ if (s.lookahead === 0) { break; } /* flush the current block */
+ }
+
+ /* See how many times the previous byte repeats */
+ s.match_length = 0;
+ if (s.lookahead >= MIN_MATCH && s.strstart > 0) {
+ scan = s.strstart - 1;
+ prev = _win[scan];
+ if (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan]) {
+ strend = s.strstart + MAX_MATCH;
+ do {
+ // Do nothing
+ } while (prev === _win[++scan] && prev === _win[++scan] &&
+ prev === _win[++scan] && prev === _win[++scan] &&
+ prev === _win[++scan] && prev === _win[++scan] &&
+ prev === _win[++scan] && prev === _win[++scan] &&
+ scan < strend);
+ s.match_length = MAX_MATCH - (strend - scan);
+ if (s.match_length > s.lookahead) {
+ s.match_length = s.lookahead;
+ }
+ }
+ //Assert(scan <= s->window+(uInt)(s->window_size-1), "wild scan");
+ }
+
+ /* Emit match if have run of MIN_MATCH or longer, else emit literal */
+ if (s.match_length >= MIN_MATCH) {
+ //check_match(s, s.strstart, s.strstart - 1, s.match_length);
+
+ /*** _tr_tally_dist(s, 1, s.match_length - MIN_MATCH, bflush); ***/
+ bflush = trees._tr_tally(s, 1, s.match_length - MIN_MATCH);
+
+ s.lookahead -= s.match_length;
+ s.strstart += s.match_length;
+ s.match_length = 0;
+ } else {
+ /* No match, output a literal byte */
+ //Tracevv((stderr,"%c", s->window[s->strstart]));
+ /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
+ bflush = trees._tr_tally(s, 0, s.window[s.strstart]);
+
+ s.lookahead--;
+ s.strstart++;
+ }
+ if (bflush) {
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+ }
+ }
+ s.insert = 0;
+ if (flush === Z_FINISH) {
+ /*** FLUSH_BLOCK(s, 1); ***/
+ flush_block_only(s, true);
+ if (s.strm.avail_out === 0) {
+ return BS_FINISH_STARTED;
+ }
+ /***/
+ return BS_FINISH_DONE;
+ }
+ if (s.last_lit) {
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+ }
+ return BS_BLOCK_DONE;
+}
+
+/* ===========================================================================
+ * For Z_HUFFMAN_ONLY, do not look for matches. Do not maintain a hash table.
+ * (It will be regenerated if this run of deflate switches away from Huffman.)
+ */
+function deflate_huff(s, flush) {
+ var bflush; /* set if current block must be flushed */
+
+ for (;;) {
+ /* Make sure that we have a literal to write. */
+ if (s.lookahead === 0) {
+ fill_window(s);
+ if (s.lookahead === 0) {
+ if (flush === Z_NO_FLUSH) {
+ return BS_NEED_MORE;
+ }
+ break; /* flush the current block */
+ }
+ }
+
+ /* Output a literal byte */
+ s.match_length = 0;
+ //Tracevv((stderr,"%c", s->window[s->strstart]));
+ /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
+ bflush = trees._tr_tally(s, 0, s.window[s.strstart]);
+ s.lookahead--;
+ s.strstart++;
+ if (bflush) {
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+ }
+ }
+ s.insert = 0;
+ if (flush === Z_FINISH) {
+ /*** FLUSH_BLOCK(s, 1); ***/
+ flush_block_only(s, true);
+ if (s.strm.avail_out === 0) {
+ return BS_FINISH_STARTED;
+ }
+ /***/
+ return BS_FINISH_DONE;
+ }
+ if (s.last_lit) {
+ /*** FLUSH_BLOCK(s, 0); ***/
+ flush_block_only(s, false);
+ if (s.strm.avail_out === 0) {
+ return BS_NEED_MORE;
+ }
+ /***/
+ }
+ return BS_BLOCK_DONE;
+}
+
+/* Values for max_lazy_match, good_match and max_chain_length, depending on
+ * the desired pack level (0..9). The values given below have been tuned to
+ * exclude worst case performance for pathological files. Better values may be
+ * found for specific files.
+ */
+function Config(good_length, max_lazy, nice_length, max_chain, func) {
+ this.good_length = good_length;
+ this.max_lazy = max_lazy;
+ this.nice_length = nice_length;
+ this.max_chain = max_chain;
+ this.func = func;
+}
+
+var configuration_table;
+
+configuration_table = [
+ /* good lazy nice chain */
+ new Config(0, 0, 0, 0, deflate_stored), /* 0 store only */
+ new Config(4, 4, 8, 4, deflate_fast), /* 1 max speed, no lazy matches */
+ new Config(4, 5, 16, 8, deflate_fast), /* 2 */
+ new Config(4, 6, 32, 32, deflate_fast), /* 3 */
+
+ new Config(4, 4, 16, 16, deflate_slow), /* 4 lazy matches */
+ new Config(8, 16, 32, 32, deflate_slow), /* 5 */
+ new Config(8, 16, 128, 128, deflate_slow), /* 6 */
+ new Config(8, 32, 128, 256, deflate_slow), /* 7 */
+ new Config(32, 128, 258, 1024, deflate_slow), /* 8 */
+ new Config(32, 258, 258, 4096, deflate_slow) /* 9 max compression */
+];
+
+
+/* ===========================================================================
+ * Initialize the "longest match" routines for a new zlib stream
+ */
+function lm_init(s) {
+ s.window_size = 2 * s.w_size;
+
+ /*** CLEAR_HASH(s); ***/
+ zero(s.head); // Fill with NIL (= 0);
+
+ /* Set the default configuration parameters:
+ */
+ s.max_lazy_match = configuration_table[s.level].max_lazy;
+ s.good_match = configuration_table[s.level].good_length;
+ s.nice_match = configuration_table[s.level].nice_length;
+ s.max_chain_length = configuration_table[s.level].max_chain;
+
+ s.strstart = 0;
+ s.block_start = 0;
+ s.lookahead = 0;
+ s.insert = 0;
+ s.match_length = s.prev_length = MIN_MATCH - 1;
+ s.match_available = 0;
+ s.ins_h = 0;
+}
+
+
+function DeflateState() {
+ this.strm = null; /* pointer back to this zlib stream */
+ this.status = 0; /* as the name implies */
+ this.pending_buf = null; /* output still pending */
+ this.pending_buf_size = 0; /* size of pending_buf */
+ this.pending_out = 0; /* next pending byte to output to the stream */
+ this.pending = 0; /* nb of bytes in the pending buffer */
+ this.wrap = 0; /* bit 0 true for zlib, bit 1 true for gzip */
+ this.gzhead = null; /* gzip header information to write */
+ this.gzindex = 0; /* where in extra, name, or comment */
+ this.method = Z_DEFLATED; /* can only be DEFLATED */
+ this.last_flush = -1; /* value of flush param for previous deflate call */
+
+ this.w_size = 0; /* LZ77 window size (32K by default) */
+ this.w_bits = 0; /* log2(w_size) (8..16) */
+ this.w_mask = 0; /* w_size - 1 */
+
+ this.window = null;
+ /* Sliding window. Input bytes are read into the second half of the window,
+ * and move to the first half later to keep a dictionary of at least wSize
+ * bytes. With this organization, matches are limited to a distance of
+ * wSize-MAX_MATCH bytes, but this ensures that IO is always
+ * performed with a length multiple of the block size.
+ */
+
+ this.window_size = 0;
+ /* Actual size of window: 2*wSize, except when the user input buffer
+ * is directly used as sliding window.
+ */
+
+ this.prev = null;
+ /* Link to older string with same hash index. To limit the size of this
+ * array to 64K, this link is maintained only for the last 32K strings.
+ * An index in this array is thus a window index modulo 32K.
+ */
+
+ this.head = null; /* Heads of the hash chains or NIL. */
+
+ this.ins_h = 0; /* hash index of string to be inserted */
+ this.hash_size = 0; /* number of elements in hash table */
+ this.hash_bits = 0; /* log2(hash_size) */
+ this.hash_mask = 0; /* hash_size-1 */
+
+ this.hash_shift = 0;
+ /* Number of bits by which ins_h must be shifted at each input
+ * step. It must be such that after MIN_MATCH steps, the oldest
+ * byte no longer takes part in the hash key, that is:
+ * hash_shift * MIN_MATCH >= hash_bits
+ */
+
+ this.block_start = 0;
+ /* Window position at the beginning of the current output block. Gets
+ * negative when the window is moved backwards.
+ */
+
+ this.match_length = 0; /* length of best match */
+ this.prev_match = 0; /* previous match */
+ this.match_available = 0; /* set if previous match exists */
+ this.strstart = 0; /* start of string to insert */
+ this.match_start = 0; /* start of matching string */
+ this.lookahead = 0; /* number of valid bytes ahead in window */
+
+ this.prev_length = 0;
+ /* Length of the best match at previous step. Matches not greater than this
+ * are discarded. This is used in the lazy match evaluation.
+ */
+
+ this.max_chain_length = 0;
+ /* To speed up deflation, hash chains are never searched beyond this
+ * length. A higher limit improves compression ratio but degrades the
+ * speed.
+ */
+
+ this.max_lazy_match = 0;
+ /* Attempt to find a better match only when the current match is strictly
+ * smaller than this value. This mechanism is used only for compression
+ * levels >= 4.
+ */
+ // That's alias to max_lazy_match, don't use directly
+ //this.max_insert_length = 0;
+ /* Insert new strings in the hash table only if the match length is not
+ * greater than this length. This saves time but degrades compression.
+ * max_insert_length is used only for compression levels <= 3.
+ */
+
+ this.level = 0; /* compression level (1..9) */
+ this.strategy = 0; /* favor or force Huffman coding*/
+
+ this.good_match = 0;
+ /* Use a faster search when the previous match is longer than this */
+
+ this.nice_match = 0; /* Stop searching when current match exceeds this */
+
+ /* used by trees.c: */
+
+ /* Didn't use ct_data typedef below to suppress compiler warning */
+
+ // struct ct_data_s dyn_ltree[HEAP_SIZE]; /* literal and length tree */
+ // struct ct_data_s dyn_dtree[2*D_CODES+1]; /* distance tree */
+ // struct ct_data_s bl_tree[2*BL_CODES+1]; /* Huffman tree for bit lengths */
+
+ // Use flat array of DOUBLE size, with interleaved fata,
+ // because JS does not support effective
+ this.dyn_ltree = new utils.Buf16(HEAP_SIZE * 2);
+ this.dyn_dtree = new utils.Buf16((2 * D_CODES + 1) * 2);
+ this.bl_tree = new utils.Buf16((2 * BL_CODES + 1) * 2);
+ zero(this.dyn_ltree);
+ zero(this.dyn_dtree);
+ zero(this.bl_tree);
+
+ this.l_desc = null; /* desc. for literal tree */
+ this.d_desc = null; /* desc. for distance tree */
+ this.bl_desc = null; /* desc. for bit length tree */
+
+ //ush bl_count[MAX_BITS+1];
+ this.bl_count = new utils.Buf16(MAX_BITS + 1);
+ /* number of codes at each bit length for an optimal tree */
+
+ //int heap[2*L_CODES+1]; /* heap used to build the Huffman trees */
+ this.heap = new utils.Buf16(2 * L_CODES + 1); /* heap used to build the Huffman trees */
+ zero(this.heap);
+
+ this.heap_len = 0; /* number of elements in the heap */
+ this.heap_max = 0; /* element of largest frequency */
+ /* The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used.
+ * The same heap array is used to build all trees.
+ */
+
+ this.depth = new utils.Buf16(2 * L_CODES + 1); //uch depth[2*L_CODES+1];
+ zero(this.depth);
+ /* Depth of each subtree used as tie breaker for trees of equal frequency
+ */
+
+ this.l_buf = 0; /* buffer index for literals or lengths */
+
+ this.lit_bufsize = 0;
+ /* Size of match buffer for literals/lengths. There are 4 reasons for
+ * limiting lit_bufsize to 64K:
+ * - frequencies can be kept in 16 bit counters
+ * - if compression is not successful for the first block, all input
+ * data is still in the window so we can still emit a stored block even
+ * when input comes from standard input. (This can also be done for
+ * all blocks if lit_bufsize is not greater than 32K.)
+ * - if compression is not successful for a file smaller than 64K, we can
+ * even emit a stored file instead of a stored block (saving 5 bytes).
+ * This is applicable only for zip (not gzip or zlib).
+ * - creating new Huffman trees less frequently may not provide fast
+ * adaptation to changes in the input data statistics. (Take for
+ * example a binary file with poorly compressible code followed by
+ * a highly compressible string table.) Smaller buffer sizes give
+ * fast adaptation but have of course the overhead of transmitting
+ * trees more frequently.
+ * - I can't count above 4
+ */
+
+ this.last_lit = 0; /* running index in l_buf */
+
+ this.d_buf = 0;
+ /* Buffer index for distances. To simplify the code, d_buf and l_buf have
+ * the same number of elements. To use different lengths, an extra flag
+ * array would be necessary.
+ */
+
+ this.opt_len = 0; /* bit length of current block with optimal trees */
+ this.static_len = 0; /* bit length of current block with static trees */
+ this.matches = 0; /* number of string matches in current block */
+ this.insert = 0; /* bytes at end of window left to insert */
+
+
+ this.bi_buf = 0;
+ /* Output buffer. bits are inserted starting at the bottom (least
+ * significant bits).
+ */
+ this.bi_valid = 0;
+ /* Number of valid bits in bi_buf. All bits above the last valid bit
+ * are always zero.
+ */
+
+ // Used for window memory init. We safely ignore it for JS. That makes
+ // sense only for pointers and memory check tools.
+ //this.high_water = 0;
+ /* High water mark offset in window for initialized bytes -- bytes above
+ * this are set to zero in order to avoid memory check warnings when
+ * longest match routines access bytes past the input. This is then
+ * updated to the new high water mark.
+ */
+}
+
+
+function deflateResetKeep(strm) {
+ var s;
+
+ if (!strm || !strm.state) {
+ return err(strm, Z_STREAM_ERROR);
+ }
+
+ strm.total_in = strm.total_out = 0;
+ strm.data_type = Z_UNKNOWN;
+
+ s = strm.state;
+ s.pending = 0;
+ s.pending_out = 0;
+
+ if (s.wrap < 0) {
+ s.wrap = -s.wrap;
+ /* was made negative by deflate(..., Z_FINISH); */
+ }
+ s.status = (s.wrap ? INIT_STATE : BUSY_STATE);
+ strm.adler = (s.wrap === 2) ?
+ 0 // crc32(0, Z_NULL, 0)
+ :
+ 1; // adler32(0, Z_NULL, 0)
+ s.last_flush = Z_NO_FLUSH;
+ trees._tr_init(s);
+ return Z_OK;
+}
+
+
+function deflateReset(strm) {
+ var ret = deflateResetKeep(strm);
+ if (ret === Z_OK) {
+ lm_init(strm.state);
+ }
+ return ret;
+}
+
+
+function deflateSetHeader(strm, head) {
+ if (!strm || !strm.state) { return Z_STREAM_ERROR; }
+ if (strm.state.wrap !== 2) { return Z_STREAM_ERROR; }
+ strm.state.gzhead = head;
+ return Z_OK;
+}
+
+
+function deflateInit2(strm, level, method, windowBits, memLevel, strategy) {
+ if (!strm) { // === Z_NULL
+ return Z_STREAM_ERROR;
+ }
+ var wrap = 1;
+
+ if (level === Z_DEFAULT_COMPRESSION) {
+ level = 6;
+ }
+
+ if (windowBits < 0) { /* suppress zlib wrapper */
+ wrap = 0;
+ windowBits = -windowBits;
+ }
+
+ else if (windowBits > 15) {
+ wrap = 2; /* write gzip wrapper instead */
+ windowBits -= 16;
+ }
+
+
+ if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method !== Z_DEFLATED ||
+ windowBits < 8 || windowBits > 15 || level < 0 || level > 9 ||
+ strategy < 0 || strategy > Z_FIXED) {
+ return err(strm, Z_STREAM_ERROR);
+ }
+
+
+ if (windowBits === 8) {
+ windowBits = 9;
+ }
+ /* until 256-byte window bug fixed */
+
+ var s = new DeflateState();
+
+ strm.state = s;
+ s.strm = strm;
+
+ s.wrap = wrap;
+ s.gzhead = null;
+ s.w_bits = windowBits;
+ s.w_size = 1 << s.w_bits;
+ s.w_mask = s.w_size - 1;
+
+ s.hash_bits = memLevel + 7;
+ s.hash_size = 1 << s.hash_bits;
+ s.hash_mask = s.hash_size - 1;
+ s.hash_shift = ~~((s.hash_bits + MIN_MATCH - 1) / MIN_MATCH);
+
+ s.window = new utils.Buf8(s.w_size * 2);
+ s.head = new utils.Buf16(s.hash_size);
+ s.prev = new utils.Buf16(s.w_size);
+
+ // Don't need mem init magic for JS.
+ //s.high_water = 0; /* nothing written to s->window yet */
+
+ s.lit_bufsize = 1 << (memLevel + 6); /* 16K elements by default */
+
+ s.pending_buf_size = s.lit_bufsize * 4;
+
+ //overlay = (ushf *) ZALLOC(strm, s->lit_bufsize, sizeof(ush)+2);
+ //s->pending_buf = (uchf *) overlay;
+ s.pending_buf = new utils.Buf8(s.pending_buf_size);
+
+ // It is offset from `s.pending_buf` (size is `s.lit_bufsize * 2`)
+ //s->d_buf = overlay + s->lit_bufsize/sizeof(ush);
+ s.d_buf = 1 * s.lit_bufsize;
+
+ //s->l_buf = s->pending_buf + (1+sizeof(ush))*s->lit_bufsize;
+ s.l_buf = (1 + 2) * s.lit_bufsize;
+
+ s.level = level;
+ s.strategy = strategy;
+ s.method = method;
+
+ return deflateReset(strm);
+}
+
+function deflateInit(strm, level) {
+ return deflateInit2(strm, level, Z_DEFLATED, MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY);
+}
+
+
+function deflate(strm, flush) {
+ var old_flush, s;
+ var beg, val; // for gzip header write only
+
+ if (!strm || !strm.state ||
+ flush > Z_BLOCK || flush < 0) {
+ return strm ? err(strm, Z_STREAM_ERROR) : Z_STREAM_ERROR;
+ }
+
+ s = strm.state;
+
+ if (!strm.output ||
+ (!strm.input && strm.avail_in !== 0) ||
+ (s.status === FINISH_STATE && flush !== Z_FINISH)) {
+ return err(strm, (strm.avail_out === 0) ? Z_BUF_ERROR : Z_STREAM_ERROR);
+ }
+
+ s.strm = strm; /* just in case */
+ old_flush = s.last_flush;
+ s.last_flush = flush;
+
+ /* Write the header */
+ if (s.status === INIT_STATE) {
+
+ if (s.wrap === 2) { // GZIP header
+ strm.adler = 0; //crc32(0L, Z_NULL, 0);
+ put_byte(s, 31);
+ put_byte(s, 139);
+ put_byte(s, 8);
+ if (!s.gzhead) { // s->gzhead == Z_NULL
+ put_byte(s, 0);
+ put_byte(s, 0);
+ put_byte(s, 0);
+ put_byte(s, 0);
+ put_byte(s, 0);
+ put_byte(s, s.level === 9 ? 2 :
+ (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2 ?
+ 4 : 0));
+ put_byte(s, OS_CODE);
+ s.status = BUSY_STATE;
+ }
+ else {
+ put_byte(s, (s.gzhead.text ? 1 : 0) +
+ (s.gzhead.hcrc ? 2 : 0) +
+ (!s.gzhead.extra ? 0 : 4) +
+ (!s.gzhead.name ? 0 : 8) +
+ (!s.gzhead.comment ? 0 : 16)
+ );
+ put_byte(s, s.gzhead.time & 0xff);
+ put_byte(s, (s.gzhead.time >> 8) & 0xff);
+ put_byte(s, (s.gzhead.time >> 16) & 0xff);
+ put_byte(s, (s.gzhead.time >> 24) & 0xff);
+ put_byte(s, s.level === 9 ? 2 :
+ (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2 ?
+ 4 : 0));
+ put_byte(s, s.gzhead.os & 0xff);
+ if (s.gzhead.extra && s.gzhead.extra.length) {
+ put_byte(s, s.gzhead.extra.length & 0xff);
+ put_byte(s, (s.gzhead.extra.length >> 8) & 0xff);
+ }
+ if (s.gzhead.hcrc) {
+ strm.adler = crc32(strm.adler, s.pending_buf, s.pending, 0);
+ }
+ s.gzindex = 0;
+ s.status = EXTRA_STATE;
+ }
+ }
+ else // DEFLATE header
+ {
+ var header = (Z_DEFLATED + ((s.w_bits - 8) << 4)) << 8;
+ var level_flags = -1;
+
+ if (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2) {
+ level_flags = 0;
+ } else if (s.level < 6) {
+ level_flags = 1;
+ } else if (s.level === 6) {
+ level_flags = 2;
+ } else {
+ level_flags = 3;
+ }
+ header |= (level_flags << 6);
+ if (s.strstart !== 0) { header |= PRESET_DICT; }
+ header += 31 - (header % 31);
+
+ s.status = BUSY_STATE;
+ putShortMSB(s, header);
+
+ /* Save the adler32 of the preset dictionary: */
+ if (s.strstart !== 0) {
+ putShortMSB(s, strm.adler >>> 16);
+ putShortMSB(s, strm.adler & 0xffff);
+ }
+ strm.adler = 1; // adler32(0L, Z_NULL, 0);
+ }
+ }
+
+//#ifdef GZIP
+ if (s.status === EXTRA_STATE) {
+ if (s.gzhead.extra/* != Z_NULL*/) {
+ beg = s.pending; /* start of bytes to update crc */
+
+ while (s.gzindex < (s.gzhead.extra.length & 0xffff)) {
+ if (s.pending === s.pending_buf_size) {
+ if (s.gzhead.hcrc && s.pending > beg) {
+ strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+ }
+ flush_pending(strm);
+ beg = s.pending;
+ if (s.pending === s.pending_buf_size) {
+ break;
+ }
+ }
+ put_byte(s, s.gzhead.extra[s.gzindex] & 0xff);
+ s.gzindex++;
+ }
+ if (s.gzhead.hcrc && s.pending > beg) {
+ strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+ }
+ if (s.gzindex === s.gzhead.extra.length) {
+ s.gzindex = 0;
+ s.status = NAME_STATE;
+ }
+ }
+ else {
+ s.status = NAME_STATE;
+ }
+ }
+ if (s.status === NAME_STATE) {
+ if (s.gzhead.name/* != Z_NULL*/) {
+ beg = s.pending; /* start of bytes to update crc */
+ //int val;
+
+ do {
+ if (s.pending === s.pending_buf_size) {
+ if (s.gzhead.hcrc && s.pending > beg) {
+ strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+ }
+ flush_pending(strm);
+ beg = s.pending;
+ if (s.pending === s.pending_buf_size) {
+ val = 1;
+ break;
+ }
+ }
+ // JS specific: little magic to add zero terminator to end of string
+ if (s.gzindex < s.gzhead.name.length) {
+ val = s.gzhead.name.charCodeAt(s.gzindex++) & 0xff;
+ } else {
+ val = 0;
+ }
+ put_byte(s, val);
+ } while (val !== 0);
+
+ if (s.gzhead.hcrc && s.pending > beg) {
+ strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+ }
+ if (val === 0) {
+ s.gzindex = 0;
+ s.status = COMMENT_STATE;
+ }
+ }
+ else {
+ s.status = COMMENT_STATE;
+ }
+ }
+ if (s.status === COMMENT_STATE) {
+ if (s.gzhead.comment/* != Z_NULL*/) {
+ beg = s.pending; /* start of bytes to update crc */
+ //int val;
+
+ do {
+ if (s.pending === s.pending_buf_size) {
+ if (s.gzhead.hcrc && s.pending > beg) {
+ strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+ }
+ flush_pending(strm);
+ beg = s.pending;
+ if (s.pending === s.pending_buf_size) {
+ val = 1;
+ break;
+ }
+ }
+ // JS specific: little magic to add zero terminator to end of string
+ if (s.gzindex < s.gzhead.comment.length) {
+ val = s.gzhead.comment.charCodeAt(s.gzindex++) & 0xff;
+ } else {
+ val = 0;
+ }
+ put_byte(s, val);
+ } while (val !== 0);
+
+ if (s.gzhead.hcrc && s.pending > beg) {
+ strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+ }
+ if (val === 0) {
+ s.status = HCRC_STATE;
+ }
+ }
+ else {
+ s.status = HCRC_STATE;
+ }
+ }
+ if (s.status === HCRC_STATE) {
+ if (s.gzhead.hcrc) {
+ if (s.pending + 2 > s.pending_buf_size) {
+ flush_pending(strm);
+ }
+ if (s.pending + 2 <= s.pending_buf_size) {
+ put_byte(s, strm.adler & 0xff);
+ put_byte(s, (strm.adler >> 8) & 0xff);
+ strm.adler = 0; //crc32(0L, Z_NULL, 0);
+ s.status = BUSY_STATE;
+ }
+ }
+ else {
+ s.status = BUSY_STATE;
+ }
+ }
+//#endif
+
+ /* Flush as much pending output as possible */
+ if (s.pending !== 0) {
+ flush_pending(strm);
+ if (strm.avail_out === 0) {
+ /* Since avail_out is 0, deflate will be called again with
+ * more output space, but possibly with both pending and
+ * avail_in equal to zero. There won't be anything to do,
+ * but this is not an error situation so make sure we
+ * return OK instead of BUF_ERROR at next call of deflate:
+ */
+ s.last_flush = -1;
+ return Z_OK;
+ }
+
+ /* Make sure there is something to do and avoid duplicate consecutive
+ * flushes. For repeated and useless calls with Z_FINISH, we keep
+ * returning Z_STREAM_END instead of Z_BUF_ERROR.
+ */
+ } else if (strm.avail_in === 0 && rank(flush) <= rank(old_flush) &&
+ flush !== Z_FINISH) {
+ return err(strm, Z_BUF_ERROR);
+ }
+
+ /* User must not provide more input after the first FINISH: */
+ if (s.status === FINISH_STATE && strm.avail_in !== 0) {
+ return err(strm, Z_BUF_ERROR);
+ }
+
+ /* Start a new block or continue the current one.
+ */
+ if (strm.avail_in !== 0 || s.lookahead !== 0 ||
+ (flush !== Z_NO_FLUSH && s.status !== FINISH_STATE)) {
+ var bstate = (s.strategy === Z_HUFFMAN_ONLY) ? deflate_huff(s, flush) :
+ (s.strategy === Z_RLE ? deflate_rle(s, flush) :
+ configuration_table[s.level].func(s, flush));
+
+ if (bstate === BS_FINISH_STARTED || bstate === BS_FINISH_DONE) {
+ s.status = FINISH_STATE;
+ }
+ if (bstate === BS_NEED_MORE || bstate === BS_FINISH_STARTED) {
+ if (strm.avail_out === 0) {
+ s.last_flush = -1;
+ /* avoid BUF_ERROR next call, see above */
+ }
+ return Z_OK;
+ /* If flush != Z_NO_FLUSH && avail_out == 0, the next call
+ * of deflate should use the same flush parameter to make sure
+ * that the flush is complete. So we don't have to output an
+ * empty block here, this will be done at next call. This also
+ * ensures that for a very small output buffer, we emit at most
+ * one empty block.
+ */
+ }
+ if (bstate === BS_BLOCK_DONE) {
+ if (flush === Z_PARTIAL_FLUSH) {
+ trees._tr_align(s);
+ }
+ else if (flush !== Z_BLOCK) { /* FULL_FLUSH or SYNC_FLUSH */
+
+ trees._tr_stored_block(s, 0, 0, false);
+ /* For a full flush, this empty block will be recognized
+ * as a special marker by inflate_sync().
+ */
+ if (flush === Z_FULL_FLUSH) {
+ /*** CLEAR_HASH(s); ***/ /* forget history */
+ zero(s.head); // Fill with NIL (= 0);
+
+ if (s.lookahead === 0) {
+ s.strstart = 0;
+ s.block_start = 0;
+ s.insert = 0;
+ }
+ }
+ }
+ flush_pending(strm);
+ if (strm.avail_out === 0) {
+ s.last_flush = -1; /* avoid BUF_ERROR at next call, see above */
+ return Z_OK;
+ }
+ }
+ }
+ //Assert(strm->avail_out > 0, "bug2");
+ //if (strm.avail_out <= 0) { throw new Error("bug2");}
+
+ if (flush !== Z_FINISH) { return Z_OK; }
+ if (s.wrap <= 0) { return Z_STREAM_END; }
+
+ /* Write the trailer */
+ if (s.wrap === 2) {
+ put_byte(s, strm.adler & 0xff);
+ put_byte(s, (strm.adler >> 8) & 0xff);
+ put_byte(s, (strm.adler >> 16) & 0xff);
+ put_byte(s, (strm.adler >> 24) & 0xff);
+ put_byte(s, strm.total_in & 0xff);
+ put_byte(s, (strm.total_in >> 8) & 0xff);
+ put_byte(s, (strm.total_in >> 16) & 0xff);
+ put_byte(s, (strm.total_in >> 24) & 0xff);
+ }
+ else
+ {
+ putShortMSB(s, strm.adler >>> 16);
+ putShortMSB(s, strm.adler & 0xffff);
+ }
+
+ flush_pending(strm);
+ /* If avail_out is zero, the application will call deflate again
+ * to flush the rest.
+ */
+ if (s.wrap > 0) { s.wrap = -s.wrap; }
+ /* write the trailer only once! */
+ return s.pending !== 0 ? Z_OK : Z_STREAM_END;
+}
+
+function deflateEnd(strm) {
+ var status;
+
+ if (!strm/*== Z_NULL*/ || !strm.state/*== Z_NULL*/) {
+ return Z_STREAM_ERROR;
+ }
+
+ status = strm.state.status;
+ if (status !== INIT_STATE &&
+ status !== EXTRA_STATE &&
+ status !== NAME_STATE &&
+ status !== COMMENT_STATE &&
+ status !== HCRC_STATE &&
+ status !== BUSY_STATE &&
+ status !== FINISH_STATE
+ ) {
+ return err(strm, Z_STREAM_ERROR);
+ }
+
+ strm.state = null;
+
+ return status === BUSY_STATE ? err(strm, Z_DATA_ERROR) : Z_OK;
+}
+
+
+/* =========================================================================
+ * Initializes the compression dictionary from the given byte
+ * sequence without producing any compressed output.
+ */
+function deflateSetDictionary(strm, dictionary) {
+ var dictLength = dictionary.length;
+
+ var s;
+ var str, n;
+ var wrap;
+ var avail;
+ var next;
+ var input;
+ var tmpDict;
+
+ if (!strm/*== Z_NULL*/ || !strm.state/*== Z_NULL*/) {
+ return Z_STREAM_ERROR;
+ }
+
+ s = strm.state;
+ wrap = s.wrap;
+
+ if (wrap === 2 || (wrap === 1 && s.status !== INIT_STATE) || s.lookahead) {
+ return Z_STREAM_ERROR;
+ }
+
+ /* when using zlib wrappers, compute Adler-32 for provided dictionary */
+ if (wrap === 1) {
+ /* adler32(strm->adler, dictionary, dictLength); */
+ strm.adler = adler32(strm.adler, dictionary, dictLength, 0);
+ }
+
+ s.wrap = 0; /* avoid computing Adler-32 in read_buf */
+
+ /* if dictionary would fill window, just replace the history */
+ if (dictLength >= s.w_size) {
+ if (wrap === 0) { /* already empty otherwise */
+ /*** CLEAR_HASH(s); ***/
+ zero(s.head); // Fill with NIL (= 0);
+ s.strstart = 0;
+ s.block_start = 0;
+ s.insert = 0;
+ }
+ /* use the tail */
+ // dictionary = dictionary.slice(dictLength - s.w_size);
+ tmpDict = new utils.Buf8(s.w_size);
+ utils.arraySet(tmpDict, dictionary, dictLength - s.w_size, s.w_size, 0);
+ dictionary = tmpDict;
+ dictLength = s.w_size;
+ }
+ /* insert dictionary into window and hash */
+ avail = strm.avail_in;
+ next = strm.next_in;
+ input = strm.input;
+ strm.avail_in = dictLength;
+ strm.next_in = 0;
+ strm.input = dictionary;
+ fill_window(s);
+ while (s.lookahead >= MIN_MATCH) {
+ str = s.strstart;
+ n = s.lookahead - (MIN_MATCH - 1);
+ do {
+ /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */
+ s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + MIN_MATCH - 1]) & s.hash_mask;
+
+ s.prev[str & s.w_mask] = s.head[s.ins_h];
+
+ s.head[s.ins_h] = str;
+ str++;
+ } while (--n);
+ s.strstart = str;
+ s.lookahead = MIN_MATCH - 1;
+ fill_window(s);
+ }
+ s.strstart += s.lookahead;
+ s.block_start = s.strstart;
+ s.insert = s.lookahead;
+ s.lookahead = 0;
+ s.match_length = s.prev_length = MIN_MATCH - 1;
+ s.match_available = 0;
+ strm.next_in = next;
+ strm.input = input;
+ strm.avail_in = avail;
+ s.wrap = wrap;
+ return Z_OK;
+}
+
+
+export { deflateInit, deflateInit2, deflateReset, deflateResetKeep, deflateSetHeader, deflate, deflateEnd, deflateSetDictionary };
+export var deflateInfo = 'pako deflate (from Nodeca project)';
+
+/* Not implemented
+exports.deflateBound = deflateBound;
+exports.deflateCopy = deflateCopy;
+exports.deflateParams = deflateParams;
+exports.deflatePending = deflatePending;
+exports.deflatePrime = deflatePrime;
+exports.deflateTune = deflateTune;
+*/
pkg/web/noVNC/vendor/pako/lib/zlib/gzheader.js
@@ -0,0 +1,35 @@
+export default function GZheader() {
+ /* true if compressed data believed to be text */
+ this.text = 0;
+ /* modification time */
+ this.time = 0;
+ /* extra flags (not used when writing a gzip file) */
+ this.xflags = 0;
+ /* operating system */
+ this.os = 0;
+ /* pointer to extra field or Z_NULL if none */
+ this.extra = null;
+ /* extra field length (valid if extra != Z_NULL) */
+ this.extra_len = 0; // Actually, we don't need it in JS,
+ // but leave for few code modifications
+
+ //
+ // Setup limits is not necessary because in js we should not preallocate memory
+ // for inflate use constant limit in 65536 bytes
+ //
+
+ /* space at extra (only when reading header) */
+ // this.extra_max = 0;
+ /* pointer to zero-terminated file name or Z_NULL */
+ this.name = '';
+ /* space at name (only when reading header) */
+ // this.name_max = 0;
+ /* pointer to zero-terminated comment or Z_NULL */
+ this.comment = '';
+ /* space at comment (only when reading header) */
+ // this.comm_max = 0;
+ /* true if there was or will be a header crc */
+ this.hcrc = 0;
+ /* true when done reading gzip header (not used when writing a gzip file) */
+ this.done = false;
+}
pkg/web/noVNC/vendor/pako/lib/zlib/inffast.js
@@ -0,0 +1,324 @@
+// See state defs from inflate.js
+var BAD = 30; /* got a data error -- remain here until reset */
+var TYPE = 12; /* i: waiting for type bits, including last-flag bit */
+
+/*
+ Decode literal, length, and distance codes and write out the resulting
+ literal and match bytes until either not enough input or output is
+ available, an end-of-block is encountered, or a data error is encountered.
+ When large enough input and output buffers are supplied to inflate(), for
+ example, a 16K input buffer and a 64K output buffer, more than 95% of the
+ inflate execution time is spent in this routine.
+
+ Entry assumptions:
+
+ state.mode === LEN
+ strm.avail_in >= 6
+ strm.avail_out >= 258
+ start >= strm.avail_out
+ state.bits < 8
+
+ On return, state.mode is one of:
+
+ LEN -- ran out of enough output space or enough available input
+ TYPE -- reached end of block code, inflate() to interpret next block
+ BAD -- error in block data
+
+ Notes:
+
+ - The maximum input bits used by a length/distance pair is 15 bits for the
+ length code, 5 bits for the length extra, 15 bits for the distance code,
+ and 13 bits for the distance extra. This totals 48 bits, or six bytes.
+ Therefore if strm.avail_in >= 6, then there is enough input to avoid
+ checking for available input while decoding.
+
+ - The maximum bytes that a single length/distance pair can output is 258
+ bytes, which is the maximum length that can be coded. inflate_fast()
+ requires strm.avail_out >= 258 for each loop to avoid checking for
+ output space.
+ */
+export default function inflate_fast(strm, start) {
+ var state;
+ var _in; /* local strm.input */
+ var last; /* have enough input while in < last */
+ var _out; /* local strm.output */
+ var beg; /* inflate()'s initial strm.output */
+ var end; /* while out < end, enough space available */
+//#ifdef INFLATE_STRICT
+ var dmax; /* maximum distance from zlib header */
+//#endif
+ var wsize; /* window size or zero if not using window */
+ var whave; /* valid bytes in the window */
+ var wnext; /* window write index */
+ // Use `s_window` instead `window`, avoid conflict with instrumentation tools
+ var s_window; /* allocated sliding window, if wsize != 0 */
+ var hold; /* local strm.hold */
+ var bits; /* local strm.bits */
+ var lcode; /* local strm.lencode */
+ var dcode; /* local strm.distcode */
+ var lmask; /* mask for first level of length codes */
+ var dmask; /* mask for first level of distance codes */
+ var here; /* retrieved table entry */
+ var op; /* code bits, operation, extra bits, or */
+ /* window position, window bytes to copy */
+ var len; /* match length, unused bytes */
+ var dist; /* match distance */
+ var from; /* where to copy match from */
+ var from_source;
+
+
+ var input, output; // JS specific, because we have no pointers
+
+ /* copy state to local variables */
+ state = strm.state;
+ //here = state.here;
+ _in = strm.next_in;
+ input = strm.input;
+ last = _in + (strm.avail_in - 5);
+ _out = strm.next_out;
+ output = strm.output;
+ beg = _out - (start - strm.avail_out);
+ end = _out + (strm.avail_out - 257);
+//#ifdef INFLATE_STRICT
+ dmax = state.dmax;
+//#endif
+ wsize = state.wsize;
+ whave = state.whave;
+ wnext = state.wnext;
+ s_window = state.window;
+ hold = state.hold;
+ bits = state.bits;
+ lcode = state.lencode;
+ dcode = state.distcode;
+ lmask = (1 << state.lenbits) - 1;
+ dmask = (1 << state.distbits) - 1;
+
+
+ /* decode literals and length/distances until end-of-block or not enough
+ input data or output space */
+
+ top:
+ do {
+ if (bits < 15) {
+ hold += input[_in++] << bits;
+ bits += 8;
+ hold += input[_in++] << bits;
+ bits += 8;
+ }
+
+ here = lcode[hold & lmask];
+
+ dolen:
+ for (;;) { // Goto emulation
+ op = here >>> 24/*here.bits*/;
+ hold >>>= op;
+ bits -= op;
+ op = (here >>> 16) & 0xff/*here.op*/;
+ if (op === 0) { /* literal */
+ //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
+ // "inflate: literal '%c'\n" :
+ // "inflate: literal 0x%02x\n", here.val));
+ output[_out++] = here & 0xffff/*here.val*/;
+ }
+ else if (op & 16) { /* length base */
+ len = here & 0xffff/*here.val*/;
+ op &= 15; /* number of extra bits */
+ if (op) {
+ if (bits < op) {
+ hold += input[_in++] << bits;
+ bits += 8;
+ }
+ len += hold & ((1 << op) - 1);
+ hold >>>= op;
+ bits -= op;
+ }
+ //Tracevv((stderr, "inflate: length %u\n", len));
+ if (bits < 15) {
+ hold += input[_in++] << bits;
+ bits += 8;
+ hold += input[_in++] << bits;
+ bits += 8;
+ }
+ here = dcode[hold & dmask];
+
+ dodist:
+ for (;;) { // goto emulation
+ op = here >>> 24/*here.bits*/;
+ hold >>>= op;
+ bits -= op;
+ op = (here >>> 16) & 0xff/*here.op*/;
+
+ if (op & 16) { /* distance base */
+ dist = here & 0xffff/*here.val*/;
+ op &= 15; /* number of extra bits */
+ if (bits < op) {
+ hold += input[_in++] << bits;
+ bits += 8;
+ if (bits < op) {
+ hold += input[_in++] << bits;
+ bits += 8;
+ }
+ }
+ dist += hold & ((1 << op) - 1);
+//#ifdef INFLATE_STRICT
+ if (dist > dmax) {
+ strm.msg = 'invalid distance too far back';
+ state.mode = BAD;
+ break top;
+ }
+//#endif
+ hold >>>= op;
+ bits -= op;
+ //Tracevv((stderr, "inflate: distance %u\n", dist));
+ op = _out - beg; /* max distance in output */
+ if (dist > op) { /* see if copy from window */
+ op = dist - op; /* distance back in window */
+ if (op > whave) {
+ if (state.sane) {
+ strm.msg = 'invalid distance too far back';
+ state.mode = BAD;
+ break top;
+ }
+
+// (!) This block is disabled in zlib defailts,
+// don't enable it for binary compatibility
+//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
+// if (len <= op - whave) {
+// do {
+// output[_out++] = 0;
+// } while (--len);
+// continue top;
+// }
+// len -= op - whave;
+// do {
+// output[_out++] = 0;
+// } while (--op > whave);
+// if (op === 0) {
+// from = _out - dist;
+// do {
+// output[_out++] = output[from++];
+// } while (--len);
+// continue top;
+// }
+//#endif
+ }
+ from = 0; // window index
+ from_source = s_window;
+ if (wnext === 0) { /* very common case */
+ from += wsize - op;
+ if (op < len) { /* some from window */
+ len -= op;
+ do {
+ output[_out++] = s_window[from++];
+ } while (--op);
+ from = _out - dist; /* rest from output */
+ from_source = output;
+ }
+ }
+ else if (wnext < op) { /* wrap around window */
+ from += wsize + wnext - op;
+ op -= wnext;
+ if (op < len) { /* some from end of window */
+ len -= op;
+ do {
+ output[_out++] = s_window[from++];
+ } while (--op);
+ from = 0;
+ if (wnext < len) { /* some from start of window */
+ op = wnext;
+ len -= op;
+ do {
+ output[_out++] = s_window[from++];
+ } while (--op);
+ from = _out - dist; /* rest from output */
+ from_source = output;
+ }
+ }
+ }
+ else { /* contiguous in window */
+ from += wnext - op;
+ if (op < len) { /* some from window */
+ len -= op;
+ do {
+ output[_out++] = s_window[from++];
+ } while (--op);
+ from = _out - dist; /* rest from output */
+ from_source = output;
+ }
+ }
+ while (len > 2) {
+ output[_out++] = from_source[from++];
+ output[_out++] = from_source[from++];
+ output[_out++] = from_source[from++];
+ len -= 3;
+ }
+ if (len) {
+ output[_out++] = from_source[from++];
+ if (len > 1) {
+ output[_out++] = from_source[from++];
+ }
+ }
+ }
+ else {
+ from = _out - dist; /* copy direct from output */
+ do { /* minimum length is three */
+ output[_out++] = output[from++];
+ output[_out++] = output[from++];
+ output[_out++] = output[from++];
+ len -= 3;
+ } while (len > 2);
+ if (len) {
+ output[_out++] = output[from++];
+ if (len > 1) {
+ output[_out++] = output[from++];
+ }
+ }
+ }
+ }
+ else if ((op & 64) === 0) { /* 2nd level distance code */
+ here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
+ continue dodist;
+ }
+ else {
+ strm.msg = 'invalid distance code';
+ state.mode = BAD;
+ break top;
+ }
+
+ break; // need to emulate goto via "continue"
+ }
+ }
+ else if ((op & 64) === 0) { /* 2nd level length code */
+ here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
+ continue dolen;
+ }
+ else if (op & 32) { /* end-of-block */
+ //Tracevv((stderr, "inflate: end of block\n"));
+ state.mode = TYPE;
+ break top;
+ }
+ else {
+ strm.msg = 'invalid literal/length code';
+ state.mode = BAD;
+ break top;
+ }
+
+ break; // need to emulate goto via "continue"
+ }
+ } while (_in < last && _out < end);
+
+ /* return unused bytes (on entry, bits < 8, so in won't go too far back) */
+ len = bits >> 3;
+ _in -= len;
+ bits -= len << 3;
+ hold &= (1 << bits) - 1;
+
+ /* update state and return */
+ strm.next_in = _in;
+ strm.next_out = _out;
+ strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last));
+ strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end));
+ state.hold = hold;
+ state.bits = bits;
+ return;
+};
pkg/web/noVNC/vendor/pako/lib/zlib/inflate.js
@@ -0,0 +1,1527 @@
+import * as utils from "../utils/common.js";
+import adler32 from "./adler32.js";
+import crc32 from "./crc32.js";
+import inflate_fast from "./inffast.js";
+import inflate_table from "./inftrees.js";
+
+var CODES = 0;
+var LENS = 1;
+var DISTS = 2;
+
+/* Public constants ==========================================================*/
+/* ===========================================================================*/
+
+
+/* Allowed flush values; see deflate() and inflate() below for details */
+//export const Z_NO_FLUSH = 0;
+//export const Z_PARTIAL_FLUSH = 1;
+//export const Z_SYNC_FLUSH = 2;
+//export const Z_FULL_FLUSH = 3;
+export const Z_FINISH = 4;
+export const Z_BLOCK = 5;
+export const Z_TREES = 6;
+
+
+/* Return codes for the compression/decompression functions. Negative values
+ * are errors, positive values are used for special but normal events.
+ */
+export const Z_OK = 0;
+export const Z_STREAM_END = 1;
+export const Z_NEED_DICT = 2;
+//export const Z_ERRNO = -1;
+export const Z_STREAM_ERROR = -2;
+export const Z_DATA_ERROR = -3;
+export const Z_MEM_ERROR = -4;
+export const Z_BUF_ERROR = -5;
+//export const Z_VERSION_ERROR = -6;
+
+/* The deflate compression method */
+export const Z_DEFLATED = 8;
+
+
+/* STATES ====================================================================*/
+/* ===========================================================================*/
+
+
+var HEAD = 1; /* i: waiting for magic header */
+var FLAGS = 2; /* i: waiting for method and flags (gzip) */
+var TIME = 3; /* i: waiting for modification time (gzip) */
+var OS = 4; /* i: waiting for extra flags and operating system (gzip) */
+var EXLEN = 5; /* i: waiting for extra length (gzip) */
+var EXTRA = 6; /* i: waiting for extra bytes (gzip) */
+var NAME = 7; /* i: waiting for end of file name (gzip) */
+var COMMENT = 8; /* i: waiting for end of comment (gzip) */
+var HCRC = 9; /* i: waiting for header crc (gzip) */
+var DICTID = 10; /* i: waiting for dictionary check value */
+var DICT = 11; /* waiting for inflateSetDictionary() call */
+var TYPE = 12; /* i: waiting for type bits, including last-flag bit */
+var TYPEDO = 13; /* i: same, but skip check to exit inflate on new block */
+var STORED = 14; /* i: waiting for stored size (length and complement) */
+var COPY_ = 15; /* i/o: same as COPY below, but only first time in */
+var COPY = 16; /* i/o: waiting for input or output to copy stored block */
+var TABLE = 17; /* i: waiting for dynamic block table lengths */
+var LENLENS = 18; /* i: waiting for code length code lengths */
+var CODELENS = 19; /* i: waiting for length/lit and distance code lengths */
+var LEN_ = 20; /* i: same as LEN below, but only first time in */
+var LEN = 21; /* i: waiting for length/lit/eob code */
+var LENEXT = 22; /* i: waiting for length extra bits */
+var DIST = 23; /* i: waiting for distance code */
+var DISTEXT = 24; /* i: waiting for distance extra bits */
+var MATCH = 25; /* o: waiting for output space to copy string */
+var LIT = 26; /* o: waiting for output space to write literal */
+var CHECK = 27; /* i: waiting for 32-bit check value */
+var LENGTH = 28; /* i: waiting for 32-bit length (gzip) */
+var DONE = 29; /* finished check, done -- remain here until reset */
+var BAD = 30; /* got a data error -- remain here until reset */
+var MEM = 31; /* got an inflate() memory error -- remain here until reset */
+var SYNC = 32; /* looking for synchronization bytes to restart inflate() */
+
+/* ===========================================================================*/
+
+
+
+var ENOUGH_LENS = 852;
+var ENOUGH_DISTS = 592;
+//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS);
+
+var MAX_WBITS = 15;
+/* 32K LZ77 window */
+var DEF_WBITS = MAX_WBITS;
+
+
+function zswap32(q) {
+ return (((q >>> 24) & 0xff) +
+ ((q >>> 8) & 0xff00) +
+ ((q & 0xff00) << 8) +
+ ((q & 0xff) << 24));
+}
+
+
+function InflateState() {
+ this.mode = 0; /* current inflate mode */
+ this.last = false; /* true if processing last block */
+ this.wrap = 0; /* bit 0 true for zlib, bit 1 true for gzip */
+ this.havedict = false; /* true if dictionary provided */
+ this.flags = 0; /* gzip header method and flags (0 if zlib) */
+ this.dmax = 0; /* zlib header max distance (INFLATE_STRICT) */
+ this.check = 0; /* protected copy of check value */
+ this.total = 0; /* protected copy of output count */
+ // TODO: may be {}
+ this.head = null; /* where to save gzip header information */
+
+ /* sliding window */
+ this.wbits = 0; /* log base 2 of requested window size */
+ this.wsize = 0; /* window size or zero if not using window */
+ this.whave = 0; /* valid bytes in the window */
+ this.wnext = 0; /* window write index */
+ this.window = null; /* allocated sliding window, if needed */
+
+ /* bit accumulator */
+ this.hold = 0; /* input bit accumulator */
+ this.bits = 0; /* number of bits in "in" */
+
+ /* for string and stored block copying */
+ this.length = 0; /* literal or length of data to copy */
+ this.offset = 0; /* distance back to copy string from */
+
+ /* for table and code decoding */
+ this.extra = 0; /* extra bits needed */
+
+ /* fixed and dynamic code tables */
+ this.lencode = null; /* starting table for length/literal codes */
+ this.distcode = null; /* starting table for distance codes */
+ this.lenbits = 0; /* index bits for lencode */
+ this.distbits = 0; /* index bits for distcode */
+
+ /* dynamic table building */
+ this.ncode = 0; /* number of code length code lengths */
+ this.nlen = 0; /* number of length code lengths */
+ this.ndist = 0; /* number of distance code lengths */
+ this.have = 0; /* number of code lengths in lens[] */
+ this.next = null; /* next available space in codes[] */
+
+ this.lens = new utils.Buf16(320); /* temporary storage for code lengths */
+ this.work = new utils.Buf16(288); /* work area for code table building */
+
+ /*
+ because we don't have pointers in js, we use lencode and distcode directly
+ as buffers so we don't need codes
+ */
+ //this.codes = new utils.Buf32(ENOUGH); /* space for code tables */
+ this.lendyn = null; /* dynamic table for length/literal codes (JS specific) */
+ this.distdyn = null; /* dynamic table for distance codes (JS specific) */
+ this.sane = 0; /* if false, allow invalid distance too far */
+ this.back = 0; /* bits back of last unprocessed length/lit */
+ this.was = 0; /* initial length of match */
+}
+
+function inflateResetKeep(strm) {
+ var state;
+
+ if (!strm || !strm.state) { return Z_STREAM_ERROR; }
+ state = strm.state;
+ strm.total_in = strm.total_out = state.total = 0;
+ strm.msg = ''; /*Z_NULL*/
+ if (state.wrap) { /* to support ill-conceived Java test suite */
+ strm.adler = state.wrap & 1;
+ }
+ state.mode = HEAD;
+ state.last = 0;
+ state.havedict = 0;
+ state.dmax = 32768;
+ state.head = null/*Z_NULL*/;
+ state.hold = 0;
+ state.bits = 0;
+ //state.lencode = state.distcode = state.next = state.codes;
+ state.lencode = state.lendyn = new utils.Buf32(ENOUGH_LENS);
+ state.distcode = state.distdyn = new utils.Buf32(ENOUGH_DISTS);
+
+ state.sane = 1;
+ state.back = -1;
+ //Tracev((stderr, "inflate: reset\n"));
+ return Z_OK;
+}
+
+function inflateReset(strm) {
+ var state;
+
+ if (!strm || !strm.state) { return Z_STREAM_ERROR; }
+ state = strm.state;
+ state.wsize = 0;
+ state.whave = 0;
+ state.wnext = 0;
+ return inflateResetKeep(strm);
+
+}
+
+function inflateReset2(strm, windowBits) {
+ var wrap;
+ var state;
+
+ /* get the state */
+ if (!strm || !strm.state) { return Z_STREAM_ERROR; }
+ state = strm.state;
+
+ /* extract wrap request from windowBits parameter */
+ if (windowBits < 0) {
+ wrap = 0;
+ windowBits = -windowBits;
+ }
+ else {
+ wrap = (windowBits >> 4) + 1;
+ if (windowBits < 48) {
+ windowBits &= 15;
+ }
+ }
+
+ /* set number of window bits, free window if different */
+ if (windowBits && (windowBits < 8 || windowBits > 15)) {
+ return Z_STREAM_ERROR;
+ }
+ if (state.window !== null && state.wbits !== windowBits) {
+ state.window = null;
+ }
+
+ /* update state and reset the rest of it */
+ state.wrap = wrap;
+ state.wbits = windowBits;
+ return inflateReset(strm);
+}
+
+function inflateInit2(strm, windowBits) {
+ var ret;
+ var state;
+
+ if (!strm) { return Z_STREAM_ERROR; }
+ //strm.msg = Z_NULL; /* in case we return an error */
+
+ state = new InflateState();
+
+ //if (state === Z_NULL) return Z_MEM_ERROR;
+ //Tracev((stderr, "inflate: allocated\n"));
+ strm.state = state;
+ state.window = null/*Z_NULL*/;
+ ret = inflateReset2(strm, windowBits);
+ if (ret !== Z_OK) {
+ strm.state = null/*Z_NULL*/;
+ }
+ return ret;
+}
+
+function inflateInit(strm) {
+ return inflateInit2(strm, DEF_WBITS);
+}
+
+
+/*
+ Return state with length and distance decoding tables and index sizes set to
+ fixed code decoding. Normally this returns fixed tables from inffixed.h.
+ If BUILDFIXED is defined, then instead this routine builds the tables the
+ first time it's called, and returns those tables the first time and
+ thereafter. This reduces the size of the code by about 2K bytes, in
+ exchange for a little execution time. However, BUILDFIXED should not be
+ used for threaded applications, since the rewriting of the tables and virgin
+ may not be thread-safe.
+ */
+var virgin = true;
+
+var lenfix, distfix; // We have no pointers in JS, so keep tables separate
+
+function fixedtables(state) {
+ /* build fixed huffman tables if first call (may not be thread safe) */
+ if (virgin) {
+ var sym;
+
+ lenfix = new utils.Buf32(512);
+ distfix = new utils.Buf32(32);
+
+ /* literal/length table */
+ sym = 0;
+ while (sym < 144) { state.lens[sym++] = 8; }
+ while (sym < 256) { state.lens[sym++] = 9; }
+ while (sym < 280) { state.lens[sym++] = 7; }
+ while (sym < 288) { state.lens[sym++] = 8; }
+
+ inflate_table(LENS, state.lens, 0, 288, lenfix, 0, state.work, { bits: 9 });
+
+ /* distance table */
+ sym = 0;
+ while (sym < 32) { state.lens[sym++] = 5; }
+
+ inflate_table(DISTS, state.lens, 0, 32, distfix, 0, state.work, { bits: 5 });
+
+ /* do this just once */
+ virgin = false;
+ }
+
+ state.lencode = lenfix;
+ state.lenbits = 9;
+ state.distcode = distfix;
+ state.distbits = 5;
+}
+
+
+/*
+ Update the window with the last wsize (normally 32K) bytes written before
+ returning. If window does not exist yet, create it. This is only called
+ when a window is already in use, or when output has been written during this
+ inflate call, but the end of the deflate stream has not been reached yet.
+ It is also called to create a window for dictionary data when a dictionary
+ is loaded.
+
+ Providing output buffers larger than 32K to inflate() should provide a speed
+ advantage, since only the last 32K of output is copied to the sliding window
+ upon return from inflate(), and since all distances after the first 32K of
+ output will fall in the output data, making match copies simpler and faster.
+ The advantage may be dependent on the size of the processor's data caches.
+ */
+function updatewindow(strm, src, end, copy) {
+ var dist;
+ var state = strm.state;
+
+ /* if it hasn't been done already, allocate space for the window */
+ if (state.window === null) {
+ state.wsize = 1 << state.wbits;
+ state.wnext = 0;
+ state.whave = 0;
+
+ state.window = new utils.Buf8(state.wsize);
+ }
+
+ /* copy state->wsize or less output bytes into the circular window */
+ if (copy >= state.wsize) {
+ utils.arraySet(state.window, src, end - state.wsize, state.wsize, 0);
+ state.wnext = 0;
+ state.whave = state.wsize;
+ }
+ else {
+ dist = state.wsize - state.wnext;
+ if (dist > copy) {
+ dist = copy;
+ }
+ //zmemcpy(state->window + state->wnext, end - copy, dist);
+ utils.arraySet(state.window, src, end - copy, dist, state.wnext);
+ copy -= dist;
+ if (copy) {
+ //zmemcpy(state->window, end - copy, copy);
+ utils.arraySet(state.window, src, end - copy, copy, 0);
+ state.wnext = copy;
+ state.whave = state.wsize;
+ }
+ else {
+ state.wnext += dist;
+ if (state.wnext === state.wsize) { state.wnext = 0; }
+ if (state.whave < state.wsize) { state.whave += dist; }
+ }
+ }
+ return 0;
+}
+
+function inflate(strm, flush) {
+ var state;
+ var input, output; // input/output buffers
+ var next; /* next input INDEX */
+ var put; /* next output INDEX */
+ var have, left; /* available input and output */
+ var hold; /* bit buffer */
+ var bits; /* bits in bit buffer */
+ var _in, _out; /* save starting available input and output */
+ var copy; /* number of stored or match bytes to copy */
+ var from; /* where to copy match bytes from */
+ var from_source;
+ var here = 0; /* current decoding table entry */
+ var here_bits, here_op, here_val; // paked "here" denormalized (JS specific)
+ //var last; /* parent table entry */
+ var last_bits, last_op, last_val; // paked "last" denormalized (JS specific)
+ var len; /* length to copy for repeats, bits to drop */
+ var ret; /* return code */
+ var hbuf = new utils.Buf8(4); /* buffer for gzip header crc calculation */
+ var opts;
+
+ var n; // temporary var for NEED_BITS
+
+ var order = /* permutation of code lengths */
+ [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ];
+
+
+ if (!strm || !strm.state || !strm.output ||
+ (!strm.input && strm.avail_in !== 0)) {
+ return Z_STREAM_ERROR;
+ }
+
+ state = strm.state;
+ if (state.mode === TYPE) { state.mode = TYPEDO; } /* skip check */
+
+
+ //--- LOAD() ---
+ put = strm.next_out;
+ output = strm.output;
+ left = strm.avail_out;
+ next = strm.next_in;
+ input = strm.input;
+ have = strm.avail_in;
+ hold = state.hold;
+ bits = state.bits;
+ //---
+
+ _in = have;
+ _out = left;
+ ret = Z_OK;
+
+ inf_leave: // goto emulation
+ for (;;) {
+ switch (state.mode) {
+ case HEAD:
+ if (state.wrap === 0) {
+ state.mode = TYPEDO;
+ break;
+ }
+ //=== NEEDBITS(16);
+ while (bits < 16) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ if ((state.wrap & 2) && hold === 0x8b1f) { /* gzip header */
+ state.check = 0/*crc32(0L, Z_NULL, 0)*/;
+ //=== CRC2(state.check, hold);
+ hbuf[0] = hold & 0xff;
+ hbuf[1] = (hold >>> 8) & 0xff;
+ state.check = crc32(state.check, hbuf, 2, 0);
+ //===//
+
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ state.mode = FLAGS;
+ break;
+ }
+ state.flags = 0; /* expect zlib header */
+ if (state.head) {
+ state.head.done = false;
+ }
+ if (!(state.wrap & 1) || /* check if zlib header allowed */
+ (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) {
+ strm.msg = 'incorrect header check';
+ state.mode = BAD;
+ break;
+ }
+ if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED) {
+ strm.msg = 'unknown compression method';
+ state.mode = BAD;
+ break;
+ }
+ //--- DROPBITS(4) ---//
+ hold >>>= 4;
+ bits -= 4;
+ //---//
+ len = (hold & 0x0f)/*BITS(4)*/ + 8;
+ if (state.wbits === 0) {
+ state.wbits = len;
+ }
+ else if (len > state.wbits) {
+ strm.msg = 'invalid window size';
+ state.mode = BAD;
+ break;
+ }
+ state.dmax = 1 << len;
+ //Tracev((stderr, "inflate: zlib header ok\n"));
+ strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
+ state.mode = hold & 0x200 ? DICTID : TYPE;
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ break;
+ case FLAGS:
+ //=== NEEDBITS(16); */
+ while (bits < 16) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ state.flags = hold;
+ if ((state.flags & 0xff) !== Z_DEFLATED) {
+ strm.msg = 'unknown compression method';
+ state.mode = BAD;
+ break;
+ }
+ if (state.flags & 0xe000) {
+ strm.msg = 'unknown header flags set';
+ state.mode = BAD;
+ break;
+ }
+ if (state.head) {
+ state.head.text = ((hold >> 8) & 1);
+ }
+ if (state.flags & 0x0200) {
+ //=== CRC2(state.check, hold);
+ hbuf[0] = hold & 0xff;
+ hbuf[1] = (hold >>> 8) & 0xff;
+ state.check = crc32(state.check, hbuf, 2, 0);
+ //===//
+ }
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ state.mode = TIME;
+ /* falls through */
+ case TIME:
+ //=== NEEDBITS(32); */
+ while (bits < 32) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ if (state.head) {
+ state.head.time = hold;
+ }
+ if (state.flags & 0x0200) {
+ //=== CRC4(state.check, hold)
+ hbuf[0] = hold & 0xff;
+ hbuf[1] = (hold >>> 8) & 0xff;
+ hbuf[2] = (hold >>> 16) & 0xff;
+ hbuf[3] = (hold >>> 24) & 0xff;
+ state.check = crc32(state.check, hbuf, 4, 0);
+ //===
+ }
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ state.mode = OS;
+ /* falls through */
+ case OS:
+ //=== NEEDBITS(16); */
+ while (bits < 16) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ if (state.head) {
+ state.head.xflags = (hold & 0xff);
+ state.head.os = (hold >> 8);
+ }
+ if (state.flags & 0x0200) {
+ //=== CRC2(state.check, hold);
+ hbuf[0] = hold & 0xff;
+ hbuf[1] = (hold >>> 8) & 0xff;
+ state.check = crc32(state.check, hbuf, 2, 0);
+ //===//
+ }
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ state.mode = EXLEN;
+ /* falls through */
+ case EXLEN:
+ if (state.flags & 0x0400) {
+ //=== NEEDBITS(16); */
+ while (bits < 16) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ state.length = hold;
+ if (state.head) {
+ state.head.extra_len = hold;
+ }
+ if (state.flags & 0x0200) {
+ //=== CRC2(state.check, hold);
+ hbuf[0] = hold & 0xff;
+ hbuf[1] = (hold >>> 8) & 0xff;
+ state.check = crc32(state.check, hbuf, 2, 0);
+ //===//
+ }
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ }
+ else if (state.head) {
+ state.head.extra = null/*Z_NULL*/;
+ }
+ state.mode = EXTRA;
+ /* falls through */
+ case EXTRA:
+ if (state.flags & 0x0400) {
+ copy = state.length;
+ if (copy > have) { copy = have; }
+ if (copy) {
+ if (state.head) {
+ len = state.head.extra_len - state.length;
+ if (!state.head.extra) {
+ // Use untyped array for more conveniend processing later
+ state.head.extra = new Array(state.head.extra_len);
+ }
+ utils.arraySet(
+ state.head.extra,
+ input,
+ next,
+ // extra field is limited to 65536 bytes
+ // - no need for additional size check
+ copy,
+ /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/
+ len
+ );
+ //zmemcpy(state.head.extra + len, next,
+ // len + copy > state.head.extra_max ?
+ // state.head.extra_max - len : copy);
+ }
+ if (state.flags & 0x0200) {
+ state.check = crc32(state.check, input, copy, next);
+ }
+ have -= copy;
+ next += copy;
+ state.length -= copy;
+ }
+ if (state.length) { break inf_leave; }
+ }
+ state.length = 0;
+ state.mode = NAME;
+ /* falls through */
+ case NAME:
+ if (state.flags & 0x0800) {
+ if (have === 0) { break inf_leave; }
+ copy = 0;
+ do {
+ // TODO: 2 or 1 bytes?
+ len = input[next + copy++];
+ /* use constant limit because in js we should not preallocate memory */
+ if (state.head && len &&
+ (state.length < 65536 /*state.head.name_max*/)) {
+ state.head.name += String.fromCharCode(len);
+ }
+ } while (len && copy < have);
+
+ if (state.flags & 0x0200) {
+ state.check = crc32(state.check, input, copy, next);
+ }
+ have -= copy;
+ next += copy;
+ if (len) { break inf_leave; }
+ }
+ else if (state.head) {
+ state.head.name = null;
+ }
+ state.length = 0;
+ state.mode = COMMENT;
+ /* falls through */
+ case COMMENT:
+ if (state.flags & 0x1000) {
+ if (have === 0) { break inf_leave; }
+ copy = 0;
+ do {
+ len = input[next + copy++];
+ /* use constant limit because in js we should not preallocate memory */
+ if (state.head && len &&
+ (state.length < 65536 /*state.head.comm_max*/)) {
+ state.head.comment += String.fromCharCode(len);
+ }
+ } while (len && copy < have);
+ if (state.flags & 0x0200) {
+ state.check = crc32(state.check, input, copy, next);
+ }
+ have -= copy;
+ next += copy;
+ if (len) { break inf_leave; }
+ }
+ else if (state.head) {
+ state.head.comment = null;
+ }
+ state.mode = HCRC;
+ /* falls through */
+ case HCRC:
+ if (state.flags & 0x0200) {
+ //=== NEEDBITS(16); */
+ while (bits < 16) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ if (hold !== (state.check & 0xffff)) {
+ strm.msg = 'header crc mismatch';
+ state.mode = BAD;
+ break;
+ }
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ }
+ if (state.head) {
+ state.head.hcrc = ((state.flags >> 9) & 1);
+ state.head.done = true;
+ }
+ strm.adler = state.check = 0;
+ state.mode = TYPE;
+ break;
+ case DICTID:
+ //=== NEEDBITS(32); */
+ while (bits < 32) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ strm.adler = state.check = zswap32(hold);
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ state.mode = DICT;
+ /* falls through */
+ case DICT:
+ if (state.havedict === 0) {
+ //--- RESTORE() ---
+ strm.next_out = put;
+ strm.avail_out = left;
+ strm.next_in = next;
+ strm.avail_in = have;
+ state.hold = hold;
+ state.bits = bits;
+ //---
+ return Z_NEED_DICT;
+ }
+ strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
+ state.mode = TYPE;
+ /* falls through */
+ case TYPE:
+ if (flush === Z_BLOCK || flush === Z_TREES) { break inf_leave; }
+ /* falls through */
+ case TYPEDO:
+ if (state.last) {
+ //--- BYTEBITS() ---//
+ hold >>>= bits & 7;
+ bits -= bits & 7;
+ //---//
+ state.mode = CHECK;
+ break;
+ }
+ //=== NEEDBITS(3); */
+ while (bits < 3) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ state.last = (hold & 0x01)/*BITS(1)*/;
+ //--- DROPBITS(1) ---//
+ hold >>>= 1;
+ bits -= 1;
+ //---//
+
+ switch ((hold & 0x03)/*BITS(2)*/) {
+ case 0: /* stored block */
+ //Tracev((stderr, "inflate: stored block%s\n",
+ // state.last ? " (last)" : ""));
+ state.mode = STORED;
+ break;
+ case 1: /* fixed block */
+ fixedtables(state);
+ //Tracev((stderr, "inflate: fixed codes block%s\n",
+ // state.last ? " (last)" : ""));
+ state.mode = LEN_; /* decode codes */
+ if (flush === Z_TREES) {
+ //--- DROPBITS(2) ---//
+ hold >>>= 2;
+ bits -= 2;
+ //---//
+ break inf_leave;
+ }
+ break;
+ case 2: /* dynamic block */
+ //Tracev((stderr, "inflate: dynamic codes block%s\n",
+ // state.last ? " (last)" : ""));
+ state.mode = TABLE;
+ break;
+ case 3:
+ strm.msg = 'invalid block type';
+ state.mode = BAD;
+ }
+ //--- DROPBITS(2) ---//
+ hold >>>= 2;
+ bits -= 2;
+ //---//
+ break;
+ case STORED:
+ //--- BYTEBITS() ---// /* go to byte boundary */
+ hold >>>= bits & 7;
+ bits -= bits & 7;
+ //---//
+ //=== NEEDBITS(32); */
+ while (bits < 32) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) {
+ strm.msg = 'invalid stored block lengths';
+ state.mode = BAD;
+ break;
+ }
+ state.length = hold & 0xffff;
+ //Tracev((stderr, "inflate: stored length %u\n",
+ // state.length));
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ state.mode = COPY_;
+ if (flush === Z_TREES) { break inf_leave; }
+ /* falls through */
+ case COPY_:
+ state.mode = COPY;
+ /* falls through */
+ case COPY:
+ copy = state.length;
+ if (copy) {
+ if (copy > have) { copy = have; }
+ if (copy > left) { copy = left; }
+ if (copy === 0) { break inf_leave; }
+ //--- zmemcpy(put, next, copy); ---
+ utils.arraySet(output, input, next, copy, put);
+ //---//
+ have -= copy;
+ next += copy;
+ left -= copy;
+ put += copy;
+ state.length -= copy;
+ break;
+ }
+ //Tracev((stderr, "inflate: stored end\n"));
+ state.mode = TYPE;
+ break;
+ case TABLE:
+ //=== NEEDBITS(14); */
+ while (bits < 14) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257;
+ //--- DROPBITS(5) ---//
+ hold >>>= 5;
+ bits -= 5;
+ //---//
+ state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1;
+ //--- DROPBITS(5) ---//
+ hold >>>= 5;
+ bits -= 5;
+ //---//
+ state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4;
+ //--- DROPBITS(4) ---//
+ hold >>>= 4;
+ bits -= 4;
+ //---//
+//#ifndef PKZIP_BUG_WORKAROUND
+ if (state.nlen > 286 || state.ndist > 30) {
+ strm.msg = 'too many length or distance symbols';
+ state.mode = BAD;
+ break;
+ }
+//#endif
+ //Tracev((stderr, "inflate: table sizes ok\n"));
+ state.have = 0;
+ state.mode = LENLENS;
+ /* falls through */
+ case LENLENS:
+ while (state.have < state.ncode) {
+ //=== NEEDBITS(3);
+ while (bits < 3) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ state.lens[order[state.have++]] = (hold & 0x07);//BITS(3);
+ //--- DROPBITS(3) ---//
+ hold >>>= 3;
+ bits -= 3;
+ //---//
+ }
+ while (state.have < 19) {
+ state.lens[order[state.have++]] = 0;
+ }
+ // We have separate tables & no pointers. 2 commented lines below not needed.
+ //state.next = state.codes;
+ //state.lencode = state.next;
+ // Switch to use dynamic table
+ state.lencode = state.lendyn;
+ state.lenbits = 7;
+
+ opts = { bits: state.lenbits };
+ ret = inflate_table(CODES, state.lens, 0, 19, state.lencode, 0, state.work, opts);
+ state.lenbits = opts.bits;
+
+ if (ret) {
+ strm.msg = 'invalid code lengths set';
+ state.mode = BAD;
+ break;
+ }
+ //Tracev((stderr, "inflate: code lengths ok\n"));
+ state.have = 0;
+ state.mode = CODELENS;
+ /* falls through */
+ case CODELENS:
+ while (state.have < state.nlen + state.ndist) {
+ for (;;) {
+ here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/
+ here_bits = here >>> 24;
+ here_op = (here >>> 16) & 0xff;
+ here_val = here & 0xffff;
+
+ if ((here_bits) <= bits) { break; }
+ //--- PULLBYTE() ---//
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ //---//
+ }
+ if (here_val < 16) {
+ //--- DROPBITS(here.bits) ---//
+ hold >>>= here_bits;
+ bits -= here_bits;
+ //---//
+ state.lens[state.have++] = here_val;
+ }
+ else {
+ if (here_val === 16) {
+ //=== NEEDBITS(here.bits + 2);
+ n = here_bits + 2;
+ while (bits < n) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ //--- DROPBITS(here.bits) ---//
+ hold >>>= here_bits;
+ bits -= here_bits;
+ //---//
+ if (state.have === 0) {
+ strm.msg = 'invalid bit length repeat';
+ state.mode = BAD;
+ break;
+ }
+ len = state.lens[state.have - 1];
+ copy = 3 + (hold & 0x03);//BITS(2);
+ //--- DROPBITS(2) ---//
+ hold >>>= 2;
+ bits -= 2;
+ //---//
+ }
+ else if (here_val === 17) {
+ //=== NEEDBITS(here.bits + 3);
+ n = here_bits + 3;
+ while (bits < n) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ //--- DROPBITS(here.bits) ---//
+ hold >>>= here_bits;
+ bits -= here_bits;
+ //---//
+ len = 0;
+ copy = 3 + (hold & 0x07);//BITS(3);
+ //--- DROPBITS(3) ---//
+ hold >>>= 3;
+ bits -= 3;
+ //---//
+ }
+ else {
+ //=== NEEDBITS(here.bits + 7);
+ n = here_bits + 7;
+ while (bits < n) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ //--- DROPBITS(here.bits) ---//
+ hold >>>= here_bits;
+ bits -= here_bits;
+ //---//
+ len = 0;
+ copy = 11 + (hold & 0x7f);//BITS(7);
+ //--- DROPBITS(7) ---//
+ hold >>>= 7;
+ bits -= 7;
+ //---//
+ }
+ if (state.have + copy > state.nlen + state.ndist) {
+ strm.msg = 'invalid bit length repeat';
+ state.mode = BAD;
+ break;
+ }
+ while (copy--) {
+ state.lens[state.have++] = len;
+ }
+ }
+ }
+
+ /* handle error breaks in while */
+ if (state.mode === BAD) { break; }
+
+ /* check for end-of-block code (better have one) */
+ if (state.lens[256] === 0) {
+ strm.msg = 'invalid code -- missing end-of-block';
+ state.mode = BAD;
+ break;
+ }
+
+ /* build code tables -- note: do not change the lenbits or distbits
+ values here (9 and 6) without reading the comments in inftrees.h
+ concerning the ENOUGH constants, which depend on those values */
+ state.lenbits = 9;
+
+ opts = { bits: state.lenbits };
+ ret = inflate_table(LENS, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts);
+ // We have separate tables & no pointers. 2 commented lines below not needed.
+ // state.next_index = opts.table_index;
+ state.lenbits = opts.bits;
+ // state.lencode = state.next;
+
+ if (ret) {
+ strm.msg = 'invalid literal/lengths set';
+ state.mode = BAD;
+ break;
+ }
+
+ state.distbits = 6;
+ //state.distcode.copy(state.codes);
+ // Switch to use dynamic table
+ state.distcode = state.distdyn;
+ opts = { bits: state.distbits };
+ ret = inflate_table(DISTS, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts);
+ // We have separate tables & no pointers. 2 commented lines below not needed.
+ // state.next_index = opts.table_index;
+ state.distbits = opts.bits;
+ // state.distcode = state.next;
+
+ if (ret) {
+ strm.msg = 'invalid distances set';
+ state.mode = BAD;
+ break;
+ }
+ //Tracev((stderr, 'inflate: codes ok\n'));
+ state.mode = LEN_;
+ if (flush === Z_TREES) { break inf_leave; }
+ /* falls through */
+ case LEN_:
+ state.mode = LEN;
+ /* falls through */
+ case LEN:
+ if (have >= 6 && left >= 258) {
+ //--- RESTORE() ---
+ strm.next_out = put;
+ strm.avail_out = left;
+ strm.next_in = next;
+ strm.avail_in = have;
+ state.hold = hold;
+ state.bits = bits;
+ //---
+ inflate_fast(strm, _out);
+ //--- LOAD() ---
+ put = strm.next_out;
+ output = strm.output;
+ left = strm.avail_out;
+ next = strm.next_in;
+ input = strm.input;
+ have = strm.avail_in;
+ hold = state.hold;
+ bits = state.bits;
+ //---
+
+ if (state.mode === TYPE) {
+ state.back = -1;
+ }
+ break;
+ }
+ state.back = 0;
+ for (;;) {
+ here = state.lencode[hold & ((1 << state.lenbits) - 1)]; /*BITS(state.lenbits)*/
+ here_bits = here >>> 24;
+ here_op = (here >>> 16) & 0xff;
+ here_val = here & 0xffff;
+
+ if (here_bits <= bits) { break; }
+ //--- PULLBYTE() ---//
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ //---//
+ }
+ if (here_op && (here_op & 0xf0) === 0) {
+ last_bits = here_bits;
+ last_op = here_op;
+ last_val = here_val;
+ for (;;) {
+ here = state.lencode[last_val +
+ ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
+ here_bits = here >>> 24;
+ here_op = (here >>> 16) & 0xff;
+ here_val = here & 0xffff;
+
+ if ((last_bits + here_bits) <= bits) { break; }
+ //--- PULLBYTE() ---//
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ //---//
+ }
+ //--- DROPBITS(last.bits) ---//
+ hold >>>= last_bits;
+ bits -= last_bits;
+ //---//
+ state.back += last_bits;
+ }
+ //--- DROPBITS(here.bits) ---//
+ hold >>>= here_bits;
+ bits -= here_bits;
+ //---//
+ state.back += here_bits;
+ state.length = here_val;
+ if (here_op === 0) {
+ //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
+ // "inflate: literal '%c'\n" :
+ // "inflate: literal 0x%02x\n", here.val));
+ state.mode = LIT;
+ break;
+ }
+ if (here_op & 32) {
+ //Tracevv((stderr, "inflate: end of block\n"));
+ state.back = -1;
+ state.mode = TYPE;
+ break;
+ }
+ if (here_op & 64) {
+ strm.msg = 'invalid literal/length code';
+ state.mode = BAD;
+ break;
+ }
+ state.extra = here_op & 15;
+ state.mode = LENEXT;
+ /* falls through */
+ case LENEXT:
+ if (state.extra) {
+ //=== NEEDBITS(state.extra);
+ n = state.extra;
+ while (bits < n) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ state.length += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
+ //--- DROPBITS(state.extra) ---//
+ hold >>>= state.extra;
+ bits -= state.extra;
+ //---//
+ state.back += state.extra;
+ }
+ //Tracevv((stderr, "inflate: length %u\n", state.length));
+ state.was = state.length;
+ state.mode = DIST;
+ /* falls through */
+ case DIST:
+ for (;;) {
+ here = state.distcode[hold & ((1 << state.distbits) - 1)];/*BITS(state.distbits)*/
+ here_bits = here >>> 24;
+ here_op = (here >>> 16) & 0xff;
+ here_val = here & 0xffff;
+
+ if ((here_bits) <= bits) { break; }
+ //--- PULLBYTE() ---//
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ //---//
+ }
+ if ((here_op & 0xf0) === 0) {
+ last_bits = here_bits;
+ last_op = here_op;
+ last_val = here_val;
+ for (;;) {
+ here = state.distcode[last_val +
+ ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
+ here_bits = here >>> 24;
+ here_op = (here >>> 16) & 0xff;
+ here_val = here & 0xffff;
+
+ if ((last_bits + here_bits) <= bits) { break; }
+ //--- PULLBYTE() ---//
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ //---//
+ }
+ //--- DROPBITS(last.bits) ---//
+ hold >>>= last_bits;
+ bits -= last_bits;
+ //---//
+ state.back += last_bits;
+ }
+ //--- DROPBITS(here.bits) ---//
+ hold >>>= here_bits;
+ bits -= here_bits;
+ //---//
+ state.back += here_bits;
+ if (here_op & 64) {
+ strm.msg = 'invalid distance code';
+ state.mode = BAD;
+ break;
+ }
+ state.offset = here_val;
+ state.extra = (here_op) & 15;
+ state.mode = DISTEXT;
+ /* falls through */
+ case DISTEXT:
+ if (state.extra) {
+ //=== NEEDBITS(state.extra);
+ n = state.extra;
+ while (bits < n) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ state.offset += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
+ //--- DROPBITS(state.extra) ---//
+ hold >>>= state.extra;
+ bits -= state.extra;
+ //---//
+ state.back += state.extra;
+ }
+//#ifdef INFLATE_STRICT
+ if (state.offset > state.dmax) {
+ strm.msg = 'invalid distance too far back';
+ state.mode = BAD;
+ break;
+ }
+//#endif
+ //Tracevv((stderr, "inflate: distance %u\n", state.offset));
+ state.mode = MATCH;
+ /* falls through */
+ case MATCH:
+ if (left === 0) { break inf_leave; }
+ copy = _out - left;
+ if (state.offset > copy) { /* copy from window */
+ copy = state.offset - copy;
+ if (copy > state.whave) {
+ if (state.sane) {
+ strm.msg = 'invalid distance too far back';
+ state.mode = BAD;
+ break;
+ }
+// (!) This block is disabled in zlib defailts,
+// don't enable it for binary compatibility
+//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
+// Trace((stderr, "inflate.c too far\n"));
+// copy -= state.whave;
+// if (copy > state.length) { copy = state.length; }
+// if (copy > left) { copy = left; }
+// left -= copy;
+// state.length -= copy;
+// do {
+// output[put++] = 0;
+// } while (--copy);
+// if (state.length === 0) { state.mode = LEN; }
+// break;
+//#endif
+ }
+ if (copy > state.wnext) {
+ copy -= state.wnext;
+ from = state.wsize - copy;
+ }
+ else {
+ from = state.wnext - copy;
+ }
+ if (copy > state.length) { copy = state.length; }
+ from_source = state.window;
+ }
+ else { /* copy from output */
+ from_source = output;
+ from = put - state.offset;
+ copy = state.length;
+ }
+ if (copy > left) { copy = left; }
+ left -= copy;
+ state.length -= copy;
+ do {
+ output[put++] = from_source[from++];
+ } while (--copy);
+ if (state.length === 0) { state.mode = LEN; }
+ break;
+ case LIT:
+ if (left === 0) { break inf_leave; }
+ output[put++] = state.length;
+ left--;
+ state.mode = LEN;
+ break;
+ case CHECK:
+ if (state.wrap) {
+ //=== NEEDBITS(32);
+ while (bits < 32) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ // Use '|' insdead of '+' to make sure that result is signed
+ hold |= input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ _out -= left;
+ strm.total_out += _out;
+ state.total += _out;
+ if (_out) {
+ strm.adler = state.check =
+ /*UPDATE(state.check, put - _out, _out);*/
+ (state.flags ? crc32(state.check, output, _out, put - _out) : adler32(state.check, output, _out, put - _out));
+
+ }
+ _out = left;
+ // NB: crc32 stored as signed 32-bit int, zswap32 returns signed too
+ if ((state.flags ? hold : zswap32(hold)) !== state.check) {
+ strm.msg = 'incorrect data check';
+ state.mode = BAD;
+ break;
+ }
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ //Tracev((stderr, "inflate: check matches trailer\n"));
+ }
+ state.mode = LENGTH;
+ /* falls through */
+ case LENGTH:
+ if (state.wrap && state.flags) {
+ //=== NEEDBITS(32);
+ while (bits < 32) {
+ if (have === 0) { break inf_leave; }
+ have--;
+ hold += input[next++] << bits;
+ bits += 8;
+ }
+ //===//
+ if (hold !== (state.total & 0xffffffff)) {
+ strm.msg = 'incorrect length check';
+ state.mode = BAD;
+ break;
+ }
+ //=== INITBITS();
+ hold = 0;
+ bits = 0;
+ //===//
+ //Tracev((stderr, "inflate: length matches trailer\n"));
+ }
+ state.mode = DONE;
+ /* falls through */
+ case DONE:
+ ret = Z_STREAM_END;
+ break inf_leave;
+ case BAD:
+ ret = Z_DATA_ERROR;
+ break inf_leave;
+ case MEM:
+ return Z_MEM_ERROR;
+ case SYNC:
+ /* falls through */
+ default:
+ return Z_STREAM_ERROR;
+ }
+ }
+
+ // inf_leave <- here is real place for "goto inf_leave", emulated via "break inf_leave"
+
+ /*
+ Return from inflate(), updating the total counts and the check value.
+ If there was no progress during the inflate() call, return a buffer
+ error. Call updatewindow() to create and/or update the window state.
+ Note: a memory error from inflate() is non-recoverable.
+ */
+
+ //--- RESTORE() ---
+ strm.next_out = put;
+ strm.avail_out = left;
+ strm.next_in = next;
+ strm.avail_in = have;
+ state.hold = hold;
+ state.bits = bits;
+ //---
+
+ if (state.wsize || (_out !== strm.avail_out && state.mode < BAD &&
+ (state.mode < CHECK || flush !== Z_FINISH))) {
+ if (updatewindow(strm, strm.output, strm.next_out, _out - strm.avail_out)) {
+ state.mode = MEM;
+ return Z_MEM_ERROR;
+ }
+ }
+ _in -= strm.avail_in;
+ _out -= strm.avail_out;
+ strm.total_in += _in;
+ strm.total_out += _out;
+ state.total += _out;
+ if (state.wrap && _out) {
+ strm.adler = state.check = /*UPDATE(state.check, strm.next_out - _out, _out);*/
+ (state.flags ? crc32(state.check, output, _out, strm.next_out - _out) : adler32(state.check, output, _out, strm.next_out - _out));
+ }
+ strm.data_type = state.bits + (state.last ? 64 : 0) +
+ (state.mode === TYPE ? 128 : 0) +
+ (state.mode === LEN_ || state.mode === COPY_ ? 256 : 0);
+ if (((_in === 0 && _out === 0) || flush === Z_FINISH) && ret === Z_OK) {
+ ret = Z_BUF_ERROR;
+ }
+ return ret;
+}
+
+function inflateEnd(strm) {
+
+ if (!strm || !strm.state /*|| strm->zfree == (free_func)0*/) {
+ return Z_STREAM_ERROR;
+ }
+
+ var state = strm.state;
+ if (state.window) {
+ state.window = null;
+ }
+ strm.state = null;
+ return Z_OK;
+}
+
+function inflateGetHeader(strm, head) {
+ var state;
+
+ /* check state */
+ if (!strm || !strm.state) { return Z_STREAM_ERROR; }
+ state = strm.state;
+ if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR; }
+
+ /* save header structure */
+ state.head = head;
+ head.done = false;
+ return Z_OK;
+}
+
+function inflateSetDictionary(strm, dictionary) {
+ var dictLength = dictionary.length;
+
+ var state;
+ var dictid;
+ var ret;
+
+ /* check state */
+ if (!strm /* == Z_NULL */ || !strm.state /* == Z_NULL */) { return Z_STREAM_ERROR; }
+ state = strm.state;
+
+ if (state.wrap !== 0 && state.mode !== DICT) {
+ return Z_STREAM_ERROR;
+ }
+
+ /* check for correct dictionary identifier */
+ if (state.mode === DICT) {
+ dictid = 1; /* adler32(0, null, 0)*/
+ /* dictid = adler32(dictid, dictionary, dictLength); */
+ dictid = adler32(dictid, dictionary, dictLength, 0);
+ if (dictid !== state.check) {
+ return Z_DATA_ERROR;
+ }
+ }
+ /* copy dictionary to window using updatewindow(), which will amend the
+ existing dictionary if appropriate */
+ ret = updatewindow(strm, dictionary, dictLength, dictLength);
+ if (ret) {
+ state.mode = MEM;
+ return Z_MEM_ERROR;
+ }
+ state.havedict = 1;
+ // Tracev((stderr, "inflate: dictionary set\n"));
+ return Z_OK;
+}
+
+export { inflateReset, inflateReset2, inflateResetKeep, inflateInit, inflateInit2, inflate, inflateEnd, inflateGetHeader, inflateSetDictionary };
+export var inflateInfo = 'pako inflate (from Nodeca project)';
+
+/* Not implemented
+exports.inflateCopy = inflateCopy;
+exports.inflateGetDictionary = inflateGetDictionary;
+exports.inflateMark = inflateMark;
+exports.inflatePrime = inflatePrime;
+exports.inflateSync = inflateSync;
+exports.inflateSyncPoint = inflateSyncPoint;
+exports.inflateUndermine = inflateUndermine;
+*/
pkg/web/noVNC/vendor/pako/lib/zlib/inftrees.js
@@ -0,0 +1,322 @@
+import * as utils from "../utils/common.js";
+
+var MAXBITS = 15;
+var ENOUGH_LENS = 852;
+var ENOUGH_DISTS = 592;
+//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS);
+
+var CODES = 0;
+var LENS = 1;
+var DISTS = 2;
+
+var lbase = [ /* Length codes 257..285 base */
+ 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
+ 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0
+];
+
+var lext = [ /* Length codes 257..285 extra */
+ 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18,
+ 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78
+];
+
+var dbase = [ /* Distance codes 0..29 base */
+ 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
+ 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
+ 8193, 12289, 16385, 24577, 0, 0
+];
+
+var dext = [ /* Distance codes 0..29 extra */
+ 16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,
+ 23, 23, 24, 24, 25, 25, 26, 26, 27, 27,
+ 28, 28, 29, 29, 64, 64
+];
+
+export default function inflate_table(type, lens, lens_index, codes, table, table_index, work, opts)
+{
+ var bits = opts.bits;
+ //here = opts.here; /* table entry for duplication */
+
+ var len = 0; /* a code's length in bits */
+ var sym = 0; /* index of code symbols */
+ var min = 0, max = 0; /* minimum and maximum code lengths */
+ var root = 0; /* number of index bits for root table */
+ var curr = 0; /* number of index bits for current table */
+ var drop = 0; /* code bits to drop for sub-table */
+ var left = 0; /* number of prefix codes available */
+ var used = 0; /* code entries in table used */
+ var huff = 0; /* Huffman code */
+ var incr; /* for incrementing code, index */
+ var fill; /* index for replicating entries */
+ var low; /* low bits for current root entry */
+ var mask; /* mask for low root bits */
+ var next; /* next available space in table */
+ var base = null; /* base value table to use */
+ var base_index = 0;
+// var shoextra; /* extra bits table to use */
+ var end; /* use base and extra for symbol > end */
+ var count = new utils.Buf16(MAXBITS + 1); //[MAXBITS+1]; /* number of codes of each length */
+ var offs = new utils.Buf16(MAXBITS + 1); //[MAXBITS+1]; /* offsets in table for each length */
+ var extra = null;
+ var extra_index = 0;
+
+ var here_bits, here_op, here_val;
+
+ /*
+ Process a set of code lengths to create a canonical Huffman code. The
+ code lengths are lens[0..codes-1]. Each length corresponds to the
+ symbols 0..codes-1. The Huffman code is generated by first sorting the
+ symbols by length from short to long, and retaining the symbol order
+ for codes with equal lengths. Then the code starts with all zero bits
+ for the first code of the shortest length, and the codes are integer
+ increments for the same length, and zeros are appended as the length
+ increases. For the deflate format, these bits are stored backwards
+ from their more natural integer increment ordering, and so when the
+ decoding tables are built in the large loop below, the integer codes
+ are incremented backwards.
+
+ This routine assumes, but does not check, that all of the entries in
+ lens[] are in the range 0..MAXBITS. The caller must assure this.
+ 1..MAXBITS is interpreted as that code length. zero means that that
+ symbol does not occur in this code.
+
+ The codes are sorted by computing a count of codes for each length,
+ creating from that a table of starting indices for each length in the
+ sorted table, and then entering the symbols in order in the sorted
+ table. The sorted table is work[], with that space being provided by
+ the caller.
+
+ The length counts are used for other purposes as well, i.e. finding
+ the minimum and maximum length codes, determining if there are any
+ codes at all, checking for a valid set of lengths, and looking ahead
+ at length counts to determine sub-table sizes when building the
+ decoding tables.
+ */
+
+ /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */
+ for (len = 0; len <= MAXBITS; len++) {
+ count[len] = 0;
+ }
+ for (sym = 0; sym < codes; sym++) {
+ count[lens[lens_index + sym]]++;
+ }
+
+ /* bound code lengths, force root to be within code lengths */
+ root = bits;
+ for (max = MAXBITS; max >= 1; max--) {
+ if (count[max] !== 0) { break; }
+ }
+ if (root > max) {
+ root = max;
+ }
+ if (max === 0) { /* no symbols to code at all */
+ //table.op[opts.table_index] = 64; //here.op = (var char)64; /* invalid code marker */
+ //table.bits[opts.table_index] = 1; //here.bits = (var char)1;
+ //table.val[opts.table_index++] = 0; //here.val = (var short)0;
+ table[table_index++] = (1 << 24) | (64 << 16) | 0;
+
+
+ //table.op[opts.table_index] = 64;
+ //table.bits[opts.table_index] = 1;
+ //table.val[opts.table_index++] = 0;
+ table[table_index++] = (1 << 24) | (64 << 16) | 0;
+
+ opts.bits = 1;
+ return 0; /* no symbols, but wait for decoding to report error */
+ }
+ for (min = 1; min < max; min++) {
+ if (count[min] !== 0) { break; }
+ }
+ if (root < min) {
+ root = min;
+ }
+
+ /* check for an over-subscribed or incomplete set of lengths */
+ left = 1;
+ for (len = 1; len <= MAXBITS; len++) {
+ left <<= 1;
+ left -= count[len];
+ if (left < 0) {
+ return -1;
+ } /* over-subscribed */
+ }
+ if (left > 0 && (type === CODES || max !== 1)) {
+ return -1; /* incomplete set */
+ }
+
+ /* generate offsets into symbol table for each length for sorting */
+ offs[1] = 0;
+ for (len = 1; len < MAXBITS; len++) {
+ offs[len + 1] = offs[len] + count[len];
+ }
+
+ /* sort symbols by length, by symbol order within each length */
+ for (sym = 0; sym < codes; sym++) {
+ if (lens[lens_index + sym] !== 0) {
+ work[offs[lens[lens_index + sym]]++] = sym;
+ }
+ }
+
+ /*
+ Create and fill in decoding tables. In this loop, the table being
+ filled is at next and has curr index bits. The code being used is huff
+ with length len. That code is converted to an index by dropping drop
+ bits off of the bottom. For codes where len is less than drop + curr,
+ those top drop + curr - len bits are incremented through all values to
+ fill the table with replicated entries.
+
+ root is the number of index bits for the root table. When len exceeds
+ root, sub-tables are created pointed to by the root entry with an index
+ of the low root bits of huff. This is saved in low to check for when a
+ new sub-table should be started. drop is zero when the root table is
+ being filled, and drop is root when sub-tables are being filled.
+
+ When a new sub-table is needed, it is necessary to look ahead in the
+ code lengths to determine what size sub-table is needed. The length
+ counts are used for this, and so count[] is decremented as codes are
+ entered in the tables.
+
+ used keeps track of how many table entries have been allocated from the
+ provided *table space. It is checked for LENS and DIST tables against
+ the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in
+ the initial root table size constants. See the comments in inftrees.h
+ for more information.
+
+ sym increments through all symbols, and the loop terminates when
+ all codes of length max, i.e. all codes, have been processed. This
+ routine permits incomplete codes, so another loop after this one fills
+ in the rest of the decoding tables with invalid code markers.
+ */
+
+ /* set up for code type */
+ // poor man optimization - use if-else instead of switch,
+ // to avoid deopts in old v8
+ if (type === CODES) {
+ base = extra = work; /* dummy value--not used */
+ end = 19;
+
+ } else if (type === LENS) {
+ base = lbase;
+ base_index -= 257;
+ extra = lext;
+ extra_index -= 257;
+ end = 256;
+
+ } else { /* DISTS */
+ base = dbase;
+ extra = dext;
+ end = -1;
+ }
+
+ /* initialize opts for loop */
+ huff = 0; /* starting code */
+ sym = 0; /* starting code symbol */
+ len = min; /* starting code length */
+ next = table_index; /* current table to fill in */
+ curr = root; /* current table index bits */
+ drop = 0; /* current bits to drop from code for index */
+ low = -1; /* trigger new sub-table when len > root */
+ used = 1 << root; /* use root table entries */
+ mask = used - 1; /* mask for comparing low */
+
+ /* check available table space */
+ if ((type === LENS && used > ENOUGH_LENS) ||
+ (type === DISTS && used > ENOUGH_DISTS)) {
+ return 1;
+ }
+
+ /* process all codes and make table entries */
+ for (;;) {
+ /* create table entry */
+ here_bits = len - drop;
+ if (work[sym] < end) {
+ here_op = 0;
+ here_val = work[sym];
+ }
+ else if (work[sym] > end) {
+ here_op = extra[extra_index + work[sym]];
+ here_val = base[base_index + work[sym]];
+ }
+ else {
+ here_op = 32 + 64; /* end of block */
+ here_val = 0;
+ }
+
+ /* replicate for those indices with low len bits equal to huff */
+ incr = 1 << (len - drop);
+ fill = 1 << curr;
+ min = fill; /* save offset to next table */
+ do {
+ fill -= incr;
+ table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0;
+ } while (fill !== 0);
+
+ /* backwards increment the len-bit code huff */
+ incr = 1 << (len - 1);
+ while (huff & incr) {
+ incr >>= 1;
+ }
+ if (incr !== 0) {
+ huff &= incr - 1;
+ huff += incr;
+ } else {
+ huff = 0;
+ }
+
+ /* go to next symbol, update count, len */
+ sym++;
+ if (--count[len] === 0) {
+ if (len === max) { break; }
+ len = lens[lens_index + work[sym]];
+ }
+
+ /* create new sub-table if needed */
+ if (len > root && (huff & mask) !== low) {
+ /* if first time, transition to sub-tables */
+ if (drop === 0) {
+ drop = root;
+ }
+
+ /* increment past last table */
+ next += min; /* here min is 1 << curr */
+
+ /* determine length of next table */
+ curr = len - drop;
+ left = 1 << curr;
+ while (curr + drop < max) {
+ left -= count[curr + drop];
+ if (left <= 0) { break; }
+ curr++;
+ left <<= 1;
+ }
+
+ /* check for enough space */
+ used += 1 << curr;
+ if ((type === LENS && used > ENOUGH_LENS) ||
+ (type === DISTS && used > ENOUGH_DISTS)) {
+ return 1;
+ }
+
+ /* point entry in root table to sub-table */
+ low = huff & mask;
+ /*table.op[low] = curr;
+ table.bits[low] = root;
+ table.val[low] = next - opts.table_index;*/
+ table[low] = (root << 24) | (curr << 16) | (next - table_index) |0;
+ }
+ }
+
+ /* fill in remaining table entry if code is incomplete (guaranteed to have
+ at most one remaining entry, since if the code is incomplete, the
+ maximum code length that was allowed to get this far is one bit) */
+ if (huff !== 0) {
+ //table.op[next + huff] = 64; /* invalid code marker */
+ //table.bits[next + huff] = len - drop;
+ //table.val[next + huff] = 0;
+ table[next + huff] = ((len - drop) << 24) | (64 << 16) |0;
+ }
+
+ /* set return parameters */
+ //opts.table_index += used;
+ opts.bits = root;
+ return 0;
+};
pkg/web/noVNC/vendor/pako/lib/zlib/messages.js
@@ -0,0 +1,11 @@
+export default {
+ 2: 'need dictionary', /* Z_NEED_DICT 2 */
+ 1: 'stream end', /* Z_STREAM_END 1 */
+ 0: '', /* Z_OK 0 */
+ '-1': 'file error', /* Z_ERRNO (-1) */
+ '-2': 'stream error', /* Z_STREAM_ERROR (-2) */
+ '-3': 'data error', /* Z_DATA_ERROR (-3) */
+ '-4': 'insufficient memory', /* Z_MEM_ERROR (-4) */
+ '-5': 'buffer error', /* Z_BUF_ERROR (-5) */
+ '-6': 'incompatible version' /* Z_VERSION_ERROR (-6) */
+};
pkg/web/noVNC/vendor/pako/lib/zlib/trees.js
@@ -0,0 +1,1195 @@
+import * as utils from "../utils/common.js";
+
+/* Public constants ==========================================================*/
+/* ===========================================================================*/
+
+
+//var Z_FILTERED = 1;
+//var Z_HUFFMAN_ONLY = 2;
+//var Z_RLE = 3;
+var Z_FIXED = 4;
+//var Z_DEFAULT_STRATEGY = 0;
+
+/* Possible values of the data_type field (though see inflate()) */
+var Z_BINARY = 0;
+var Z_TEXT = 1;
+//var Z_ASCII = 1; // = Z_TEXT
+var Z_UNKNOWN = 2;
+
+/*============================================================================*/
+
+
+function zero(buf) { var len = buf.length; while (--len >= 0) { buf[len] = 0; } }
+
+// From zutil.h
+
+var STORED_BLOCK = 0;
+var STATIC_TREES = 1;
+var DYN_TREES = 2;
+/* The three kinds of block type */
+
+var MIN_MATCH = 3;
+var MAX_MATCH = 258;
+/* The minimum and maximum match lengths */
+
+// From deflate.h
+/* ===========================================================================
+ * Internal compression state.
+ */
+
+var LENGTH_CODES = 29;
+/* number of length codes, not counting the special END_BLOCK code */
+
+var LITERALS = 256;
+/* number of literal bytes 0..255 */
+
+var L_CODES = LITERALS + 1 + LENGTH_CODES;
+/* number of Literal or Length codes, including the END_BLOCK code */
+
+var D_CODES = 30;
+/* number of distance codes */
+
+var BL_CODES = 19;
+/* number of codes used to transfer the bit lengths */
+
+var HEAP_SIZE = 2 * L_CODES + 1;
+/* maximum heap size */
+
+var MAX_BITS = 15;
+/* All codes must not exceed MAX_BITS bits */
+
+var Buf_size = 16;
+/* size of bit buffer in bi_buf */
+
+
+/* ===========================================================================
+ * Constants
+ */
+
+var MAX_BL_BITS = 7;
+/* Bit length codes must not exceed MAX_BL_BITS bits */
+
+var END_BLOCK = 256;
+/* end of block literal code */
+
+var REP_3_6 = 16;
+/* repeat previous bit length 3-6 times (2 bits of repeat count) */
+
+var REPZ_3_10 = 17;
+/* repeat a zero length 3-10 times (3 bits of repeat count) */
+
+var REPZ_11_138 = 18;
+/* repeat a zero length 11-138 times (7 bits of repeat count) */
+
+/* eslint-disable comma-spacing,array-bracket-spacing */
+var extra_lbits = /* extra bits for each length code */
+ [0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0];
+
+var extra_dbits = /* extra bits for each distance code */
+ [0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13];
+
+var extra_blbits = /* extra bits for each bit length code */
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7];
+
+var bl_order =
+ [16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];
+/* eslint-enable comma-spacing,array-bracket-spacing */
+
+/* The lengths of the bit length codes are sent in order of decreasing
+ * probability, to avoid transmitting the lengths for unused bit length codes.
+ */
+
+/* ===========================================================================
+ * Local data. These are initialized only once.
+ */
+
+// We pre-fill arrays with 0 to avoid uninitialized gaps
+
+var DIST_CODE_LEN = 512; /* see definition of array dist_code below */
+
+// !!!! Use flat array insdead of structure, Freq = i*2, Len = i*2+1
+var static_ltree = new Array((L_CODES + 2) * 2);
+zero(static_ltree);
+/* The static literal tree. Since the bit lengths are imposed, there is no
+ * need for the L_CODES extra codes used during heap construction. However
+ * The codes 286 and 287 are needed to build a canonical tree (see _tr_init
+ * below).
+ */
+
+var static_dtree = new Array(D_CODES * 2);
+zero(static_dtree);
+/* The static distance tree. (Actually a trivial tree since all codes use
+ * 5 bits.)
+ */
+
+var _dist_code = new Array(DIST_CODE_LEN);
+zero(_dist_code);
+/* Distance codes. The first 256 values correspond to the distances
+ * 3 .. 258, the last 256 values correspond to the top 8 bits of
+ * the 15 bit distances.
+ */
+
+var _length_code = new Array(MAX_MATCH - MIN_MATCH + 1);
+zero(_length_code);
+/* length code for each normalized match length (0 == MIN_MATCH) */
+
+var base_length = new Array(LENGTH_CODES);
+zero(base_length);
+/* First normalized length for each code (0 = MIN_MATCH) */
+
+var base_dist = new Array(D_CODES);
+zero(base_dist);
+/* First normalized distance for each code (0 = distance of 1) */
+
+
+function StaticTreeDesc(static_tree, extra_bits, extra_base, elems, max_length) {
+
+ this.static_tree = static_tree; /* static tree or NULL */
+ this.extra_bits = extra_bits; /* extra bits for each code or NULL */
+ this.extra_base = extra_base; /* base index for extra_bits */
+ this.elems = elems; /* max number of elements in the tree */
+ this.max_length = max_length; /* max bit length for the codes */
+
+ // show if `static_tree` has data or dummy - needed for monomorphic objects
+ this.has_stree = static_tree && static_tree.length;
+}
+
+
+var static_l_desc;
+var static_d_desc;
+var static_bl_desc;
+
+
+function TreeDesc(dyn_tree, stat_desc) {
+ this.dyn_tree = dyn_tree; /* the dynamic tree */
+ this.max_code = 0; /* largest code with non zero frequency */
+ this.stat_desc = stat_desc; /* the corresponding static tree */
+}
+
+
+
+function d_code(dist) {
+ return dist < 256 ? _dist_code[dist] : _dist_code[256 + (dist >>> 7)];
+}
+
+
+/* ===========================================================================
+ * Output a short LSB first on the stream.
+ * IN assertion: there is enough room in pendingBuf.
+ */
+function put_short(s, w) {
+// put_byte(s, (uch)((w) & 0xff));
+// put_byte(s, (uch)((ush)(w) >> 8));
+ s.pending_buf[s.pending++] = (w) & 0xff;
+ s.pending_buf[s.pending++] = (w >>> 8) & 0xff;
+}
+
+
+/* ===========================================================================
+ * Send a value on a given number of bits.
+ * IN assertion: length <= 16 and value fits in length bits.
+ */
+function send_bits(s, value, length) {
+ if (s.bi_valid > (Buf_size - length)) {
+ s.bi_buf |= (value << s.bi_valid) & 0xffff;
+ put_short(s, s.bi_buf);
+ s.bi_buf = value >> (Buf_size - s.bi_valid);
+ s.bi_valid += length - Buf_size;
+ } else {
+ s.bi_buf |= (value << s.bi_valid) & 0xffff;
+ s.bi_valid += length;
+ }
+}
+
+
+function send_code(s, c, tree) {
+ send_bits(s, tree[c * 2]/*.Code*/, tree[c * 2 + 1]/*.Len*/);
+}
+
+
+/* ===========================================================================
+ * Reverse the first len bits of a code, using straightforward code (a faster
+ * method would use a table)
+ * IN assertion: 1 <= len <= 15
+ */
+function bi_reverse(code, len) {
+ var res = 0;
+ do {
+ res |= code & 1;
+ code >>>= 1;
+ res <<= 1;
+ } while (--len > 0);
+ return res >>> 1;
+}
+
+
+/* ===========================================================================
+ * Flush the bit buffer, keeping at most 7 bits in it.
+ */
+function bi_flush(s) {
+ if (s.bi_valid === 16) {
+ put_short(s, s.bi_buf);
+ s.bi_buf = 0;
+ s.bi_valid = 0;
+
+ } else if (s.bi_valid >= 8) {
+ s.pending_buf[s.pending++] = s.bi_buf & 0xff;
+ s.bi_buf >>= 8;
+ s.bi_valid -= 8;
+ }
+}
+
+
+/* ===========================================================================
+ * Compute the optimal bit lengths for a tree and update the total bit length
+ * for the current block.
+ * IN assertion: the fields freq and dad are set, heap[heap_max] and
+ * above are the tree nodes sorted by increasing frequency.
+ * OUT assertions: the field len is set to the optimal bit length, the
+ * array bl_count contains the frequencies for each bit length.
+ * The length opt_len is updated; static_len is also updated if stree is
+ * not null.
+ */
+function gen_bitlen(s, desc)
+// deflate_state *s;
+// tree_desc *desc; /* the tree descriptor */
+{
+ var tree = desc.dyn_tree;
+ var max_code = desc.max_code;
+ var stree = desc.stat_desc.static_tree;
+ var has_stree = desc.stat_desc.has_stree;
+ var extra = desc.stat_desc.extra_bits;
+ var base = desc.stat_desc.extra_base;
+ var max_length = desc.stat_desc.max_length;
+ var h; /* heap index */
+ var n, m; /* iterate over the tree elements */
+ var bits; /* bit length */
+ var xbits; /* extra bits */
+ var f; /* frequency */
+ var overflow = 0; /* number of elements with bit length too large */
+
+ for (bits = 0; bits <= MAX_BITS; bits++) {
+ s.bl_count[bits] = 0;
+ }
+
+ /* In a first pass, compute the optimal bit lengths (which may
+ * overflow in the case of the bit length tree).
+ */
+ tree[s.heap[s.heap_max] * 2 + 1]/*.Len*/ = 0; /* root of the heap */
+
+ for (h = s.heap_max + 1; h < HEAP_SIZE; h++) {
+ n = s.heap[h];
+ bits = tree[tree[n * 2 + 1]/*.Dad*/ * 2 + 1]/*.Len*/ + 1;
+ if (bits > max_length) {
+ bits = max_length;
+ overflow++;
+ }
+ tree[n * 2 + 1]/*.Len*/ = bits;
+ /* We overwrite tree[n].Dad which is no longer needed */
+
+ if (n > max_code) { continue; } /* not a leaf node */
+
+ s.bl_count[bits]++;
+ xbits = 0;
+ if (n >= base) {
+ xbits = extra[n - base];
+ }
+ f = tree[n * 2]/*.Freq*/;
+ s.opt_len += f * (bits + xbits);
+ if (has_stree) {
+ s.static_len += f * (stree[n * 2 + 1]/*.Len*/ + xbits);
+ }
+ }
+ if (overflow === 0) { return; }
+
+ // Trace((stderr,"\nbit length overflow\n"));
+ /* This happens for example on obj2 and pic of the Calgary corpus */
+
+ /* Find the first bit length which could increase: */
+ do {
+ bits = max_length - 1;
+ while (s.bl_count[bits] === 0) { bits--; }
+ s.bl_count[bits]--; /* move one leaf down the tree */
+ s.bl_count[bits + 1] += 2; /* move one overflow item as its brother */
+ s.bl_count[max_length]--;
+ /* The brother of the overflow item also moves one step up,
+ * but this does not affect bl_count[max_length]
+ */
+ overflow -= 2;
+ } while (overflow > 0);
+
+ /* Now recompute all bit lengths, scanning in increasing frequency.
+ * h is still equal to HEAP_SIZE. (It is simpler to reconstruct all
+ * lengths instead of fixing only the wrong ones. This idea is taken
+ * from 'ar' written by Haruhiko Okumura.)
+ */
+ for (bits = max_length; bits !== 0; bits--) {
+ n = s.bl_count[bits];
+ while (n !== 0) {
+ m = s.heap[--h];
+ if (m > max_code) { continue; }
+ if (tree[m * 2 + 1]/*.Len*/ !== bits) {
+ // Trace((stderr,"code %d bits %d->%d\n", m, tree[m].Len, bits));
+ s.opt_len += (bits - tree[m * 2 + 1]/*.Len*/) * tree[m * 2]/*.Freq*/;
+ tree[m * 2 + 1]/*.Len*/ = bits;
+ }
+ n--;
+ }
+ }
+}
+
+
+/* ===========================================================================
+ * Generate the codes for a given tree and bit counts (which need not be
+ * optimal).
+ * IN assertion: the array bl_count contains the bit length statistics for
+ * the given tree and the field len is set for all tree elements.
+ * OUT assertion: the field code is set for all tree elements of non
+ * zero code length.
+ */
+function gen_codes(tree, max_code, bl_count)
+// ct_data *tree; /* the tree to decorate */
+// int max_code; /* largest code with non zero frequency */
+// ushf *bl_count; /* number of codes at each bit length */
+{
+ var next_code = new Array(MAX_BITS + 1); /* next code value for each bit length */
+ var code = 0; /* running code value */
+ var bits; /* bit index */
+ var n; /* code index */
+
+ /* The distribution counts are first used to generate the code values
+ * without bit reversal.
+ */
+ for (bits = 1; bits <= MAX_BITS; bits++) {
+ next_code[bits] = code = (code + bl_count[bits - 1]) << 1;
+ }
+ /* Check that the bit counts in bl_count are consistent. The last code
+ * must be all ones.
+ */
+ //Assert (code + bl_count[MAX_BITS]-1 == (1<<MAX_BITS)-1,
+ // "inconsistent bit counts");
+ //Tracev((stderr,"\ngen_codes: max_code %d ", max_code));
+
+ for (n = 0; n <= max_code; n++) {
+ var len = tree[n * 2 + 1]/*.Len*/;
+ if (len === 0) { continue; }
+ /* Now reverse the bits */
+ tree[n * 2]/*.Code*/ = bi_reverse(next_code[len]++, len);
+
+ //Tracecv(tree != static_ltree, (stderr,"\nn %3d %c l %2d c %4x (%x) ",
+ // n, (isgraph(n) ? n : ' '), len, tree[n].Code, next_code[len]-1));
+ }
+}
+
+
+/* ===========================================================================
+ * Initialize the various 'constant' tables.
+ */
+function tr_static_init() {
+ var n; /* iterates over tree elements */
+ var bits; /* bit counter */
+ var length; /* length value */
+ var code; /* code value */
+ var dist; /* distance index */
+ var bl_count = new Array(MAX_BITS + 1);
+ /* number of codes at each bit length for an optimal tree */
+
+ // do check in _tr_init()
+ //if (static_init_done) return;
+
+ /* For some embedded targets, global variables are not initialized: */
+/*#ifdef NO_INIT_GLOBAL_POINTERS
+ static_l_desc.static_tree = static_ltree;
+ static_l_desc.extra_bits = extra_lbits;
+ static_d_desc.static_tree = static_dtree;
+ static_d_desc.extra_bits = extra_dbits;
+ static_bl_desc.extra_bits = extra_blbits;
+#endif*/
+
+ /* Initialize the mapping length (0..255) -> length code (0..28) */
+ length = 0;
+ for (code = 0; code < LENGTH_CODES - 1; code++) {
+ base_length[code] = length;
+ for (n = 0; n < (1 << extra_lbits[code]); n++) {
+ _length_code[length++] = code;
+ }
+ }
+ //Assert (length == 256, "tr_static_init: length != 256");
+ /* Note that the length 255 (match length 258) can be represented
+ * in two different ways: code 284 + 5 bits or code 285, so we
+ * overwrite length_code[255] to use the best encoding:
+ */
+ _length_code[length - 1] = code;
+
+ /* Initialize the mapping dist (0..32K) -> dist code (0..29) */
+ dist = 0;
+ for (code = 0; code < 16; code++) {
+ base_dist[code] = dist;
+ for (n = 0; n < (1 << extra_dbits[code]); n++) {
+ _dist_code[dist++] = code;
+ }
+ }
+ //Assert (dist == 256, "tr_static_init: dist != 256");
+ dist >>= 7; /* from now on, all distances are divided by 128 */
+ for (; code < D_CODES; code++) {
+ base_dist[code] = dist << 7;
+ for (n = 0; n < (1 << (extra_dbits[code] - 7)); n++) {
+ _dist_code[256 + dist++] = code;
+ }
+ }
+ //Assert (dist == 256, "tr_static_init: 256+dist != 512");
+
+ /* Construct the codes of the static literal tree */
+ for (bits = 0; bits <= MAX_BITS; bits++) {
+ bl_count[bits] = 0;
+ }
+
+ n = 0;
+ while (n <= 143) {
+ static_ltree[n * 2 + 1]/*.Len*/ = 8;
+ n++;
+ bl_count[8]++;
+ }
+ while (n <= 255) {
+ static_ltree[n * 2 + 1]/*.Len*/ = 9;
+ n++;
+ bl_count[9]++;
+ }
+ while (n <= 279) {
+ static_ltree[n * 2 + 1]/*.Len*/ = 7;
+ n++;
+ bl_count[7]++;
+ }
+ while (n <= 287) {
+ static_ltree[n * 2 + 1]/*.Len*/ = 8;
+ n++;
+ bl_count[8]++;
+ }
+ /* Codes 286 and 287 do not exist, but we must include them in the
+ * tree construction to get a canonical Huffman tree (longest code
+ * all ones)
+ */
+ gen_codes(static_ltree, L_CODES + 1, bl_count);
+
+ /* The static distance tree is trivial: */
+ for (n = 0; n < D_CODES; n++) {
+ static_dtree[n * 2 + 1]/*.Len*/ = 5;
+ static_dtree[n * 2]/*.Code*/ = bi_reverse(n, 5);
+ }
+
+ // Now data ready and we can init static trees
+ static_l_desc = new StaticTreeDesc(static_ltree, extra_lbits, LITERALS + 1, L_CODES, MAX_BITS);
+ static_d_desc = new StaticTreeDesc(static_dtree, extra_dbits, 0, D_CODES, MAX_BITS);
+ static_bl_desc = new StaticTreeDesc(new Array(0), extra_blbits, 0, BL_CODES, MAX_BL_BITS);
+
+ //static_init_done = true;
+}
+
+
+/* ===========================================================================
+ * Initialize a new block.
+ */
+function init_block(s) {
+ var n; /* iterates over tree elements */
+
+ /* Initialize the trees. */
+ for (n = 0; n < L_CODES; n++) { s.dyn_ltree[n * 2]/*.Freq*/ = 0; }
+ for (n = 0; n < D_CODES; n++) { s.dyn_dtree[n * 2]/*.Freq*/ = 0; }
+ for (n = 0; n < BL_CODES; n++) { s.bl_tree[n * 2]/*.Freq*/ = 0; }
+
+ s.dyn_ltree[END_BLOCK * 2]/*.Freq*/ = 1;
+ s.opt_len = s.static_len = 0;
+ s.last_lit = s.matches = 0;
+}
+
+
+/* ===========================================================================
+ * Flush the bit buffer and align the output on a byte boundary
+ */
+function bi_windup(s)
+{
+ if (s.bi_valid > 8) {
+ put_short(s, s.bi_buf);
+ } else if (s.bi_valid > 0) {
+ //put_byte(s, (Byte)s->bi_buf);
+ s.pending_buf[s.pending++] = s.bi_buf;
+ }
+ s.bi_buf = 0;
+ s.bi_valid = 0;
+}
+
+/* ===========================================================================
+ * Copy a stored block, storing first the length and its
+ * one's complement if requested.
+ */
+function copy_block(s, buf, len, header)
+//DeflateState *s;
+//charf *buf; /* the input data */
+//unsigned len; /* its length */
+//int header; /* true if block header must be written */
+{
+ bi_windup(s); /* align on byte boundary */
+
+ if (header) {
+ put_short(s, len);
+ put_short(s, ~len);
+ }
+// while (len--) {
+// put_byte(s, *buf++);
+// }
+ utils.arraySet(s.pending_buf, s.window, buf, len, s.pending);
+ s.pending += len;
+}
+
+/* ===========================================================================
+ * Compares to subtrees, using the tree depth as tie breaker when
+ * the subtrees have equal frequency. This minimizes the worst case length.
+ */
+function smaller(tree, n, m, depth) {
+ var _n2 = n * 2;
+ var _m2 = m * 2;
+ return (tree[_n2]/*.Freq*/ < tree[_m2]/*.Freq*/ ||
+ (tree[_n2]/*.Freq*/ === tree[_m2]/*.Freq*/ && depth[n] <= depth[m]));
+}
+
+/* ===========================================================================
+ * Restore the heap property by moving down the tree starting at node k,
+ * exchanging a node with the smallest of its two sons if necessary, stopping
+ * when the heap property is re-established (each father smaller than its
+ * two sons).
+ */
+function pqdownheap(s, tree, k)
+// deflate_state *s;
+// ct_data *tree; /* the tree to restore */
+// int k; /* node to move down */
+{
+ var v = s.heap[k];
+ var j = k << 1; /* left son of k */
+ while (j <= s.heap_len) {
+ /* Set j to the smallest of the two sons: */
+ if (j < s.heap_len &&
+ smaller(tree, s.heap[j + 1], s.heap[j], s.depth)) {
+ j++;
+ }
+ /* Exit if v is smaller than both sons */
+ if (smaller(tree, v, s.heap[j], s.depth)) { break; }
+
+ /* Exchange v with the smallest son */
+ s.heap[k] = s.heap[j];
+ k = j;
+
+ /* And continue down the tree, setting j to the left son of k */
+ j <<= 1;
+ }
+ s.heap[k] = v;
+}
+
+
+// inlined manually
+// var SMALLEST = 1;
+
+/* ===========================================================================
+ * Send the block data compressed using the given Huffman trees
+ */
+function compress_block(s, ltree, dtree)
+// deflate_state *s;
+// const ct_data *ltree; /* literal tree */
+// const ct_data *dtree; /* distance tree */
+{
+ var dist; /* distance of matched string */
+ var lc; /* match length or unmatched char (if dist == 0) */
+ var lx = 0; /* running index in l_buf */
+ var code; /* the code to send */
+ var extra; /* number of extra bits to send */
+
+ if (s.last_lit !== 0) {
+ do {
+ dist = (s.pending_buf[s.d_buf + lx * 2] << 8) | (s.pending_buf[s.d_buf + lx * 2 + 1]);
+ lc = s.pending_buf[s.l_buf + lx];
+ lx++;
+
+ if (dist === 0) {
+ send_code(s, lc, ltree); /* send a literal byte */
+ //Tracecv(isgraph(lc), (stderr," '%c' ", lc));
+ } else {
+ /* Here, lc is the match length - MIN_MATCH */
+ code = _length_code[lc];
+ send_code(s, code + LITERALS + 1, ltree); /* send the length code */
+ extra = extra_lbits[code];
+ if (extra !== 0) {
+ lc -= base_length[code];
+ send_bits(s, lc, extra); /* send the extra length bits */
+ }
+ dist--; /* dist is now the match distance - 1 */
+ code = d_code(dist);
+ //Assert (code < D_CODES, "bad d_code");
+
+ send_code(s, code, dtree); /* send the distance code */
+ extra = extra_dbits[code];
+ if (extra !== 0) {
+ dist -= base_dist[code];
+ send_bits(s, dist, extra); /* send the extra distance bits */
+ }
+ } /* literal or match pair ? */
+
+ /* Check that the overlay between pending_buf and d_buf+l_buf is ok: */
+ //Assert((uInt)(s->pending) < s->lit_bufsize + 2*lx,
+ // "pendingBuf overflow");
+
+ } while (lx < s.last_lit);
+ }
+
+ send_code(s, END_BLOCK, ltree);
+}
+
+
+/* ===========================================================================
+ * Construct one Huffman tree and assigns the code bit strings and lengths.
+ * Update the total bit length for the current block.
+ * IN assertion: the field freq is set for all tree elements.
+ * OUT assertions: the fields len and code are set to the optimal bit length
+ * and corresponding code. The length opt_len is updated; static_len is
+ * also updated if stree is not null. The field max_code is set.
+ */
+function build_tree(s, desc)
+// deflate_state *s;
+// tree_desc *desc; /* the tree descriptor */
+{
+ var tree = desc.dyn_tree;
+ var stree = desc.stat_desc.static_tree;
+ var has_stree = desc.stat_desc.has_stree;
+ var elems = desc.stat_desc.elems;
+ var n, m; /* iterate over heap elements */
+ var max_code = -1; /* largest code with non zero frequency */
+ var node; /* new node being created */
+
+ /* Construct the initial heap, with least frequent element in
+ * heap[SMALLEST]. The sons of heap[n] are heap[2*n] and heap[2*n+1].
+ * heap[0] is not used.
+ */
+ s.heap_len = 0;
+ s.heap_max = HEAP_SIZE;
+
+ for (n = 0; n < elems; n++) {
+ if (tree[n * 2]/*.Freq*/ !== 0) {
+ s.heap[++s.heap_len] = max_code = n;
+ s.depth[n] = 0;
+
+ } else {
+ tree[n * 2 + 1]/*.Len*/ = 0;
+ }
+ }
+
+ /* The pkzip format requires that at least one distance code exists,
+ * and that at least one bit should be sent even if there is only one
+ * possible code. So to avoid special checks later on we force at least
+ * two codes of non zero frequency.
+ */
+ while (s.heap_len < 2) {
+ node = s.heap[++s.heap_len] = (max_code < 2 ? ++max_code : 0);
+ tree[node * 2]/*.Freq*/ = 1;
+ s.depth[node] = 0;
+ s.opt_len--;
+
+ if (has_stree) {
+ s.static_len -= stree[node * 2 + 1]/*.Len*/;
+ }
+ /* node is 0 or 1 so it does not have extra bits */
+ }
+ desc.max_code = max_code;
+
+ /* The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree,
+ * establish sub-heaps of increasing lengths:
+ */
+ for (n = (s.heap_len >> 1/*int /2*/); n >= 1; n--) { pqdownheap(s, tree, n); }
+
+ /* Construct the Huffman tree by repeatedly combining the least two
+ * frequent nodes.
+ */
+ node = elems; /* next internal node of the tree */
+ do {
+ //pqremove(s, tree, n); /* n = node of least frequency */
+ /*** pqremove ***/
+ n = s.heap[1/*SMALLEST*/];
+ s.heap[1/*SMALLEST*/] = s.heap[s.heap_len--];
+ pqdownheap(s, tree, 1/*SMALLEST*/);
+ /***/
+
+ m = s.heap[1/*SMALLEST*/]; /* m = node of next least frequency */
+
+ s.heap[--s.heap_max] = n; /* keep the nodes sorted by frequency */
+ s.heap[--s.heap_max] = m;
+
+ /* Create a new node father of n and m */
+ tree[node * 2]/*.Freq*/ = tree[n * 2]/*.Freq*/ + tree[m * 2]/*.Freq*/;
+ s.depth[node] = (s.depth[n] >= s.depth[m] ? s.depth[n] : s.depth[m]) + 1;
+ tree[n * 2 + 1]/*.Dad*/ = tree[m * 2 + 1]/*.Dad*/ = node;
+
+ /* and insert the new node in the heap */
+ s.heap[1/*SMALLEST*/] = node++;
+ pqdownheap(s, tree, 1/*SMALLEST*/);
+
+ } while (s.heap_len >= 2);
+
+ s.heap[--s.heap_max] = s.heap[1/*SMALLEST*/];
+
+ /* At this point, the fields freq and dad are set. We can now
+ * generate the bit lengths.
+ */
+ gen_bitlen(s, desc);
+
+ /* The field len is now set, we can generate the bit codes */
+ gen_codes(tree, max_code, s.bl_count);
+}
+
+
+/* ===========================================================================
+ * Scan a literal or distance tree to determine the frequencies of the codes
+ * in the bit length tree.
+ */
+function scan_tree(s, tree, max_code)
+// deflate_state *s;
+// ct_data *tree; /* the tree to be scanned */
+// int max_code; /* and its largest code of non zero frequency */
+{
+ var n; /* iterates over all tree elements */
+ var prevlen = -1; /* last emitted length */
+ var curlen; /* length of current code */
+
+ var nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */
+
+ var count = 0; /* repeat count of the current code */
+ var max_count = 7; /* max repeat count */
+ var min_count = 4; /* min repeat count */
+
+ if (nextlen === 0) {
+ max_count = 138;
+ min_count = 3;
+ }
+ tree[(max_code + 1) * 2 + 1]/*.Len*/ = 0xffff; /* guard */
+
+ for (n = 0; n <= max_code; n++) {
+ curlen = nextlen;
+ nextlen = tree[(n + 1) * 2 + 1]/*.Len*/;
+
+ if (++count < max_count && curlen === nextlen) {
+ continue;
+
+ } else if (count < min_count) {
+ s.bl_tree[curlen * 2]/*.Freq*/ += count;
+
+ } else if (curlen !== 0) {
+
+ if (curlen !== prevlen) { s.bl_tree[curlen * 2]/*.Freq*/++; }
+ s.bl_tree[REP_3_6 * 2]/*.Freq*/++;
+
+ } else if (count <= 10) {
+ s.bl_tree[REPZ_3_10 * 2]/*.Freq*/++;
+
+ } else {
+ s.bl_tree[REPZ_11_138 * 2]/*.Freq*/++;
+ }
+
+ count = 0;
+ prevlen = curlen;
+
+ if (nextlen === 0) {
+ max_count = 138;
+ min_count = 3;
+
+ } else if (curlen === nextlen) {
+ max_count = 6;
+ min_count = 3;
+
+ } else {
+ max_count = 7;
+ min_count = 4;
+ }
+ }
+}
+
+
+/* ===========================================================================
+ * Send a literal or distance tree in compressed form, using the codes in
+ * bl_tree.
+ */
+function send_tree(s, tree, max_code)
+// deflate_state *s;
+// ct_data *tree; /* the tree to be scanned */
+// int max_code; /* and its largest code of non zero frequency */
+{
+ var n; /* iterates over all tree elements */
+ var prevlen = -1; /* last emitted length */
+ var curlen; /* length of current code */
+
+ var nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */
+
+ var count = 0; /* repeat count of the current code */
+ var max_count = 7; /* max repeat count */
+ var min_count = 4; /* min repeat count */
+
+ /* tree[max_code+1].Len = -1; */ /* guard already set */
+ if (nextlen === 0) {
+ max_count = 138;
+ min_count = 3;
+ }
+
+ for (n = 0; n <= max_code; n++) {
+ curlen = nextlen;
+ nextlen = tree[(n + 1) * 2 + 1]/*.Len*/;
+
+ if (++count < max_count && curlen === nextlen) {
+ continue;
+
+ } else if (count < min_count) {
+ do { send_code(s, curlen, s.bl_tree); } while (--count !== 0);
+
+ } else if (curlen !== 0) {
+ if (curlen !== prevlen) {
+ send_code(s, curlen, s.bl_tree);
+ count--;
+ }
+ //Assert(count >= 3 && count <= 6, " 3_6?");
+ send_code(s, REP_3_6, s.bl_tree);
+ send_bits(s, count - 3, 2);
+
+ } else if (count <= 10) {
+ send_code(s, REPZ_3_10, s.bl_tree);
+ send_bits(s, count - 3, 3);
+
+ } else {
+ send_code(s, REPZ_11_138, s.bl_tree);
+ send_bits(s, count - 11, 7);
+ }
+
+ count = 0;
+ prevlen = curlen;
+ if (nextlen === 0) {
+ max_count = 138;
+ min_count = 3;
+
+ } else if (curlen === nextlen) {
+ max_count = 6;
+ min_count = 3;
+
+ } else {
+ max_count = 7;
+ min_count = 4;
+ }
+ }
+}
+
+
+/* ===========================================================================
+ * Construct the Huffman tree for the bit lengths and return the index in
+ * bl_order of the last bit length code to send.
+ */
+function build_bl_tree(s) {
+ var max_blindex; /* index of last bit length code of non zero freq */
+
+ /* Determine the bit length frequencies for literal and distance trees */
+ scan_tree(s, s.dyn_ltree, s.l_desc.max_code);
+ scan_tree(s, s.dyn_dtree, s.d_desc.max_code);
+
+ /* Build the bit length tree: */
+ build_tree(s, s.bl_desc);
+ /* opt_len now includes the length of the tree representations, except
+ * the lengths of the bit lengths codes and the 5+5+4 bits for the counts.
+ */
+
+ /* Determine the number of bit length codes to send. The pkzip format
+ * requires that at least 4 bit length codes be sent. (appnote.txt says
+ * 3 but the actual value used is 4.)
+ */
+ for (max_blindex = BL_CODES - 1; max_blindex >= 3; max_blindex--) {
+ if (s.bl_tree[bl_order[max_blindex] * 2 + 1]/*.Len*/ !== 0) {
+ break;
+ }
+ }
+ /* Update opt_len to include the bit length tree and counts */
+ s.opt_len += 3 * (max_blindex + 1) + 5 + 5 + 4;
+ //Tracev((stderr, "\ndyn trees: dyn %ld, stat %ld",
+ // s->opt_len, s->static_len));
+
+ return max_blindex;
+}
+
+
+/* ===========================================================================
+ * Send the header for a block using dynamic Huffman trees: the counts, the
+ * lengths of the bit length codes, the literal tree and the distance tree.
+ * IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4.
+ */
+function send_all_trees(s, lcodes, dcodes, blcodes)
+// deflate_state *s;
+// int lcodes, dcodes, blcodes; /* number of codes for each tree */
+{
+ var rank; /* index in bl_order */
+
+ //Assert (lcodes >= 257 && dcodes >= 1 && blcodes >= 4, "not enough codes");
+ //Assert (lcodes <= L_CODES && dcodes <= D_CODES && blcodes <= BL_CODES,
+ // "too many codes");
+ //Tracev((stderr, "\nbl counts: "));
+ send_bits(s, lcodes - 257, 5); /* not +255 as stated in appnote.txt */
+ send_bits(s, dcodes - 1, 5);
+ send_bits(s, blcodes - 4, 4); /* not -3 as stated in appnote.txt */
+ for (rank = 0; rank < blcodes; rank++) {
+ //Tracev((stderr, "\nbl code %2d ", bl_order[rank]));
+ send_bits(s, s.bl_tree[bl_order[rank] * 2 + 1]/*.Len*/, 3);
+ }
+ //Tracev((stderr, "\nbl tree: sent %ld", s->bits_sent));
+
+ send_tree(s, s.dyn_ltree, lcodes - 1); /* literal tree */
+ //Tracev((stderr, "\nlit tree: sent %ld", s->bits_sent));
+
+ send_tree(s, s.dyn_dtree, dcodes - 1); /* distance tree */
+ //Tracev((stderr, "\ndist tree: sent %ld", s->bits_sent));
+}
+
+
+/* ===========================================================================
+ * Check if the data type is TEXT or BINARY, using the following algorithm:
+ * - TEXT if the two conditions below are satisfied:
+ * a) There are no non-portable control characters belonging to the
+ * "black list" (0..6, 14..25, 28..31).
+ * b) There is at least one printable character belonging to the
+ * "white list" (9 {TAB}, 10 {LF}, 13 {CR}, 32..255).
+ * - BINARY otherwise.
+ * - The following partially-portable control characters form a
+ * "gray list" that is ignored in this detection algorithm:
+ * (7 {BEL}, 8 {BS}, 11 {VT}, 12 {FF}, 26 {SUB}, 27 {ESC}).
+ * IN assertion: the fields Freq of dyn_ltree are set.
+ */
+function detect_data_type(s) {
+ /* black_mask is the bit mask of black-listed bytes
+ * set bits 0..6, 14..25, and 28..31
+ * 0xf3ffc07f = binary 11110011111111111100000001111111
+ */
+ var black_mask = 0xf3ffc07f;
+ var n;
+
+ /* Check for non-textual ("black-listed") bytes. */
+ for (n = 0; n <= 31; n++, black_mask >>>= 1) {
+ if ((black_mask & 1) && (s.dyn_ltree[n * 2]/*.Freq*/ !== 0)) {
+ return Z_BINARY;
+ }
+ }
+
+ /* Check for textual ("white-listed") bytes. */
+ if (s.dyn_ltree[9 * 2]/*.Freq*/ !== 0 || s.dyn_ltree[10 * 2]/*.Freq*/ !== 0 ||
+ s.dyn_ltree[13 * 2]/*.Freq*/ !== 0) {
+ return Z_TEXT;
+ }
+ for (n = 32; n < LITERALS; n++) {
+ if (s.dyn_ltree[n * 2]/*.Freq*/ !== 0) {
+ return Z_TEXT;
+ }
+ }
+
+ /* There are no "black-listed" or "white-listed" bytes:
+ * this stream either is empty or has tolerated ("gray-listed") bytes only.
+ */
+ return Z_BINARY;
+}
+
+
+var static_init_done = false;
+
+/* ===========================================================================
+ * Initialize the tree data structures for a new zlib stream.
+ */
+function _tr_init(s)
+{
+
+ if (!static_init_done) {
+ tr_static_init();
+ static_init_done = true;
+ }
+
+ s.l_desc = new TreeDesc(s.dyn_ltree, static_l_desc);
+ s.d_desc = new TreeDesc(s.dyn_dtree, static_d_desc);
+ s.bl_desc = new TreeDesc(s.bl_tree, static_bl_desc);
+
+ s.bi_buf = 0;
+ s.bi_valid = 0;
+
+ /* Initialize the first block of the first file: */
+ init_block(s);
+}
+
+
+/* ===========================================================================
+ * Send a stored block
+ */
+function _tr_stored_block(s, buf, stored_len, last)
+//DeflateState *s;
+//charf *buf; /* input block */
+//ulg stored_len; /* length of input block */
+//int last; /* one if this is the last block for a file */
+{
+ send_bits(s, (STORED_BLOCK << 1) + (last ? 1 : 0), 3); /* send block type */
+ copy_block(s, buf, stored_len, true); /* with header */
+}
+
+
+/* ===========================================================================
+ * Send one empty static block to give enough lookahead for inflate.
+ * This takes 10 bits, of which 7 may remain in the bit buffer.
+ */
+function _tr_align(s) {
+ send_bits(s, STATIC_TREES << 1, 3);
+ send_code(s, END_BLOCK, static_ltree);
+ bi_flush(s);
+}
+
+
+/* ===========================================================================
+ * Determine the best encoding for the current block: dynamic trees, static
+ * trees or store, and output the encoded block to the zip file.
+ */
+function _tr_flush_block(s, buf, stored_len, last)
+//DeflateState *s;
+//charf *buf; /* input block, or NULL if too old */
+//ulg stored_len; /* length of input block */
+//int last; /* one if this is the last block for a file */
+{
+ var opt_lenb, static_lenb; /* opt_len and static_len in bytes */
+ var max_blindex = 0; /* index of last bit length code of non zero freq */
+
+ /* Build the Huffman trees unless a stored block is forced */
+ if (s.level > 0) {
+
+ /* Check if the file is binary or text */
+ if (s.strm.data_type === Z_UNKNOWN) {
+ s.strm.data_type = detect_data_type(s);
+ }
+
+ /* Construct the literal and distance trees */
+ build_tree(s, s.l_desc);
+ // Tracev((stderr, "\nlit data: dyn %ld, stat %ld", s->opt_len,
+ // s->static_len));
+
+ build_tree(s, s.d_desc);
+ // Tracev((stderr, "\ndist data: dyn %ld, stat %ld", s->opt_len,
+ // s->static_len));
+ /* At this point, opt_len and static_len are the total bit lengths of
+ * the compressed block data, excluding the tree representations.
+ */
+
+ /* Build the bit length tree for the above two trees, and get the index
+ * in bl_order of the last bit length code to send.
+ */
+ max_blindex = build_bl_tree(s);
+
+ /* Determine the best encoding. Compute the block lengths in bytes. */
+ opt_lenb = (s.opt_len + 3 + 7) >>> 3;
+ static_lenb = (s.static_len + 3 + 7) >>> 3;
+
+ // Tracev((stderr, "\nopt %lu(%lu) stat %lu(%lu) stored %lu lit %u ",
+ // opt_lenb, s->opt_len, static_lenb, s->static_len, stored_len,
+ // s->last_lit));
+
+ if (static_lenb <= opt_lenb) { opt_lenb = static_lenb; }
+
+ } else {
+ // Assert(buf != (char*)0, "lost buf");
+ opt_lenb = static_lenb = stored_len + 5; /* force a stored block */
+ }
+
+ if ((stored_len + 4 <= opt_lenb) && (buf !== -1)) {
+ /* 4: two words for the lengths */
+
+ /* The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE.
+ * Otherwise we can't have processed more than WSIZE input bytes since
+ * the last block flush, because compression would have been
+ * successful. If LIT_BUFSIZE <= WSIZE, it is never too late to
+ * transform a block into a stored block.
+ */
+ _tr_stored_block(s, buf, stored_len, last);
+
+ } else if (s.strategy === Z_FIXED || static_lenb === opt_lenb) {
+
+ send_bits(s, (STATIC_TREES << 1) + (last ? 1 : 0), 3);
+ compress_block(s, static_ltree, static_dtree);
+
+ } else {
+ send_bits(s, (DYN_TREES << 1) + (last ? 1 : 0), 3);
+ send_all_trees(s, s.l_desc.max_code + 1, s.d_desc.max_code + 1, max_blindex + 1);
+ compress_block(s, s.dyn_ltree, s.dyn_dtree);
+ }
+ // Assert (s->compressed_len == s->bits_sent, "bad compressed size");
+ /* The above check is made mod 2^32, for files larger than 512 MB
+ * and uLong implemented on 32 bits.
+ */
+ init_block(s);
+
+ if (last) {
+ bi_windup(s);
+ }
+ // Tracev((stderr,"\ncomprlen %lu(%lu) ", s->compressed_len>>3,
+ // s->compressed_len-7*last));
+}
+
+/* ===========================================================================
+ * Save the match info and tally the frequency counts. Return true if
+ * the current block must be flushed.
+ */
+function _tr_tally(s, dist, lc)
+// deflate_state *s;
+// unsigned dist; /* distance of matched string */
+// unsigned lc; /* match length-MIN_MATCH or unmatched char (if dist==0) */
+{
+ //var out_length, in_length, dcode;
+
+ s.pending_buf[s.d_buf + s.last_lit * 2] = (dist >>> 8) & 0xff;
+ s.pending_buf[s.d_buf + s.last_lit * 2 + 1] = dist & 0xff;
+
+ s.pending_buf[s.l_buf + s.last_lit] = lc & 0xff;
+ s.last_lit++;
+
+ if (dist === 0) {
+ /* lc is the unmatched char */
+ s.dyn_ltree[lc * 2]/*.Freq*/++;
+ } else {
+ s.matches++;
+ /* Here, lc is the match length - MIN_MATCH */
+ dist--; /* dist = match distance - 1 */
+ //Assert((ush)dist < (ush)MAX_DIST(s) &&
+ // (ush)lc <= (ush)(MAX_MATCH-MIN_MATCH) &&
+ // (ush)d_code(dist) < (ush)D_CODES, "_tr_tally: bad match");
+
+ s.dyn_ltree[(_length_code[lc] + LITERALS + 1) * 2]/*.Freq*/++;
+ s.dyn_dtree[d_code(dist) * 2]/*.Freq*/++;
+ }
+
+// (!) This block is disabled in zlib defailts,
+// don't enable it for binary compatibility
+
+//#ifdef TRUNCATE_BLOCK
+// /* Try to guess if it is profitable to stop the current block here */
+// if ((s.last_lit & 0x1fff) === 0 && s.level > 2) {
+// /* Compute an upper bound for the compressed length */
+// out_length = s.last_lit*8;
+// in_length = s.strstart - s.block_start;
+//
+// for (dcode = 0; dcode < D_CODES; dcode++) {
+// out_length += s.dyn_dtree[dcode*2]/*.Freq*/ * (5 + extra_dbits[dcode]);
+// }
+// out_length >>>= 3;
+// //Tracev((stderr,"\nlast_lit %u, in %ld, out ~%ld(%ld%%) ",
+// // s->last_lit, in_length, out_length,
+// // 100L - out_length*100L/in_length));
+// if (s.matches < (s.last_lit>>1)/*int /2*/ && out_length < (in_length>>1)/*int /2*/) {
+// return true;
+// }
+// }
+//#endif
+
+ return (s.last_lit === s.lit_bufsize - 1);
+ /* We avoid equality with lit_bufsize because of wraparound at 64K
+ * on 16 bit machines and because stored blocks are restricted to
+ * 64K-1 bytes.
+ */
+}
+
+export { _tr_init, _tr_stored_block, _tr_flush_block, _tr_tally, _tr_align };
pkg/web/noVNC/vendor/pako/lib/zlib/zstream.js
@@ -0,0 +1,24 @@
+export default function ZStream() {
+ /* next input byte */
+ this.input = null; // JS specific, because we have no pointers
+ this.next_in = 0;
+ /* number of bytes available at input */
+ this.avail_in = 0;
+ /* total number of input bytes read so far */
+ this.total_in = 0;
+ /* next output byte should be put there */
+ this.output = null; // JS specific, because we have no pointers
+ this.next_out = 0;
+ /* remaining free space at output */
+ this.avail_out = 0;
+ /* total number of bytes output so far */
+ this.total_out = 0;
+ /* last error message, NULL if no error */
+ this.msg = ''/*Z_NULL*/;
+ /* not visible by applications */
+ this.state = null;
+ /* best guess about the data type: binary or text */
+ this.data_type = 2/*Z_UNKNOWN*/;
+ /* adler32 value of the uncompressed data */
+ this.adler = 0;
+}
pkg/web/noVNC/vendor/pako/LICENSE
@@ -0,0 +1,21 @@
+(The MIT License)
+
+Copyright (C) 2014-2016 by Vitaly Puzrin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
pkg/web/noVNC/vendor/pako/README.md
@@ -0,0 +1,6 @@
+This is an ES6-modules-compatible version of
+https://github.com/nodeca/pako, based on pako version 1.0.3.
+
+It's more-or-less a direct translation of the original, with unused parts
+removed, and the dynamic support for non-typed arrays removed (since ES6
+modules don't work well with dynamic exports).
pkg/web/noVNC/.gitkeep
@@ -0,0 +1,1 @@
+placeholder
pkg/web/noVNC/LICENSE.txt
@@ -0,0 +1,62 @@
+noVNC is Copyright (C) 2022 The noVNC authors
+(./AUTHORS)
+
+The noVNC core library files are licensed under the MPL 2.0 (Mozilla
+Public License 2.0). The noVNC core library is composed of the
+Javascript code necessary for full noVNC operation. This includes (but
+is not limited to):
+
+ core/**/*.js
+ app/*.js
+ test/playback.js
+
+The HTML, CSS, font and images files that included with the noVNC
+source distibution (or repository) are not considered part of the
+noVNC core library and are licensed under more permissive licenses.
+The intent is to allow easy integration of noVNC into existing web
+sites and web applications.
+
+The HTML, CSS, font and image files are licensed as follows:
+
+ *.html : 2-Clause BSD license
+
+ app/styles/*.css : 2-Clause BSD license
+
+ app/styles/Orbitron* : SIL Open Font License 1.1
+ (Copyright 2009 Matt McInerney)
+
+ app/images/ : Creative Commons Attribution-ShareAlike
+ http://creativecommons.org/licenses/by-sa/3.0/
+
+Some portions of noVNC are copyright to their individual authors.
+Please refer to the individual source files and/or to the noVNC commit
+history: https://github.com/novnc/noVNC/commits/master
+
+The are several files and projects that have been incorporated into
+the noVNC core library. Here is a list of those files and the original
+licenses (all MPL 2.0 compatible):
+
+ core/base64.js : MPL 2.0
+
+ core/des.js : Various BSD style licenses
+
+ vendor/pako/ : MIT
+
+Any other files not mentioned above are typically marked with
+a copyright/license header at the top of the file. The default noVNC
+license is MPL-2.0.
+
+The following license texts are included:
+
+ docs/LICENSE.MPL-2.0
+ docs/LICENSE.OFL-1.1
+ docs/LICENSE.BSD-3-Clause (New BSD)
+ docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD)
+ vendor/pako/LICENSE (MIT)
+
+Or alternatively the license texts may be found here:
+
+ http://www.mozilla.org/MPL/2.0/
+ http://scripts.sil.org/OFL
+ http://en.wikipedia.org/wiki/BSD_licenses
+ https://opensource.org/licenses/MIT
pkg/web/noVNC/vnc.html
@@ -0,0 +1,423 @@
+<!DOCTYPE html>
+<html lang="en" class="noVNC_loading">
+<head>
+
+ <!--
+ noVNC example: simple example using default UI
+ Copyright (C) 2019 The noVNC authors
+ noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
+ This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
+
+ Connect parameters are provided in query string:
+ http://example.com/?host=HOST&port=PORT&encrypt=1
+ or the fragment:
+ http://example.com/#host=HOST&port=PORT&encrypt=1
+ -->
+ <title>noVNC</title>
+
+ <link rel="icon" type="image/x-icon" href="app/images/icons/novnc.ico">
+ <meta name="theme-color" content="#313131">
+
+ <!-- Apple iOS Safari settings -->
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+
+ <!-- @2x -->
+ <link rel="apple-touch-icon" sizes="40x40" type="image/png" href="app/images/icons/novnc-ios-40.png">
+ <link rel="apple-touch-icon" sizes="58x58" type="image/png" href="app/images/icons/novnc-ios-58.png">
+ <link rel="apple-touch-icon" sizes="80x80" type="image/png" href="app/images/icons/novnc-ios-80.png">
+ <link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
+ <link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-ios-152.png">
+ <link rel="apple-touch-icon" sizes="167x167" type="image/png" href="app/images/icons/novnc-ios-167.png">
+ <!-- @3x -->
+ <link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-ios-60.png">
+ <link rel="apple-touch-icon" sizes="87x87" type="image/png" href="app/images/icons/novnc-ios-87.png">
+ <link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
+ <link rel="apple-touch-icon" sizes="180x180" type="image/png" href="app/images/icons/novnc-ios-180.png">
+
+ <!-- Stylesheets -->
+ <link rel="stylesheet" href="app/styles/constants.css">
+ <link rel="stylesheet" href="app/styles/base.css">
+ <link rel="stylesheet" href="app/styles/input.css">
+
+ <!-- Images that will later appear via CSS -->
+ <link rel="preload" as="image" href="app/images/info.svg">
+ <link rel="preload" as="image" href="app/images/error.svg">
+ <link rel="preload" as="image" href="app/images/warning.svg">
+
+ <script type="module" crossorigin="anonymous" src="app/error-handler.js"></script>
+
+ <script type="module">
+ import UI from "./app/ui.js";
+ import * as Log from './core/util/logging.js';
+
+ let response;
+
+ let defaults = {};
+ let mandatory = {};
+
+ // Default settings will be loaded from defaults.json. Mandatory
+ // settings will be loaded from mandatory.json, which the user
+ // cannot change.
+
+ try {
+ response = await fetch('./defaults.json');
+ if (!response.ok) {
+ throw Error("" + response.status + " " + response.statusText);
+ }
+
+ defaults = await response.json();
+ } catch (err) {
+ Log.Error("Couldn't fetch defaults.json: " + err);
+ }
+
+ try {
+ response = await fetch('./mandatory.json');
+ if (!response.ok) {
+ throw Error("" + response.status + " " + response.statusText);
+ }
+
+ mandatory = await response.json();
+ } catch (err) {
+ Log.Error("Couldn't fetch mandatory.json: " + err);
+ }
+
+ // You can also override any defaults you need here:
+ //
+ // defaults['host'] = 'vnc.example.com';
+
+ // Or force a specific setting, preventing the user from
+ // changing it:
+ //
+ // mandatory['view_only'] = true;
+
+ // See docs/EMBEDDING.md for a list of possible settings.
+
+ UI.start({ settings: { defaults: defaults,
+ mandatory: mandatory } });
+ </script>
+</head>
+
+<body>
+
+ <div id="noVNC_fallback_error" class="noVNC_center">
+ <div>
+ <div>noVNC encountered an error:</div>
+ <br>
+ <div id="noVNC_fallback_errormsg"></div>
+ </div>
+ </div>
+
+ <!-- noVNC control bar -->
+ <div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
+
+ <div id="noVNC_control_bar">
+ <div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>
+
+ <div class="noVNC_scroll">
+
+ <h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>
+
+ <hr>
+
+ <!-- Drag/Pan the viewport -->
+ <input type="image" alt="Drag" src="app/images/drag.svg"
+ id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
+ title="Move/Drag viewport">
+
+ <!--noVNC touch device only buttons-->
+ <div id="noVNC_mobile_buttons">
+ <input type="image" alt="Keyboard" src="app/images/keyboard.svg"
+ id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard">
+ </div>
+
+ <!-- Extra manual keys -->
+ <input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
+ id="noVNC_toggle_extra_keys_button" class="noVNC_button"
+ title="Show extra keys">
+ <div class="noVNC_vcenter">
+ <div id="noVNC_modifiers" class="noVNC_panel">
+ <input type="image" alt="Ctrl" src="app/images/ctrl.svg"
+ id="noVNC_toggle_ctrl_button" class="noVNC_button"
+ title="Toggle Ctrl">
+ <input type="image" alt="Alt" src="app/images/alt.svg"
+ id="noVNC_toggle_alt_button" class="noVNC_button"
+ title="Toggle Alt">
+ <input type="image" alt="Windows" src="app/images/windows.svg"
+ id="noVNC_toggle_windows_button" class="noVNC_button"
+ title="Toggle Windows">
+ <input type="image" alt="Tab" src="app/images/tab.svg"
+ id="noVNC_send_tab_button" class="noVNC_button"
+ title="Send Tab">
+ <input type="image" alt="Esc" src="app/images/esc.svg"
+ id="noVNC_send_esc_button" class="noVNC_button"
+ title="Send Escape">
+ <input type="image" alt="Ctrl+Alt+Del" src="app/images/ctrlaltdel.svg"
+ id="noVNC_send_ctrl_alt_del_button" class="noVNC_button"
+ title="Send Ctrl-Alt-Del">
+ </div>
+ </div>
+
+ <!-- Shutdown/Reboot -->
+ <input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
+ id="noVNC_power_button" class="noVNC_button"
+ title="Shutdown/Reboot...">
+ <div class="noVNC_vcenter">
+ <div id="noVNC_power" class="noVNC_panel">
+ <div class="noVNC_heading">
+ <img alt="" src="app/images/power.svg"> Power
+ </div>
+ <input type="button" id="noVNC_shutdown_button" value="Shutdown">
+ <input type="button" id="noVNC_reboot_button" value="Reboot">
+ <input type="button" id="noVNC_reset_button" value="Reset">
+ </div>
+ </div>
+
+ <!-- Clipboard -->
+ <input type="image" alt="Clipboard" src="app/images/clipboard.svg"
+ id="noVNC_clipboard_button" class="noVNC_button"
+ title="Clipboard">
+ <div class="noVNC_vcenter">
+ <div id="noVNC_clipboard" class="noVNC_panel">
+ <div class="noVNC_heading">
+ <img alt="" src="app/images/clipboard.svg"> Clipboard
+ </div>
+ <p class="noVNC_subheading">
+ Edit clipboard content in the textarea below.
+ </p>
+ <textarea id="noVNC_clipboard_text" rows=5></textarea>
+ </div>
+ </div>
+
+ <!-- Toggle fullscreen -->
+ <input type="image" alt="Full screen" src="app/images/fullscreen.svg"
+ id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
+ title="Full screen">
+
+ <!-- Settings -->
+ <input type="image" alt="Settings" src="app/images/settings.svg"
+ id="noVNC_settings_button" class="noVNC_button"
+ title="Settings">
+ <div class="noVNC_vcenter">
+ <div id="noVNC_settings" class="noVNC_panel">
+ <div class="noVNC_heading">
+ <img alt="" src="app/images/settings.svg"> Settings
+ </div>
+ <ul>
+ <li>
+ <label>
+ <input id="noVNC_setting_shared" type="checkbox"
+ class="toggle">
+ Shared mode
+ </label>
+ </li>
+ <li>
+ <label>
+ <input id="noVNC_setting_view_only" type="checkbox"
+ class="toggle">
+ View only
+ </label>
+ </li>
+ <li><hr></li>
+ <li>
+ <label>
+ <input id="noVNC_setting_view_clip" type="checkbox"
+ class="toggle">
+ Clip to window
+ </label>
+ </li>
+ <li>
+ <label for="noVNC_setting_resize">Scaling mode:</label>
+ <select id="noVNC_setting_resize" name="vncResize">
+ <option value="off">None</option>
+ <option value="scale">Local scaling</option>
+ <option value="remote">Remote resizing</option>
+ </select>
+ </li>
+ <li><hr></li>
+ <li>
+ <div class="noVNC_expander">Advanced</div>
+ <div><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>
+ <label for="noVNC_setting_repeaterID">Repeater ID:</label>
+ <input id="noVNC_setting_repeaterID" type="text" value="">
+ </li>
+ <li>
+ <div class="noVNC_expander">WebSocket</div>
+ <div><ul>
+ <li>
+ <label>
+ <input id="noVNC_setting_encrypt" type="checkbox"
+ class="toggle">
+ Encrypt
+ </label>
+ </li>
+ <li>
+ <label for="noVNC_setting_host">Host:</label>
+ <input id="noVNC_setting_host">
+ </li>
+ <li>
+ <label for="noVNC_setting_port">Port:</label>
+ <input id="noVNC_setting_port" type="number">
+ </li>
+ <li>
+ <label for="noVNC_setting_path">Path:</label>
+ <input id="noVNC_setting_path" type="text" value="websockify">
+ </li>
+ </ul></div>
+ </li>
+ <li><hr></li>
+ <li>
+ <label>
+ <input id="noVNC_setting_reconnect" type="checkbox"
+ class="toggle">
+ Automatic reconnect
+ </label>
+ </li>
+ <li>
+ <label for="noVNC_setting_reconnect_delay">Reconnect delay (ms):</label>
+ <input id="noVNC_setting_reconnect_delay" type="number">
+ </li>
+ <li><hr></li>
+ <li>
+ <label>
+ <input id="noVNC_setting_show_dot" type="checkbox"
+ class="toggle">
+ Show dot when no cursor
+ </label>
+ </li>
+ <li>
+ <label>
+ <input id="noVNC_setting_keep_device_awake" type="checkbox"
+ class="toggle">
+ Keep client display awake while connected
+ </label>
+ </li>
+ <li><hr></li>
+ <!-- Logging selection dropdown -->
+ <li>
+ <label>Logging:
+ <select id="noVNC_setting_logging" name="vncLogging">
+ </select>
+ </label>
+ </li>
+ </ul></div>
+ </li>
+ <li class="noVNC_version_separator"><hr></li>
+ <li class="noVNC_version_wrapper">
+ <span>Version:</span>
+ <span class="noVNC_version"></span>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <!-- Connection controls -->
+ <input type="image" alt="Disconnect" src="app/images/disconnect.svg"
+ id="noVNC_disconnect_button" class="noVNC_button"
+ title="Disconnect">
+
+ </div>
+ </div>
+
+ </div> <!-- End of noVNC_control_bar -->
+
+ <div id="noVNC_hint_anchor" class="noVNC_vcenter">
+ <div id="noVNC_control_bar_hint">
+ </div>
+ </div>
+
+ <!-- Status dialog -->
+ <div id="noVNC_status"></div>
+
+ <!-- Connect button -->
+ <div class="noVNC_center">
+ <div id="noVNC_connect_dlg">
+ <p class="noVNC_logo" translate="no"><span>no</span>VNC</p>
+ <div>
+ <button id="noVNC_connect_button">
+ <img alt="" src="app/images/connect.svg"> Connect
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Server key verification dialog -->
+ <div class="noVNC_center noVNC_connect_layer">
+ <div id="noVNC_verify_server_dlg" class="noVNC_panel"><form>
+ <div class="noVNC_heading">
+ Server identity
+ </div>
+ <div>
+ The server has provided the following identifying information:
+ </div>
+ <div id="noVNC_fingerprint_block">
+ Fingerprint:
+ <span id="noVNC_fingerprint"></span>
+ </div>
+ <div>
+ Please verify that the information is correct and press
+ "Approve". Otherwise press "Reject".
+ </div>
+ <div class="button_row">
+ <input id="noVNC_approve_server_button" type="submit" value="Approve">
+ <input id="noVNC_reject_server_button" type="button" value="Reject">
+ </div>
+ </form></div>
+ </div>
+
+ <!-- Password dialog -->
+ <div class="noVNC_center noVNC_connect_layer">
+ <div id="noVNC_credentials_dlg" class="noVNC_panel"><form>
+ <div class="noVNC_heading">
+ Credentials
+ </div>
+ <div id="noVNC_username_block">
+ <label for="noVNC_username_input">Username:</label>
+ <input id="noVNC_username_input">
+ </div>
+ <div id="noVNC_password_block">
+ <label for="noVNC_password_input">Password:</label>
+ <input id="noVNC_password_input" type="password">
+ </div>
+ <div class="button_row">
+ <input id="noVNC_credentials_button" type="submit" value="Send credentials">
+ </div>
+ </form></div>
+ </div>
+
+ <!-- Transition screens -->
+ <div id="noVNC_transition">
+ <div id="noVNC_transition_text"></div>
+ <div>
+ <input type="button" id="noVNC_cancel_reconnect_button" value="Cancel">
+ </div>
+ <div class="noVNC_spinner"></div>
+ </div>
+
+ <!-- This is where the RFB elements will attach -->
+ <div id="noVNC_container">
+ <!-- Note that Google Chrome on Android doesn't respect any of these,
+ html attributes which attempt to disable text suggestions on the
+ on-screen keyboard. Let's hope Chrome implements the ime-mode
+ style for example -->
+ <textarea id="noVNC_keyboardinput" autocapitalize="off"
+ autocomplete="off" spellcheck="false" tabindex="-1"></textarea>
+ </div>
+
+ <audio id="noVNC_bell">
+ <source src="app/sounds/bell.oga" type="audio/ogg">
+ <source src="app/sounds/bell.mp3" type="audio/mpeg">
+ </audio>
+ </body>
+</html>
pkg/web/api.go
@@ -0,0 +1,171 @@
+// Package web provides HTTP/WebSocket server for VNC proxy.
+package web
+
+import (
+ "encoding/json"
+ "net/http"
+ "time"
+
+ "goVNC/pkg/input"
+ "goVNC/pkg/rfb"
+)
+
+// API provides REST API handlers for VNC operations.
+type API struct {
+ vnc *rfb.Client
+}
+
+// NewAPI creates a new API handler.
+func NewAPI(vnc *rfb.Client) *API {
+ return &API{vnc: vnc}
+}
+
+// ClipboardRequest is the request body for PUT /api/clipboard.
+type ClipboardRequest struct {
+ Text string `json:"text"`
+}
+
+// ClipboardResponse is the response for GET /api/clipboard.
+type ClipboardResponse struct {
+ Text string `json:"text"`
+ Timestamp time.Time `json:"timestamp"`
+}
+
+// KeysRequest is the request body for PUT /api/keys.
+type KeysRequest struct {
+ // Text to type (simple mode)
+ Text string `json:"text,omitempty"`
+
+ // DelayMs between keystrokes
+ DelayMs int `json:"delay_ms,omitempty"`
+
+ // Keys for raw key events
+ Keys []KeyEvent `json:"keys,omitempty"`
+}
+
+// KeyEvent represents a single key event.
+type KeyEvent struct {
+ Key string `json:"key"`
+ Down bool `json:"down"`
+}
+
+// SessionResponse is the response for GET /api/session.
+type SessionResponse struct {
+ Name string `json:"name"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Connected bool `json:"connected"`
+}
+
+// HandleClipboard handles clipboard API requests.
+func (a *API) HandleClipboard(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case http.MethodGet:
+ a.getClipboard(w, r)
+ case http.MethodPut:
+ a.setClipboard(w, r)
+ default:
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ }
+}
+
+func (a *API) getClipboard(w http.ResponseWriter, _ *http.Request) {
+ text, timestamp := a.vnc.GetLastClipboard()
+ resp := ClipboardResponse{
+ Text: text,
+ Timestamp: timestamp,
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(resp)
+}
+
+func (a *API) setClipboard(w http.ResponseWriter, r *http.Request) {
+ var req ClipboardRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Invalid JSON", http.StatusBadRequest)
+ return
+ }
+
+ if err := a.vnc.SetClipboard(r.Context(), req.Text); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// HandleKeys handles keyboard API requests.
+func (a *API) HandleKeys(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPut {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ var req KeysRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Invalid JSON", http.StatusBadRequest)
+ return
+ }
+
+ ctx := r.Context()
+
+ // If keys array is provided, send raw key events
+ if len(req.Keys) > 0 {
+ for _, ke := range req.Keys {
+ key, ok := input.ParseKeyName(ke.Key)
+ if !ok {
+ // Try as single character
+ if len(ke.Key) == 1 {
+ key = rfb.RuneToKeysym(rune(ke.Key[0]))
+ } else {
+ http.Error(w, "Unknown key: "+ke.Key, http.StatusBadRequest)
+ return
+ }
+ }
+ if err := a.vnc.KeyEvent(ctx, key, ke.Down); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ // Otherwise, type the text
+ if req.Text != "" {
+ // Add delay between keystrokes if specified
+ for _, r := range req.Text {
+ key := rfb.RuneToKeysym(r)
+ if err := a.vnc.Tap(ctx, key); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if req.DelayMs > 0 {
+ time.Sleep(time.Duration(req.DelayMs) * time.Millisecond)
+ }
+ }
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// HandleSession handles session info API requests.
+func (a *API) HandleSession(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ session := a.vnc.Session()
+ resp := SessionResponse{
+ Connected: session != nil,
+ }
+ if session != nil {
+ resp.Name = session.Name
+ resp.Width = int(session.Width)
+ resp.Height = int(session.Height)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(resp)
+}
pkg/web/embed_test.go
@@ -0,0 +1,51 @@
+package web
+
+import (
+ "testing"
+)
+
+func TestEmbeddedNoVNC(t *testing.T) {
+ if !hasEmbeddedNoVNC() {
+ t.Fatal("noVNC files should be embedded")
+ }
+
+ // Check that key files exist
+ files := []string{
+ "noVNC/vnc.html",
+ "noVNC/app/ui.js",
+ "noVNC/core/rfb.js",
+ "noVNC/vendor/pako/lib/zlib/inflate.js",
+ }
+
+ for _, path := range files {
+ data, err := embeddedNoVNC.ReadFile(path)
+ if err != nil {
+ t.Errorf("missing embedded file %s: %v", path, err)
+ continue
+ }
+ if len(data) == 0 {
+ t.Errorf("embedded file %s is empty", path)
+ }
+ }
+}
+
+func TestEmbeddedNoVNCDirectories(t *testing.T) {
+ // Verify directory structure
+ dirs := []string{
+ "noVNC",
+ "noVNC/app",
+ "noVNC/core",
+ "noVNC/vendor",
+ }
+
+ for _, dir := range dirs {
+ entries, err := embeddedNoVNC.ReadDir(dir)
+ if err != nil {
+ t.Errorf("cannot read directory %s: %v", dir, err)
+ continue
+ }
+ if len(entries) == 0 {
+ t.Errorf("directory %s is empty", dir)
+ }
+ }
+}
pkg/web/server.go
@@ -0,0 +1,235 @@
+package web
+
+import (
+ "context"
+ "embed"
+ "fmt"
+ "io/fs"
+ "log/slog"
+ "net/http"
+ "strings"
+
+ "goVNC/pkg/proxy"
+ "goVNC/pkg/rfb"
+)
+
+// Server is the HTTP/WebSocket server for VNC proxy.
+type Server struct {
+ httpServer *http.Server
+ mux *proxy.Multiplexer
+ api *API
+ config *ServerConfig
+}
+
+// ServerConfig configures the web server.
+type ServerConfig struct {
+ // ListenAddr is the address to listen on (e.g., ":8080").
+ ListenAddr string
+
+ // APIPrefix is the prefix for API routes (default: "/api").
+ APIPrefix string
+
+ // NoVNCPath is the path to noVNC static files.
+ // If empty or "embedded", uses embedded noVNC files.
+ NoVNCPath string
+
+ // CORSOrigin is the allowed CORS origin (* for any).
+ CORSOrigin string
+
+ // ProxyMode determines how clients interact with the session.
+ ProxyMode proxy.SessionMode
+
+ // MaxClients is the maximum number of concurrent clients.
+ MaxClients int
+}
+
+// DefaultServerConfig returns default server configuration.
+func DefaultServerConfig() *ServerConfig {
+ return &ServerConfig{
+ ListenAddr: ":8080",
+ APIPrefix: "/api",
+ CORSOrigin: "*",
+ ProxyMode: proxy.SharedMode,
+ MaxClients: 10,
+ }
+}
+
+// NewServer creates a new web server.
+func NewServer(vnc *rfb.Client, cfg *ServerConfig) *Server {
+ if cfg == nil {
+ cfg = DefaultServerConfig()
+ }
+
+ mux := proxy.NewMultiplexer(vnc, cfg.ProxyMode)
+ api := NewAPI(vnc)
+
+ return &Server{
+ mux: mux,
+ api: api,
+ config: cfg,
+ }
+}
+
+// ListenAndServe starts the HTTP server.
+func (s *Server) ListenAndServe(ctx context.Context) error {
+ mux := http.NewServeMux()
+
+ // API routes
+ apiPrefix := s.config.APIPrefix
+ if !strings.HasSuffix(apiPrefix, "/") {
+ apiPrefix += "/"
+ }
+
+ mux.HandleFunc(apiPrefix+"clipboard", s.corsMiddleware(s.api.HandleClipboard))
+ mux.HandleFunc(apiPrefix+"keys", s.corsMiddleware(s.api.HandleKeys))
+ mux.HandleFunc(apiPrefix+"session", s.corsMiddleware(s.api.HandleSession))
+
+ // WebSocket proxy endpoint
+ wsHandler := NewWebsockifyHandler(s.mux)
+ mux.Handle("/websockify", wsHandler)
+
+ // noVNC static files
+ if err := s.setupNoVNC(mux); err != nil {
+ return fmt.Errorf("setting up noVNC: %w", err)
+ }
+
+ // Root redirect to noVNC
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/" {
+ http.Redirect(w, r, "/noVNC/vnc.html", http.StatusFound)
+ return
+ }
+ http.NotFound(w, r)
+ })
+
+ s.httpServer = &http.Server{
+ Addr: s.config.ListenAddr,
+ Handler: mux,
+ }
+
+ slog.Info("starting web server",
+ slog.String("addr", s.config.ListenAddr),
+ slog.String("api", s.config.APIPrefix))
+
+ // Start server in goroutine
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- s.httpServer.ListenAndServe()
+ }()
+
+ // Wait for context or error
+ select {
+ case <-ctx.Done():
+ s.httpServer.Shutdown(context.Background())
+ return ctx.Err()
+ case err := <-errCh:
+ return err
+ }
+}
+
+// corsMiddleware adds CORS headers to responses.
+func (s *Server) corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if s.config.CORSOrigin != "" {
+ w.Header().Set("Access-Control-Allow-Origin", s.config.CORSOrigin)
+ w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
+ }
+
+ if r.Method == http.MethodOptions {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ next(w, r)
+ }
+}
+
+// setupNoVNC sets up the noVNC static file handler.
+func (s *Server) setupNoVNC(mux *http.ServeMux) error {
+ if s.config.NoVNCPath != "" && s.config.NoVNCPath != "embedded" {
+ // Serve from filesystem path
+ fileServer := http.FileServer(http.Dir(s.config.NoVNCPath))
+ mux.Handle("/noVNC/", http.StripPrefix("/noVNC/", fileServer))
+ slog.Info("serving noVNC from filesystem", slog.String("path", s.config.NoVNCPath))
+ return nil
+ }
+
+ // Try embedded files
+ if hasEmbeddedNoVNC() {
+ subFS, err := fs.Sub(embeddedNoVNC, "noVNC")
+ if err != nil {
+ return fmt.Errorf("creating sub filesystem: %w", err)
+ }
+ fileServer := http.FileServer(http.FS(subFS))
+ mux.Handle("/noVNC/", http.StripPrefix("/noVNC/", fileServer))
+ slog.Info("serving noVNC from embedded files")
+ return nil
+ }
+
+ // No noVNC available - serve placeholder
+ mux.HandleFunc("/noVNC/", func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte(noVNCPlaceholder))
+ })
+ slog.Warn("noVNC not available - serving placeholder")
+ return nil
+}
+
+// Close shuts down the server.
+func (s *Server) Close() error {
+ if s.httpServer != nil {
+ s.httpServer.Close()
+ }
+ return s.mux.Close()
+}
+
+// Multiplexer returns the proxy multiplexer for registering message handlers.
+func (s *Server) Multiplexer() *proxy.Multiplexer {
+ return s.mux
+}
+
+// embeddedNoVNC holds embedded noVNC files.
+// The "all:" prefix includes files starting with . or _
+//
+//go:embed all:noVNC
+var embeddedNoVNC embed.FS
+
+func hasEmbeddedNoVNC() bool {
+ entries, err := embeddedNoVNC.ReadDir("noVNC")
+ return err == nil && len(entries) > 0
+}
+
+const noVNCPlaceholder = `<!DOCTYPE html>
+<html>
+<head>
+ <title>noVNC Not Available</title>
+ <style>
+ body { font-family: sans-serif; padding: 20px; max-width: 600px; margin: 0 auto; }
+ h1 { color: #333; }
+ code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
+ pre { background: #f4f4f4; padding: 10px; border-radius: 5px; overflow-x: auto; }
+ </style>
+</head>
+<body>
+ <h1>noVNC Not Available</h1>
+ <p>The noVNC web client is not embedded in this build.</p>
+ <p>You can:</p>
+ <ul>
+ <li>Download noVNC and specify the path with <code>--novnc-path /path/to/noVNC</code></li>
+ <li>Use the REST API directly:
+ <pre>
+# Send keys
+curl -X PUT localhost:8080/api/keys -d '{"text":"hello\n"}'
+
+# Send clipboard
+curl -X PUT localhost:8080/api/clipboard -d '{"text":"copied"}'
+
+# Get session info
+curl localhost:8080/api/session
+ </pre>
+ </li>
+ <li>Connect with any noVNC-compatible client to <code>ws://localhost:8080/websockify</code></li>
+ </ul>
+</body>
+</html>`
pkg/web/websockify.go
@@ -0,0 +1,111 @@
+package web
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "net/http"
+ "sync/atomic"
+
+ "github.com/coder/websocket"
+
+ "goVNC/pkg/proxy"
+ "goVNC/pkg/transport"
+)
+
+// WebsockifyHandler handles WebSocket proxy connections.
+type WebsockifyHandler struct {
+ mux *proxy.Multiplexer
+ clientSeq uint64
+}
+
+// NewWebsockifyHandler creates a new WebSocket proxy handler.
+func NewWebsockifyHandler(mux *proxy.Multiplexer) *WebsockifyHandler {
+ return &WebsockifyHandler{
+ mux: mux,
+ }
+}
+
+// ServeHTTP handles WebSocket upgrade and proxying.
+func (h *WebsockifyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // Accept WebSocket connection
+ conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
+ Subprotocols: []string{"binary"},
+ OriginPatterns: []string{"*"}, // Allow all origins for now
+ })
+ if err != nil {
+ slog.Error("accepting websocket", slog.String("error", err.Error()))
+ return
+ }
+
+ // Generate client ID
+ seq := atomic.AddUint64(&h.clientSeq, 1)
+ clientID := fmt.Sprintf("client-%d", seq)
+
+ // Wrap in transport
+ clientTransport := &wsTransport{conn: conn}
+
+ // Register with multiplexer
+ client := h.mux.AddClient(clientID, clientTransport)
+ defer h.mux.RemoveClient(clientID)
+
+ // Set up context
+ ctx := r.Context()
+
+ // Get current session info to send initial state
+ session := h.mux.VNCClient().Session()
+ if session != nil {
+ // Send ServerInit-like message so client knows dimensions
+ // noVNC expects to receive the full handshake, but since we've
+ // already done that, we need to replay it
+ slog.Info("client connected to session",
+ slog.String("client", clientID),
+ slog.String("session", session.Name))
+ }
+
+ // Run reader and writer concurrently
+ errCh := make(chan error, 2)
+
+ go func() {
+ errCh <- h.mux.RunClientWriter(ctx, client)
+ }()
+
+ go func() {
+ errCh <- h.mux.RunClientReader(ctx, client)
+ }()
+
+ // Wait for either to complete
+ err = <-errCh
+ if err != nil && err != context.Canceled {
+ slog.Debug("client connection ended",
+ slog.String("client", clientID),
+ slog.String("error", err.Error()))
+ }
+
+ // Close the connection
+ conn.Close(websocket.StatusNormalClosure, "")
+}
+
+// wsTransport wraps a WebSocket connection as a Transport.
+type wsTransport struct {
+ conn *websocket.Conn
+}
+
+func (t *wsTransport) Read(ctx context.Context) ([]byte, error) {
+ _, data, err := t.conn.Read(ctx)
+ return data, err
+}
+
+func (t *wsTransport) Write(ctx context.Context, data []byte) error {
+ return t.conn.Write(ctx, websocket.MessageBinary, data)
+}
+
+func (t *wsTransport) Close() error {
+ return t.conn.CloseNow()
+}
+
+func (t *wsTransport) SetReadLimit(limit int64) {
+ t.conn.SetReadLimit(limit)
+}
+
+var _ transport.Transport = (*wsTransport)(nil)
.gitignore
@@ -0,0 +1,3 @@
+.govnc_history
+clipboard_in
+clipboard_out