ssh-hull 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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