source: trunk/packages/invirt-vnc-client/debian/patches/invirt-ssl-proxy.patch @ 2186

Last change on this file since 2186 was 1472, checked in by quentin, 16 years ago

Document source for included code in VNC server

File size: 24.6 KB
  • new file invirt-vnc-client/InvirtTrustManager.java

    - +  
     1// This code is based on http://svntrac.hanhuy.com/repo/browser/hanhuy/trunk/cms/src/com/hanhuy/ria/client/RIATrustManager.java
     2/*
     3 * Copyright 2006 Perry Nguyen <pfnguyen@hanhuy.com>
     4 * Licensed under the Apache License, Version 2.0 (the "License");
     5 * you may not use this file except in compliance with the License.
     6 * You may obtain a copy of the License at
     7 *
     8 *     http://www.apache.org/licenses/LICENSE-2.0
     9 *
     10 * Unless required by applicable law or agreed to in writing, software
     11 * distributed under the License is distributed on an "AS IS" BASIS,
     12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13 * See the License for the specific language governing permissions and
     14 * limitations under the License.
     15 */
     16import java.io.IOException;
     17import java.io.InputStream;
     18import java.security.KeyStore;
     19import java.security.KeyStoreException;
     20import java.security.NoSuchAlgorithmException;
     21import java.security.cert.CertificateException;
     22import java.security.cert.X509Certificate;
     23import java.util.Enumeration;
     24import java.util.logging.Level;
     25import java.util.logging.Logger;
     26
     27import javax.net.ssl.TrustManager;
     28import javax.net.ssl.TrustManagerFactory;
     29import javax.net.ssl.X509TrustManager;
     30
     31public class InvirtTrustManager implements X509TrustManager {
     32    private X509TrustManager trustManager;
     33    private final static char[] KEY_STORE_PASSWORD =
     34        { 'f', 'o', 'o', 'b', 'a', 'r' };
     35    private final static String KEY_STORE_RESOURCE =
     36        "trust.store";
     37
     38    private KeyStore loadKeyStore() throws Exception {
     39        InputStream in = getClass().getClassLoader().getResourceAsStream(
     40                KEY_STORE_RESOURCE);
     41        KeyStore ks = null;
     42        try {
     43            if (in == null) {
     44                //log.severe("Unable to open KeyStore");
     45                throw new NullPointerException();
     46            }
     47            ks = KeyStore.getInstance(KeyStore.getDefaultType());
     48            ks.load(in, KEY_STORE_PASSWORD);
     49            /*if (log.isLoggable(Level.FINEST)) {
     50                for (Enumeration<String> aliases = ks.aliases();
     51                aliases.hasMoreElements();) {
     52                    String alias = aliases.nextElement();
     53                    log.finest("ALIAS: " + alias);
     54                }
     55                }*/
     56        } catch (NoSuchAlgorithmException e) {
     57            throwError(e);
     58        } catch (CertificateException e) {
     59            throwError(e);
     60        } catch (IOException e) {
     61            throwError(e);
     62        } catch (KeyStoreException e) {
     63            throwError(e);
     64        } finally {
     65            try {
     66                if (in != null)
     67                    in.close();
     68            }
     69            catch (IOException e) { } // ignore
     70        }
     71        return ks;
     72    }
     73    private void createTrustManager() {
     74        try {
     75            try {
     76                KeyStore keystore = loadKeyStore();
     77                TrustManagerFactory factory = TrustManagerFactory.getInstance(
     78                                                                              TrustManagerFactory.getDefaultAlgorithm());
     79                factory.init(keystore);
     80                TrustManager[] trustManagers = factory.getTrustManagers();
     81                if (trustManagers.length == 0)
     82                    throw new IllegalStateException("No trust manager found");
     83                setTrustManager((X509TrustManager) trustManagers[0]);
     84            } catch (NoSuchAlgorithmException e) {
     85                throwError(e);
     86            } catch (KeyStoreException e) {
     87                throwError(e);
     88            }
     89        } catch (Exception e) {
     90            e.printStackTrace();
     91        }
     92    }
     93    private void throwError(Exception e) throws Exception {
     94        //HttpClientError error = new HttpClientError(e.getMessage());
     95        //error.initCause(e);
     96        throw e;
     97    }
     98    public X509TrustManager getTrustManager() {
     99        if (trustManager == null)
     100            createTrustManager();
     101        return trustManager;
     102    }
     103
     104    public void setTrustManager(X509TrustManager trustManager) {
     105        this.trustManager = trustManager;
     106    }
     107
     108    public void checkClientTrusted(X509Certificate[] chain, String authType)
     109            throws CertificateException {
     110        getTrustManager().checkClientTrusted(chain, authType);
     111    }
     112
     113    public void checkServerTrusted(X509Certificate[] chain, String authType)
     114            throws CertificateException {
     115        getTrustManager().checkServerTrusted(chain, authType);
     116
     117    }
     118
     119    public X509Certificate[] getAcceptedIssuers() {
     120        return getTrustManager().getAcceptedIssuers();
     121    }
     122
     123}
     124 No newline at end of file
  • invirt-vnc-client

    old new  
    1717          DesCipher.class CapabilityInfo.class CapsContainer.class \
    1818          RecordingFrame.class SessionRecorder.class \
    1919          SocketFactory.class HTTPConnectSocketFactory.class \
     20          VNCProxyConnectSocketFactory.class VNCProxyConnectSocket.class \
    2021          HTTPConnectSocket.class ReloginPanel.class \
    21           InStream.class MemInStream.class ZlibInStream.class
     22          InStream.class MemInStream.class ZlibInStream.class \
     23          VNCProxyConnectSocketWrapper.class SocketWrapper.class SocketWrapper\$$WrappingSocketImpl.class InvirtTrustManager.class
    2224
    2325SOURCES = VncViewer.java RfbProto.java AuthPanel.java VncCanvas.java \
    2426          VncCanvas2.java \
     
    2628          DesCipher.java CapabilityInfo.java CapsContainer.java \
    2729          RecordingFrame.java SessionRecorder.java \
    2830          SocketFactory.java HTTPConnectSocketFactory.java \
     31          VNCProxyConnectSocketFactory.java VNCProxyConnectSocket.java \
    2932          HTTPConnectSocket.java ReloginPanel.java \
    30           InStream.java MemInStream.java ZlibInStream.java
     33          InStream.java MemInStream.java ZlibInStream.java \
     34          VNCProxyConnectSocketWrapper.java SocketWrapper.java InvirtTrustManager.java
    3135
    3236all: $(CLASSES) $(ARCHIVE)
    3337
  • invirt-vnc-client

    old new  
    208208    port = p;
    209209
    210210    if (viewer.socketFactory == null) {
     211        System.out.println("Null socketFactory");
    211212      sock = new Socket(host, port);
    212213    } else {
    213214      try {
    214215        Class factoryClass = Class.forName(viewer.socketFactory);
    215216        SocketFactory factory = (SocketFactory)factoryClass.newInstance();
     217        System.out.println("Using socketFactory " + factory);
    216218        if (viewer.inAnApplet)
    217219          sock = factory.createSocket(host, port, viewer);
    218220        else
     
    236238    try {
    237239      sock.close();
    238240      closed = true;
    239       System.out.println("RFB socket closed");
     241      System.out.println("RFB socket closed " + sock);
    240242      if (rec != null) {
    241243        rec.close();
    242244        rec = null;
  • new file invirt-vnc-client/SocketWrapper.java

    - +  
     1/*
     2 * Written by Dawid Kurzyniec and released to the public domain, as explained
     3 * at http://creativecommons.org/licenses/publicdomain
     4 */
     5// Upstream is at http://www.dcl.mathcs.emory.edu/downloads/h2o/doc/api/edu/emory/mathcs/util/net/SocketWrapper.html
     6
     7//package edu.emory.mathcs.util.net;
     8
     9import java.io.*;
     10import java.net.*;
     11import java.nio.channels.*;
     12
     13/**
     14 * Wrapper for sockets which enables to add functionality in subclasses
     15 * on top of existing, connected sockets. It is useful when direct subclassing
     16 * of delegate socket class is not possible, e.g. if the delegate socket is
     17 * created by a library. Possible usage example is socket factory chaining.
     18 * This class delegates all socket-related requests to the wrapped delegate,
     19 * as of JDK 1.4.
     20 *
     21 * @author Dawid Kurzyniec
     22 * @version 1.4
     23 */
     24public abstract class SocketWrapper extends Socket {
     25
     26    /**
     27     * the wrapped delegate socket.
     28     */
     29    protected final Socket delegate;
     30
     31    /**
     32     * Creates new socket wrapper for a given socket. The delegate
     33     * must be connected and bound and it must not be closed.
     34     * @param delegate the delegate socket to wrap
     35     * @throws SocketException if the delegate socket is closed, not bound,
     36     *                         or not connected
     37     */
     38    protected SocketWrapper(Socket delegate) throws SocketException {
     39        super(new WrappingSocketImpl(delegate));
     40        this.delegate = delegate;
     41        System.out.println("Creating SocketWrapper $Rev$");
     42    }
     43
     44    public SocketChannel getChannel() {
     45        return delegate.getChannel();
     46    }
     47
     48    /**
     49     * Returns true, indicating that the socket is bound.
     50     *
     51     * @return true
     52     */
     53    public boolean isBound() {
     54        return true;
     55    }
     56
     57    public boolean isClosed() {
     58        return super.isClosed() || delegate.isClosed();
     59    }
     60
     61    /**
     62     * Returns true, indicating that the socket is connected.
     63     *
     64     * @return true
     65     */
     66    public boolean isConnected() {
     67        return true;
     68    }
     69
     70    public boolean isInputShutdown() {
     71        return super.isInputShutdown() || delegate.isInputShutdown();
     72    }
     73
     74    public boolean isOutputShutdown() {
     75        return super.isInputShutdown() || delegate.isOutputShutdown();
     76    }
     77
     78    private static class WrappingSocketImpl extends SocketImpl {
     79        private final Socket delegate;
     80        WrappingSocketImpl(Socket delegate) throws SocketException {
     81            if (delegate == null) {
     82                throw new NullPointerException();
     83            }
     84            if (delegate.isClosed()) {
     85                throw new SocketException("Delegate server socket is closed");
     86            }
     87            if (!(delegate.isBound())) {
     88                throw new SocketException("Delegate server socket is not bound");
     89            }
     90            if (!(delegate.isConnected())) {
     91                throw new SocketException("Delegate server socket is not connected");
     92            }
     93            this.delegate = delegate;
     94        }
     95
     96        protected void create(boolean stream) {}
     97
     98        protected void connect(String host, int port) {
     99            // delegate is always connected, thus this method is never called
     100            throw new UnsupportedOperationException();
     101        }
     102
     103        protected void connect(InetAddress address, int port) {
     104            // delegate is always connected, thus this method is never called
     105            throw new UnsupportedOperationException();
     106        }
     107
     108        protected void connect(SocketAddress address, int timeout) {
     109            // delegate is always connected, thus this method is never called
     110            throw new UnsupportedOperationException();
     111        }
     112
     113        protected void bind(InetAddress host, int port) {
     114            // delegate is always bound, thus this method is never called
     115            throw new UnsupportedOperationException();
     116        }
     117
     118        protected void listen(int backlog) {
     119            // this wrapper is never used by a ServerSocket
     120            throw new UnsupportedOperationException();
     121        }
     122
     123        protected void accept(SocketImpl s) {
     124            // this wrapper is never used by a ServerSocket
     125            throw new UnsupportedOperationException();
     126        }
     127
     128        protected InputStream getInputStream() throws IOException {
     129            return delegate.getInputStream();
     130        }
     131
     132        protected OutputStream getOutputStream() throws IOException {
     133            return delegate.getOutputStream();
     134        }
     135
     136        protected int available() throws IOException {
     137            return getInputStream().available();
     138        }
     139
     140        protected void close() throws IOException {
     141            System.out.println("Calling delegate.close");
     142            delegate.close();
     143        }
     144
     145        protected void shutdownInput() throws IOException {
     146            delegate.shutdownInput();
     147        }
     148
     149        protected void shutdownOutput() throws IOException {
     150            delegate.shutdownOutput();
     151        }
     152
     153        protected FileDescriptor getFileDescriptor() {
     154            // this wrapper is never used by a ServerSocket
     155            throw new UnsupportedOperationException();
     156        }
     157
     158        protected InetAddress getInetAddress() {
     159            return delegate.getInetAddress();
     160        }
     161
     162        protected int getPort() {
     163            return delegate.getPort();
     164        }
     165
     166        protected boolean supportsUrgentData() {
     167            return false; // must be overridden in sub-class
     168        }
     169
     170        protected void sendUrgentData (int data) throws IOException {
     171            delegate.sendUrgentData(data);
     172        }
     173
     174        protected int getLocalPort() {
     175            return delegate.getLocalPort();
     176        }
     177
     178        public Object getOption(int optID) throws SocketException {
     179            switch (optID) {
     180                case SocketOptions.IP_TOS:
     181                    return new Integer(delegate.getTrafficClass());
     182                case SocketOptions.SO_BINDADDR:
     183                    return delegate.getLocalAddress();
     184                case SocketOptions.SO_KEEPALIVE:
     185                    return Boolean.valueOf(delegate.getKeepAlive());
     186                case SocketOptions.SO_LINGER:
     187                    return new Integer(delegate.getSoLinger());
     188                case SocketOptions.SO_OOBINLINE:
     189                    return Boolean.valueOf(delegate.getOOBInline());
     190                case SocketOptions.SO_RCVBUF:
     191                    return new Integer(delegate.getReceiveBufferSize());
     192                case SocketOptions.SO_REUSEADDR:
     193                    return Boolean.valueOf(delegate.getReuseAddress());
     194                case SocketOptions.SO_SNDBUF:
     195                    return new Integer(delegate.getSendBufferSize());
     196                case SocketOptions.SO_TIMEOUT:
     197                    return new Integer(delegate.getSoTimeout());
     198                case SocketOptions.TCP_NODELAY:
     199                    return Boolean.valueOf(delegate.getTcpNoDelay());
     200                case SocketOptions.SO_BROADCAST:
     201                default:
     202                    throw new IllegalArgumentException("Unsupported option type");
     203            }
     204        }
     205
     206        public void setOption(int optID, Object value) throws SocketException {
     207            switch (optID) {
     208                case SocketOptions.SO_BINDADDR:
     209                    throw new IllegalArgumentException("Socket is bound");
     210                case SocketOptions.SO_KEEPALIVE:
     211                    delegate.setKeepAlive(((Boolean)value).booleanValue());
     212                    break;
     213                case SocketOptions.SO_LINGER:
     214                    if (value instanceof Boolean) {
     215                        delegate.setSoLinger(((Boolean)value).booleanValue(), 0);
     216                    }
     217                    else {
     218                        delegate.setSoLinger(true, ((Integer)value).intValue());
     219                    }
     220                    break;
     221                case SocketOptions.SO_OOBINLINE:
     222                    delegate.setOOBInline(((Boolean)value).booleanValue());
     223                    break;
     224                case SocketOptions.SO_RCVBUF:
     225                    delegate.setReceiveBufferSize(((Integer)value).intValue());
     226                    break;
     227                case SocketOptions.SO_REUSEADDR:
     228                    delegate.setReuseAddress(((Boolean)value).booleanValue());
     229                    break;
     230                case SocketOptions.SO_SNDBUF:
     231                    delegate.setSendBufferSize(((Integer)value).intValue());
     232                    break;
     233                case SocketOptions.SO_TIMEOUT:
     234                    delegate.setSoTimeout(((Integer)value).intValue());
     235                    break;
     236                case SocketOptions.TCP_NODELAY:
     237                    delegate.setTcpNoDelay(((Boolean)value).booleanValue());
     238                    break;
     239                case SocketOptions.SO_BROADCAST:
     240                default:
     241                    throw new IllegalArgumentException("Unsupported option type");
     242            }
     243        }
     244    }
     245
     246    public void close() throws IOException {
     247        System.out.println("Calling SocketWrapper.delegate.close");
     248        delegate.close();
     249    }
     250
     251    public boolean equals(Object obj) {
     252        if (!(obj instanceof SocketWrapper)) return false;
     253        SocketWrapper that = (SocketWrapper)obj;
     254        return this.delegate.equals(that.delegate);
     255    }
     256
     257    public int hashCode() {
     258        return delegate.hashCode() ^ 0x01010101;
     259    }
     260    public String toString() {
     261        return "<SocketWrapper " + super.toString() + "(delegating to " + delegate.toString() +  ")" + ">";
     262    }
     263}
     264 No newline at end of file
  • new file invirt-vnc-client/VNCProxyConnectSocket.java

    - +  
     1//
     2//  Copyright (C) 2002 Constantin Kaplinsky, Inc.  All Rights Reserved.
     3//  Copyright 2007 MIT Student Information Processing Board
     4//
     5//  This is free software; you can redistribute it and/or modify
     6//  it under the terms of the GNU General Public License as published by
     7//  the Free Software Foundation; either version 2 of the License, or
     8//  (at your option) any later version.
     9//
     10//  This software is distributed in the hope that it will be useful,
     11//  but WITHOUT ANY WARRANTY; without even the implied warranty of
     12//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13//  GNU General Public License for more details.
     14//
     15//  You should have received a copy of the GNU General Public License
     16//  along with this software; if not, write to the Free Software
     17//  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
     18//  USA.
     19//
     20
     21//
     22// VNCProxySocket.java together with VNCProxySocketFactory.java
     23// implement an alternate way to connect to VNC servers via one or two
     24// VNCProxy proxies supporting the VNCProxy VNCCONNECT method.
     25//
     26
     27import java.net.*;
     28import java.io.*;
     29
     30class VNCProxyConnectSocket extends Socket {
     31
     32  public VNCProxyConnectSocket(String host, int port,
     33                               String vmname, String authtoken)
     34    throws IOException {
     35
     36    // Connect to the specified HTTP proxy
     37    super(host, port);
     38
     39    // Send the CONNECT request
     40    getOutputStream().write(("CONNECTVNC " + vmname +
     41                             " VNCProxy/1.0\r\nAuth-token: " + authtoken +
     42                             "\r\n\r\n").getBytes());
     43
     44    // Read the first line of the response
     45    DataInputStream is = new DataInputStream(getInputStream());
     46    String str = is.readLine();
     47
     48    // Check the HTTP error code -- it should be "200" on success
     49    if (!str.startsWith("VNCProxy/1.0 200 ")) {
     50      if (str.startsWith("VNCProxy/1.0 "))
     51        str = str.substring(13);
     52      throw new IOException("Proxy reports \"" + str + "\"");
     53    }
     54
     55    // Success -- skip remaining HTTP headers
     56    do {
     57      str = is.readLine();
     58    } while (str.length() != 0);
     59  }
     60}
     61
  • new file invirt-vnc-client/VNCProxyConnectSocketFactory.java

    - +  
     1//
     2//  Copyright (C) 2002 Constantin Kaplinsky, Inc.  All Rights Reserved.
     3//  Copyright 2007 MIT Student Information Processing Board
     4//
     5//  This is free software; you can redistribute it and/or modify
     6//  it under the terms of the GNU General Public License as published by
     7//  the Free Software Foundation; either version 2 of the License, or
     8//  (at your option) any later version.
     9//
     10//  This software is distributed in the hope that it will be useful,
     11//  but WITHOUT ANY WARRANTY; without even the implied warranty of
     12//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13//  GNU General Public License for more details.
     14//
     15//  You should have received a copy of the GNU General Public License
     16//  along with this software; if not, write to the Free Software
     17//  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
     18//  USA.
     19//
     20
     21//
     22// VNCProxyConnectSocketFactory.java together with VNCProxyConnectSocket.java
     23// implement an alternate way to connect to VNC servers via one or two
     24// VNCProxy proxies supporting the VNCProxy CONNECT method.
     25//
     26
     27import java.applet.*;
     28import java.net.*;
     29import javax.net.ssl.*;
     30import java.io.*;
     31
     32class VNCProxyConnectSocketFactory implements SocketFactory {
     33
     34    SSLSocketFactory factory;
     35   
     36    public VNCProxyConnectSocketFactory() {
     37        try {
     38            SSLContext c = SSLContext.getInstance("SSL");
     39            c.init(null,
     40                   new TrustManager[] { new InvirtTrustManager() },
     41                   null);
     42            factory =
     43                (SSLSocketFactory)c.getSocketFactory();
     44        } catch (Exception e) {
     45            e.printStackTrace();
     46        }
     47    }
     48
     49  public Socket createSocket(String host, int port, Applet applet)
     50    throws IOException {
     51
     52    return createSocket(host, port,
     53                        applet.getParameter("VMNAME"),
     54                        applet.getParameter("AUTHTOKEN"));
     55  }
     56
     57  public Socket createSocket(String host, int port, String[] args)
     58    throws IOException {
     59
     60    return createSocket(host, port,
     61                        readArg(args, "VMNAME"),
     62                        readArg(args, "AUTHTOKEN"));
     63  }
     64
     65  public Socket createSocket(String host, int port,
     66                             String vmname, String authtoken)
     67    throws IOException {
     68
     69    if (vmname == null || authtoken == null) {
     70      System.out.println("Incomplete parameter list for VNCProxyConnectSocket");
     71      return new Socket(host, port);
     72    }
     73
     74    System.out.println("VNCProxy CONNECT via proxy " + host +
     75                       " port " + port + " to vm " + vmname);
     76    SSLSocket ssls = (SSLSocket)factory.createSocket(host, port);
     77    ssls.startHandshake();
     78    VNCProxyConnectSocketWrapper s =
     79      new VNCProxyConnectSocketWrapper(ssls, vmname, authtoken);
     80
     81    return (Socket)s;
     82  }
     83
     84  private String readArg(String[] args, String name) {
     85
     86    for (int i = 0; i < args.length; i += 2) {
     87      if (args[i].equalsIgnoreCase(name)) {
     88        try {
     89          return args[i+1];
     90        } catch (Exception e) {
     91          return null;
     92        }
     93      }
     94    }
     95    return null;
     96  }
     97}
     98
  • new file invirt-vnc-client/VNCProxyConnectSocketWrapper.java

    - +  
     1//
     2//  Copyright (C) 2002 Constantin Kaplinsky, Inc.  All Rights Reserved.
     3//  Copyright 2007 MIT Student Information Processing Board
     4//
     5//  This is free software; you can redistribute it and/or modify
     6//  it under the terms of the GNU General Public License as published by
     7//  the Free Software Foundation; either version 2 of the License, or
     8//  (at your option) any later version.
     9//
     10//  This software is distributed in the hope that it will be useful,
     11//  but WITHOUT ANY WARRANTY; without even the implied warranty of
     12//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13//  GNU General Public License for more details.
     14//
     15//  You should have received a copy of the GNU General Public License
     16//  along with this software; if not, write to the Free Software
     17//  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
     18//  USA.
     19//
     20
     21//
     22// VNCProxySocket.java together with VNCProxySocketFactory.java
     23// implement an alternate way to connect to VNC servers via one or two
     24// VNCProxy proxies supporting the VNCProxy VNCCONNECT method.
     25//
     26
     27import java.net.*;
     28import java.io.*;
     29
     30class VNCProxyConnectSocketWrapper extends SocketWrapper {
     31
     32  public VNCProxyConnectSocketWrapper(Socket sock,
     33                               String vmname, String authtoken)
     34    throws IOException {
     35
     36    super(sock);
     37
     38    // Send the CONNECT request
     39    getOutputStream().write(("CONNECTVNC " + vmname +
     40                             " VNCProxy/1.0\r\nAuth-token: " + authtoken +
     41                             "\r\n\r\n").getBytes());
     42
     43    // Read the first line of the response
     44    DataInputStream is = new DataInputStream(getInputStream());
     45    String str = is.readLine();
     46
     47    // Check the HTTP error code -- it should be "200" on success
     48    if (!str.startsWith("VNCProxy/1.0 200 ")) {
     49      if (str.startsWith("VNCProxy/1.0 "))
     50        str = str.substring(13);
     51      throw new IOException("Proxy reports \"" + str + "\"");
     52    }
     53
     54    // Success -- skip remaining HTTP headers
     55    do {
     56      str = is.readLine();
     57    } while (str.length() != 0);
     58  }
     59}
     60
Note: See TracBrowser for help on using the repository browser.