8a17f1dab1f642b31761a7ccefbb255e256da21b
[invirt/scripts/vnc-client.git] / invirt-vnc-client
1 #!/usr/bin/python
2 from twisted.internet import reactor, ssl, protocol, error
3 from OpenSSL import SSL
4 import base64, pickle
5 import getopt, sys, os, time
6
7 verbose = False
8
9 def usage():
10     print """%s [-v] [-l [HOST:]PORT] {-a AUTHTOKEN|VMNAME}
11  -l, --listen [HOST:]PORT  port (and optionally host) to listen on for
12                            connections (default is 127.0.0.1 and a randomly
13                            chosen port)
14  -a, --authtoken AUTHTOKEN Authentication token for connecting to the VNC server
15  VMNAME                    VM name to connect to (automatically fetches an
16                            authentication token using remctl)
17  -v                        verbose status messages""" % (sys.argv[0])
18
19 class ClientContextFactory(ssl.ClientContextFactory):
20
21     def _verify(self, connection, x509, errnum, errdepth, ok):
22         if verbose:
23             print '_verify (ok=%d):' % ok
24             print '  subject:', x509.get_subject()
25             print '  issuer:', x509.get_issuer()
26             print '  errnum %s, errdepth %d' % (errnum, errdepth)
27         if errnum == 10:
28             print 'The VNC server certificate has expired. Please contact xvm@mit.edu.'
29         return ok
30
31     def getContext(self):
32         ctx = ssl.ClientContextFactory.getContext(self)
33
34         certFile = '/mit/xvm/vnc/servers.cert'
35         if verbose: print "Loading certificates from %s" % certFile
36         ctx.load_verify_locations(certFile)
37         ctx.set_verify(SSL.VERIFY_PEER|SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
38                        self._verify)
39
40         return ctx
41
42 class Proxy(protocol.Protocol):
43     peer = None
44
45     def setPeer(self, peer):
46         self.peer = peer
47
48     def connectionLost(self, reason):
49         if self.peer is not None:
50             self.peer.transport.loseConnection()
51             self.peer = None
52
53     def dataReceived(self, data):
54         self.peer.transport.write(data)
55
56 class ProxyClient(Proxy):
57     ready = False
58
59     def connectionMade(self):
60         self.peer.setPeer(self)
61         data = "CONNECTVNC %s VNCProxy/1.0\r\nAuth-token: %s\r\n\r\n" % (self.factory.machine, self.factory.authtoken)
62         self.transport.write(data)
63         if verbose: print "ProxyClient: connection made"
64     def dataReceived(self, data):
65         if not self.ready:
66             if verbose: print 'ProxyClient: received data "%s"' % data
67             if data.startswith("VNCProxy/1.0 200 "):
68                 self.ready = True
69                 if "\n" in data:
70                     self.peer.transport.write(data[data.find("\n")+3:])
71                 self.peer.transport.resumeProducing() # Allow reading
72             else:
73                 print "Failed to connect: %s" % data
74                 self.transport.loseConnection()
75         else:
76             self.peer.transport.write(data)
77
78 class ProxyClientFactory(protocol.ClientFactory):
79     protocol = ProxyClient
80     
81     def __init__(self, authtoken, machine):
82         self.authtoken = authtoken
83         self.machine = machine
84
85     def setServer(self, server):
86         self.server = server
87
88     def buildProtocol(self, *args, **kw):
89         prot = protocol.ClientFactory.buildProtocol(self, *args, **kw)
90         prot.setPeer(self.server)
91         return prot
92
93     def clientConnectionFailed(self, connector, reason):
94         self.server.transport.loseConnection()
95
96
97 class ProxyServer(Proxy):
98     clientProtocolFactory = ProxyClientFactory
99     authtoken = None
100     machine = None
101
102     def connectionMade(self):
103         # Don't read anything from the connecting client until we have
104         # somewhere to send it to.
105         self.transport.pauseProducing()
106         
107         if verbose: print "ProxyServer: connection made"
108
109         client = self.clientProtocolFactory(self.factory.authtoken, self.factory.machine)
110         client.setServer(self)
111
112         reactor.connectSSL(self.factory.host, self.factory.port, client, ClientContextFactory())
113         
114
115 class ProxyFactory(protocol.Factory):
116     protocol = ProxyServer
117
118     def __init__(self, host, port, authtoken, machine):
119         self.host = host
120         self.port = port
121         self.authtoken = authtoken
122         self.machine = machine
123
124 def main():
125     global verbose
126     try:
127         opts, args = getopt.gnu_getopt(sys.argv[1:], "hl:a:v",
128                                        ["help", "listen=", "authtoken="])
129     except getopt.GetoptError, err:
130         print str(err) # will print something like "option -a not recognized"
131         usage()
132         sys.exit(2)
133     listen = ["127.0.0.1", None]
134     authtoken = None
135     for o, a in opts:
136         if o == "-v":
137             verbose = True
138         elif o in ("-h", "--help"):
139             usage()
140             sys.exit()
141         elif o in ("-l", "--listen"):
142             if ":" in a:
143                 listen = a.split(":", 2)
144             else:
145                 listen[1] = a
146         elif o in ("-a", "--authtoken"):
147             authtoken = a
148         else:
149             assert False, "unhandled option"
150
151     # Get authentication token
152     if authtoken is None:
153         # User didn't give us an authentication token, so we need to get one
154         if len(args) != 1:
155             print "VMNAME not given or too many arguments"
156             usage()
157             sys.exit(2)
158         from subprocess import PIPE, Popen
159         try:
160             p = Popen(["remctl", "remote", "control", args[0], "vnctoken"],
161                       stdout=PIPE)
162         except OSError:
163             if verbose: print "remctl not found in path. Trying remctl locker."
164             p = Popen(["athrun", "remctl", "remctl",
165                        "remote", "control", args[0], "vnctoken"],
166                       stdout=PIPE)
167         authtoken = p.communicate()[0]
168         if p.returncode != 0:
169             print "Unable to get authentication token"
170             sys.exit(1)
171         if verbose: print 'Got authentication token "%s" for VM %s' % \
172                           (authtoken, args[0])
173
174     # Unpack authentication token
175     try:
176         token_outer = base64.urlsafe_b64decode(authtoken)
177         token_outer = pickle.loads(token_outer)
178         token_inner = pickle.loads(token_outer["data"])
179         machine = token_inner["machine"]
180         connect_host = token_inner["connect_host"]
181         connect_port = token_inner["connect_port"]
182         token_expires = token_inner["expires"]
183         if verbose: print "Unpacked authentication token:\n%s" % \
184                           repr(token_inner)
185     except:
186         print "Invalid authentication token"
187         sys.exit(1)
188     
189     if verbose: print "Will connect to %s:%s" % (connect_host, connect_port) 
190     if listen[1] is None:
191         listen[1] = 5900
192         ready = False
193         while not ready:
194             try:
195                 reactor.listenTCP(listen[1], ProxyFactory(connect_host, connect_port, authtoken, machine))
196                 ready = True
197             except error.CannotListenError:
198                 listen[1] += 1
199     else:
200         reactor.listenTCP(listen[1], ProxyFactory(connect_host, connect_port, authtoken, machine))
201     
202     print "Ready to connect. Connect to %s:%s (display %d) now with your VNC client. The password is 'moocow'." % (listen[0], listen[1], listen[1]-5900)
203     print "You must connect before your authentication token expires at %s." % \
204           (time.ctime(token_expires))
205     
206     reactor.run()
207
208 if '__main__' == __name__:
209     main()