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