Fix tinc-gui on Windows.
[tinc] / gui / tinc-gui
1 #!/usr/bin/python
2
3 # tinc-gui -- GUI for controlling a running tincd
4 # Copyright (C) 2009-2012 Guus Sliepen <guus@tinc-vpn.org>
5 #
6 # This program 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.
10 #
11 # This program 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.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 import string
21 import socket
22 import wx
23 import sys
24 import os
25 import platform
26 import time
27 from wx.lib.mixins.listctrl import ColumnSorterMixin
28 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
29
30 if platform.system() == 'Windows':
31         import _winreg
32
33 # Classes to interface with a running tinc daemon
34
35 REQ_STOP = 0
36 REQ_RELOAD = 1
37 REQ_RESTART = 2
38 REQ_DUMP_NODES = 3
39 REQ_DUMP_EDGES = 4
40 REQ_DUMP_SUBNETS = 5
41 REQ_DUMP_CONNECTIONS = 6
42 REQ_DUMP_GRAPH = 7
43 REQ_PURGE = 8
44 REQ_SET_DEBUG = 9
45 REQ_RETRY = 10
46 REQ_CONNECT = 11
47 REQ_DISCONNECT = 12
48
49 ID = 0
50 ACK = 4
51 CONTROL = 18
52
53 class Node:
54         def parse(self, args):
55                 self.name = args[0]
56                 self.address = args[1]
57                 self.port = args[3]
58                 self.cipher = int(args[4])
59                 self.digest = int(args[5])
60                 self.maclength = int(args[6])
61                 self.compression = int(args[7])
62                 self.options = int(args[8], 0x10)
63                 self.status = int(args[9], 0x10)
64                 self.nexthop = args[10]
65                 self.via = args[11]
66                 self.distance = int(args[12])
67                 self.pmtu = int(args[13])
68                 self.minmtu = int(args[14])
69                 self.maxmtu = int(args[15])
70                 self.last_state_change = float(args[16])
71
72                 self.subnets = {}
73
74 class Edge:
75         def parse(self, args):
76                 self.fr = args[0]
77                 self.to = args[1]
78                 self.address = args[2]
79                 self.port = args[4]
80                 self.options = int(args[5], 16)
81                 self.weight = int(args[6])
82
83 class Subnet:
84         def parse(self, args):
85                 if args[0].find('#') >= 0:
86                         (address, self.weight) = args[0].split('#', 1)
87                 else:
88                         self.weight = 10
89                         address = args[0]
90
91                 if address.find('/') >= 0:
92                         (self.address, self.prefixlen) = address.split('/', 1)
93                 else:
94                         self.address = address
95                         self.prefixlen = '48'
96
97                 self.owner = args[1]    
98
99 class Connection:
100         def parse(self, args):
101                 self.name = args[0]
102                 self.address = args[1]
103                 self.port = args[3]
104                 self.options = int(args[4], 0x10)
105                 self.socket = int(args[5])
106                 self.status = int(args[6], 0x10)
107                 self.weight = 123
108
109 class VPN:
110         confdir = '/etc/tinc'
111         piddir = '/var/run/'
112
113         def connect(self):
114                 # read the pidfile
115                 f = open(self.pidfile)
116                 info = string.split(f.readline())
117                 f.close()
118
119                 # check if there is a UNIX socket as well
120                 if self.pidfile.endswith(".pid"):
121                         unixfile = self.pidfile.replace(".pid", ".socket");
122                 else:
123                         unixfile = self.pidfile + ".socket";
124
125                 if os.path.exists(unixfile):
126                         # use it if it exists
127                         print(unixfile + " exists!");
128                         s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
129                         s.connect(unixfile)
130                 else:
131                         # otherwise connect via TCP
132                         print(unixfile + " does not exist.");
133                         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
134                         s.connect((info[2], int(info[4])))
135
136                 self.sf = s.makefile()
137                 s.close()
138                 hello = string.split(self.sf.readline())
139                 self.name = hello[1]
140                 self.sf.write('0 ^' + info[1] + ' 17\r\n')
141                 self.sf.flush()
142                 resp = string.split(self.sf.readline())
143                 self.port = info[4]
144                 self.nodes = {}
145                 self.edges = {}
146                 self.subnets = {}
147                 self.connections = {}
148                 self.refresh()
149
150         def refresh(self):
151                 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
152                 self.sf.flush()
153
154                 for node in self.nodes.values():
155                         node.visited = False
156                 for edge in self.edges.values():
157                         edge.visited = False
158                 for subnet in self.subnets.values():
159                         subnet.visited = False
160                 for connections in self.connections.values():
161                         connections.visited = False
162
163                 while True:
164                         resp = string.split(self.sf.readline())
165                         if len(resp) < 2:
166                                 break
167                         if resp[0] != '18':
168                                 break
169                         if resp[1] == '3':
170                                 if len(resp) < 19:
171                                         continue
172                                 node = self.nodes.get(resp[2]) or Node()
173                                 node.parse(resp[2:])
174                                 node.visited = True
175                                 self.nodes[resp[2]] = node
176                         elif resp[1] == '4':
177                                 if len(resp) < 9:
178                                         continue
179                                 edge = self.nodes.get((resp[2], resp[3])) or Edge()
180                                 edge.parse(resp[2:])
181                                 edge.visited = True
182                                 self.edges[(resp[2], resp[3])] = edge
183                         elif resp[1] == '5':
184                                 if len(resp) < 4:
185                                         continue
186                                 subnet = self.subnets.get((resp[2], resp[3])) or Subnet()
187                                 subnet.parse(resp[2:])
188                                 subnet.visited = True
189                                 self.subnets[(resp[2], resp[3])] = subnet
190                                 self.nodes[subnet.owner].subnets[resp[2]] = subnet
191                         elif resp[1] == '6':
192                                 if len(resp) < 9:
193                                         break
194                                 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection()
195                                 connection.parse(resp[2:])
196                                 connection.visited = True
197                                 self.connections[(resp[2], resp[3], resp[5])] = connection
198                         else:
199                                 break
200
201                 for key, subnet in self.subnets.items():
202                         if not subnet.visited:
203                                 del self.subnets[key]
204
205                 for key, edge in self.edges.items():
206                         if not edge.visited:
207                                 del self.edges[key]
208
209                 for key, node in self.nodes.items():
210                         if not node.visited:
211                                 del self.nodes[key]
212                         else:
213                                 for key, subnet in node.subnets.items():
214                                         if not subnet.visited:
215                                                 del node.subnets[key]
216
217                 for key, connection in self.connections.items():
218                         if not connection.visited:
219                                 del self.connections[key]
220
221         def close(self):
222                 self.sf.close()
223
224         def disconnect(self, name):
225                 self.sf.write('18 12 ' + name + '\r\n')
226                 self.sf.flush()
227                 resp = string.split(self.sf.readline())
228
229         def debug(self, level = -1):
230                 self.sf.write('18 9 ' + str(level) + '\r\n')
231                 self.sf.flush()
232                 resp = string.split(self.sf.readline())
233                 return int(resp[2])
234
235         def __init__(self, netname = None, pidfile = None):
236                 if platform.system() == 'Windows':
237                         try:
238                                 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
239                                 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc")
240                                 VPN.confdir = _winreg.QueryValue(key, None)
241                         except WindowsError:
242                                 pass
243
244                 if netname:
245                         self.netname = netname
246                         self.confbase = os.path.join(VPN.confdir, netname)
247                 else:
248                         self.confbase = VPN.confdir
249
250                 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
251
252                 if pidfile != None:
253                         self.pidfile = pidfile
254                 else:
255                         if platform.system() == 'Windows':
256                                 self.pidfile = os.path.join(self.confbase, 'pid')
257                         else:
258                                 if netname:
259                                         self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
260                                 else:
261                                         self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
262
263 # GUI starts here
264
265 argv0 = sys.argv[0]
266 del sys.argv[0]
267 netname = None
268 pidfile = None
269
270 def usage(exitcode = 0):
271         print('Usage: ' + argv0 + ' [options]')
272         print('\nValid options are:')
273         print('  -n, --net=NETNAME       Connect to net NETNAME.')
274         print('      --pidfile=FILENAME  Read control cookie from FILENAME.')
275         print('      --help              Display this help and exit.')
276         print('\nReport bugs to tinc@tinc-vpn.org.')
277         sys.exit(exitcode)
278
279 while sys.argv:
280         if sys.argv[0] in ('-n', '--net'):
281                 del sys.argv[0]
282                 netname = sys.argv[0]
283         elif sys.argv[0] in ('--pidfile'):
284                 del sys.argv[0]
285                 pidfile = sys.argv[0]
286         elif sys.argv[0] in ('--help'):
287                 usage(0)
288         else:
289                 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
290                 usage(1)
291
292         del sys.argv[0]
293
294 if netname == None:
295         netname = os.getenv("NETNAME")
296
297 if netname == ".":
298         netname = None
299
300 vpn = VPN(netname, pidfile)
301 vpn.connect()
302
303 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
304     def __init__(self, parent, style):
305         wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
306         ListCtrlAutoWidthMixin.__init__(self)
307         ColumnSorterMixin.__init__(self, 16)
308
309     def GetListCtrl(self):
310         return self
311
312
313 class SettingsPage(wx.Panel):
314         def OnDebugLevel(self, event):
315                 vpn.debug(self.debug.GetValue())
316
317         def __init__(self, parent, id):
318                 wx.Panel.__init__(self, parent, id)
319                 grid = wx.FlexGridSizer(cols = 2)
320                 grid.AddGrowableCol(1, 1)
321
322                 namelabel = wx.StaticText(self, -1, 'Name:')
323                 self.name = wx.TextCtrl(self, -1, vpn.name)
324                 grid.Add(namelabel)
325                 grid.Add(self.name, 1, wx.EXPAND)
326
327                 portlabel = wx.StaticText(self, -1, 'Port:')
328                 self.port = wx.TextCtrl(self, -1, vpn.port)
329                 grid.Add(portlabel)
330                 grid.Add(self.port)
331
332                 debuglabel = wx.StaticText(self, -1, 'Debug level:')
333                 self.debug = wx.SpinCtrl(self, min = 0, max = 5, initial = vpn.debug())
334                 self.debug.Bind(wx.EVT_SPINCTRL, self.OnDebugLevel)
335                 grid.Add(debuglabel)
336                 grid.Add(self.debug)
337
338                 modelabel = wx.StaticText(self, -1, 'Mode:')
339                 self.mode = wx.ComboBox(self, -1, style = wx.CB_READONLY, value = 'Router', choices = ['Router', 'Switch', 'Hub'])
340                 grid.Add(modelabel)
341                 grid.Add(self.mode)
342
343                 self.SetSizer(grid)
344
345 class ConnectionsPage(wx.Panel):
346         def __init__(self, parent, id):
347                 wx.Panel.__init__(self, parent, id)
348                 self.list = SuperListCtrl(self, id)
349                 self.list.InsertColumn(0, 'Name')
350                 self.list.InsertColumn(1, 'Address')
351                 self.list.InsertColumn(2, 'Port')
352                 self.list.InsertColumn(3, 'Options')
353                 self.list.InsertColumn(4, 'Weight')
354
355                 hbox = wx.BoxSizer(wx.HORIZONTAL)
356                 hbox.Add(self.list, 1, wx.EXPAND)
357                 self.SetSizer(hbox)
358                 self.refresh()
359
360         class ContextMenu(wx.Menu):
361                 def __init__(self, item):
362                         wx.Menu.__init__(self)
363
364                         self.item = item
365
366                         disconnect = wx.MenuItem(self, -1, 'Disconnect')
367                         self.AppendItem(disconnect)
368                         self.Bind(wx.EVT_MENU, self.OnDisconnect, id=disconnect.GetId())
369
370                 def OnDisconnect(self, event):
371                         vpn.disconnect(self.item[0])
372
373         def OnContext(self, event):
374                 i = event.GetIndex()
375                 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
376
377         def refresh(self):
378                 sortstate = self.list.GetSortState()
379                 self.list.itemDataMap = {}
380                 i = 0
381
382                 for key, connection in vpn.connections.items():
383                         if self.list.GetItemCount() <= i:
384                                 self.list.InsertStringItem(i, connection.name)
385                         else:
386                                 self.list.SetStringItem(i, 0, connection.name)
387                         self.list.SetStringItem(i, 1, connection.address)
388                         self.list.SetStringItem(i, 2, connection.port)
389                         self.list.SetStringItem(i, 3, str(connection.options))
390                         self.list.SetStringItem(i, 4, str(connection.weight))
391                         self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options, connection.weight)
392                         self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnContext)
393                         self.list.SetItemData(i, i)
394                         i += 1
395
396                 while self.list.GetItemCount() > i:
397                         self.list.DeleteItem(self.list.GetItemCount() - 1)
398
399                 self.list.SortListItems(sortstate[0], sortstate[1])
400
401 class NodesPage(wx.Panel):
402         def __init__(self, parent, id):
403                 wx.Panel.__init__(self, parent, id)
404                 self.list = SuperListCtrl(self, id)
405                 self.list.InsertColumn( 0, 'Name')
406                 self.list.InsertColumn( 1, 'Address')
407                 self.list.InsertColumn( 2, 'Port')
408                 self.list.InsertColumn( 3, 'Cipher')
409                 self.list.InsertColumn( 4, 'Digest')
410                 self.list.InsertColumn( 5, 'MACLength')
411                 self.list.InsertColumn( 6, 'Compression')
412                 self.list.InsertColumn( 7, 'Options')
413                 self.list.InsertColumn( 8, 'Status')
414                 self.list.InsertColumn( 9, 'Nexthop')
415                 self.list.InsertColumn(10, 'Via')
416                 self.list.InsertColumn(11, 'Distance')
417                 self.list.InsertColumn(12, 'PMTU')
418                 self.list.InsertColumn(13, 'Min MTU')
419                 self.list.InsertColumn(14, 'Max MTU')
420                 self.list.InsertColumn(15, 'Since')
421
422                 hbox = wx.BoxSizer(wx.HORIZONTAL)
423                 hbox.Add(self.list, 1, wx.EXPAND)
424                 self.SetSizer(hbox)
425                 self.refresh()
426
427         def refresh(self):
428                 sortstate = self.list.GetSortState()
429                 self.list.itemDataMap = {}
430                 i = 0
431
432                 for key, node in vpn.nodes.items():
433                         if self.list.GetItemCount() <= i:
434                                 self.list.InsertStringItem(i, node.name)
435                         else:
436                                 self.list.SetStringItem(i,  0, node.name)
437                         self.list.SetStringItem(i,  1, node.address)
438                         self.list.SetStringItem(i,  2, node.port)
439                         self.list.SetStringItem(i,  3, str(node.cipher))
440                         self.list.SetStringItem(i,  4, str(node.digest))
441                         self.list.SetStringItem(i,  5, str(node.maclength))
442                         self.list.SetStringItem(i,  6, str(node.compression))
443                         self.list.SetStringItem(i,  7, format(node.options, "x"))
444                         self.list.SetStringItem(i,  8, format(node.status, "04x"))
445                         self.list.SetStringItem(i,  9, node.nexthop)
446                         self.list.SetStringItem(i, 10, node.via)
447                         self.list.SetStringItem(i, 11, str(node.distance))
448                         self.list.SetStringItem(i, 12, str(node.pmtu))
449                         self.list.SetStringItem(i, 13, str(node.minmtu))
450                         self.list.SetStringItem(i, 14, str(node.maxmtu))
451                         if node.last_state_change:
452                                 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
453                         else:
454                                 since = "never"
455                         self.list.SetStringItem(i, 15, since)
456                         self.list.itemDataMap[i] = (node.name, node.address, node.port, node.cipher, node.digest, node.maclength, node.compression, node.options, node.status, node.nexthop, node.via, node.distance, node.pmtu, node.minmtu, node.maxmtu, since)
457                         self.list.SetItemData(i, i)
458                         i += 1
459
460                 while self.list.GetItemCount() > i:
461                         self.list.DeleteItem(self.list.GetItemCount() - 1)
462
463                 self.list.SortListItems(sortstate[0], sortstate[1])
464
465 class EdgesPage(wx.Panel):
466         def __init__(self, parent, id):
467                 wx.Panel.__init__(self, parent, id)
468                 self.list = SuperListCtrl(self, id)
469                 self.list.InsertColumn(0, 'From')
470                 self.list.InsertColumn(1, 'To')
471                 self.list.InsertColumn(2, 'Address')
472                 self.list.InsertColumn(3, 'Port')
473                 self.list.InsertColumn(4, 'Options')
474                 self.list.InsertColumn(5, 'Weight')
475
476                 hbox = wx.BoxSizer(wx.HORIZONTAL)
477                 hbox.Add(self.list, 1, wx.EXPAND)
478                 self.SetSizer(hbox)
479                 self.refresh()
480
481         def refresh(self):
482                 sortstate = self.list.GetSortState()
483                 self.list.itemDataMap = {}
484                 i = 0
485
486                 for key, edge in vpn.edges.items():
487                         if self.list.GetItemCount() <= i:
488                                 self.list.InsertStringItem(i, edge.fr)
489                         else:
490                                 self.list.SetStringItem(i, 0, edge.fr)
491                         self.list.SetStringItem(i, 1, edge.to)
492                         self.list.SetStringItem(i, 2, edge.address)
493                         self.list.SetStringItem(i, 3, edge.port)
494                         self.list.SetStringItem(i, 4, format(edge.options, "x"))
495                         self.list.SetStringItem(i, 5, str(edge.weight))
496                         self.list.itemDataMap[i] = (edge.fr, edge.to, edge.address, edge.port, edge.options, edge.weight)
497                         self.list.SetItemData(i, i)
498                         i += 1
499
500                 while self.list.GetItemCount() > i:
501                         self.list.DeleteItem(self.list.GetItemCount() - 1)
502
503                 self.list.SortListItems(sortstate[0], sortstate[1])
504
505 class SubnetsPage(wx.Panel):
506         def __init__(self, parent, id):
507                 wx.Panel.__init__(self, parent, id)
508                 self.list = SuperListCtrl(self, id)
509                 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
510                 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
511                 self.list.InsertColumn(2, 'Owner')
512                 hbox = wx.BoxSizer(wx.HORIZONTAL)
513                 hbox.Add(self.list, 1, wx.EXPAND)
514                 self.SetSizer(hbox)
515                 self.refresh()
516
517         def refresh(self):
518                 sortstate = self.list.GetSortState()
519                 self.list.itemDataMap = {}
520                 i = 0
521
522                 for key, subnet in vpn.subnets.items():
523                         if self.list.GetItemCount() <= i:
524                                 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
525                         else:
526                                 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
527                         self.list.SetStringItem(i, 1, subnet.weight)
528                         self.list.SetStringItem(i, 2, subnet.owner)
529                         self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
530                         self.list.SetItemData(i, i)
531                         i += 1
532
533                 while self.list.GetItemCount() > i:
534                         self.list.DeleteItem(self.list.GetItemCount() - 1)
535
536                 self.list.SortListItems(sortstate[0], sortstate[1])
537
538 class StatusPage(wx.Panel):
539         def __init__(self, parent, id):
540                 wx.Panel.__init__(self, parent, id)
541
542 class GraphPage(wx.Window):
543         def __init__(self, parent, id):
544                 wx.Window.__init__(self, parent, id)
545
546 class NetPage(wx.Notebook):
547         def __init__(self, parent, id):
548                 wx.Notebook.__init__(self, parent)
549                 self.settings = SettingsPage(self, id)
550                 self.connections = ConnectionsPage(self, id)
551                 self.nodes = NodesPage(self, id)
552                 self.edges = EdgesPage(self, id)
553                 self.subnets = SubnetsPage(self, id)
554                 self.graph = GraphPage(self, id)
555                 self.status = StatusPage(self, id)
556
557                 self.AddPage(self.settings, 'Settings')
558                 #self.AddPage(self.status, 'Status')
559                 self.AddPage(self.connections, 'Connections')
560                 self.AddPage(self.nodes, 'Nodes')
561                 self.AddPage(self.edges, 'Edges')
562                 self.AddPage(self.subnets, 'Subnets')
563                 #self.AddPage(self.graph, 'Graph')
564                 
565
566 class MainWindow(wx.Frame):
567         def OnQuit(self, event):
568                 app.ExitMainLoop()
569
570         def OnTimer(self, event):
571                 vpn.refresh()
572                 self.np.nodes.refresh()
573                 self.np.subnets.refresh()
574                 self.np.edges.refresh()
575                 self.np.connections.refresh()
576
577         def __init__(self, parent, id, title):
578                 wx.Frame.__init__(self, parent, id, title)
579
580                 menubar = wx.MenuBar()
581                 file = wx.Menu()
582                 file.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
583                 menubar.Append(file, '&File')
584
585                 #nb = wx.Notebook(self, -1)
586                 #nb.SetPadding((0, 0))
587                 self.np = NetPage(self, -1)
588                 #nb.AddPage(np, 'VPN')
589                 
590                 self.timer = wx.Timer(self, -1)
591                 self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
592                 self.timer.Start(1000)
593                 self.Bind(wx.EVT_MENU, self.OnQuit, id=1)
594                 self.SetMenuBar(menubar)
595                 self.Show()
596
597 app = wx.App()
598 mw = MainWindow(None, -1, 'Tinc GUI')
599
600 #def OnTaskBarIcon(event):
601 #       mw.Raise()
602 #
603 #icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
604 #taskbaricon = wx.TaskBarIcon()
605 #taskbaricon.SetIcon(icon, 'Tinc GUI')
606 #wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)
607
608 app.MainLoop()
609 vpn.close()