ssh-hull 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +44 -0
  3. data/.gitignore +27 -0
  4. data/.rubocop.yml +62 -0
  5. data/.tool-versions +1 -0
  6. data/Gemfile +5 -0
  7. data/Gemfile.lock +142 -0
  8. data/LICENSE +19 -0
  9. data/README.md +12 -0
  10. data/Rakefile +7 -0
  11. data/bin/bundle +114 -0
  12. data/bin/rake +29 -0
  13. data/bin/rspec +29 -0
  14. data/bin/rubocop +29 -0
  15. data/bomb +12 -0
  16. data/config/locales/en.yml +71 -0
  17. data/config/locales/fr.yml +71 -0
  18. data/exe/ssh-hull +12 -0
  19. data/exe/ssh-tunnel +1 -0
  20. data/lib/ssh-hull/cli.rb +113 -0
  21. data/lib/ssh-hull/logger.rb +6 -0
  22. data/lib/ssh-hull/ui/application.rb +47 -0
  23. data/lib/ssh-hull/ui/forms/application_form.rb +69 -0
  24. data/lib/ssh-hull/ui/forms/host_form.rb +27 -0
  25. data/lib/ssh-hull/ui/forms/tunnel_form.rb +31 -0
  26. data/lib/ssh-hull/ui/helpers/application_window_helper.rb +233 -0
  27. data/lib/ssh-hull/ui/helpers/common/form_helper.rb +169 -0
  28. data/lib/ssh-hull/ui/helpers/common/minimize_helper.rb +46 -0
  29. data/lib/ssh-hull/ui/helpers/common/modal_helper.rb +43 -0
  30. data/lib/ssh-hull/ui/helpers/common/toolbar_helper.rb +106 -0
  31. data/lib/ssh-hull/ui/helpers/common/translation_helper.rb +18 -0
  32. data/lib/ssh-hull/ui/helpers/common/tree_view_helper.rb +40 -0
  33. data/lib/ssh-hull/ui/helpers/host_window_helper.rb +230 -0
  34. data/lib/ssh-hull/ui/helpers/tunnel_window_helper.rb +96 -0
  35. data/lib/ssh-hull/ui/models/config.rb +82 -0
  36. data/lib/ssh-hull/ui/models/host.rb +90 -0
  37. data/lib/ssh-hull/ui/models/tunnel.rb +118 -0
  38. data/lib/ssh-hull/ui/status_icon.rb +45 -0
  39. data/lib/ssh-hull/ui/windows/about_window.rb +32 -0
  40. data/lib/ssh-hull/ui/windows/application_window.rb +42 -0
  41. data/lib/ssh-hull/ui/windows/hosts/delete_window.rb +56 -0
  42. data/lib/ssh-hull/ui/windows/hosts/edit_window.rb +39 -0
  43. data/lib/ssh-hull/ui/windows/hosts/new_window.rb +45 -0
  44. data/lib/ssh-hull/ui/windows/tunnels/delete_window.rb +57 -0
  45. data/lib/ssh-hull/ui/windows/tunnels/edit_window.rb +39 -0
  46. data/lib/ssh-hull/ui/windows/tunnels/new_window.rb +45 -0
  47. data/lib/ssh-hull/version.rb +17 -0
  48. data/lib/ssh-tunnel.rb +94 -0
  49. data/resources/gresources.xml +13 -0
  50. data/resources/ui/about_window.glade +48 -0
  51. data/resources/ui/application_window.glade +196 -0
  52. data/resources/ui/hosts/delete_window.glade +74 -0
  53. data/resources/ui/hosts/edit_window.glade +331 -0
  54. data/resources/ui/hosts/new_window.glade +328 -0
  55. data/resources/ui/tunnels/delete_window.glade +73 -0
  56. data/resources/ui/tunnels/edit_window.glade +305 -0
  57. data/resources/ui/tunnels/new_window.glade +305 -0
  58. data/snap/snapcraft.yaml +48 -0
  59. data/spec/factories/host.rb +22 -0
  60. data/spec/factories/tunnel.rb +16 -0
  61. data/spec/spec_helper.rb +31 -0
  62. data/spec/ssh_tunnel/ui/forms/host_form_spec.rb +103 -0
  63. data/spec/ssh_tunnel/ui/forms/tunnel_form_spec.rb +132 -0
  64. data/spec/ssh_tunnel/ui/models/host_spec.rb +116 -0
  65. data/spec/ssh_tunnel/ui/models/tunnel_spec.rb +43 -0
  66. data/spec/ssh_tunnel_spec.rb +45 -0
  67. data/ssh-hull.gemspec +38 -0
  68. metadata +320 -0
data/exe/ssh-hull ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/ssh-hull'
5
+
6
+ # Parse options
7
+ cli = SSHTunnel::CLI.instance
8
+ cli.parse
9
+
10
+ # Start application
11
+ status = cli.run
12
+ exit status
data/exe/ssh-tunnel ADDED
@@ -0,0 +1 @@
1
+ ssh-hull
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ class CLI
5
+ include Singleton
6
+
7
+ def parse(args = ARGV)
8
+ setup_options(args)
9
+ validate!
10
+ load_config!
11
+ end
12
+
13
+
14
+ def run
15
+ compile_resources!
16
+ load_resources!
17
+ set_locales!
18
+ boot_application!
19
+ end
20
+
21
+
22
+ private
23
+
24
+
25
+ def setup_options(args)
26
+ @opts = parse_options(args)
27
+ end
28
+
29
+
30
+ def parse_options(argv)
31
+ opts = {}
32
+ @parser = option_parser(opts)
33
+ @parser.parse!(argv)
34
+ opts
35
+ end
36
+
37
+
38
+ def option_parser(opts)
39
+ parser = OptionParser.new do |o|
40
+ o.on '-C', '--config PATH', 'path to YAML config file' do |arg|
41
+ opts[:config_file] = arg
42
+ end
43
+ end
44
+
45
+ parser.banner = 'ssh-hull [options]'
46
+ parser.on_tail '-h', '--help', 'Show help' do
47
+ puts parser
48
+ exit 1
49
+ end
50
+
51
+ parser
52
+ end
53
+
54
+
55
+ def validate!
56
+ if @opts[:config_file]
57
+ raise ArgumentError, "No such file #{@opts[:config_file]}" unless File.exist?(@opts[:config_file])
58
+ else
59
+ @opts[:config_file] = Pathname.new(File.expand_path('~/.config/ssh-hull/config.json'))
60
+ end
61
+ end
62
+
63
+
64
+ def load_config!
65
+ SSHTunnel.load_config(@opts[:config_file])
66
+ end
67
+
68
+
69
+ def compile_resources!
70
+ cmd = [
71
+ 'glib-compile-resources',
72
+ '--target', SSHTunnel.resources_bin.to_s,
73
+ '--sourcedir', SSHTunnel.resources_path.to_s,
74
+ SSHTunnel.resources_xml.to_s
75
+ ]
76
+ system(*cmd)
77
+ end
78
+
79
+
80
+ def load_resources!
81
+ resources = Gio::Resource.load(SSHTunnel.resources_bin.to_s)
82
+ Gio::Resources.register(resources)
83
+ end
84
+
85
+
86
+ def set_locales!
87
+ I18n::Backend::Simple.include(I18n::Backend::Fallbacks)
88
+ I18n.load_path << Dir[SSHTunnel.locales_path]
89
+
90
+ I18n.enforce_available_locales = false
91
+ I18n.available_locales = %i[en fr]
92
+ I18n.default_locale = SSHTunnel.current_locale
93
+ I18n.fallbacks = [:en]
94
+ end
95
+
96
+
97
+ def boot_application!
98
+ SSHTunnel.config.hosts.each(&:auto_start!)
99
+ app = SSHTunnel::UI::Application.new
100
+
101
+ begin
102
+ status = app.run
103
+ rescue Interrupt => e
104
+ status = 0
105
+ ensure
106
+ SSHTunnel.config.hosts.each(&:stop_tunnels!)
107
+ end
108
+
109
+ status
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ class Logger < ::Logger
5
+ end
6
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ class Application < Gtk::Application
6
+
7
+ attr_reader :config
8
+
9
+
10
+ # rubocop:disable Metrics/MethodLength
11
+ def initialize
12
+ super 'com.ungtb10d.ssh-hull', Gio::ApplicationFlags::FLAGS_NONE
13
+
14
+ @config = SSHTunnel.config
15
+
16
+ signal_connect :startup do |application|
17
+ quit_accels = ['<Ctrl>Q']
18
+
19
+ action = Gio::SimpleAction.new('quit')
20
+ action.signal_connect :activate do |_action, _parameter|
21
+ application.quit
22
+ end
23
+
24
+ application.add_action(action)
25
+ application.set_accels_for_action('app.quit', quit_accels)
26
+ end
27
+
28
+ signal_connect :activate do |application|
29
+ window = SSHTunnel::UI::Windows::ApplicationWindow.new(application)
30
+ window.present
31
+
32
+ # Gtk::StatusIcon is deprecated
33
+ # See: https://developer.gnome.org/gtk3/stable/GtkStatusIcon.html#gtk-status-icon-new
34
+ # SSHTunnel::UI::StatusIcon.new(application, window)
35
+ end
36
+ end
37
+ # rubocop:enable Metrics/MethodLength
38
+
39
+
40
+ def quit
41
+ @config.hosts.map(&:stop_tunnels!)
42
+ super
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Forms
6
+ class ApplicationForm
7
+
8
+ include ActiveModel::Model
9
+ include ActiveModel::Validations::Callbacks
10
+
11
+ class_attribute :attributes
12
+
13
+ attr_reader :model
14
+
15
+ class << self
16
+
17
+ def attribute(name, opts = {})
18
+ self.attributes ||= []
19
+ create_attribute(name, opts)
20
+ self.attributes += [name]
21
+ end
22
+
23
+ # rubocop:disable Layout/EmptyLinesAroundAttributeAccessor
24
+ private def create_attribute(name, opts = {})
25
+ required = opts.fetch(:required, false)
26
+ attr_accessor name
27
+ validates_presence_of(name) if required
28
+ end
29
+ # rubocop:enable Layout/EmptyLinesAroundAttributeAccessor
30
+
31
+ end
32
+
33
+
34
+ def initialize(model)
35
+ @model = model
36
+ end
37
+
38
+
39
+ def submit(params)
40
+ self.class.attributes.each do |attr|
41
+ if params[attr]
42
+ method = "#{attr}="
43
+ __send__(method, params[attr])
44
+ end
45
+ end
46
+ end
47
+
48
+
49
+ def save
50
+ self.class.attributes.each do |attr|
51
+ value = __send__(attr)
52
+ method = "#{attr}="
53
+ model.__send__(method, value)
54
+ end
55
+ model
56
+ end
57
+
58
+
59
+ private
60
+
61
+
62
+ def cast_to_int(value)
63
+ Integer(value) rescue value
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Forms
6
+ class HostForm < ApplicationForm
7
+
8
+ attribute :name, required: true
9
+ attribute :user, required: true
10
+ attribute :host, required: true
11
+ attribute :port, required: true
12
+ attribute :identity_file
13
+
14
+ validates_inclusion_of :port, in: 0..65_535
15
+
16
+ # Callbacks
17
+ before_validation :cast_port_to_int
18
+
19
+
20
+ def cast_port_to_int
21
+ self.port = cast_to_int(port)
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Forms
6
+ class TunnelForm < ApplicationForm
7
+
8
+ attribute :name, required: true
9
+ attribute :type, required: true
10
+ attribute :local_host, required: true
11
+ attribute :local_port, required: true
12
+ attribute :remote_host, required: true
13
+ attribute :remote_port, required: true
14
+ attribute :auto_start
15
+
16
+ validates_inclusion_of :local_port, in: 0..65_535
17
+ validates_inclusion_of :remote_port, in: 0..65_535
18
+
19
+ # Callbacks
20
+ before_validation :cast_port_to_int
21
+
22
+
23
+ def cast_port_to_int
24
+ self.local_port = cast_to_int(local_port)
25
+ self.remote_port = cast_to_int(remote_port)
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Helpers
6
+ module ApplicationWindowHelper
7
+
8
+ include SSHTunnel::UI::Helpers::Common::MinimizeHelper
9
+ include SSHTunnel::UI::Helpers::Common::ToolbarHelper
10
+ include SSHTunnel::UI::Helpers::Common::TranslationHelper
11
+ include SSHTunnel::UI::Helpers::Common::TreeViewHelper
12
+
13
+
14
+ def self.included(base)
15
+ base.extend(ClassMethods)
16
+ base.extend(SSHTunnel::UI::Helpers::Common::FormHelper::ClassMethods)
17
+ end
18
+
19
+
20
+ module ClassMethods
21
+
22
+ MENU_ITEMS = %w[quit about].freeze
23
+ TOOLBAR_BUTTONS = %w[add edit remove start stop].freeze
24
+
25
+ def init
26
+ bind_menu_entries(MENU_ITEMS)
27
+ bind_buttons(TOOLBAR_BUTTONS)
28
+ end
29
+
30
+ end
31
+
32
+
33
+ def load_hosts_treeview
34
+ # create treeview
35
+ treeview_model = create_hosts_treeview_model(all_hosts)
36
+ @hosts_treeview = create_hosts_treeview(treeview_model)
37
+
38
+ # Render treeview
39
+ hosts_scrolled_window.shadow_type = :etched_in
40
+ hosts_scrolled_window.set_policy(:automatic, :automatic)
41
+ hosts_scrolled_window.add(@hosts_treeview)
42
+ @hosts_treeview.show
43
+ end
44
+
45
+
46
+ def reload_hosts_treeview
47
+ @hosts_treeview.destroy
48
+ load_hosts_treeview
49
+ end
50
+
51
+
52
+ private
53
+
54
+
55
+ HOST_STATE_COLUMN = 0
56
+ HOST_UUID_COLUMN = 1
57
+ HOST_NAME_COLUMN = 2
58
+ HOST_HOST_COLUMN = 3
59
+ HOST_USER_COLUMN = 4
60
+ HOST_PORT_COLUMN = 5
61
+ TUNNEL_NAME_COLUMN = 6
62
+ TUNNEL_TYPE_COLUMN = 7
63
+ TUNNEL_LOCAL_HOST_COLUMN = 8
64
+ TUNNEL_LOCAL_PORT_COLUMN = 9
65
+ TUNNEL_REMOTE_HOST_COLUMN = 10
66
+ TUNNEL_REMOTE_PORT_COLUMN = 11
67
+ TUNNEL_AUTO_START_COLUMN = 12
68
+
69
+
70
+ # rubocop:disable Metrics/MethodLength
71
+ def create_hosts_treeview_model(hosts)
72
+ model = Gtk::TreeStore.new(String, String, String, String, String, String, String, String, String, String, String, String, String)
73
+
74
+ hosts.each do |host|
75
+ iter = model.append(nil)
76
+
77
+ iter[HOST_STATE_COLUMN] = host.started? ? 'gtk-yes' : 'gtk-no'
78
+ iter[HOST_UUID_COLUMN] = host.uuid
79
+ iter[HOST_NAME_COLUMN] = host.name
80
+ iter[HOST_USER_COLUMN] = host.user
81
+ iter[HOST_HOST_COLUMN] = host.host
82
+ iter[HOST_PORT_COLUMN] = host.port
83
+ iter[TUNNEL_NAME_COLUMN] = ''
84
+ iter[TUNNEL_TYPE_COLUMN] = ''
85
+ iter[TUNNEL_LOCAL_HOST_COLUMN] = ''
86
+ iter[TUNNEL_LOCAL_PORT_COLUMN] = ''
87
+ iter[TUNNEL_REMOTE_HOST_COLUMN] = ''
88
+ iter[TUNNEL_REMOTE_PORT_COLUMN] = ''
89
+ iter[TUNNEL_AUTO_START_COLUMN] = ''
90
+
91
+ tunnels = host.tunnels
92
+
93
+ # add children
94
+ tunnels.each do |tunnel|
95
+ child_iter = model.append(iter)
96
+
97
+ child_iter[TUNNEL_NAME_COLUMN] = tunnel.name
98
+ child_iter[TUNNEL_TYPE_COLUMN] = tunnel.type
99
+ child_iter[TUNNEL_LOCAL_HOST_COLUMN] = tunnel.local_host
100
+ child_iter[TUNNEL_LOCAL_PORT_COLUMN] = tunnel.local_port
101
+ child_iter[TUNNEL_REMOTE_HOST_COLUMN] = tunnel.remote_host
102
+ child_iter[TUNNEL_REMOTE_PORT_COLUMN] = tunnel.remote_port
103
+ child_iter[TUNNEL_AUTO_START_COLUMN] = tunnel.auto_start?.to_s
104
+ end
105
+ end
106
+
107
+ model
108
+ end
109
+ # rubocop:enable Metrics/MethodLength
110
+
111
+
112
+ def create_hosts_treeview(model)
113
+ treeview = Gtk::TreeView.new(model)
114
+
115
+ # configure treeview selection
116
+ # sub-rows are not clickable
117
+ treeview.selection.mode = :single
118
+ treeview.selection.set_select_function do |_selection, _model, path, _path_currentry_selected|
119
+ path.to_s.include?(':') ? false : true
120
+ end
121
+
122
+ # Disable buttons if tunnels are running
123
+ hosts_treeview_bind_single_click(treeview)
124
+
125
+ # Start host tunnels on double-click
126
+ hosts_treeview_bind_double_click(treeview)
127
+
128
+ # Popup the menu on right click
129
+ hosts_treeview_bind_right_click(treeview)
130
+
131
+ # add columns to the tree view
132
+ hosts_treeview_add_columns(treeview)
133
+
134
+ treeview
135
+ end
136
+
137
+
138
+ def hosts_treeview_bind_single_click(treeview)
139
+ treeview.selection.signal_connect :changed do
140
+ with_host_model do |object|
141
+ disable_buttons_if_tunnels_running(object)
142
+ end
143
+ end
144
+ end
145
+
146
+
147
+ def hosts_treeview_bind_double_click(treeview)
148
+ treeview.signal_connect :row_activated do |_widget, path|
149
+ if path.to_s.include?(':')
150
+ false
151
+ else
152
+ with_host_model do |object|
153
+ object.toggle_tunnels!
154
+ reload_hosts_treeview
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+
161
+ # rubocop:disable Style/SoleNestedConditional
162
+ def hosts_treeview_bind_right_click(treeview)
163
+ treeview.signal_connect :button_press_event do |widget, event|
164
+ if event.is_a?(Gdk::EventButton) && event.button == 3
165
+ if widget.selection.selected && !widget.selection.selected.path.to_s.include?(':')
166
+ # TODO: finish implementation of right click
167
+ puts 'RIGHT CLICK'
168
+ end
169
+ end
170
+ end
171
+ end
172
+ # rubocop:enable Style/SoleNestedConditional
173
+
174
+
175
+ def hosts_treeview_add_columns(treeview)
176
+ add_image_column treeview, t('view.host.state'), 'icon-name': HOST_STATE_COLUMN
177
+ add_text_column treeview, t('view.host.uuid'), text: HOST_UUID_COLUMN, visible: false
178
+ add_text_column treeview, t('view.host.name'), text: HOST_NAME_COLUMN
179
+ add_text_column treeview, t('view.host.user'), text: HOST_USER_COLUMN
180
+ add_text_column treeview, t('view.host.host'), text: HOST_HOST_COLUMN
181
+ add_text_column treeview, t('view.host.port'), text: HOST_PORT_COLUMN
182
+ add_text_column treeview, t('view.tunnel.name'), text: TUNNEL_NAME_COLUMN
183
+ add_text_column treeview, t('view.tunnel.type'), text: TUNNEL_TYPE_COLUMN
184
+ add_text_column treeview, t('view.tunnel.local_host'), text: TUNNEL_LOCAL_HOST_COLUMN
185
+ add_text_column treeview, t('view.tunnel.local_port'), text: TUNNEL_LOCAL_PORT_COLUMN
186
+ add_text_column treeview, t('view.tunnel.remote_host'), text: TUNNEL_REMOTE_HOST_COLUMN
187
+ add_text_column treeview, t('view.tunnel.remote_port'), text: TUNNEL_REMOTE_PORT_COLUMN
188
+ add_text_column treeview, t('view.tunnel.auto_start'), text: TUNNEL_AUTO_START_COLUMN
189
+ end
190
+
191
+
192
+ def all_hosts
193
+ @application.config.hosts
194
+ end
195
+
196
+
197
+ def find_host_model
198
+ return nil if @hosts_treeview.selection.selected.blank?
199
+
200
+ uuid = @hosts_treeview.selection.selected.get_value(HOST_UUID_COLUMN)
201
+ all_hosts.find { |h| h.uuid == uuid }
202
+ end
203
+
204
+
205
+ def with_host_model
206
+ object = find_host_model
207
+ yield object if object
208
+ end
209
+
210
+
211
+ def with_new_host_model
212
+ yield SSHTunnel::UI::Models::Host.new
213
+ end
214
+
215
+
216
+ def disable_buttons_if_tunnels_running(host)
217
+ if host.started?
218
+ button_edit.sensitive = false
219
+ button_remove.sensitive = false
220
+ button_start.sensitive = false
221
+ button_stop.sensitive = true
222
+ else
223
+ button_edit.sensitive = true
224
+ button_remove.sensitive = true
225
+ button_start.sensitive = true
226
+ button_stop.sensitive = false
227
+ end
228
+ end
229
+
230
+ end
231
+ end
232
+ end
233
+ end