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
@@ -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