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
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Helpers
6
+ module Common
7
+ module FormHelper
8
+ module ClassMethods
9
+
10
+ def bind_menu_entries(entries)
11
+ entries.each do |f|
12
+ bind_template_child "menu_#{f}"
13
+ end
14
+ end
15
+
16
+
17
+ def bind_buttons(buttons)
18
+ buttons.each do |f|
19
+ bind_template_child "button_#{f}"
20
+ end
21
+ end
22
+
23
+
24
+ def bind_form_fields(fields)
25
+ fields.each do |f|
26
+ bind_template_child "label_#{f}"
27
+ bind_template_child "input_#{f}"
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ module InstanceMethods
34
+
35
+ def params
36
+ params = {}
37
+ form_fields.each do |field_name, opts|
38
+ # get input value
39
+ input = "input_#{field_name}"
40
+ field = __send__(input)
41
+ getter = getter_for(opts[:type])
42
+ value = field.__send__(getter)
43
+ params[field_name] = value
44
+ end
45
+ params
46
+ end
47
+
48
+
49
+ def bind_submit_button
50
+ button_submit.label = t('button.submit')
51
+ button_submit.signal_connect :clicked do
52
+ submit_form(params)
53
+ end
54
+ end
55
+
56
+
57
+ def submit_form(params)
58
+ form = form_object
59
+ form.submit(params)
60
+ if form.valid?
61
+ form.save
62
+ save_and_reload_view
63
+ else
64
+ render_form_errors(form.errors)
65
+ end
66
+ end
67
+
68
+
69
+ def form_object
70
+ raise NotImplementedError
71
+ end
72
+
73
+
74
+ def save_and_reload_view
75
+ raise NotImplementedError
76
+ end
77
+
78
+
79
+ def render_form_errors(errors)
80
+ form_fields.each do |field_name, _method|
81
+ has_errors = errors.of_kind?(field_name, :blank) || errors.of_kind?(field_name, :inclusion)
82
+ input = "input_#{field_name}"
83
+ field = __send__(input)
84
+ color = has_errors ? red : white
85
+ field.override_background_color(:normal, color)
86
+ end
87
+ end
88
+
89
+
90
+ def red
91
+ Gdk::RGBA.new(255, 0, 0)
92
+ end
93
+
94
+
95
+ def white
96
+ Gdk::RGBA.new(255, 255, 255)
97
+ end
98
+
99
+
100
+ def restore_form_values(model)
101
+ form_fields.each do |field_name, opts|
102
+ restore_form_value(model, field_name, opts)
103
+ end
104
+ end
105
+
106
+
107
+ def restore_form_value(model, field_name, opts)
108
+ # get model value
109
+ value = model.__send__(field_name)
110
+
111
+ # set input form
112
+ input = "input_#{field_name}"
113
+ field = __send__(input)
114
+ setter = setter_for(opts[:type])
115
+
116
+ if opts[:type] == :file && value.nil?
117
+ field.unselect_all
118
+ else
119
+ field.__send__(setter, value)
120
+ end
121
+ end
122
+
123
+
124
+ INPUT_TYPES = {
125
+ text: {
126
+ getter: :text,
127
+ setter: :text=,
128
+ },
129
+ select: {
130
+ getter: :active_id,
131
+ setter: :active_id=,
132
+ },
133
+ file: {
134
+ getter: :filename,
135
+ setter: :filename=,
136
+ },
137
+ checkbox: {
138
+ getter: :active?,
139
+ setter: :active=,
140
+ },
141
+ }.freeze
142
+ private_constant :INPUT_TYPES
143
+
144
+
145
+ def setter_for(input_type)
146
+ INPUT_TYPES.dig(input_type, :setter)
147
+ end
148
+
149
+
150
+ def getter_for(input_type)
151
+ INPUT_TYPES.dig(input_type, :getter)
152
+ end
153
+
154
+
155
+ def set_input_labels(scope:)
156
+ form_fields.each do |field_name, _method|
157
+ label = "label_#{field_name}"
158
+ field = __send__(label)
159
+ value = t("form.#{scope}.#{field_name}")
160
+ field.text = value
161
+ end
162
+ end
163
+
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Helpers
6
+ module Common
7
+ module MinimizeHelper
8
+
9
+ def initialize(*)
10
+ super
11
+
12
+ # Set instance variables
13
+ @minimized = false
14
+ end
15
+
16
+
17
+ def minimized?
18
+ @minimized
19
+ end
20
+
21
+
22
+ def minimize!
23
+ @minimized = true
24
+ hide
25
+ end
26
+
27
+
28
+ def maximize!
29
+ @minimized = false
30
+ show
31
+ end
32
+
33
+
34
+ def toggle!
35
+ if minimized?
36
+ maximize!
37
+ else
38
+ minimize!
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Helpers
6
+ module Common
7
+ module ModalHelper
8
+
9
+ def initialize(application, window, *)
10
+ super application: application
11
+
12
+ # Set instance variables
13
+ @application = application
14
+ @window = window
15
+
16
+ # Bind listeners
17
+ bind_submit_button
18
+ bind_cancel_button
19
+ end
20
+
21
+
22
+ private
23
+
24
+ def bind_cancel_button
25
+ # Not all modals have a cancel button
26
+ if respond_to?(:button_cancel)
27
+ button_cancel.label = t('button.cancel')
28
+ button_cancel.signal_connect :clicked do
29
+ close
30
+ end
31
+ end
32
+
33
+ # Bind escape key stroke
34
+ signal_connect :key_press_event do |_widget, event|
35
+ event.keyval == 65_307 ? close : false
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Helpers
6
+ module Common
7
+ module ToolbarHelper
8
+
9
+ def initialize(*)
10
+ super
11
+
12
+ # Bind listeners
13
+ bind_menu_entries
14
+ bind_toolbar_buttons
15
+ end
16
+
17
+
18
+ private
19
+
20
+
21
+ def bind_menu_entries
22
+ signal_connect :destroy do
23
+ @application.quit
24
+ end
25
+
26
+ menu_quit.label = t('menu.quit')
27
+ menu_quit.signal_connect :activate do
28
+ @application.quit
29
+ end
30
+
31
+ menu_about.label = t('menu.about')
32
+ menu_about.signal_connect :activate do
33
+ window = SSHTunnel::UI::Windows::AboutWindow.new(@application)
34
+ window.present
35
+ end
36
+ end
37
+
38
+
39
+ def bind_toolbar_buttons
40
+ bind_toolbar_button_add
41
+ bind_toolbar_button_edit
42
+ bind_toolbar_button_remove
43
+ bind_toolbar_button_start
44
+ bind_toolbar_button_stop
45
+ end
46
+
47
+
48
+ def bind_toolbar_button_add
49
+ button_add.tooltip_text = t('tooltip.host.add')
50
+ button_add.signal_connect :clicked do
51
+ with_new_host_model do |object|
52
+ window = SSHTunnel::UI::Windows::Hosts::NewWindow.new(@application, self, object)
53
+ window.present
54
+ end
55
+ end
56
+ end
57
+
58
+
59
+ def bind_toolbar_button_edit
60
+ button_edit.tooltip_text = t('tooltip.host.edit')
61
+ button_edit.signal_connect :clicked do
62
+ with_host_model do |object|
63
+ window = SSHTunnel::UI::Windows::Hosts::EditWindow.new(@application, self, object)
64
+ window.present
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ def bind_toolbar_button_remove
71
+ button_remove.tooltip_text = t('tooltip.host.remove')
72
+ button_remove.signal_connect :clicked do
73
+ with_host_model do |object|
74
+ window = SSHTunnel::UI::Windows::Hosts::DeleteWindow.new(@application, self, object)
75
+ window.present
76
+ end
77
+ end
78
+ end
79
+
80
+
81
+ def bind_toolbar_button_start
82
+ button_start.tooltip_text = t('tooltip.tunnel.start')
83
+ button_start.signal_connect :clicked do
84
+ with_host_model do |object|
85
+ object.start_tunnels!
86
+ reload_hosts_treeview
87
+ end
88
+ end
89
+ end
90
+
91
+
92
+ def bind_toolbar_button_stop
93
+ button_stop.tooltip_text = t('tooltip.tunnel.stop')
94
+ button_stop.signal_connect :clicked do
95
+ with_host_model do |object|
96
+ object.stop_tunnels!
97
+ reload_hosts_treeview
98
+ end
99
+ end
100
+ end
101
+
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Helpers
6
+ module Common
7
+ module TranslationHelper
8
+ extend ActiveSupport::Concern
9
+
10
+ ruby2_keywords def t(*args)
11
+ I18n.t(*args)
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Helpers
6
+ module Common
7
+ module TreeViewHelper
8
+
9
+ def add_text_column(treeview, label, attributes)
10
+ renderer = Gtk::CellRendererText.new
11
+ add_column(treeview, renderer, label, attributes)
12
+ end
13
+
14
+
15
+ def add_image_column(treeview, label, attributes)
16
+ renderer = Gtk::CellRendererPixbuf.new
17
+ add_column(treeview, renderer, label, attributes)
18
+ end
19
+
20
+
21
+ def add_column(treeview, renderer, label, attributes)
22
+ visible = attributes.delete(:visible) { true }
23
+ sortable = attributes.delete(:sortable) { true }
24
+
25
+ column = Gtk::TreeViewColumn.new(label, renderer, attributes)
26
+ column.visible = visible
27
+
28
+ if sortable
29
+ column.clickable = true
30
+ column.sort_column_id = attributes[:text] if attributes[:text]
31
+ end
32
+
33
+ treeview.append_column(column)
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHTunnel
4
+ module UI
5
+ module Helpers
6
+ module HostWindowHelper
7
+
8
+ include SSHTunnel::UI::Helpers::Common::ModalHelper
9
+ include SSHTunnel::UI::Helpers::Common::TranslationHelper
10
+ include SSHTunnel::UI::Helpers::Common::TreeViewHelper
11
+ include SSHTunnel::UI::Helpers::Common::FormHelper::InstanceMethods
12
+
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ base.extend(SSHTunnel::UI::Helpers::Common::FormHelper::ClassMethods)
16
+ end
17
+
18
+ FORM_BUTTONS = %w[submit cancel add edit remove reset_identity_file].freeze
19
+ FORM_FIELDS = {
20
+ name: {
21
+ type: :text,
22
+ },
23
+ user: {
24
+ type: :text,
25
+ },
26
+ host: {
27
+ type: :text,
28
+ },
29
+ port: {
30
+ type: :text,
31
+ },
32
+ identity_file: {
33
+ type: :file,
34
+ },
35
+ }.freeze
36
+
37
+
38
+ module ClassMethods
39
+
40
+ def init
41
+ bind_buttons(FORM_BUTTONS)
42
+ bind_form_fields(FORM_FIELDS.keys)
43
+ bind_template_child 'tunnels_scrolled_window'
44
+ end
45
+
46
+ end
47
+
48
+
49
+ def initialize(application, window, host)
50
+ super
51
+
52
+ # Set instance variables
53
+ @host = host
54
+
55
+ # Bind listeners
56
+ set_input_labels(scope: :host)
57
+ bind_host_buttons
58
+ bind_tunnels_buttons
59
+
60
+ # Load tunnels treeview
61
+ load_tunnels_treeview(@host)
62
+ end
63
+
64
+
65
+ def load_tunnels_treeview(host)
66
+ # create treeview
67
+ treeview_model = create_tunnels_treeview_model(host.tunnels)
68
+ @tunnels_treeview = create_tunnels_treeview(treeview_model)
69
+
70
+ # Render treeview
71
+ tunnels_scrolled_window.add(@tunnels_treeview)
72
+ @tunnels_treeview.show
73
+ end
74
+
75
+
76
+ def reload_tunnels_treeview(host)
77
+ @tunnels_treeview.destroy
78
+ load_tunnels_treeview(host)
79
+ end
80
+
81
+
82
+ private
83
+
84
+
85
+ TUNNEL_UUID_COLUMN = 0
86
+ TUNNEL_NAME_COLUMN = 1
87
+ TUNNEL_TYPE_COLUMN = 2
88
+ TUNNEL_LOCAL_HOST_COLUMN = 3
89
+ TUNNEL_LOCAL_PORT_COLUMN = 4
90
+ TUNNEL_REMOTE_HOST_COLUMN = 5
91
+ TUNNEL_REMOTE_PORT_COLUMN = 6
92
+ TUNNEL_AUTO_START_COLUMN = 7
93
+
94
+
95
+ def create_tunnels_treeview_model(tunnels)
96
+ model = Gtk::ListStore.new(String, String, String, String, String, String, String, String)
97
+
98
+ tunnels.each do |tunnel|
99
+ iter = model.append
100
+ iter.set_values([tunnel.uuid, tunnel.name, tunnel.type, tunnel.local_host, tunnel.local_port, tunnel.remote_host, tunnel.remote_port, tunnel.auto_start?.to_s])
101
+ end
102
+
103
+ model
104
+ end
105
+
106
+
107
+ def create_tunnels_treeview(model)
108
+ treeview = Gtk::TreeView.new(model)
109
+
110
+ # edit tunnel on double-click
111
+ tunnels_treeview_bind_double_click(treeview)
112
+
113
+ # add columns to the tree view
114
+ tunnels_treeview_add_columns(treeview)
115
+
116
+ treeview
117
+ end
118
+
119
+
120
+ def tunnels_treeview_bind_double_click(treeview)
121
+ treeview.signal_connect :row_activated do |_widget, path|
122
+ if path.to_s.include?(':')
123
+ false
124
+ else
125
+ with_tunnel_model do |object|
126
+ window = SSHTunnel::UI::Windows::Tunnels::EditWindow.new(application, self, object)
127
+ window.present
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+
134
+ def tunnels_treeview_add_columns(treeview)
135
+ add_text_column treeview, t('view.tunnel.uuid'), text: TUNNEL_UUID_COLUMN, visible: false
136
+ add_text_column treeview, t('view.tunnel.name'), text: TUNNEL_NAME_COLUMN
137
+ add_text_column treeview, t('view.tunnel.type'), text: TUNNEL_TYPE_COLUMN
138
+ add_text_column treeview, t('view.tunnel.local_host'), text: TUNNEL_LOCAL_HOST_COLUMN
139
+ add_text_column treeview, t('view.tunnel.local_port'), text: TUNNEL_LOCAL_PORT_COLUMN
140
+ add_text_column treeview, t('view.tunnel.remote_host'), text: TUNNEL_REMOTE_HOST_COLUMN
141
+ add_text_column treeview, t('view.tunnel.remote_port'), text: TUNNEL_REMOTE_PORT_COLUMN
142
+ add_text_column treeview, t('view.tunnel.auto_start'), text: TUNNEL_AUTO_START_COLUMN
143
+ end
144
+
145
+
146
+ def bind_host_buttons
147
+ button_reset_identity_file.label = t('button.reset')
148
+ button_reset_identity_file.tooltip_text = t('tooltip.host.reset_identity_file')
149
+ button_reset_identity_file.signal_connect :clicked do
150
+ input_identity_file.unselect_all
151
+ end
152
+ end
153
+
154
+
155
+ # rubocop:disable Metrics/MethodLength
156
+ def bind_tunnels_buttons
157
+ button_add.label = t('button.add')
158
+ button_add.tooltip_text = t('tooltip.tunnel.add')
159
+ button_add.signal_connect :clicked do
160
+ with_new_tunnel_model do |object|
161
+ window = SSHTunnel::UI::Windows::Tunnels::NewWindow.new(application, self, object)
162
+ window.present
163
+ end
164
+ end
165
+
166
+ button_edit.label = t('button.edit')
167
+ button_edit.tooltip_text = t('tooltip.tunnel.edit')
168
+ button_edit.signal_connect :clicked do
169
+ with_tunnel_model do |object|
170
+ window = SSHTunnel::UI::Windows::Tunnels::EditWindow.new(application, self, object)
171
+ window.present
172
+ end
173
+ end
174
+
175
+ button_remove.label = t('button.remove')
176
+ button_remove.tooltip_text = t('tooltip.tunnel.remove')
177
+ button_remove.signal_connect :clicked do
178
+ with_tunnel_model do |object|
179
+ window = SSHTunnel::UI::Windows::Tunnels::DeleteWindow.new(application, self, object)
180
+ window.present
181
+ end
182
+ end
183
+ end
184
+ # rubocop:enable Metrics/MethodLength
185
+
186
+
187
+ def all_tunnels
188
+ @host.tunnels
189
+ end
190
+
191
+
192
+ def find_tunnel_model
193
+ return nil if @tunnels_treeview.selection.selected.blank?
194
+
195
+ uuid = @tunnels_treeview.selection.selected.get_value(TUNNEL_UUID_COLUMN)
196
+ all_tunnels.find { |t| t.uuid == uuid }
197
+ end
198
+
199
+
200
+ def with_tunnel_model
201
+ object = find_tunnel_model
202
+ yield object if object
203
+ end
204
+
205
+
206
+ def with_new_tunnel_model
207
+ yield SSHTunnel::UI::Models::Tunnel.new(parent: @host)
208
+ end
209
+
210
+
211
+ def form_fields
212
+ FORM_FIELDS
213
+ end
214
+
215
+
216
+ def form_object
217
+ SSHTunnel::UI::Forms::HostForm.new(@host)
218
+ end
219
+
220
+
221
+ def save_and_reload_view
222
+ @application.config.save!
223
+ close
224
+ @window.reload_hosts_treeview
225
+ end
226
+
227
+ end
228
+ end
229
+ end
230
+ end