2 // Copyright (C) 2001-2004 HorizonLive.com, Inc. All Rights Reserved.
3 // Copyright (C) 2002 Constantin Kaplinsky. All Rights Reserved.
4 // Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved.
6 // This is free software; you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation; either version 2 of the License, or
9 // (at your option) any later version.
11 // This software is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this software; if not, write to the Free Software
18 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
23 // VncViewer.java - the VNC viewer applet. This class mainly just sets up the
24 // user interface, leaving it to the VncCanvas to do the actual rendering of
29 import java.awt.event.*;
33 public class VncViewer extends java.applet.Applet
34 implements java.lang.Runnable, WindowListener {
36 boolean inAnApplet = true;
37 boolean inSeparateFrame = false;
40 // main() is called when run as a java program from the command line.
41 // It simply runs the applet inside a newly-created frame.
44 public static void main(String[] argv) {
45 VncViewer v = new VncViewer();
48 v.inSeparateFrame = true;
60 Container vncContainer;
61 ScrollPane desktopScrollPane;
62 GridBagLayout gridbag;
63 ButtonPanel buttonPanel;
64 Label connStatusLabel;
67 ClipboardFrame clipboard;
70 // Control session recording.
72 String sessionFileName;
73 boolean recordingActive;
74 boolean recordingStatusChanged;
75 String cursorUpdatesDef;
76 String eightBitColorsDef;
78 // Variables read from parameter values.
85 boolean showOfflineDesktop;
86 int deferScreenUpdates;
87 int deferCursorUpdates;
88 int deferUpdateRequests;
90 // Reference to this applet for inter-applet communication.
91 public static java.applet.Applet refApplet;
103 if (inSeparateFrame) {
104 vncFrame = new Frame("TightVNC");
106 vncFrame.add("Center", this);
108 vncContainer = vncFrame;
113 recordingSync = new Object();
115 options = new OptionsFrame(this);
116 clipboard = new ClipboardFrame(this);
117 if (RecordingFrame.checkSecurity())
118 rec = new RecordingFrame(this);
120 sessionFileName = null;
121 recordingActive = false;
122 recordingStatusChanged = false;
123 cursorUpdatesDef = null;
124 eightBitColorsDef = null;
127 vncFrame.addWindowListener(this);
129 rfbThread = new Thread(this);
133 public void update(Graphics g) {
137 // run() - executed by the rfbThread to deal with the RFB socket.
142 gridbag = new GridBagLayout();
143 vncContainer.setLayout(gridbag);
145 GridBagConstraints gbc = new GridBagConstraints();
146 gbc.gridwidth = GridBagConstraints.REMAINDER;
147 gbc.anchor = GridBagConstraints.NORTHWEST;
150 buttonPanel = new ButtonPanel(this);
151 gridbag.setConstraints(buttonPanel, gbc);
152 vncContainer.add(buttonPanel);
156 connectAndAuthenticate();
157 doProtocolInitialisation();
159 // FIXME: Use auto-scaling not only in a separate frame.
160 if (options.autoScale && inSeparateFrame) {
161 Dimension screenSize;
163 screenSize = vncContainer.getToolkit().getScreenSize();
164 } catch (Exception e) {
165 screenSize = new Dimension(0, 0);
167 createCanvas(screenSize.width - 32, screenSize.height - 32);
175 if (inSeparateFrame) {
177 // Create a panel which itself is resizeable and can hold
178 // non-resizeable VncCanvas component at the top left corner.
179 Panel canvasPanel = new Panel();
180 canvasPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
183 // Create a ScrollPane which will hold a panel with VncCanvas
185 desktopScrollPane = new ScrollPane(ScrollPane.SCROLLBARS_AS_NEEDED);
186 gbc.fill = GridBagConstraints.BOTH;
187 gridbag.setConstraints(desktopScrollPane, gbc);
188 desktopScrollPane.add(canvasPanel);
190 // Finally, add our ScrollPane to the Frame window.
191 vncFrame.add(desktopScrollPane);
192 vncFrame.setTitle(rfb.desktopName);
194 vc.resizeDesktopFrame();
198 // Just add the VncCanvas component to the Applet.
199 gridbag.setConstraints(vc, gbc);
206 buttonPanel.enableButtons();
208 moveFocusToDesktop();
209 processNormalProtocol();
211 } catch (NoRouteToHostException e) {
212 fatalError("Network error: no route to server: " + host, e);
213 } catch (UnknownHostException e) {
214 fatalError("Network error: server name unknown: " + host, e);
215 } catch (ConnectException e) {
216 fatalError("Network error: could not connect to server: " +
217 host + ":" + port, e);
218 } catch (EOFException e) {
219 if (showOfflineDesktop) {
221 System.out.println("Network error: remote side closed connection");
223 vc.enableInput(false);
225 if (inSeparateFrame) {
226 vncFrame.setTitle(rfb.desktopName + " [disconnected]");
228 if (rfb != null && !rfb.closed())
230 if (showControls && buttonPanel != null) {
231 buttonPanel.disableButtonsOnDisconnect();
232 if (inSeparateFrame) {
239 fatalError("Network error: remote side closed connection", e);
241 } catch (IOException e) {
242 String str = e.getMessage();
243 if (str != null && str.length() != 0) {
244 fatalError("Network Error: " + str, e);
246 fatalError(e.toString(), e);
248 } catch (Exception e) {
249 String str = e.getMessage();
250 if (str != null && str.length() != 0) {
251 fatalError("Error: " + str, e);
253 fatalError(e.toString(), e);
260 // Create a VncCanvas instance.
263 void createCanvas(int maxWidth, int maxHeight) throws IOException {
264 // Determine if Java 2D API is available and use a special
265 // version of VncCanvas if it is present.
268 // This throws ClassNotFoundException if there is no Java 2D API.
269 Class cl = Class.forName("java.awt.Graphics2D");
270 // If we could load Graphics2D class, then we can use VncCanvas2D.
271 cl = Class.forName("VncCanvas2");
272 Class[] argClasses = { this.getClass(), Integer.TYPE, Integer.TYPE };
273 java.lang.reflect.Constructor cstr = cl.getConstructor(argClasses);
274 Object[] argObjects =
275 { this, new Integer(maxWidth), new Integer(maxHeight) };
276 vc = (VncCanvas)cstr.newInstance(argObjects);
277 } catch (Exception e) {
278 System.out.println("Warning: Java 2D API is not available");
281 // If we failed to create VncCanvas2D, use old VncCanvas.
283 vc = new VncCanvas(this, maxWidth, maxHeight);
288 // Process RFB socket messages.
289 // If the rfbThread is being stopped, ignore any exceptions,
290 // otherwise rethrow the exception so it can be handled.
293 void processNormalProtocol() throws Exception {
295 vc.processNormalProtocol();
296 } catch (Exception e) {
297 if (rfbThread == null) {
298 System.out.println("Ignoring RFB socket exceptions" +
299 " because applet is stopping");
308 // Connect to the RFB server and authenticate the user.
311 void connectAndAuthenticate() throws Exception
313 showConnectionStatus("Initializing...");
314 if (inSeparateFrame) {
321 showConnectionStatus("Connecting to " + host + ", port " + port + "...");
323 rfb = new RfbProto(host, port, this);
324 showConnectionStatus("Connected to server");
326 rfb.readVersionMsg();
327 showConnectionStatus("RFB server supports protocol version " +
328 rfb.serverMajor + "." + rfb.serverMinor);
330 rfb.writeVersionMsg();
331 showConnectionStatus("Using RFB protocol version " +
332 rfb.clientMajor + "." + rfb.clientMinor);
334 int secType = rfb.negotiateSecurity();
336 if (secType == RfbProto.SecTypeTight) {
337 showConnectionStatus("Enabling TightVNC protocol extensions");
338 rfb.initCapabilities();
339 rfb.setupTunneling();
340 authType = rfb.negotiateAuthenticationTight();
346 case RfbProto.AuthNone:
347 showConnectionStatus("No authentication needed");
348 rfb.authenticateNone();
350 case RfbProto.AuthVNC:
351 showConnectionStatus("Performing standard VNC authentication");
352 if (passwordParam != null) {
353 rfb.authenticateVNC(passwordParam);
355 String pw = askPassword();
356 rfb.authenticateVNC(pw);
360 throw new Exception("Unknown authentication scheme " + authType);
366 // Show a message describing the connection status.
367 // To hide the connection status label, use (msg == null).
370 void showConnectionStatus(String msg)
373 if (vncContainer.isAncestorOf(connStatusLabel)) {
374 vncContainer.remove(connStatusLabel);
379 System.out.println(msg);
381 if (connStatusLabel == null) {
382 connStatusLabel = new Label("Status: " + msg);
383 connStatusLabel.setFont(new Font("Helvetica", Font.PLAIN, 12));
385 connStatusLabel.setText("Status: " + msg);
388 if (!vncContainer.isAncestorOf(connStatusLabel)) {
389 GridBagConstraints gbc = new GridBagConstraints();
390 gbc.gridwidth = GridBagConstraints.REMAINDER;
391 gbc.fill = GridBagConstraints.HORIZONTAL;
392 gbc.anchor = GridBagConstraints.NORTHWEST;
395 gbc.insets = new Insets(20, 30, 20, 30);
396 gridbag.setConstraints(connStatusLabel, gbc);
397 vncContainer.add(connStatusLabel);
400 if (inSeparateFrame) {
409 // Show an authentication panel.
412 String askPassword() throws Exception
414 showConnectionStatus(null);
416 AuthPanel authPanel = new AuthPanel(this);
418 GridBagConstraints gbc = new GridBagConstraints();
419 gbc.gridwidth = GridBagConstraints.REMAINDER;
420 gbc.anchor = GridBagConstraints.NORTHWEST;
425 gridbag.setConstraints(authPanel, gbc);
426 vncContainer.add(authPanel);
428 if (inSeparateFrame) {
434 authPanel.moveFocusToDefaultField();
435 String pw = authPanel.getPassword();
436 vncContainer.remove(authPanel);
443 // Do the rest of the protocol initialisation.
446 void doProtocolInitialisation() throws IOException
448 rfb.writeClientInit();
449 rfb.readServerInit();
451 System.out.println("Desktop name is " + rfb.desktopName);
452 System.out.println("Desktop size is " + rfb.framebufferWidth + " x " +
453 rfb.framebufferHeight);
457 showConnectionStatus(null);
462 // Send current encoding list to the RFB server.
465 int[] encodingsSaved;
468 void setEncodings() { setEncodings(false); }
469 void autoSelectEncodings() { setEncodings(true); }
471 void setEncodings(boolean autoSelectOnly) {
472 if (options == null || rfb == null || !rfb.inNormalProtocol)
475 int preferredEncoding = options.preferredEncoding;
476 if (preferredEncoding == -1) {
477 long kbitsPerSecond = rfb.kbitsPerSecond();
478 if (nEncodingsSaved < 1) {
479 // Choose Tight or ZRLE encoding for the very first update.
480 System.out.println("Using Tight/ZRLE encodings");
481 preferredEncoding = RfbProto.EncodingTight;
482 } else if (kbitsPerSecond > 2000 &&
483 encodingsSaved[0] != RfbProto.EncodingHextile) {
484 // Switch to Hextile if the connection speed is above 2Mbps.
485 System.out.println("Throughput " + kbitsPerSecond +
486 " kbit/s - changing to Hextile encoding");
487 preferredEncoding = RfbProto.EncodingHextile;
488 } else if (kbitsPerSecond < 1000 &&
489 encodingsSaved[0] != RfbProto.EncodingTight) {
490 // Switch to Tight/ZRLE if the connection speed is below 1Mbps.
491 System.out.println("Throughput " + kbitsPerSecond +
492 " kbit/s - changing to Tight/ZRLE encodings");
493 preferredEncoding = RfbProto.EncodingTight;
495 // Don't change the encoder.
498 preferredEncoding = encodingsSaved[0];
501 // Auto encoder selection is not enabled.
506 int[] encodings = new int[20];
509 encodings[nEncodings++] = preferredEncoding;
510 if (options.useCopyRect) {
511 encodings[nEncodings++] = RfbProto.EncodingCopyRect;
514 if (preferredEncoding != RfbProto.EncodingTight) {
515 encodings[nEncodings++] = RfbProto.EncodingTight;
517 if (preferredEncoding != RfbProto.EncodingZRLE) {
518 encodings[nEncodings++] = RfbProto.EncodingZRLE;
520 if (preferredEncoding != RfbProto.EncodingHextile) {
521 encodings[nEncodings++] = RfbProto.EncodingHextile;
523 if (preferredEncoding != RfbProto.EncodingZlib) {
524 encodings[nEncodings++] = RfbProto.EncodingZlib;
526 if (preferredEncoding != RfbProto.EncodingCoRRE) {
527 encodings[nEncodings++] = RfbProto.EncodingCoRRE;
529 if (preferredEncoding != RfbProto.EncodingRRE) {
530 encodings[nEncodings++] = RfbProto.EncodingRRE;
533 if (options.compressLevel >= 0 && options.compressLevel <= 9) {
534 encodings[nEncodings++] =
535 RfbProto.EncodingCompressLevel0 + options.compressLevel;
537 if (options.jpegQuality >= 0 && options.jpegQuality <= 9) {
538 encodings[nEncodings++] =
539 RfbProto.EncodingQualityLevel0 + options.jpegQuality;
542 if (options.requestCursorUpdates) {
543 encodings[nEncodings++] = RfbProto.EncodingXCursor;
544 encodings[nEncodings++] = RfbProto.EncodingRichCursor;
545 if (!options.ignoreCursorUpdates)
546 encodings[nEncodings++] = RfbProto.EncodingPointerPos;
549 encodings[nEncodings++] = RfbProto.EncodingLastRect;
550 encodings[nEncodings++] = RfbProto.EncodingNewFBSize;
552 boolean encodingsWereChanged = false;
553 if (nEncodings != nEncodingsSaved) {
554 encodingsWereChanged = true;
556 for (int i = 0; i < nEncodings; i++) {
557 if (encodings[i] != encodingsSaved[i]) {
558 encodingsWereChanged = true;
564 if (encodingsWereChanged) {
566 rfb.writeSetEncodings(encodings, nEncodings);
570 } catch (Exception e) {
573 encodingsSaved = encodings;
574 nEncodingsSaved = nEncodings;
580 // setCutText() - send the given cut text to the RFB server.
583 void setCutText(String text) {
585 if (rfb != null && rfb.inNormalProtocol) {
586 rfb.writeClientCutText(text);
588 } catch (Exception e) {
595 // Order change in session recording status. To stop recording, pass
596 // null in place of the fname argument.
599 void setRecordingStatus(String fname) {
600 synchronized(recordingSync) {
601 sessionFileName = fname;
602 recordingStatusChanged = true;
607 // Start or stop session recording. Returns true if this method call
608 // causes recording of a new session.
611 boolean checkRecordingStatus() throws IOException {
612 synchronized(recordingSync) {
613 if (recordingStatusChanged) {
614 recordingStatusChanged = false;
615 if (sessionFileName != null) {
627 // Start session recording.
630 protected void startRecording() throws IOException {
631 synchronized(recordingSync) {
632 if (!recordingActive) {
633 // Save settings to restore them after recording the session.
635 options.choices[options.cursorUpdatesIndex].getSelectedItem();
637 options.choices[options.eightBitColorsIndex].getSelectedItem();
638 // Set options to values suitable for recording.
639 options.choices[options.cursorUpdatesIndex].select("Disable");
640 options.choices[options.cursorUpdatesIndex].setEnabled(false);
641 options.setEncodings();
642 options.choices[options.eightBitColorsIndex].select("No");
643 options.choices[options.eightBitColorsIndex].setEnabled(false);
644 options.setColorFormat();
649 System.out.println("Recording the session in " + sessionFileName);
650 rfb.startSession(sessionFileName);
651 recordingActive = true;
656 // Stop session recording.
659 protected void stopRecording() throws IOException {
660 synchronized(recordingSync) {
661 if (recordingActive) {
663 options.choices[options.cursorUpdatesIndex].select(cursorUpdatesDef);
664 options.choices[options.cursorUpdatesIndex].setEnabled(true);
665 options.setEncodings();
666 options.choices[options.eightBitColorsIndex].select(eightBitColorsDef);
667 options.choices[options.eightBitColorsIndex].setEnabled(true);
668 options.setColorFormat();
671 System.out.println("Session recording stopped.");
673 sessionFileName = null;
674 recordingActive = false;
680 // readParameters() - read parameters from the html source or from the
681 // command line. On the command line, the arguments are just a sequence of
682 // param_name/param_value pairs where the names and values correspond to
683 // those expected in the html applet tag source.
686 void readParameters() {
687 host = readParameter("HOST", !inAnApplet);
689 host = getCodeBase().getHost();
690 if (host.equals("")) {
691 fatalError("HOST parameter not specified");
695 String str = readParameter("PORT", true);
696 port = Integer.parseInt(str);
698 // Read "ENCPASSWORD" or "PASSWORD" parameter if specified.
699 readPasswordParameters();
702 str = readParameter("Open New Window", false);
703 if (str != null && str.equalsIgnoreCase("Yes"))
704 inSeparateFrame = true;
707 // "Show Controls" set to "No" disables button panel.
709 str = readParameter("Show Controls", false);
710 if (str != null && str.equalsIgnoreCase("No"))
711 showControls = false;
713 // "Offer Relogin" set to "No" disables "Login again" and "Close
714 // window" buttons under error messages in applet mode.
716 str = readParameter("Offer Relogin", false);
717 if (str != null && str.equalsIgnoreCase("No"))
718 offerRelogin = false;
720 // Do we continue showing desktop on remote disconnect?
721 showOfflineDesktop = false;
722 str = readParameter("Show Offline Desktop", false);
723 if (str != null && str.equalsIgnoreCase("Yes"))
724 showOfflineDesktop = true;
726 // Fine tuning options.
727 deferScreenUpdates = readIntParameter("Defer screen updates", 20);
728 deferCursorUpdates = readIntParameter("Defer cursor updates", 10);
729 deferUpdateRequests = readIntParameter("Defer update requests", 50);
732 socketFactory = readParameter("SocketFactory", false);
736 // Read password parameters. If an "ENCPASSWORD" parameter is set,
737 // then decrypt the password into the passwordParam string. Otherwise,
738 // try to read the "PASSWORD" parameter directly to passwordParam.
741 private void readPasswordParameters() {
742 String encPasswordParam = readParameter("ENCPASSWORD", false);
743 if (encPasswordParam == null) {
744 passwordParam = readParameter("PASSWORD", false);
746 // ENCPASSWORD is hexascii-encoded. Decode.
747 byte[] pw = {0, 0, 0, 0, 0, 0, 0, 0};
748 int len = encPasswordParam.length() / 2;
751 for (int i = 0; i < len; i++) {
752 String hex = encPasswordParam.substring(i*2, i*2+2);
753 Integer x = new Integer(Integer.parseInt(hex, 16));
754 pw[i] = x.byteValue();
756 // Decrypt the password.
757 byte[] key = {23, 82, 107, 6, 35, 78, 88, 7};
758 DesCipher des = new DesCipher(key);
759 des.decrypt(pw, 0, pw, 0);
760 passwordParam = new String(pw);
764 public String readParameter(String name, boolean required) {
766 String s = getParameter(name);
767 if ((s == null) && required) {
768 fatalError(name + " parameter not specified");
773 for (int i = 0; i < mainArgs.length; i += 2) {
774 if (mainArgs[i].equalsIgnoreCase(name)) {
776 return mainArgs[i+1];
777 } catch (Exception e) {
779 fatalError(name + " parameter not specified");
786 fatalError(name + " parameter not specified");
791 int readIntParameter(String name, int defaultValue) {
792 String str = readParameter(name, false);
793 int result = defaultValue;
796 result = Integer.parseInt(str);
797 } catch (NumberFormatException e) { }
803 // moveFocusToDesktop() - move keyboard focus either to VncCanvas.
806 void moveFocusToDesktop() {
807 if (vncContainer != null) {
808 if (vc != null && vncContainer.isAncestorOf(vc))
814 // disconnect() - close connection to server.
817 synchronized public void disconnect() {
818 System.out.println("Disconnect");
820 if (rfb != null && !rfb.closed())
828 showMessage("Disconnected");
835 // fatalError() - print out a fatal error message.
836 // FIXME: Do we really need two versions of the fatalError() method?
839 synchronized public void fatalError(String str) {
840 System.out.println(str);
843 // vncContainer null, applet not inited,
844 // can not present the error to the user.
845 Thread.currentThread().stop();
851 synchronized public void fatalError(String str, Exception e) {
853 if (rfb != null && rfb.closed()) {
854 // Not necessary to show error message if the error was caused
855 // by I/O problems after the rfb.close() method call.
856 System.out.println("RFB thread finished");
860 System.out.println(str);
874 // Show message text and optionally "Relogin" and "Close" buttons.
877 void showMessage(String msg) {
878 vncContainer.removeAll();
880 Label errLabel = new Label(msg, Label.CENTER);
881 errLabel.setFont(new Font("Helvetica", Font.PLAIN, 12));
885 Panel gridPanel = new Panel(new GridLayout(0, 1));
886 Panel outerPanel = new Panel(new FlowLayout(FlowLayout.LEFT));
887 outerPanel.add(gridPanel);
888 vncContainer.setLayout(new FlowLayout(FlowLayout.LEFT, 30, 16));
889 vncContainer.add(outerPanel);
890 Panel textPanel = new Panel(new FlowLayout(FlowLayout.CENTER));
891 textPanel.add(errLabel);
892 gridPanel.add(textPanel);
893 gridPanel.add(new ReloginPanel(this));
897 vncContainer.setLayout(new FlowLayout(FlowLayout.LEFT, 30, 30));
898 vncContainer.add(errLabel);
902 if (inSeparateFrame) {
911 // Main applet thread will terminate on first exception
912 // after seeing that rfbThread has been set to null.
916 System.out.println("Stopping applet");
921 // This method is called before the applet is destroyed.
924 public void destroy() {
925 System.out.println("Destroying applet");
927 vncContainer.removeAll();
932 if (rfb != null && !rfb.closed())
939 // Start/stop receiving mouse events.
942 public void enableInput(boolean enable) {
943 vc.enableInput(enable);
947 // Close application properly on window close event.
950 public void windowClosing(WindowEvent evt) {
951 System.out.println("Closing window");
963 // Ignore window events we're not interested in.
966 public void windowActivated(WindowEvent evt) {}
967 public void windowDeactivated (WindowEvent evt) {}
968 public void windowOpened(WindowEvent evt) {}
969 public void windowClosed(WindowEvent evt) {}
970 public void windowIconified(WindowEvent evt) {}
971 public void windowDeiconified(WindowEvent evt) {}