├── .editorconfig
├── .github
└── workflows
│ ├── gradle-wrapper-validation.yml
│ └── gradle.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle
├── buildscript-gradle.lockfile
├── gradle.lockfile
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── release.sh
├── settings.gradle
├── src
├── main
│ └── java
│ │ └── de
│ │ └── cronn
│ │ └── proxy
│ │ └── ssh
│ │ ├── HostKeyType.java
│ │ ├── JSchHelper.java
│ │ ├── JSchSlf4JLogger.java
│ │ ├── ProxyAwareHostnameVerifier.java
│ │ ├── SshConfiguration.java
│ │ ├── SshProxy.java
│ │ ├── SshProxyConfig.java
│ │ ├── SshProxyRuntimeException.java
│ │ └── util
│ │ ├── Assert.java
│ │ └── Utils.java
└── test
│ ├── java
│ └── de
│ │ └── cronn
│ │ └── proxy
│ │ └── ssh
│ │ ├── DummyServerSocketThread.java
│ │ ├── HostKeyTypeTest.java
│ │ ├── JSchHelperTest.java
│ │ ├── JSchSlf4JLoggerTest.java
│ │ ├── ProxyAwareHostnameVerifierTest.java
│ │ ├── SshProxyConfigTest.java
│ │ ├── SshProxyTest.java
│ │ └── util
│ │ ├── AssertTest.java
│ │ └── UtilsTest.java
│ └── resources
│ ├── id_rsa
│ ├── id_rsa.pub
│ ├── logback.xml
│ ├── server-ecdsa.key
│ └── server-rsa.key
└── updateDependencies.sh
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | max_line_length = 120
9 | ij_formatter_off_tag = @formatter:off
10 | ij_formatter_on_tag = @formatter:on
11 | ij_formatter_tags_enabled = false
12 | ij_smart_tabs = false
13 | ij_wrap_on_typing = false
14 |
15 | [*.yml]
16 | indent_style = space
17 | indent_size = 2
18 |
19 | [*.{md,xml,sh,json,gradle,html}]
20 | indent_style = space
21 | indent_size = 4
22 |
23 | [*.java]
24 | indent_style = tab
25 | tab_width = 4
26 | ij_continuation_indent_size = 4
27 | ### import rules
28 | # explicitly disable imports on demand
29 | ij_java_packages_to_use_import_on_demand = false
30 | ij_java_imports_layout = $*,|,java.**,|,javax.**,|,org.**,|,com.**,|,*
31 | ij_java_class_count_to_use_import_on_demand = 99
32 | ij_java_names_count_to_use_import_on_demand = 1
33 | ij_java_layout_static_imports_separately = true
34 | ij_java_use_single_class_imports = true
35 | ###
36 | ij_java_align_consecutive_assignments = false
37 | ij_java_align_consecutive_variable_declarations = false
38 | ij_java_align_group_field_declarations = false
39 | ij_java_align_multiline_annotation_parameters = false
40 | ij_java_align_multiline_array_initializer_expression = false
41 | ij_java_align_multiline_assignment = false
42 | ij_java_align_multiline_binary_operation = true
43 | ij_java_align_multiline_chained_methods = false
44 | ij_java_align_multiline_extends_list = false
45 | ij_java_align_multiline_for = true
46 | ij_java_align_multiline_method_parentheses = false
47 | ij_java_align_multiline_parameters = true
48 | ij_java_align_multiline_parameters_in_calls = false
49 | ij_java_align_multiline_parenthesized_expression = false
50 | ij_java_align_multiline_records = true
51 | ij_java_align_multiline_resources = true
52 | ij_java_align_multiline_ternary_operation = false
53 | ij_java_align_multiline_text_blocks = false
54 | ij_java_align_multiline_throws_list = false
55 | ij_java_align_subsequent_simple_methods = false
56 | ij_java_align_throws_keyword = false
57 | ij_java_annotation_parameter_wrap = off
58 | ij_java_array_initializer_new_line_after_left_brace = false
59 | ij_java_array_initializer_right_brace_on_new_line = false
60 | ij_java_array_initializer_wrap = off
61 | ij_java_assert_statement_colon_on_next_line = false
62 | ij_java_assert_statement_wrap = off
63 | ij_java_assignment_wrap = off
64 | ij_java_binary_operation_sign_on_next_line = false
65 | ij_java_binary_operation_wrap = off
66 | ij_java_blank_lines_after_anonymous_class_header = 0
67 | ij_java_blank_lines_after_class_header = 0
68 | ij_java_blank_lines_after_imports = 1
69 | ij_java_blank_lines_after_package = 1
70 | ij_java_blank_lines_around_class = 1
71 | ij_java_blank_lines_around_field = 0
72 | ij_java_blank_lines_around_field_in_interface = 0
73 | ij_java_blank_lines_around_initializer = 1
74 | ij_java_blank_lines_around_method = 1
75 | ij_java_blank_lines_around_method_in_interface = 1
76 | ij_java_blank_lines_before_class_end = 0
77 | ij_java_blank_lines_before_imports = 1
78 | ij_java_blank_lines_before_method_body = 0
79 | ij_java_blank_lines_before_package = 0
80 | ij_java_block_brace_style = end_of_line
81 | ij_java_block_comment_at_first_column = true
82 | ij_java_call_parameters_new_line_after_left_paren = false
83 | ij_java_call_parameters_right_paren_on_new_line = false
84 | ij_java_call_parameters_wrap = off
85 | ij_java_case_statement_on_separate_line = true
86 | ij_java_catch_on_new_line = false
87 | ij_java_class_annotation_wrap = split_into_lines
88 | ij_java_class_brace_style = end_of_line
89 | ij_java_class_names_in_javadoc = 1
90 | ij_java_do_not_indent_top_level_class_members = false
91 | ij_java_do_not_wrap_after_single_annotation = false
92 | ij_java_do_while_brace_force = never
93 | ij_java_doc_add_blank_line_after_description = true
94 | ij_java_doc_add_blank_line_after_param_comments = false
95 | ij_java_doc_add_blank_line_after_return = false
96 | ij_java_doc_add_p_tag_on_empty_lines = true
97 | ij_java_doc_align_exception_comments = true
98 | ij_java_doc_align_param_comments = true
99 | ij_java_doc_do_not_wrap_if_one_line = false
100 | ij_java_doc_enable_formatting = true
101 | ij_java_doc_enable_leading_asterisks = true
102 | ij_java_doc_indent_on_continuation = false
103 | ij_java_doc_keep_empty_lines = true
104 | ij_java_doc_keep_empty_parameter_tag = true
105 | ij_java_doc_keep_empty_return_tag = true
106 | ij_java_doc_keep_empty_throws_tag = true
107 | ij_java_doc_keep_invalid_tags = true
108 | ij_java_doc_param_description_on_new_line = false
109 | ij_java_doc_preserve_line_breaks = false
110 | ij_java_doc_use_throws_not_exception_tag = true
111 | ij_java_else_on_new_line = false
112 | ij_java_entity_dd_suffix = EJB
113 | ij_java_entity_eb_suffix = Bean
114 | ij_java_entity_hi_suffix = Home
115 | ij_java_entity_lhi_prefix = Local
116 | ij_java_entity_lhi_suffix = Home
117 | ij_java_entity_li_prefix = Local
118 | ij_java_entity_pk_class = java.lang.String
119 | ij_java_entity_vo_suffix = VO
120 | ij_java_enum_constants_wrap = off
121 | ij_java_extends_keyword_wrap = off
122 | ij_java_extends_list_wrap = off
123 | ij_java_field_annotation_wrap = split_into_lines
124 | ij_java_finally_on_new_line = false
125 | ij_java_for_brace_force = never
126 | ij_java_for_statement_new_line_after_left_paren = false
127 | ij_java_for_statement_right_paren_on_new_line = false
128 | ij_java_for_statement_wrap = off
129 | ij_java_generate_final_locals = false
130 | ij_java_generate_final_parameters = false
131 | ij_java_if_brace_force = never
132 | ij_java_indent_case_from_switch = true
133 | ij_java_insert_inner_class_imports = false
134 | ij_java_insert_override_annotation = true
135 | ij_java_keep_blank_lines_before_right_brace = 2
136 | ij_java_keep_blank_lines_between_package_declaration_and_header = 2
137 | ij_java_keep_blank_lines_in_code = 2
138 | ij_java_keep_blank_lines_in_declarations = 2
139 | ij_java_keep_control_statement_in_one_line = true
140 | ij_java_keep_first_column_comment = true
141 | ij_java_keep_indents_on_empty_lines = false
142 | ij_java_keep_line_breaks = true
143 | ij_java_keep_multiple_expressions_in_one_line = false
144 | ij_java_keep_simple_blocks_in_one_line = false
145 | ij_java_keep_simple_classes_in_one_line = false
146 | ij_java_keep_simple_lambdas_in_one_line = false
147 | ij_java_keep_simple_methods_in_one_line = false
148 | ij_java_label_indent_absolute = false
149 | ij_java_label_indent_size = 0
150 | ij_java_lambda_brace_style = end_of_line
151 | ij_java_line_comment_add_space = false
152 | ij_java_line_comment_at_first_column = true
153 | ij_java_message_dd_suffix = EJB
154 | ij_java_message_eb_suffix = Bean
155 | ij_java_method_annotation_wrap = split_into_lines
156 | ij_java_method_brace_style = end_of_line
157 | ij_java_method_call_chain_wrap = off
158 | ij_java_method_parameters_new_line_after_left_paren = false
159 | ij_java_method_parameters_right_paren_on_new_line = false
160 | ij_java_method_parameters_wrap = off
161 | ij_java_modifier_list_wrap = false
162 | ij_java_new_line_after_lparen_in_record_header = false
163 | ij_java_parameter_annotation_wrap = off
164 | ij_java_parentheses_expression_new_line_after_left_paren = false
165 | ij_java_parentheses_expression_right_paren_on_new_line = false
166 | ij_java_place_assignment_sign_on_next_line = false
167 | ij_java_prefer_longer_names = true
168 | ij_java_prefer_parameters_wrap = false
169 | ij_java_record_components_wrap = normal
170 | ij_java_repeat_synchronized = true
171 | ij_java_replace_instanceof_and_cast = false
172 | ij_java_replace_null_check = true
173 | ij_java_replace_sum_lambda_with_method_ref = true
174 | ij_java_resource_list_new_line_after_left_paren = false
175 | ij_java_resource_list_right_paren_on_new_line = false
176 | ij_java_resource_list_wrap = off
177 | ij_java_rparen_on_new_line_in_record_header = false
178 | ij_java_session_dd_suffix = EJB
179 | ij_java_session_eb_suffix = Bean
180 | ij_java_session_hi_suffix = Home
181 | ij_java_session_lhi_prefix = Local
182 | ij_java_session_lhi_suffix = Home
183 | ij_java_session_li_prefix = Local
184 | ij_java_session_si_suffix = Service
185 | ij_java_space_after_closing_angle_bracket_in_type_argument = false
186 | ij_java_space_after_colon = true
187 | ij_java_space_after_comma = true
188 | ij_java_space_after_comma_in_type_arguments = true
189 | ij_java_space_after_for_semicolon = true
190 | ij_java_space_after_quest = true
191 | ij_java_space_after_type_cast = true
192 | ij_java_space_before_annotation_array_initializer_left_brace = false
193 | ij_java_space_before_annotation_parameter_list = false
194 | ij_java_space_before_array_initializer_left_brace = true
195 | ij_java_space_before_catch_keyword = true
196 | ij_java_space_before_catch_left_brace = true
197 | ij_java_space_before_catch_parentheses = true
198 | ij_java_space_before_class_left_brace = true
199 | ij_java_space_before_colon = true
200 | ij_java_space_before_colon_in_foreach = true
201 | ij_java_space_before_comma = false
202 | ij_java_space_before_do_left_brace = true
203 | ij_java_space_before_else_keyword = true
204 | ij_java_space_before_else_left_brace = true
205 | ij_java_space_before_finally_keyword = true
206 | ij_java_space_before_finally_left_brace = true
207 | ij_java_space_before_for_left_brace = true
208 | ij_java_space_before_for_parentheses = true
209 | ij_java_space_before_for_semicolon = false
210 | ij_java_space_before_if_left_brace = true
211 | ij_java_space_before_if_parentheses = true
212 | ij_java_space_before_method_call_parentheses = false
213 | ij_java_space_before_method_left_brace = true
214 | ij_java_space_before_method_parentheses = false
215 | ij_java_space_before_opening_angle_bracket_in_type_parameter = false
216 | ij_java_space_before_quest = true
217 | ij_java_space_before_switch_left_brace = true
218 | ij_java_space_before_switch_parentheses = true
219 | ij_java_space_before_synchronized_left_brace = true
220 | ij_java_space_before_synchronized_parentheses = true
221 | ij_java_space_before_try_left_brace = true
222 | ij_java_space_before_try_parentheses = true
223 | ij_java_space_before_type_parameter_list = false
224 | ij_java_space_before_while_keyword = true
225 | ij_java_space_before_while_left_brace = true
226 | ij_java_space_before_while_parentheses = true
227 | ij_java_space_inside_one_line_enum_braces = false
228 | ij_java_space_within_empty_array_initializer_braces = false
229 | ij_java_space_within_empty_method_call_parentheses = false
230 | ij_java_space_within_empty_method_parentheses = false
231 | ij_java_spaces_around_additive_operators = true
232 | ij_java_spaces_around_assignment_operators = true
233 | ij_java_spaces_around_bitwise_operators = true
234 | ij_java_spaces_around_equality_operators = true
235 | ij_java_spaces_around_lambda_arrow = true
236 | ij_java_spaces_around_logical_operators = true
237 | ij_java_spaces_around_method_ref_dbl_colon = false
238 | ij_java_spaces_around_multiplicative_operators = true
239 | ij_java_spaces_around_relational_operators = true
240 | ij_java_spaces_around_shift_operators = true
241 | ij_java_spaces_around_type_bounds_in_type_parameters = true
242 | ij_java_spaces_around_unary_operator = false
243 | ij_java_spaces_within_angle_brackets = false
244 | ij_java_spaces_within_annotation_parentheses = false
245 | ij_java_spaces_within_array_initializer_braces = true
246 | ij_java_spaces_within_braces = false
247 | ij_java_spaces_within_brackets = false
248 | ij_java_spaces_within_cast_parentheses = false
249 | ij_java_spaces_within_catch_parentheses = false
250 | ij_java_spaces_within_for_parentheses = false
251 | ij_java_spaces_within_if_parentheses = false
252 | ij_java_spaces_within_method_call_parentheses = false
253 | ij_java_spaces_within_method_parentheses = false
254 | ij_java_spaces_within_parentheses = false
255 | ij_java_spaces_within_switch_parentheses = false
256 | ij_java_spaces_within_synchronized_parentheses = false
257 | ij_java_spaces_within_try_parentheses = false
258 | ij_java_spaces_within_while_parentheses = false
259 | ij_java_special_else_if_treatment = true
260 | ij_java_subclass_name_suffix = Impl
261 | ij_java_ternary_operation_signs_on_next_line = false
262 | ij_java_ternary_operation_wrap = off
263 | ij_java_test_name_suffix = Test
264 | ij_java_throws_keyword_wrap = off
265 | ij_java_throws_list_wrap = off
266 | ij_java_use_external_annotations = false
267 | ij_java_use_fq_class_names = false
268 | ij_java_use_relative_indents = false
269 | ij_java_variable_annotation_wrap = off
270 | ij_java_visibility = public
271 | ij_java_while_brace_force = never
272 | ij_java_while_on_new_line = false
273 | ij_java_wrap_comments = false
274 | ij_java_wrap_first_method_in_call_chain = false
275 | ij_java_wrap_long_lines = false
276 |
--------------------------------------------------------------------------------
/.github/workflows/gradle-wrapper-validation.yml:
--------------------------------------------------------------------------------
1 | name: "Validate Gradle Wrapper"
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | validation:
11 | name: "Validation"
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: gradle/wrapper-validation-action@v3
16 |
--------------------------------------------------------------------------------
/.github/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | java: [ '11', '17', '21', '24' ]
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Setup Java
19 | uses: actions/setup-java@v1
20 | with:
21 | java-version: ${{ matrix.java }}
22 | - name: Build with Gradle
23 | run: ./gradlew build
24 | - name: Archive test reports
25 | if: failure()
26 | uses: actions/upload-artifact@v4
27 | with:
28 | name: Gradle Test Reports Java ${{ matrix.java }}
29 | path: build/reports/tests/test
30 |
31 | publishCoverage:
32 | runs-on: ubuntu-latest
33 |
34 | steps:
35 | - uses: actions/checkout@v2
36 | - name: Setup Java
37 | uses: actions/setup-java@v1
38 | with:
39 | java-version: 11
40 | - name: Build with Gradle
41 | run: ./gradlew jacocoTestReport
42 | - name: Upload coverage reports to Codecov
43 | uses: codecov/codecov-action@v4.0.1
44 | with:
45 | token: ${{ secrets.CODECOV_TOKEN }}
46 | files: ./build/reports/jacoco/test/jacocoTestReport.xml
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binary files
2 | *.class
3 |
4 | # Backup files
5 | *~
6 | *.bak
7 | *.sav
8 | *.bck
9 |
10 | # Editor temp files
11 | .*.swp
12 | .swp
13 |
14 | # Gradle
15 | .gradle
16 | /caches
17 | /native
18 | gradle/wrapper/dists/
19 |
20 | # Build output directories
21 | build
22 |
23 | # Eclipse specific files/directories
24 | .classpath
25 | .project
26 | .settings
27 | .metadata
28 | bin
29 | classes
30 | .recommenders/
31 |
32 | # Jetbrains IDEA
33 | .idea
34 | *.iml
35 | *.ipr
36 | *.iws
37 | *.ids
38 | out/
39 |
40 | # Profiler and heap dumps
41 | *.jps
42 | *.hprof
43 |
44 | # Miscellaneous
45 | nohup.out
46 | *.log
47 |
48 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
49 | hs_err_pid*
50 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2016 cronn GmbH
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/cronn/ssh-proxy/actions)
2 | [](http://maven-badges.herokuapp.com/maven-central/de.cronn/ssh-proxy)
3 | [](http://www.apache.org/licenses/LICENSE-2.0)
4 | [](https://codecov.io/gh/cronn/ssh-proxy)
5 | [](https://github.com/cronn/ssh-proxy/actions/workflows/gradle-wrapper-validation.yml)
6 |
7 | # SSH Proxy #
8 |
9 | A pure Java implementation for SSH port tunneling that is able to understand
10 | OpenSSH configurations which involve multiple hops to reach a target host.
11 | This library essentially combines [JSch][jsch] with the ability to understand
12 | `ProxyJump` or `ProxyCommand` configurations in your local `~/.ssh/config`
13 | file.
14 |
15 | See our [blog post "Tunneling Basics – Part II: OpenSSH Configuration Files"][blog-post-ssh-configuration-files] for
16 | some context where and how this library can be used.
17 |
18 | ## Usage ##
19 | Add the following Maven dependency to your project:
20 |
21 | ```xml
22 |
23 | de.cronn
24 | ssh-proxy
25 | 1.6
26 |
27 | ```
28 |
29 | ### Example ###
30 |
31 | ```
32 | # cat ~/.ssh/config
33 |
34 | Host jumpHost1
35 | User my-user
36 | HostName jumphost1.my.domain
37 |
38 | Host jumpHost2
39 | User other-user
40 | ProxyJump jumpHost1
41 |
42 | Host targetHost
43 | ProxyCommand ssh -q -W %h:%p jumpHost2
44 | ```
45 |
46 | ```java
47 | try (SshProxy sshProxy = new SshProxy()) {
48 | int targetPort = 1234;
49 | int randomLocalPort = sshProxy.connect("jumpHost2", "targetHost", targetPort);
50 | try (Socket s = new Socket(SshProxy.LOCALHOST, randomLocalPort)) {
51 | OutputStream out = s.getOutputStream();
52 | InputStream in = s.getInputStream();
53 | // ...
54 | }
55 | }
56 | ```
57 |
58 | ## Dependencies ##
59 |
60 | - Java 11+
61 | - [JSch (with JZlib)][jsch]
62 |
63 | [jsch]: http://www.jcraft.com/jsch/
64 | [blog-post-ssh-configuration-files]: https://blog.cronn.de/en/ssh/configuration/2021/08/16/ssh-configuration.html
65 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | mavenCentral()
4 | }
5 | dependencyLocking {
6 | lockAllConfigurations()
7 | }
8 | }
9 |
10 | plugins {
11 | id 'java-library'
12 | id 'maven-publish'
13 | id 'signing'
14 | id 'jacoco'
15 |
16 | id 'org.sonarqube' version 'latest.release'
17 | }
18 |
19 | group = 'de.cronn'
20 |
21 | version = "1.6"
22 |
23 | if (System.env.BUILD_NUMBER) {
24 | version = "${version}-SNAPSHOT-b${System.env.BUILD_NUMBER}"
25 | }
26 |
27 | java {
28 | sourceCompatibility = JavaVersion.VERSION_11
29 | targetCompatibility = JavaVersion.VERSION_11
30 | }
31 |
32 | tasks.withType(JavaCompile) {
33 | options.encoding = 'UTF-8'
34 | options.compilerArgs.addAll(['-Xlint:all', '-Werror'])
35 | }
36 |
37 | repositories {
38 | mavenCentral()
39 | }
40 |
41 | jacocoTestReport {
42 | reports {
43 | html.required = false
44 | xml.required = true
45 | csv.required = false
46 | }
47 | dependsOn test
48 | }
49 |
50 | wrapper {
51 | gradleVersion = "8.14.1"
52 | distributionType = Wrapper.DistributionType.ALL
53 | }
54 |
55 | task sourcesJar(type: Jar, dependsOn: classes) {
56 | archiveClassifier = 'sources'
57 | from sourceSets.main.allSource
58 | }
59 |
60 | task javadocJar(type: Jar, dependsOn: javadoc) {
61 | archiveClassifier = 'javadoc'
62 | from javadoc.destinationDir
63 | }
64 |
65 | publishing {
66 | publications {
67 | mavenJava(MavenPublication) {
68 | groupId = project.group
69 | artifactId = project.name
70 | version = project.version
71 | pom {
72 | name = project.name
73 | description = 'Pure Java implementation to tunnel to TCP endpoints through SSH'
74 | url = 'https://github.com/cronn/ssh-proxy'
75 |
76 | licenses {
77 | license {
78 | name = "The Apache Software License, Version 2.0"
79 | url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
80 | distribution = "repo"
81 | }
82 | }
83 |
84 | developers {
85 | developer {
86 | id = "benedikt.waldvogel"
87 | name = "Benedikt Waldvogel"
88 | email = "benedikt.waldvogel@cronn.de"
89 | }
90 | }
91 |
92 | scm {
93 | url = "https://github.com/cronn/ssh-proxy"
94 | }
95 | }
96 |
97 | from components.java
98 |
99 | artifact sourcesJar
100 | artifact javadocJar
101 |
102 | versionMapping {
103 | usage('java-api') {
104 | fromResolutionOf('runtimeClasspath')
105 | }
106 | usage('java-runtime') {
107 | fromResolutionResult()
108 | }
109 | }
110 | }
111 | }
112 | repositories {
113 | maven {
114 | url = "https://oss.sonatype.org/service/local/staging/deploy/maven2"
115 | credentials {
116 | username = project.hasProperty('nexusUsername') ? project.property('nexusUsername') : System.getenv('NEXUS_USERNAME')
117 | password = project.hasProperty('nexusPassword') ? project.property('nexusPassword') : System.getenv('NEXUS_PASSWORD')
118 | }
119 | }
120 | }
121 | }
122 |
123 | signing {
124 | useGpgCmd()
125 | sign publishing.publications.mavenJava
126 | }
127 |
128 | test {
129 | useJUnitPlatform()
130 |
131 | maxHeapSize = "256m"
132 | }
133 |
134 | dependencies {
135 | implementation "org.slf4j:slf4j-api:latest.release"
136 |
137 | implementation "com.jcraft:jsch:latest.release"
138 | runtimeOnly "com.jcraft:jzlib:latest.release"
139 |
140 | testImplementation 'org.junit.jupiter:junit-jupiter-api:latest.release'
141 | testImplementation 'org.junit.jupiter:junit-jupiter-params:latest.release'
142 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:latest.release'
143 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
144 |
145 | testImplementation 'org.assertj:assertj-core:latest.release'
146 | testImplementation "org.mockito:mockito-core:latest.release"
147 | testImplementation "org.mockito:mockito-junit-jupiter:latest.release"
148 |
149 | testImplementation "org.apache.sshd:sshd-core:latest.release"
150 | testRuntimeOnly "org.bouncycastle:bcprov-jdk15on:latest.release"
151 |
152 | testRuntimeOnly "ch.qos.logback:logback-classic:latest.release"
153 | testRuntimeOnly "org.slf4j:jcl-over-slf4j:latest.release"
154 |
155 | components.all { ComponentMetadataDetails details ->
156 | if (details.id.version =~ /(?i).+(-|\.)(CANDIDATE|RC|BETA|ALPHA|M\d+).*/) {
157 | details.status = 'milestone'
158 | }
159 | }
160 | }
161 |
162 | dependencyLocking {
163 | lockAllConfigurations()
164 | }
165 |
--------------------------------------------------------------------------------
/buildscript-gradle.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 | com.google.code.gson:gson:2.11.0=classpath
5 | com.google.errorprone:error_prone_annotations:2.27.0=classpath
6 | com.squareup.okhttp3:logging-interceptor:4.12.0=classpath
7 | com.squareup.okhttp3:okhttp-urlconnection:4.12.0=classpath
8 | com.squareup.okhttp3:okhttp:4.12.0=classpath
9 | com.squareup.okio:okio-jvm:3.6.0=classpath
10 | com.squareup.okio:okio:3.6.0=classpath
11 | commons-codec:commons-codec:1.17.1=classpath
12 | commons-io:commons-io:2.16.1=classpath
13 | io.github.hakky54:sslcontext-kickstart:8.3.6=classpath
14 | org.apache.commons:commons-compress:1.27.1=classpath
15 | org.apache.commons:commons-lang3:3.17.0=classpath
16 | org.bouncycastle:bcprov-jdk18on:1.78.1=classpath
17 | org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10=classpath
18 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=classpath
19 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=classpath
20 | org.jetbrains.kotlin:kotlin-stdlib:1.9.10=classpath
21 | org.jetbrains:annotations:13.0=classpath
22 | org.slf4j:slf4j-api:2.0.13=classpath
23 | org.sonarqube:org.sonarqube.gradle.plugin:6.2.0.5505=classpath
24 | org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:6.2.0.5505=classpath
25 | org.sonarsource.scanner.lib:sonar-scanner-java-library-batch-interface:3.3.1.450=classpath
26 | org.sonarsource.scanner.lib:sonar-scanner-java-library:3.3.1.450=classpath
27 | empty=
28 |
--------------------------------------------------------------------------------
/gradle.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 | ch.qos.logback:logback-classic:1.5.18=testRuntimeClasspath
5 | ch.qos.logback:logback-core:1.5.18=testRuntimeClasspath
6 | com.jcraft:jsch:0.1.55=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
7 | com.jcraft:jzlib:1.1.3=runtimeClasspath,testRuntimeClasspath
8 | net.bytebuddy:byte-buddy-agent:1.17.5=testCompileClasspath,testRuntimeClasspath
9 | net.bytebuddy:byte-buddy:1.17.5=testCompileClasspath,testRuntimeClasspath
10 | org.apache.sshd:sshd-common:2.15.0=testCompileClasspath,testRuntimeClasspath
11 | org.apache.sshd:sshd-core:2.15.0=testCompileClasspath,testRuntimeClasspath
12 | org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
13 | org.assertj:assertj-core:3.27.3=testCompileClasspath,testRuntimeClasspath
14 | org.bouncycastle:bcprov-jdk15on:1.70=testRuntimeClasspath
15 | org.jacoco:org.jacoco.agent:0.8.13=jacocoAgent,jacocoAnt
16 | org.jacoco:org.jacoco.ant:0.8.13=jacocoAnt
17 | org.jacoco:org.jacoco.core:0.8.13=jacocoAnt
18 | org.jacoco:org.jacoco.report:0.8.13=jacocoAnt
19 | org.junit.jupiter:junit-jupiter-api:5.12.2=testCompileClasspath,testRuntimeClasspath
20 | org.junit.jupiter:junit-jupiter-engine:5.12.2=testRuntimeClasspath
21 | org.junit.jupiter:junit-jupiter-params:5.12.2=testCompileClasspath,testRuntimeClasspath
22 | org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testRuntimeClasspath
23 | org.junit.platform:junit-platform-engine:1.12.2=testRuntimeClasspath
24 | org.junit.platform:junit-platform-launcher:1.12.2=testRuntimeClasspath
25 | org.junit:junit-bom:5.12.2=testCompileClasspath,testRuntimeClasspath
26 | org.mockito:mockito-core:5.18.0=testCompileClasspath,testRuntimeClasspath
27 | org.mockito:mockito-junit-jupiter:5.18.0=testCompileClasspath,testRuntimeClasspath
28 | org.objenesis:objenesis:3.3=testRuntimeClasspath
29 | org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
30 | org.ow2.asm:asm-commons:9.8=jacocoAnt
31 | org.ow2.asm:asm-tree:9.8=jacocoAnt
32 | org.ow2.asm:asm:9.8=jacocoAnt
33 | org.slf4j:jcl-over-slf4j:1.7.36=testCompileClasspath
34 | org.slf4j:jcl-over-slf4j:2.0.17=testRuntimeClasspath
35 | org.slf4j:slf4j-api:2.0.17=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
36 | empty=annotationProcessor,testAnnotationProcessor
37 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cronn/ssh-proxy/a93f04d23a900badda7f437bffa441d90b081cce/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH="\\\"\\\""
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | if ! command -v java >/dev/null 2>&1
137 | then
138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
139 |
140 | Please set the JAVA_HOME variable in your environment to match the
141 | location of your Java installation."
142 | fi
143 | fi
144 |
145 | # Increase the maximum file descriptors if we can.
146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
147 | case $MAX_FD in #(
148 | max*)
149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
150 | # shellcheck disable=SC2039,SC3045
151 | MAX_FD=$( ulimit -H -n ) ||
152 | warn "Could not query maximum file descriptor limit"
153 | esac
154 | case $MAX_FD in #(
155 | '' | soft) :;; #(
156 | *)
157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
158 | # shellcheck disable=SC2039,SC3045
159 | ulimit -n "$MAX_FD" ||
160 | warn "Could not set maximum file descriptor limit to $MAX_FD"
161 | esac
162 | fi
163 |
164 | # Collect all arguments for the java command, stacking in reverse order:
165 | # * args from the command line
166 | # * the main class name
167 | # * -classpath
168 | # * -D...appname settings
169 | # * --module-path (only if needed)
170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
171 |
172 | # For Cygwin or MSYS, switch paths to Windows format before running java
173 | if "$cygwin" || "$msys" ; then
174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
176 |
177 | JAVACMD=$( cygpath --unix "$JAVACMD" )
178 |
179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
180 | for arg do
181 | if
182 | case $arg in #(
183 | -*) false ;; # don't mess with options #(
184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
185 | [ -e "$t" ] ;; #(
186 | *) false ;;
187 | esac
188 | then
189 | arg=$( cygpath --path --ignore --mixed "$arg" )
190 | fi
191 | # Roll the args list around exactly as many times as the number of
192 | # args, so each arg winds up back in the position where it started, but
193 | # possibly modified.
194 | #
195 | # NB: a `for` loop captures its iteration list before it begins, so
196 | # changing the positional parameters here affects neither the number of
197 | # iterations, nor the values presented in `arg`.
198 | shift # remove old arg
199 | set -- "$@" "$arg" # push replacement arg
200 | done
201 | fi
202 |
203 |
204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
206 |
207 | # Collect all arguments for the java command:
208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
209 | # and any embedded shellness will be escaped.
210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
211 | # treated as '${Hostname}' itself on the command line.
212 |
213 | set -- \
214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
215 | -classpath "$CLASSPATH" \
216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
217 | "$@"
218 |
219 | # Stop when "xargs" is not available.
220 | if ! command -v xargs >/dev/null 2>&1
221 | then
222 | die "xargs is not available"
223 | fi
224 |
225 | # Use "xargs" to parse quoted args.
226 | #
227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
228 | #
229 | # In Bash we could simply go:
230 | #
231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
232 | # set -- "${ARGS[@]}" "$@"
233 | #
234 | # but POSIX shell has neither arrays nor command substitution, so instead we
235 | # post-process each arg (as a line of input to sed) to backslash-escape any
236 | # character that might be a shell metacharacter, then use eval to reverse
237 | # that process (while maintaining the separation between arguments), and wrap
238 | # the whole thing up as a single "set" statement.
239 | #
240 | # This will of course break if any of these variables contains a newline or
241 | # an unmatched quote.
242 | #
243 |
244 | eval "set -- $(
245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
246 | xargs -n1 |
247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
248 | tr '\n' ' '
249 | )" '"$@"'
250 |
251 | exec "$JAVACMD" "$@"
252 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ./gradlew --no-daemon clean build publish
4 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = "ssh-proxy"
2 |
--------------------------------------------------------------------------------
/src/main/java/de/cronn/proxy/ssh/HostKeyType.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | import com.jcraft.jsch.HostKey;
7 |
8 | import de.cronn.proxy.ssh.util.Assert;
9 |
10 | public enum HostKeyType {
11 |
12 | SSH_DSS(HostKey.SSHDSS, "ssh-dss"), //
13 | SSH_RSA(HostKey.SSHRSA, "ssh-rsa"), //
14 | ECDSA256(HostKey.ECDSA256, "ecdsa-sha2-nistp256"), //
15 | ECDSA384(HostKey.ECDSA384, "ecdsa-sha2-nistp384"), //
16 | ECDSA521(HostKey.ECDSA521, "ecdsa-sha2-nistp521"), //
17 | ;
18 |
19 | private final int type;
20 | private final String typeString;
21 |
22 | private static final Map valuesByTypeString = new HashMap<>();
23 |
24 | static {
25 | for (HostKeyType hostKeyType : HostKeyType.values()) {
26 | HostKeyType oldValue = valuesByTypeString.put(hostKeyType.getTypeString(), hostKeyType);
27 | Assert.isNull(oldValue, "Duplicate value for " + hostKeyType.getTypeString());
28 | }
29 | }
30 |
31 | HostKeyType(int type, String typeString) {
32 | this.type = type;
33 | this.typeString = typeString;
34 | }
35 |
36 | public int getType() {
37 | return type;
38 | }
39 |
40 | public String getTypeString() {
41 | return typeString;
42 | }
43 |
44 | public static HostKeyType byTypeString(String typeString) {
45 | HostKeyType hostKeyType = valuesByTypeString.get(typeString);
46 | Assert.notNull(hostKeyType, "No hostKeyType found for " + typeString);
47 | return hostKeyType;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/de/cronn/proxy/ssh/JSchHelper.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Arrays;
5 | import java.util.Comparator;
6 | import java.util.List;
7 |
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 |
11 | import com.jcraft.jsch.JSch;
12 |
13 | import de.cronn.proxy.ssh.util.Utils;
14 |
15 | public final class JSchHelper {
16 |
17 | private static final Logger log = LoggerFactory.getLogger(JSchHelper.class);
18 |
19 | private static final String SERVER_HOST_KEY_SEPARATOR = ",";
20 |
21 | private static final String JSCH_CONFIG_KEY_SERVER_HOST_KEY = "server_host_key";
22 | private static final String JSCH_CONFIG_KEY_PREFERRED_AUTHENTICATIONS = "PreferredAuthentications";
23 |
24 | private static class HostKeyComparator implements Comparator {
25 |
26 | private final List sortOrder;
27 |
28 | protected HostKeyComparator(List sortOrder) {
29 | this.sortOrder = sortOrder;
30 | }
31 |
32 | protected HostKeyComparator(HostKeyType... sortOrder) {
33 | this(Arrays.asList(sortOrder));
34 | }
35 |
36 | @Override
37 | public int compare(HostKeyType a, HostKeyType b) {
38 | int indexA = sortOrder.indexOf(a);
39 | int indexB = sortOrder.indexOf(b);
40 | return Integer.compare(indexA, indexB);
41 | }
42 | }
43 |
44 | private static final Comparator CMP_PREFER_ECDSA = new HostKeyComparator(
45 | HostKeyType.ECDSA256, HostKeyType.ECDSA384, HostKeyType.ECDSA521, HostKeyType.SSH_RSA, HostKeyType.SSH_DSS
46 | );
47 |
48 | private static final Comparator CMP_PREFER_RSA = new HostKeyComparator(
49 | HostKeyType.SSH_RSA, HostKeyType.ECDSA256, HostKeyType.ECDSA384, HostKeyType.ECDSA521, HostKeyType.SSH_DSS
50 | );
51 |
52 | public enum ServerHostKeySortOrder {
53 | PREFER_ECDSA,
54 | PREFER_RSA,
55 | }
56 |
57 | private JSchHelper() {
58 | }
59 |
60 | protected static void reconfigureServerHostKeyOrder(ServerHostKeySortOrder hostKeySortOrder) {
61 | List serverHostKeys = new ArrayList<>(getServerHostKeys());
62 | if (hostKeySortOrder == ServerHostKeySortOrder.PREFER_ECDSA) {
63 | serverHostKeys.sort(CMP_PREFER_ECDSA);
64 | } else if (hostKeySortOrder == ServerHostKeySortOrder.PREFER_RSA) {
65 | serverHostKeys.sort(CMP_PREFER_RSA);
66 | } else {
67 | throw new IllegalArgumentException("Unknown host key sort order: " + hostKeySortOrder);
68 | }
69 |
70 | if (!getServerHostKeys().equals(serverHostKeys)) {
71 | log.debug("changing server host key order to: {}", serverHostKeys);
72 |
73 | List serverHostKeyNames = new ArrayList<>();
74 | for (HostKeyType serverHostKey : serverHostKeys) {
75 | serverHostKeyNames.add(serverHostKey.getTypeString());
76 | }
77 |
78 | String newHostKeyOrder = Utils.join(serverHostKeyNames, SERVER_HOST_KEY_SEPARATOR);
79 | JSch.setConfig(JSCH_CONFIG_KEY_SERVER_HOST_KEY, newHostKeyOrder);
80 | }
81 | }
82 |
83 | protected static void reconfigurePreferredAuthentications() {
84 | JSch.setConfig(JSCH_CONFIG_KEY_PREFERRED_AUTHENTICATIONS, "publickey");
85 | }
86 |
87 | protected static void registerLogger() {
88 | JSch.setLogger(new JSchSlf4JLogger());
89 | }
90 |
91 | protected static List getServerHostKeys() {
92 | String serverHostKey = JSch.getConfig(JSCH_CONFIG_KEY_SERVER_HOST_KEY);
93 |
94 | List hostKeyTypes = new ArrayList<>();
95 | for (String hostKeyString : serverHostKey.split(SERVER_HOST_KEY_SEPARATOR)) {
96 | hostKeyTypes.add(HostKeyType.byTypeString(hostKeyString));
97 | }
98 | return hostKeyTypes;
99 | }
100 |
101 | public static void configureGlobalSettings() {
102 | reconfigurePreferredAuthentications();
103 | registerLogger();
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/main/java/de/cronn/proxy/ssh/JSchSlf4JLogger.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import org.slf4j.LoggerFactory;
4 |
5 | import com.jcraft.jsch.Logger;
6 |
7 | public class JSchSlf4JLogger implements Logger {
8 |
9 | private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JSchSlf4JLogger.class);
10 |
11 | @Override
12 | public boolean isEnabled(int level) {
13 | switch (level) {
14 | case DEBUG:
15 | return logger.isTraceEnabled();
16 | case INFO:
17 | return logger.isTraceEnabled();
18 | case WARN:
19 | return logger.isWarnEnabled();
20 | case ERROR:
21 | case FATAL:
22 | return logger.isErrorEnabled();
23 | default:
24 | throw new IllegalArgumentException("Unknown log level: " + level);
25 | }
26 | }
27 |
28 | @Override
29 | public void log(int level, String message) {
30 | switch (level) {
31 | case DEBUG:
32 | logger.trace(message);
33 | break;
34 | case INFO:
35 | logger.trace(message);
36 | break;
37 | case WARN:
38 | logger.warn(message);
39 | break;
40 | case ERROR:
41 | logger.error(message);
42 | break;
43 | case FATAL:
44 | logger.error("FATAL: {}", message);
45 | break;
46 | default:
47 | throw new IllegalArgumentException("Unknown log level: " + level);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/de/cronn/proxy/ssh/ProxyAwareHostnameVerifier.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import javax.net.ssl.HostnameVerifier;
4 | import javax.net.ssl.SSLSession;
5 |
6 | public class ProxyAwareHostnameVerifier implements HostnameVerifier {
7 |
8 | private final HostnameVerifier hostnameVerifier;
9 | private final String originalHost;
10 |
11 | public ProxyAwareHostnameVerifier(HostnameVerifier hostnameVerifier, String originalHost) {
12 | this.hostnameVerifier = hostnameVerifier;
13 | this.originalHost = originalHost;
14 | }
15 |
16 | @Override
17 | public boolean verify(String host, SSLSession sslSession) {
18 | return hostnameVerifier.verify(originalHost, sslSession);
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/de/cronn/proxy/ssh/SshConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import static de.cronn.proxy.ssh.SshProxy.*;
4 |
5 | import java.io.IOException;
6 | import java.nio.file.Files;
7 | import java.nio.file.Path;
8 | import java.nio.file.Paths;
9 | import java.util.ArrayList;
10 | import java.util.EnumSet;
11 | import java.util.List;
12 |
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 |
16 | import com.jcraft.jsch.ConfigRepository;
17 | import com.jcraft.jsch.ConfigRepository.Config;
18 | import com.jcraft.jsch.HostKey;
19 | import com.jcraft.jsch.HostKeyRepository;
20 | import com.jcraft.jsch.JSch;
21 | import com.jcraft.jsch.JSchException;
22 | import com.jcraft.jsch.OpenSSHConfig;
23 | import com.jcraft.jsch.Session;
24 |
25 | import de.cronn.proxy.ssh.JSchHelper.ServerHostKeySortOrder;
26 | import de.cronn.proxy.ssh.util.Assert;
27 | import de.cronn.proxy.ssh.util.Utils;
28 |
29 | public class SshConfiguration {
30 |
31 | private static final Logger log = LoggerFactory.getLogger(SshConfiguration.class);
32 |
33 | private static final String SSH_CONFIG_KEY_IDENTITY_FILE = "IdentityFile";
34 | private static final String SSH_CONFIG_KEY_PROXY_COMMAND = "ProxyCommand";
35 | private static final String SSH_CONFIG_KEY_PROXY_JUMP = "ProxyJump";
36 |
37 | private static final String USER_NAME = System.getProperty("user.name");
38 |
39 | protected static final int SSH_DEFAULT_PORT = 22;
40 |
41 | private final JSch jsch = new JSch();
42 | private final ConfigRepository configRepository;
43 |
44 | public SshConfiguration(ConfigRepository configRepository) throws JSchException {
45 | JSchHelper.configureGlobalSettings();
46 |
47 | this.configRepository = configRepository;
48 | jsch.setConfigRepository(this.configRepository);
49 |
50 | Assert.isTrue(Files.isRegularFile(getLocalSshKnownHostsPath()), getLocalSshKnownHostsPath() + " does not exist");
51 | jsch.setKnownHosts(getLocalSshKnownHostsPath().toString());
52 | }
53 |
54 | public static SshConfiguration getConfiguration() throws IOException, JSchException {
55 | Assert.isTrue(Files.isRegularFile(getLocalSshConfigPath()), getLocalSshConfigPath() + " does not exist");
56 | return new SshConfiguration(OpenSSHConfig.parseFile(getLocalSshConfigPath().toString()));
57 | }
58 |
59 | private static String getUserHome() {
60 | return System.getProperty("user.home");
61 | }
62 |
63 | private static Path getSshHome() {
64 | return Paths.get(getUserHome(), ".ssh");
65 | }
66 |
67 | public static Path getLocalSshConfigPath() {
68 | return getSshHome().resolve("config");
69 | }
70 |
71 | private static Path getLocalSshKnownHostsPath() {
72 | return getSshHome().resolve("known_hosts");
73 | }
74 |
75 | private static Path getDefaultSshKeyPath() {
76 | return getSshHome().resolve("id_rsa");
77 | }
78 |
79 | private Config getHostConfig(String host) {
80 | return configRepository.getConfig(host);
81 | }
82 |
83 | public SshProxyConfig getProxyConfiguration(String host) {
84 | Config config = getHostConfig(host);
85 | String sshProxyCommand = config.getValue(SSH_CONFIG_KEY_PROXY_COMMAND);
86 | if (sshProxyCommand != null) {
87 | return SshProxyConfig.parseProxyCommand(sshProxyCommand, host, config);
88 | }
89 |
90 | String sshProxyJump = config.getValue(SSH_CONFIG_KEY_PROXY_JUMP);
91 | if (sshProxyJump != null) {
92 | return SshProxyConfig.parseProxyJump(sshProxyJump, host, config);
93 | }
94 |
95 | return null;
96 | }
97 |
98 | void addIdentity(String host) throws JSchException {
99 | Config hostConfig = getHostConfig(host);
100 | String identityFile = hostConfig.getValue(SSH_CONFIG_KEY_IDENTITY_FILE);
101 | if (identityFile == null) {
102 | identityFile = getDefaultSshKeyPath().toString();
103 | }
104 | log.debug("using SSH key file {}", identityFile);
105 | jsch.addIdentity(identityFile);
106 | }
107 |
108 | public String getHostUser(String host) {
109 | Config hostConfig = getHostConfig(host);
110 | String hostUser = hostConfig.getUser();
111 | if (hostUser == null) {
112 | return USER_NAME;
113 | } else {
114 | return hostUser;
115 | }
116 | }
117 |
118 | public String getHostName(String host) {
119 | Config hostConfig = getHostConfig(host);
120 | String hostname = hostConfig.getHostname();
121 | if (hostname == null) {
122 | return host;
123 | } else {
124 | return hostname;
125 | }
126 | }
127 |
128 | private void configureHostKeyOrder(String host) {
129 | Config hostConfig = getHostConfig(host);
130 | ServerHostKeySortOrder hostKeySortOrder = guessPreferredHostKeySortOrder(host, hostConfig);
131 | JSchHelper.reconfigureServerHostKeyOrder(hostKeySortOrder);
132 | }
133 |
134 | private ServerHostKeySortOrder guessPreferredHostKeySortOrder(String jumpHostName, Config hostConfig) {
135 | HostKeyRepository hostKeyRepository = jsch.getHostKeyRepository();
136 |
137 | List potentialHostNames = collectPotentialHostnames(jumpHostName, hostConfig);
138 |
139 | for (String hostname : potentialHostNames) {
140 | for (HostKeyType hostKeyType : EnumSet.of(HostKeyType.ECDSA256, HostKeyType.ECDSA384, HostKeyType.ECDSA521)) {
141 | HostKey[] hostKeys = hostKeyRepository.getHostKey(hostname, hostKeyType.getTypeString());
142 | if (Utils.isNotEmpty(hostKeys)) {
143 | return ServerHostKeySortOrder.PREFER_ECDSA;
144 | }
145 | }
146 |
147 | for (HostKeyType hostKeyType : EnumSet.of(HostKeyType.SSH_RSA)) {
148 | HostKey[] hostKeys = hostKeyRepository.getHostKey(hostname, hostKeyType.getTypeString());
149 | if (Utils.isNotEmpty(hostKeys)) {
150 | return ServerHostKeySortOrder.PREFER_RSA;
151 | }
152 | }
153 | }
154 |
155 | String hostDescription = jumpHostName;
156 | if (hostConfig.getHostname() != null) {
157 | hostDescription += " (" + hostConfig.getHostname() + ")";
158 | }
159 | throw new IllegalArgumentException("Found no host key for " + hostDescription + " in " + hostKeyRepository.getKnownHostsRepositoryID());
160 | }
161 |
162 | private List collectPotentialHostnames(String jumpHostName, Config hostConfig) {
163 | List potentialHostNames = new ArrayList<>();
164 | potentialHostNames.add(jumpHostName);
165 |
166 | String configHostName = hostConfig.getHostname();
167 | if (configHostName != null) {
168 | if (hostConfig.getPort() > 0 && hostConfig.getPort() != SSH_DEFAULT_PORT) {
169 | configHostName = "[" + configHostName + "]:" + hostConfig.getPort();
170 | }
171 | potentialHostNames.add(configHostName);
172 | }
173 | return potentialHostNames;
174 | }
175 |
176 | public Session openSession(String host) throws JSchException {
177 | configureHostKeyOrder(host);
178 | return jsch.getSession(host);
179 | }
180 |
181 | public Session openSession(String hostUser, String jumpHost, int jumpPort) throws JSchException {
182 | configureHostKeyOrder(jumpHost);
183 | return jsch.getSession(hostUser, LOCALHOST, jumpPort);
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/main/java/de/cronn/proxy/ssh/SshProxy.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import java.io.Closeable;
4 | import java.util.ArrayDeque;
5 | import java.util.Deque;
6 | import java.util.LinkedHashMap;
7 | import java.util.LinkedHashSet;
8 | import java.util.Map;
9 | import java.util.Set;
10 |
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 |
14 | import com.jcraft.jsch.JSchException;
15 | import com.jcraft.jsch.Session;
16 |
17 | import de.cronn.proxy.ssh.util.Assert;
18 |
19 | public class SshProxy implements Closeable {
20 |
21 | private static final Logger log = LoggerFactory.getLogger(SshProxy.class);
22 |
23 | public static final String LOCALHOST = "localhost";
24 |
25 | private static final int DEFAULT_TIMEOUT_MILLIS = 10_000;
26 |
27 | private final Deque sshSessions = new ArrayDeque<>();
28 | private final Map> portForwardings = new LinkedHashMap<>();
29 |
30 | private final SshConfiguration sshConfiguration;
31 | private int timeoutMillis;
32 |
33 | public SshProxy() {
34 | this(DEFAULT_TIMEOUT_MILLIS);
35 | }
36 |
37 | public SshProxy(int timeoutMillis) {
38 | try {
39 | sshConfiguration = SshConfiguration.getConfiguration();
40 | } catch (Exception e) {
41 | throw new SshProxyRuntimeException("Failed to open SSH proxy", e);
42 | }
43 | this.timeoutMillis = timeoutMillis;
44 | }
45 |
46 | public int connect(String sshTunnelHost, String host, int port) {
47 | return connect(sshTunnelHost, host, port, 0);
48 | }
49 |
50 | public int connect(String sshTunnelHost, String host, int port, int localPort) {
51 | Assert.notNull(sshTunnelHost, "sshTunnelHost must not be null");
52 | Assert.notNull(host, "host must not be null");
53 | Assert.isTrue(port > 0, "illegal port: " + port);
54 | Assert.isTrue(localPort >= 0, "illegal local port: " + localPort);
55 |
56 | log.debug("tunneling to {}:{} via {}", host, port, sshTunnelHost);
57 |
58 | try {
59 | sshConfiguration.addIdentity(sshTunnelHost);
60 |
61 | SshProxyConfig proxyConfig = sshConfiguration.getProxyConfiguration(sshTunnelHost);
62 | if (proxyConfig == null) {
63 | return directConnect(sshTunnelHost, host, port, localPort);
64 | }
65 |
66 | int jumpPort = connect(proxyConfig);
67 |
68 | String hostUser = sshConfiguration.getHostUser(sshTunnelHost);
69 | String jumpHost = proxyConfig.getJumpHost();
70 | Session jumpHostSession = sshConfiguration.openSession(hostUser, jumpHost, jumpPort);
71 | String hostname = sshConfiguration.getHostName(sshTunnelHost);
72 | jumpHostSession.setHostKeyAlias(hostname);
73 | sshSessions.push(jumpHostSession);
74 | jumpHostSession.setTimeout(timeoutMillis);
75 | jumpHostSession.connect(timeoutMillis);
76 |
77 | log.debug("[{}] connected via {}@localhost:{}", sshTunnelHost, hostUser, jumpPort);
78 |
79 | return addLocalPortForwarding(sshTunnelHost, jumpHostSession, host, port, localPort);
80 | } catch (Exception e) {
81 | throw new SshProxyRuntimeException("Failed to create SSH tunnel to " + host + " via " + sshTunnelHost, e);
82 | }
83 | }
84 |
85 | private int connect(SshProxyConfig proxyConfig) {
86 | String jumpHost = proxyConfig.getJumpHost();
87 | String forwardingHost = proxyConfig.getForwardingHost();
88 | int forwardingPort = proxyConfig.getForwardingPort();
89 | return connect(jumpHost, forwardingHost, forwardingPort);
90 | }
91 |
92 | private int directConnect(String jumpHost, String targetHost, int targetPort, int localPort) throws JSchException {
93 | Session jumpHostSession = sshConfiguration.openSession(jumpHost);
94 | sshSessions.add(jumpHostSession);
95 | jumpHostSession.setTimeout(timeoutMillis);
96 | try {
97 | jumpHostSession.connect(timeoutMillis);
98 | } catch (JSchException e) {
99 | log.debug("Failed to connect to {} via {}", targetHost, jumpHost, e);
100 | throw new SshProxyRuntimeException("Failed to connect to " + targetHost + " via " + jumpHost);
101 | }
102 |
103 | log.debug("[{}] connected", jumpHost);
104 |
105 | return addLocalPortForwarding(jumpHost, jumpHostSession, targetHost, targetPort, localPort);
106 | }
107 |
108 | private int addLocalPortForwarding(String sshTunnelHost, Session session, String targetHost, int targetPort, int localPort) throws JSchException {
109 | int localPortReturned = session.setPortForwardingL(localPort, targetHost, targetPort);
110 |
111 | log.debug("[{}] local port {} forwarded to {}:{}", sshTunnelHost, localPortReturned, targetHost, targetPort);
112 |
113 | Set ports = portForwardings.computeIfAbsent(session, k -> new LinkedHashSet<>());
114 | ports.add(Integer.valueOf(localPortReturned));
115 | return localPortReturned;
116 | }
117 |
118 | @Override
119 | public void close() {
120 | if (!sshSessions.isEmpty()) {
121 | log.debug("closing SSH sessions");
122 | }
123 |
124 | while (!sshSessions.isEmpty()) {
125 | Session session = sshSessions.pop();
126 |
127 | deletePortForwarding(session);
128 |
129 | try {
130 | session.disconnect();
131 | } catch (Exception e) {
132 | log.error("Failed to disconnect SSH session", e);
133 | }
134 | }
135 |
136 | Assert.isTrue(portForwardings.isEmpty(), "port forwardings must be empty at this point");
137 | }
138 |
139 | private void deletePortForwarding(Session session) {
140 | Set ports = portForwardings.remove(session);
141 | if (ports != null) {
142 | for (Integer localPort : ports) {
143 | deletePortForwarding(session, localPort);
144 | }
145 | }
146 | }
147 |
148 | private void deletePortForwarding(Session session, Integer localPort) {
149 | try {
150 | String host = session.getHost();
151 | if (host.equals(LOCALHOST)) {
152 | host = session.getHostKeyAlias();
153 | }
154 | session.delPortForwardingL(LOCALHOST, localPort.intValue());
155 | log.debug("deleted local port forwarding on port {} for {}", localPort, host);
156 | } catch (Exception e) {
157 | log.error("failed to delete port forwarding of port {}", localPort, e);
158 | }
159 | }
160 |
161 | }
162 |
--------------------------------------------------------------------------------
/src/main/java/de/cronn/proxy/ssh/SshProxyConfig.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import java.util.regex.Matcher;
4 | import java.util.regex.Pattern;
5 |
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 |
9 | import com.jcraft.jsch.ConfigRepository.Config;
10 |
11 | import de.cronn.proxy.ssh.util.Assert;
12 |
13 | public class SshProxyConfig {
14 |
15 | private static final Logger log = LoggerFactory.getLogger(SshProxyConfig.class);
16 |
17 | private static final Pattern SSH_PROXY_COMMAND_PATTERN = Pattern.compile("ssh -q -W ([\\w\\.\\-_0-9]+|%h):(\\d+|%p) (.+)");
18 |
19 | private final int forwardingPort;
20 | private final String forwardingHost;
21 | private final String jumpHost;
22 |
23 | public SshProxyConfig(int forwardingPort, String forwardingHost, String jumpHost) {
24 | this.forwardingPort = forwardingPort;
25 | this.forwardingHost = forwardingHost;
26 | this.jumpHost = jumpHost;
27 | }
28 |
29 | public static SshProxyConfig parseProxyCommand(String proxyCommandConfig, String sshTunnelHost, Config hostConfig) {
30 | Matcher matcher = SSH_PROXY_COMMAND_PATTERN.matcher(proxyCommandConfig);
31 | Assert.isTrue(matcher.matches(),
32 | "Illegal ProxyCommand configured for host " + sshTunnelHost + ": " //
33 | + proxyCommandConfig + "." //
34 | + " Please check your SSH configuration in " + SshConfiguration.getLocalSshConfigPath());
35 |
36 | log.debug("[{}] emulating proxy command: {}", sshTunnelHost, proxyCommandConfig);
37 |
38 | String forwardingHost = matcher.group(1);
39 | if ("%h".equals(forwardingHost)) {
40 | forwardingHost = hostConfig.getHostname();
41 | if (forwardingHost == null) {
42 | forwardingHost = sshTunnelHost;
43 | }
44 | }
45 | String portConfig = matcher.group(2);
46 | final int forwardingPort;
47 | if ("%p".equals(portConfig)) {
48 | int port = hostConfig.getPort();
49 | if (port <= 0) {
50 | port = SshConfiguration.SSH_DEFAULT_PORT;
51 | }
52 | forwardingPort = port;
53 | } else {
54 | forwardingPort = Integer.parseInt(portConfig);
55 | }
56 | String jumpHost = matcher.group(3);
57 |
58 | return new SshProxyConfig(forwardingPort, forwardingHost, jumpHost);
59 | }
60 |
61 | public static SshProxyConfig parseProxyJump(String proxyJumpConfig, String sshTunnelHost, Config hostConfig) {
62 | log.debug("[{}] emulating proxy jump: {}", sshTunnelHost, proxyJumpConfig);
63 |
64 | String forwardingHost = hostConfig.getHostname();
65 | if (forwardingHost == null) {
66 | forwardingHost = sshTunnelHost;
67 | }
68 | int forwardingPort = hostConfig.getPort();
69 | if (forwardingPort <= 0) {
70 | forwardingPort = SshConfiguration.SSH_DEFAULT_PORT;
71 | }
72 | String jumpHost = proxyJumpConfig;
73 |
74 | return new SshProxyConfig(forwardingPort, forwardingHost, jumpHost);
75 | }
76 |
77 | public int getForwardingPort() {
78 | return forwardingPort;
79 | }
80 |
81 | public String getForwardingHost() {
82 | return forwardingHost;
83 | }
84 |
85 | public String getJumpHost() {
86 | return jumpHost;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/main/java/de/cronn/proxy/ssh/SshProxyRuntimeException.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | public class SshProxyRuntimeException extends RuntimeException {
4 |
5 | private static final long serialVersionUID = 1L;
6 |
7 | public SshProxyRuntimeException(String message) {
8 | super(message);
9 | }
10 |
11 | public SshProxyRuntimeException(String message, Throwable cause) {
12 | super(message, cause);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/de/cronn/proxy/ssh/util/Assert.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh.util;
2 |
3 | public final class Assert {
4 |
5 | private Assert() {
6 | }
7 |
8 | public static void notNull(Object object, String message) {
9 | if (object == null) {
10 | throw new IllegalArgumentException(message);
11 | }
12 | }
13 |
14 | public static void isNull(Object object, String message) {
15 | if (object != null) {
16 | throw new IllegalArgumentException(message);
17 | }
18 | }
19 |
20 | public static void isTrue(boolean expression, String message) {
21 | if (!expression) {
22 | throw new IllegalArgumentException(message);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/de/cronn/proxy/ssh/util/Utils.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh.util;
2 |
3 | import java.util.List;
4 |
5 | public final class Utils {
6 |
7 | private Utils() {
8 | }
9 |
10 | public static String join(List strings, String separator) {
11 | if (strings == null || strings.isEmpty()) {
12 | return "";
13 | }
14 |
15 | StringBuilder b = new StringBuilder();
16 | for (int i = 0; ; i++) {
17 | b.append(strings.get(i));
18 | if (i == strings.size() - 1) {
19 | return b.toString();
20 | }
21 | b.append(separator);
22 | }
23 | }
24 |
25 |
26 | public static boolean isNotEmpty(Object[] array) {
27 | return array != null && array.length > 0;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/java/de/cronn/proxy/ssh/DummyServerSocketThread.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import java.io.Closeable;
4 | import java.io.IOException;
5 | import java.io.OutputStream;
6 | import java.net.InetSocketAddress;
7 | import java.net.ServerSocket;
8 | import java.net.Socket;
9 | import java.nio.charset.Charset;
10 |
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 |
14 | public final class DummyServerSocketThread extends Thread implements Closeable {
15 |
16 | private static final Logger log = LoggerFactory.getLogger(DummyServerSocketThread.class);
17 | private final Charset transferCharset;
18 |
19 | private final int port;
20 | private final String textToSend;
21 | private ServerSocket serverSocket;
22 |
23 | public DummyServerSocketThread(Charset transferCharset, String textToSend) throws Exception {
24 | super(DummyServerSocketThread.class.getSimpleName());
25 | this.transferCharset = transferCharset;
26 | this.textToSend = textToSend;
27 | this.serverSocket = new ServerSocket();
28 | serverSocket.bind(new InetSocketAddress("localhost", 0));
29 | log.info("Listening on local port {}", serverSocket.getLocalPort());
30 | this.port = serverSocket.getLocalPort();
31 | setDaemon(true);
32 | start();
33 | }
34 |
35 | @Override
36 | public void close() throws IOException {
37 | if (serverSocket != null) {
38 | serverSocket.close();
39 | serverSocket = null;
40 | }
41 | }
42 |
43 | @Override
44 | public void run() {
45 | try (Socket socket = serverSocket.accept()) {
46 | log.info("got incoming connection");
47 | serverSocket.close();
48 | OutputStream outputStream = socket.getOutputStream();
49 | outputStream.write((textToSend + "\r\n").getBytes(transferCharset));
50 | outputStream.flush();
51 | log.info("wrote '{}' to socket", textToSend);
52 | } catch (IOException e) {
53 | log.error("Failed to send dummy text", e);
54 | }
55 | }
56 |
57 | public int getPort() {
58 | return port;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/test/java/de/cronn/proxy/ssh/HostKeyTypeTest.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import static org.assertj.core.api.Assertions.*;
4 |
5 | import org.junit.jupiter.api.Test;
6 |
7 | import com.jcraft.jsch.HostKey;
8 |
9 | class HostKeyTypeTest {
10 |
11 | @Test
12 | void testGetType() {
13 | assertThat(HostKeyType.SSH_RSA.getType()).isEqualTo(HostKey.SSHRSA);
14 | assertThat(HostKeyType.SSH_DSS.getType()).isEqualTo(HostKey.SSHDSS);
15 |
16 | for (HostKeyType hostKeyType : HostKeyType.values()) {
17 | assertThat(hostKeyType.getType()).isPositive();
18 | }
19 | }
20 |
21 | @Test
22 | void testGetTypeString() {
23 | assertThat(HostKeyType.SSH_RSA.getTypeString()).isEqualTo("ssh-rsa");
24 | assertThat(HostKeyType.SSH_DSS.getTypeString()).isEqualTo("ssh-dss");
25 |
26 | for (HostKeyType hostKeyType : HostKeyType.values()) {
27 | assertThat(hostKeyType.getTypeString()).isNotNull();
28 | }
29 | }
30 |
31 | @Test
32 | void testByTypeString() {
33 | assertThat(HostKeyType.byTypeString("ssh-rsa")).isEqualTo(HostKeyType.SSH_RSA);
34 | assertThat(HostKeyType.byTypeString("ssh-dss")).isEqualTo(HostKeyType.SSH_DSS);
35 |
36 | assertThatExceptionOfType(IllegalArgumentException.class)
37 | .isThrownBy(() -> HostKeyType.byTypeString(null))
38 | .withMessage("No hostKeyType found for null");
39 |
40 | assertThatExceptionOfType(IllegalArgumentException.class)
41 | .isThrownBy(() -> HostKeyType.byTypeString("ssh-does-not-exist"))
42 | .withMessage("No hostKeyType found for ssh-does-not-exist");
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/java/de/cronn/proxy/ssh/JSchHelperTest.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import static org.assertj.core.api.Assertions.*;
4 |
5 | import java.util.List;
6 |
7 | import org.junit.jupiter.api.Test;
8 |
9 | class JSchHelperTest {
10 |
11 | @Test
12 | void testHostKeysPreferEcdsa() throws Exception {
13 | JSchHelper.reconfigureServerHostKeyOrder(JSchHelper.ServerHostKeySortOrder.PREFER_ECDSA);
14 | List expectedHostKeys = List.of(//
15 | HostKeyType.ECDSA256, //
16 | HostKeyType.ECDSA384, //
17 | HostKeyType.ECDSA521, //
18 | HostKeyType.SSH_RSA, //
19 | HostKeyType.SSH_DSS //
20 | );
21 | assertThat(JSchHelper.getServerHostKeys()).isEqualTo(expectedHostKeys);
22 | }
23 |
24 | @Test
25 | void testHostKeysPreferRsa() throws Exception {
26 | JSchHelper.reconfigureServerHostKeyOrder(JSchHelper.ServerHostKeySortOrder.PREFER_RSA);
27 | List expectedHostKeys = List.of(//
28 | HostKeyType.SSH_RSA, //
29 | HostKeyType.ECDSA256, //
30 | HostKeyType.ECDSA384, //
31 | HostKeyType.ECDSA521, //
32 | HostKeyType.SSH_DSS //
33 | );
34 | assertThat(expectedHostKeys).isEqualTo(JSchHelper.getServerHostKeys());
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/test/java/de/cronn/proxy/ssh/JSchSlf4JLoggerTest.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 |
4 | import static org.assertj.core.api.Assertions.*;
5 |
6 | import org.junit.jupiter.api.Test;
7 |
8 | import com.jcraft.jsch.Logger;
9 |
10 | class JSchSlf4JLoggerTest {
11 |
12 | @Test
13 | void testIsEnabled() {
14 | JSchSlf4JLogger logger = new JSchSlf4JLogger();
15 |
16 | assertThat(logger.isEnabled(Logger.DEBUG)).isFalse();
17 | assertThat(logger.isEnabled(Logger.INFO)).isFalse();
18 | assertThat(logger.isEnabled(Logger.WARN)).isTrue();
19 | assertThat(logger.isEnabled(Logger.ERROR)).isTrue();
20 | assertThat(logger.isEnabled(Logger.FATAL)).isTrue();
21 |
22 | assertThatExceptionOfType(IllegalArgumentException.class)
23 | .isThrownBy(() -> logger.isEnabled(100))
24 | .withMessage("Unknown log level: 100");
25 | }
26 |
27 | @Test
28 | void testLog() {
29 | JSchSlf4JLogger logger = new JSchSlf4JLogger();
30 |
31 | logger.log(Logger.DEBUG, "some debug message");
32 | logger.log(Logger.INFO, "some info message");
33 | logger.log(Logger.WARN, "some warning message");
34 | logger.log(Logger.ERROR, "some error message");
35 | logger.log(Logger.FATAL, "some fatal message");
36 |
37 | assertThatExceptionOfType(IllegalArgumentException.class)
38 | .isThrownBy(() -> logger.log(100, "some message"))
39 | .withMessage("Unknown log level: 100");
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/java/de/cronn/proxy/ssh/ProxyAwareHostnameVerifierTest.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import static org.mockito.ArgumentMatchers.*;
4 | import static org.mockito.Mockito.*;
5 |
6 | import javax.net.ssl.HostnameVerifier;
7 | import javax.net.ssl.SSLSession;
8 |
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.extension.ExtendWith;
11 | import org.mockito.Mock;
12 | import org.mockito.junit.jupiter.MockitoExtension;
13 |
14 | @ExtendWith(MockitoExtension.class)
15 | class ProxyAwareHostnameVerifierTest {
16 |
17 | @Mock
18 | private HostnameVerifier hostnameVerifier;
19 |
20 | @Mock
21 | private SSLSession session;
22 |
23 | @Test
24 | void shouldInvokeVerifierWithOriginalHost() throws Exception {
25 | String originalHost = "originalHost";
26 |
27 | ProxyAwareHostnameVerifier sut = new ProxyAwareHostnameVerifier(hostnameVerifier, originalHost);
28 | sut.verify("proxy", session);
29 |
30 | verify(hostnameVerifier).verify(eq(originalHost), eq(session));
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/test/java/de/cronn/proxy/ssh/SshProxyConfigTest.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import static org.assertj.core.api.Assertions.*;
4 | import static org.mockito.Mockito.*;
5 |
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.extension.ExtendWith;
8 | import org.mockito.Mock;
9 | import org.mockito.junit.jupiter.MockitoExtension;
10 |
11 | import com.jcraft.jsch.ConfigRepository.Config;
12 |
13 | @ExtendWith(MockitoExtension.class)
14 | class SshProxyConfigTest {
15 |
16 | @Mock
17 | private Config hostConfig;
18 |
19 | @Test
20 | void testParseProxyCommand_EmptyHostConfig() throws Exception {
21 | SshProxyConfig sshProxyConfig = SshProxyConfig.parseProxyCommand("ssh -q -W %h:%p jumphost", "tunnel-host", hostConfig);
22 | assertThat(sshProxyConfig.getForwardingHost()).isEqualTo("tunnel-host");
23 | assertThat(sshProxyConfig.getJumpHost()).isEqualTo("jumphost");
24 | assertThat(sshProxyConfig.getForwardingPort()).isEqualTo(22);
25 | }
26 |
27 | @Test
28 | void testParseProxyCommand_FullHostConfig() throws Exception {
29 | when(hostConfig.getHostname()).thenReturn("some-host");
30 | when(hostConfig.getPort()).thenReturn(1234);
31 |
32 | SshProxyConfig sshProxyConfig = SshProxyConfig.parseProxyCommand("ssh -q -W %h:%p jumphost", "tunnel-host", hostConfig);
33 | assertThat(sshProxyConfig.getForwardingHost()).isEqualTo("some-host");
34 | assertThat(sshProxyConfig.getJumpHost()).isEqualTo("jumphost");
35 | assertThat(sshProxyConfig.getForwardingPort()).isEqualTo(1234);
36 | }
37 |
38 | @Test
39 | void testParseProxyCommand_ConcreteHosts() throws Exception {
40 | SshProxyConfig sshProxyConfig = SshProxyConfig.parseProxyCommand("ssh -q -W forwarding-host.123:5432 jumphost", "tunnel-host", hostConfig);
41 | assertThat(sshProxyConfig.getForwardingHost()).isEqualTo("forwarding-host.123");
42 | assertThat(sshProxyConfig.getJumpHost()).isEqualTo("jumphost");
43 | assertThat(sshProxyConfig.getForwardingPort()).isEqualTo(5432);
44 | }
45 |
46 | @Test
47 | void testParseProxyJump_EmptyHostConfig() throws Exception {
48 | SshProxyConfig sshProxyConfig = SshProxyConfig.parseProxyJump("jumphost", "tunnel-host", hostConfig);
49 | assertThat(sshProxyConfig.getForwardingHost()).isEqualTo("tunnel-host");
50 | assertThat(sshProxyConfig.getJumpHost()).isEqualTo("jumphost");
51 | assertThat(sshProxyConfig.getForwardingPort()).isEqualTo(22);
52 | }
53 |
54 | @Test
55 | void testParseProxyJump_FullHostConfig() throws Exception {
56 | when(hostConfig.getHostname()).thenReturn("some-host");
57 | when(hostConfig.getPort()).thenReturn(1234);
58 |
59 | SshProxyConfig sshProxyConfig = SshProxyConfig.parseProxyJump("jumphost", "tunnel-host", hostConfig);
60 | assertThat(sshProxyConfig.getForwardingHost()).isEqualTo("some-host");
61 | assertThat(sshProxyConfig.getJumpHost()).isEqualTo("jumphost");
62 | assertThat(sshProxyConfig.getForwardingPort()).isEqualTo(1234);
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/src/test/java/de/cronn/proxy/ssh/SshProxyTest.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh;
2 |
3 | import static org.assertj.core.api.Assertions.*;
4 |
5 | import java.io.BufferedReader;
6 | import java.io.IOException;
7 | import java.io.InputStream;
8 | import java.io.InputStreamReader;
9 | import java.net.Socket;
10 | import java.nio.charset.Charset;
11 | import java.nio.charset.StandardCharsets;
12 | import java.nio.file.Files;
13 | import java.nio.file.Path;
14 | import java.nio.file.Paths;
15 | import java.nio.file.StandardOpenOption;
16 | import java.util.Arrays;
17 | import java.util.concurrent.TimeUnit;
18 |
19 | import org.apache.sshd.common.config.keys.KeyUtils;
20 | import org.apache.sshd.common.util.security.SecurityUtils;
21 | import org.apache.sshd.server.SshServer;
22 | import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator;
23 | import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
24 | import org.apache.sshd.server.keyprovider.AbstractGeneratorHostKeyProvider;
25 | import org.junit.jupiter.api.AfterEach;
26 | import org.junit.jupiter.api.BeforeAll;
27 | import org.junit.jupiter.api.BeforeEach;
28 | import org.junit.jupiter.api.Test;
29 | import org.junit.jupiter.api.Timeout;
30 | import org.junit.jupiter.api.io.TempDir;
31 | import org.slf4j.Logger;
32 | import org.slf4j.LoggerFactory;
33 |
34 | class SshProxyTest {
35 |
36 | private static final Logger log = LoggerFactory.getLogger(SshProxyTest.class);
37 |
38 | private static final Charset CONFIG_CHARSET = StandardCharsets.ISO_8859_1;
39 | private static final Charset TRANSFER_CHARSET = StandardCharsets.UTF_16;
40 | private static final String KNOWN_HOSTS_FILENAME = "known_hosts";
41 | private static final String CONFIG_FILENAME = "config";
42 | private static final Path TEST_RESOURCES = Paths.get("src", "test", "resources");
43 | private static final Path SERVER_RSA_KEY = TEST_RESOURCES.resolve("server-rsa.key");
44 | private static final Path SERVER_ECDSA_KEY = TEST_RESOURCES.resolve("server-ecdsa.key");
45 |
46 | private static final long TEST_TIMEOUT_MILLIS = 30_000L;
47 |
48 | @TempDir
49 | Path userHome;
50 |
51 | private String oldUserHome;
52 | private Path dotSsh;
53 |
54 | private static final String TEST_TEXT = "Hello World";
55 |
56 | @BeforeAll
57 | public static void checkBouncyCastleIsRegistered() {
58 | assertThat(SecurityUtils.isBouncyCastleRegistered())
59 | .describedAs("BouncyCastle is registered")
60 | .isTrue();
61 | }
62 |
63 | @BeforeEach
64 | public void setUp() throws Exception {
65 | oldUserHome = System.getProperty("user.home");
66 | System.setProperty("user.home", userHome.toAbsolutePath().toString());
67 | log.debug("changed 'user.home' to {}", System.getProperty("user.home"));
68 |
69 | dotSsh = userHome.resolve(".ssh");
70 | Files.createDirectories(dotSsh);
71 |
72 | for (String file : Arrays.asList("id_rsa", "id_rsa.pub")) {
73 | Files.copy(TEST_RESOURCES.resolve(file), dotSsh.resolve(file));
74 | }
75 |
76 | appendToSshFile(CONFIG_FILENAME, "");
77 | appendToSshFile(KNOWN_HOSTS_FILENAME, "");
78 | }
79 |
80 | @AfterEach
81 | public void tearDown() {
82 | System.setProperty("user.home", oldUserHome);
83 | }
84 |
85 | @Test
86 | @Timeout(value = TEST_TIMEOUT_MILLIS,unit = TimeUnit.MILLISECONDS)
87 | void testSingleHop() throws Exception {
88 | SshServer sshServer = setUpSshServer();
89 | int sshServerPort = sshServer.getPort();
90 |
91 | String hostConfigName = "localhost-" + sshServerPort;
92 | appendToSshFile(CONFIG_FILENAME, "Host " + hostConfigName + "\n\tHostName localhost\n\tPort " + sshServerPort + "\n\n");
93 |
94 | try (DummyServerSocketThread dummyServerSocketThread = new DummyServerSocketThread(TRANSFER_CHARSET, TEST_TEXT);
95 | SshProxy sshProxy = new SshProxy()) {
96 | int port = sshProxy.connect(hostConfigName, "localhost", dummyServerSocketThread.getPort());
97 |
98 | final String receivedText;
99 | try (Socket s = new Socket(SshProxy.LOCALHOST, port);
100 | InputStream is = s.getInputStream()) {
101 | log.info("connected to port: {}", port);
102 | receivedText = readLine(is);
103 | }
104 | assertThat(receivedText).isEqualTo(TEST_TEXT);
105 | } finally {
106 | tryStop(sshServer);
107 | }
108 | }
109 |
110 | @Test
111 | @Timeout(value = TEST_TIMEOUT_MILLIS,unit = TimeUnit.MILLISECONDS)
112 | void testSingleHop_EcDsaServer() throws Exception {
113 | SshServer sshServer = setUpSshServer(KeyUtils.EC_ALGORITHM);
114 | int sshServerPort = sshServer.getPort();
115 |
116 | String hostConfigName = "localhost-" + sshServerPort;
117 | appendToSshFile(CONFIG_FILENAME, "Host " + hostConfigName + "\n\tHostName localhost\n\tPort " + sshServerPort + "\n\n");
118 |
119 | try (DummyServerSocketThread dummyServerSocketThread = new DummyServerSocketThread(TRANSFER_CHARSET, TEST_TEXT);
120 | SshProxy sshProxy = new SshProxy()) {
121 | int port = sshProxy.connect(hostConfigName, "localhost", dummyServerSocketThread.getPort());
122 |
123 | final String receivedText;
124 | try (Socket s = new Socket(SshProxy.LOCALHOST, port);
125 | InputStream is = s.getInputStream()) {
126 | log.info("connected to port: {}", port);
127 | receivedText = readLine(is);
128 | }
129 | assertThat(receivedText).isEqualTo(TEST_TEXT);
130 | } finally {
131 | tryStop(sshServer);
132 | }
133 | }
134 |
135 | @Test
136 | @Timeout(value = TEST_TIMEOUT_MILLIS,unit = TimeUnit.MILLISECONDS)
137 | void testSingleHopWithLocalPort() throws Exception {
138 | SshServer sshServer = setUpSshServer();
139 | int sshServerPort = sshServer.getPort();
140 |
141 | String hostConfigName = "localhost-" + sshServerPort;
142 | appendToSshFile(CONFIG_FILENAME, "Host " + hostConfigName + "\n\tHostName localhost\n\tPort " + sshServerPort + "\n\n");
143 |
144 | try (DummyServerSocketThread dummyServerSocketThread = new DummyServerSocketThread(TRANSFER_CHARSET, TEST_TEXT);
145 | SshProxy sshProxy = new SshProxy()) {
146 | int port = sshProxy.connect(hostConfigName, "localhost", dummyServerSocketThread.getPort(), 2345);
147 |
148 | final String receivedText;
149 | try (Socket s = new Socket(SshProxy.LOCALHOST, port);
150 | InputStream is = s.getInputStream()) {
151 | log.info("connected to port: {}", port);
152 | receivedText = readLine(is);
153 | }
154 | assertThat(receivedText).isEqualTo(TEST_TEXT);
155 | } finally {
156 | tryStop(sshServer);
157 | }
158 | }
159 |
160 | @Test
161 | @Timeout(value = TEST_TIMEOUT_MILLIS,unit = TimeUnit.MILLISECONDS)
162 | void testTwoHops_ProxyCommand() throws Exception {
163 | doTestTwoHops("ProxyCommand ssh -q -W %h:%p firsthop");
164 | }
165 |
166 | @Test
167 | @Timeout(value = TEST_TIMEOUT_MILLIS,unit = TimeUnit.MILLISECONDS)
168 | void testTwoHops_ProxyJump() throws Exception {
169 | doTestTwoHops("ProxyJump firsthop");
170 | }
171 |
172 | private void doTestTwoHops(String proxyConfiguration) throws Exception {
173 | SshServer firstSshServer = setUpSshServer();
174 | int firstServerPort = firstSshServer.getPort();
175 |
176 | SshServer secondSshServer = setUpSshServer();
177 | int secondServerPort = secondSshServer.getPort();
178 |
179 | appendToSshFile(CONFIG_FILENAME, "Host firsthop\n\tHostName localhost\n\tPort " + firstServerPort + "\n\n");
180 | appendToSshFile(CONFIG_FILENAME, "Host secondhop\n\tHostName localhost\n\tPort " + secondServerPort + "\n\t" + proxyConfiguration + "\n\n");
181 |
182 | try (DummyServerSocketThread dummyServerSocketThread = new DummyServerSocketThread(TRANSFER_CHARSET, TEST_TEXT);
183 | SshProxy sshProxy = new SshProxy()) {
184 | int port = sshProxy.connect("secondhop", "localhost", dummyServerSocketThread.getPort());
185 |
186 | final String receivedText;
187 | try (Socket s = new Socket(SshProxy.LOCALHOST, port);
188 | InputStream is = s.getInputStream()) {
189 | log.info("connected to port: {}", port);
190 | receivedText = readLine(is);
191 | }
192 | assertThat(receivedText).isEqualTo(TEST_TEXT);
193 | } finally {
194 | tryStop(firstSshServer);
195 | tryStop(secondSshServer);
196 | }
197 | }
198 |
199 | @Test
200 | @Timeout(value = TEST_TIMEOUT_MILLIS,unit = TimeUnit.MILLISECONDS)
201 | void testSingleHop_NoHostKeyFound() {
202 | try (SshProxy sshProxy = new SshProxy()) {
203 | sshProxy.connect("jumphost", "targethost", 1234);
204 | fail("SshProxyRuntimeException expected");
205 | } catch (SshProxyRuntimeException e) {
206 | log.debug("Expected exception", e);
207 | assertThat(e.getMessage()).isEqualTo("Failed to create SSH tunnel to targethost via jumphost");
208 | assertThat(e.getCause().getMessage()).startsWith("Found no host key for jumphost");
209 | }
210 | }
211 |
212 | @Test
213 | @Timeout(value = TEST_TIMEOUT_MILLIS,unit = TimeUnit.MILLISECONDS)
214 | void testSingleHop_ConnectionRefused() throws Exception {
215 | try (SshServer sshServer = setUpSshServer()) {
216 | sshServer.stop();
217 | try (SshProxy sshProxy = new SshProxy()) {
218 | sshProxy.connect("localhost", "targethost", 1234);
219 | fail("SshProxyRuntimeException expected");
220 | } catch (SshProxyRuntimeException e) {
221 | log.debug("Expected exception", e);
222 | assertThat(e.getMessage()).isEqualTo("Failed to create SSH tunnel to targethost via localhost");
223 | assertThat(e.getCause().getMessage()).isEqualTo("Failed to connect to targethost via localhost");
224 | }
225 | }
226 | }
227 |
228 | @Test
229 | @Timeout(value = TEST_TIMEOUT_MILLIS,unit = TimeUnit.MILLISECONDS)
230 | void testSingleHop_IllegalPort() {
231 | try (SshProxy sshProxy = new SshProxy()) {
232 | sshProxy.connect("localhost", "targethost", 0);
233 | fail("IllegalArgumentException expected");
234 | } catch (IllegalArgumentException e) {
235 | assertThat(e.getMessage()).isEqualTo("illegal port: 0");
236 | }
237 | }
238 |
239 | @Test
240 | @Timeout(value = TEST_TIMEOUT_MILLIS,unit = TimeUnit.MILLISECONDS)
241 | void testSingleHop_IllegalLocalPort() {
242 | try (SshProxy sshProxy = new SshProxy()) {
243 | sshProxy.connect("localhost", "targethost", 1234, -1);
244 | fail("IllegalArgumentException expected");
245 | } catch (IllegalArgumentException e) {
246 | assertThat(e.getMessage()).isEqualTo("illegal local port: -1");
247 | }
248 | }
249 |
250 | private void tryStop(SshServer sshServer) {
251 | try {
252 | log.debug("stopping SSH server");
253 | sshServer.stop();
254 | } catch (IOException e) {
255 | log.error("Failed to stop SSH server", e);
256 | }
257 | }
258 |
259 | private String readLine(InputStream is) throws IOException {
260 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, TRANSFER_CHARSET))) {
261 | String line = reader.readLine();
262 | assertThat(line).isNotNull();
263 | return line.trim();
264 | }
265 | }
266 |
267 | private SshServer setUpSshServer() throws IOException {
268 | return setUpSshServer(KeyUtils.RSA_ALGORITHM);
269 | }
270 |
271 | private SshServer setUpSshServer(String algorithm) throws IOException {
272 | SshServer sshServer = SshServer.setUpDefaultServer();
273 | sshServer.setPort(0);
274 | AbstractGeneratorHostKeyProvider hostKeyProvider = SecurityUtils.createGeneratorHostKeyProvider(getServerKeyFile(algorithm));
275 | hostKeyProvider.setAlgorithm(algorithm);
276 | if (algorithm.equals(KeyUtils.EC_ALGORITHM)) {
277 | hostKeyProvider.setKeySize(256);
278 | }
279 | sshServer.setKeyPairProvider(hostKeyProvider);
280 |
281 | sshServer.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE);
282 | sshServer.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
283 |
284 | writeFingerprintToKnownHosts(algorithm);
285 |
286 | sshServer.start();
287 |
288 | int sshServerPort = sshServer.getPort();
289 | assertThat(sshServerPort).isPositive();
290 |
291 | return sshServer;
292 | }
293 |
294 | private void writeFingerprintToKnownHosts(String algorithm) throws IOException {
295 | switch (algorithm) {
296 | case KeyUtils.RSA_ALGORITHM:
297 | appendToSshFile(KNOWN_HOSTS_FILENAME, "localhost ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDL8360Wxcgo33sggS0bSid0u7Ad4XFig8/e0UfD5l02x/w2DRJuqJow4SiDfi9jvD8p3lu7To7b/oGH/c/vsK9j35ICG0eJ/bbnQDuHROBAnbAC6PXN+/XX2F9s48KlOC5dQXrGYyYhoozW67yoHTooisZSzF/iyPdNat64rM0+ZO3dV6eEQ0FItYO632YcSiBRE7YZe9rP7ne50xaltKgrAmHRDRo+tjIcykrlcZFG1Bp/ct9Ejs2DQDsFOZRCmFbag0pQxxbkA1U6z7O3qwhhDWcJz2ZHDHK8DUkgHdX+Hbp7LxBWEaCiU8cL+S6rmCpNsui9NT/XeoLuXQ4J8jX\n");
298 | break;
299 | case KeyUtils.EC_ALGORITHM:
300 | appendToSshFile(KNOWN_HOSTS_FILENAME, "localhost ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCH+0xjLYNGoqVGlD4VtKHF1Tig2/Y76BxVld88bYAaRV4ojJni62vIYMKqk+FMZhL1lcQ/VQTvIeLMnYk+grKo=\n");
301 | break;
302 | default:
303 | throw new IllegalArgumentException("Unknown algorithm: " + algorithm);
304 | }
305 | }
306 |
307 | private static Path getServerKeyFile(String algorithm) {
308 | switch (algorithm) {
309 | case KeyUtils.RSA_ALGORITHM:
310 | return SERVER_RSA_KEY;
311 | case KeyUtils.EC_ALGORITHM:
312 | return SERVER_ECDSA_KEY;
313 | default:
314 | throw new IllegalArgumentException("Unknown algorithm: " + algorithm);
315 | }
316 | }
317 |
318 | private void appendToSshFile(String filename, String text) throws IOException {
319 | Path config = dotSsh.resolve(filename);
320 | Files.writeString(config, text, CONFIG_CHARSET, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
321 | }
322 |
323 | }
324 |
--------------------------------------------------------------------------------
/src/test/java/de/cronn/proxy/ssh/util/AssertTest.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh.util;
2 |
3 | import static org.assertj.core.api.Assertions.*;
4 |
5 | import java.lang.reflect.Constructor;
6 | import java.lang.reflect.Modifier;
7 |
8 | import org.junit.jupiter.api.Test;
9 |
10 | class AssertTest {
11 |
12 | @Test
13 | void testConstructorIsPrivate() throws Exception {
14 | Constructor> constructor = Assert.class.getDeclaredConstructor();
15 | assertThat(Modifier.isPrivate(constructor.getModifiers())).isTrue();
16 | constructor.setAccessible(true);
17 | constructor.newInstance();
18 | }
19 |
20 | @Test
21 | void testNotNull() {
22 | Assert.notNull("", "should not be null");
23 | Assert.notNull(new Object(), "should not be null");
24 |
25 | assertThatExceptionOfType(IllegalArgumentException.class)
26 | .isThrownBy(() -> Assert.notNull(null, "should not be null"))
27 | .withMessage("should not be null");
28 | }
29 |
30 | @Test
31 | void testIsNull() {
32 | Assert.isNull(null, "should be null");
33 |
34 | assertThatExceptionOfType(IllegalArgumentException.class)
35 | .isThrownBy(() -> Assert.isNull("", "should not be null"))
36 | .withMessage("should not be null");
37 | }
38 |
39 | @Test
40 | void testIsTrue() {
41 | Assert.isTrue(true, "should be true");
42 |
43 | assertThatExceptionOfType(IllegalArgumentException.class)
44 | .isThrownBy(() -> Assert.isTrue(false, "should not be true"))
45 | .withMessage("should not be true");
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/test/java/de/cronn/proxy/ssh/util/UtilsTest.java:
--------------------------------------------------------------------------------
1 | package de.cronn.proxy.ssh.util;
2 |
3 | import static org.assertj.core.api.Assertions.*;
4 |
5 | import java.lang.reflect.Constructor;
6 | import java.lang.reflect.Modifier;
7 | import java.util.Arrays;
8 | import java.util.Collections;
9 |
10 | import org.junit.jupiter.api.Test;
11 |
12 | class UtilsTest {
13 |
14 | @Test
15 | void testConstructorIsPrivate() throws Exception {
16 | Constructor> constructor = Utils.class.getDeclaredConstructor();
17 | assertThat(Modifier.isPrivate(constructor.getModifiers())).isTrue();
18 | constructor.setAccessible(true);
19 | constructor.newInstance();
20 | }
21 |
22 | @Test
23 | void testJoin() throws Exception {
24 | assertThat(Utils.join(null, "")).isEqualTo("");
25 | assertThat(Utils.join(Collections.emptyList(), "")).isEqualTo("");
26 | assertThat(Utils.join(Collections.singletonList("foo"), ";")).isEqualTo("foo");
27 | assertThat(Utils.join(Arrays.asList("foo", "bar"), ";")).isEqualTo("foo;bar");
28 | }
29 |
30 | @Test
31 | void testIsNotEmpty() throws Exception {
32 | assertThat(Utils.isNotEmpty(null)).isFalse();
33 | assertThat(Utils.isNotEmpty(new Object[0])).isFalse();
34 | assertThat(Utils.isNotEmpty(new Object[] { null })).isTrue();
35 | assertThat(Utils.isNotEmpty(new Object[] { "foo", "bar" })).isTrue();
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/test/resources/id_rsa:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpQIBAAKCAQEA4c4xc7Fw9240guXSUPyDFqWoiSPmXYT0n/MsXAE+eAROVg8g
3 | DcVDRc14qiWEqcXygVny0/fMnZ9X0zWTkA10AvpnsO0xu0S0oc5ABpGoBcn9Lf2Q
4 | UPgDIeUlO9BjC0gKNF7SCBUTWu01Gy7lrcAlt6XSNL/sJsetunxk5UU4LAeZ4Xj2
5 | tNOxLMbT9wrd0EM7JFkbPDeCu8kiBuB1vmibvInErUApC/QqbH7A7W5OZnwt4Fou
6 | mHA+BvfHcPYPdEODS9UFNSDIabRNZbUTS7zrXPnVxzAgqyVyrVCie0fIJN/EltQ+
7 | ZqzRBf4Fn90oTJHTkKf1QvY5MeKCR/zMI2FliwIDAQABAoIBAQDWjXQhiMVc5SMS
8 | CUsoVnY8S5VzEWBvFcjWPlm05h20Les9DEwZqYi9R3nSualvIz3EOHJpg/exAVmS
9 | v0KByrajqchirU3msWaso+vkEfxD2+QsEdVRigZ362HQjrW9X2sKzdnlghkM+XzP
10 | YmUsDfoIoI+2PXImeu6Q1meMDZRNB/YxuiVautxLpuj6/EbB9YU+CpVMnzrOuSZE
11 | BiqWuREEMIVVv4l/c8ES8ggWAkfgB/VRbqW5WCo5LR38u9Dcm6kt+8YQZIIyUBrC
12 | U8XGP8nDQFDlxuzNri/rbo568UWtlm5BJU527p0oO1O5EX3Oix+2GpQgkxxCwLWU
13 | WXxel/axAoGBAP8s4RlsKbLgt4IgmIj/NashA6SfRPT+5PgiX9jo1agRsxqruw47
14 | h7OrjR4w6O7bK2FK6Pj91FLOfWMGdOulcJ5OSv4tYppfZbkqwpUl93y+YhylCJxJ
15 | 3TCF+cmsTrYDo/dXGpJ9E9uJjhMoP3W6LDp/jLUBqyXNOlQi5MgE3JBpAoGBAOKJ
16 | A7nb/LlqBTqA867PmLV4gQO4aUu8JKqQZHMHgie0QTh+iKXAbjzZwPPNYx2/3Sln
17 | 5Vmy+L+XtZ+vk0JNUa44XEZ2mGi59pTYW48MS/iAEmJkypNvtDM7aV8o+0evag1G
18 | v7AH4iTJ4jTCuFqsTyaE824niVamlx4zsz0YF4fTAoGAAMRcxNvLYEtGofCBJOBH
19 | hAUsYqFL7sSZYZmQ2jEQ+1laRXlArbFGHick3HNfL+Cex8MW4jC5I6qVO+f4EAFG
20 | TmOD3DG8uyQQRTc4sIQVs11LJDTmyrl6Hbw5XP7Umb13D7ZGUSxpE97c+3fCgRMX
21 | MPHTNXQU1J9CTqBwOZ2yIAkCgYEAwT6kYQ4aXojkgO2z8nHBET0EwYm0uRh8Jswa
22 | BE5pZzlLUcgPBWZMI7iV8uRIIv6iyUmJyqTzsWzXUKtT8YFHplkJzkoo5V2NzZdr
23 | M3IH9Ko8BJd6f58Ql4uc7cJl6Nbonv61UpLHBR76yos4/JB0zKUpi9RKQhLGYssz
24 | oXBF0mUCgYEA0O6awicpT/5VUXdy7dYAZh+VeXezqXW7qBDqfSB7pYbEt5t+fxvv
25 | XNz6cPU0wuaZwn+E0Ozd0Jm6mANo3u4efOW9fVg/X5Eu/PX9aAibpbAnD9/qHSO4
26 | lI8NGcQ2+sW5IT9ajMwYGU74CwTerY4GseUxibUNE3o18SQlq0/Vl60=
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/src/test/resources/id_rsa.pub:
--------------------------------------------------------------------------------
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDhzjFzsXD3bjSC5dJQ/IMWpaiJI+ZdhPSf8yxcAT54BE5WDyANxUNFzXiqJYSpxfKBWfLT98ydn1fTNZOQDXQC+mew7TG7RLShzkAGkagFyf0t/ZBQ+AMh5SU70GMLSAo0XtIIFRNa7TUbLuWtwCW3pdI0v+wmx626fGTlRTgsB5nhePa007EsxtP3Ct3QQzskWRs8N4K7ySIG4HW+aJu8icStQCkL9CpsfsDtbk5mfC3gWi6YcD4G98dw9g90Q4NL1QU1IMhptE1ltRNLvOtc+dXHMCCrJXKtUKJ7R8gk38SW1D5mrNEF/gWf3ShMkdOQp/VC9jkx4oJH/MwjYWWL bene@leo
2 |
--------------------------------------------------------------------------------
/src/test/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | %date{HH:mm:ss.SSS} [%t] %-5p %c{20}:%L - %m%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/test/resources/server-ecdsa.key:
--------------------------------------------------------------------------------
1 | -----BEGIN EC PRIVATE KEY-----
2 | MHcCAQEEINLBdsi0qqeTHLhSXnjhbiNAAe8akg9C9UVkteXKFFSboAoGCCqGSM49
3 | AwEHoUQDQgAEIf7TGMtg0aipUaUPhW0ocXVOKDb9jvoHFWV3zxtgBpFXiiMmeLra
4 | 8hgwqqT4UxmEvWVxD9VBO8h4sydiT6Csqg==
5 | -----END EC PRIVATE KEY-----
6 |
--------------------------------------------------------------------------------
/src/test/resources/server-rsa.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpAIBAAKCAQEAy/N+tFsXIKN97IIEtG0ondLuwHeFxYoPP3tFHw+ZdNsf8Ng0
3 | SbqiaMOEog34vY7w/Kd5bu06O2/6Bh/3P77CvY9+SAhtHif2250A7h0TgQJ2wAuj
4 | 1zfv119hfbOPCpTguXUF6xmMmIaKM1uu8qB06KIrGUsxf4sj3TWreuKzNPmTt3Ve
5 | nhENBSLWDut9mHEogURO2GXvaz+53udMWpbSoKwJh0Q0aPrYyHMpK5XGRRtQaf3L
6 | fRI7Ng0A7BTmUQphW2oNKUMcW5ANVOs+zt6sIYQ1nCc9mRwxyvA1JIB3V/h26ey8
7 | QVhGgolPHC/kuq5gqTbLovTU/13qC7l0OCfI1wIDAQABAoIBADgIem+ntRdxA5g2
8 | cn91nBqcSJ8VV2nZz+2jeu6ZNRJ/X2umotr8zUEWMnrpdsTxoDDx/DFnOL/6uEj1
9 | gFMBoHQ/F7VHp18JIM/ed1J0J5ciq6iAqi9nfVwBJwJHkk2YcJNTXOBmIRQIprCM
10 | iGi9f9EoOMoWuA8wFZbhd27oGM89Iuxe7/17/WmTTW/hFsOD/wNrJfB0L3YE54/H
11 | dzvrQj8WCvvYzPGU/PAui6EthYTUv8t/drseo9orpZPyZJDWFnBsOkgbBJzg0oA5
12 | 1tFRoE+opDWmX9ceBF7s0kxYazROOTm4+gEFUHbU6mrHuMmodEIY9iJAReqK2GkP
13 | aZ+77qUCgYEA6VmDVC1HuJctSWZrIhyjbtSbzcFC+5SzanKDlfJ2XVwmk+V7bI9P
14 | BaLsZrp/j+xmFdl617bMwDLc8fq3i+ib9AD8EOpbqWrAtGdSPn3r6FsyYOyJa8Z/
15 | 3p/wCEUDC64WZJ+siKlEca1q3q38N9QRDWyLDq6YHQxlNklh8d3tEM0CgYEA3791
16 | 9MuC0VUo/yUkXZuR3EoBU0sLUtbKrQKNHWNy/2Oyb8+yZ5qhTaNnlRkGtHQ+gCZ/
17 | 1dSZBnFZDVW6pMHPUz/C+hZD4FWhPZJ95OxX6KxSq+l/C4/GQOm85nTjZ5npC3zQ
18 | VQGP/w65bo0J2G46dIjXCt9ikRvF6NQm+U3jMDMCgYEAhIc/LD6vPio2ITrW27/S
19 | Hm8HsfryNPpGT82L6EyVZv6gNl0eFRDrO3NFui5vpmkHrZ8fwoXikcIRdCkFt6M7
20 | d4BbqKvBtV2Y5pzLvAw+QHATE7MjdR9+ngzOAZRYX3jW5P0+uzsPUVTBnojH91ks
21 | +ifMbmFAqTbSANv6kaiOLH0CgYA8/gsgjO1/NNC1JKHSJptPSpkr5HQUw9qB6QUg
22 | ssBhYAL/OcXvOhaofFe7LBRI0rtK4bPNTWPdfr6AxRLY4MAseGAlHjaoi48loq5Q
23 | 3iBkm7z5gfAQ7cNkZJMK22g2EJ2XdRGxanifVZs0yJubdQjYRYkhPJ244rJDcrmh
24 | EhtRQwKBgQC85O7t8ftoGKTQJDMRaemxrt9x5758+z4qJeZymB/iG3k6gjscAD0g
25 | byjwfNIsHxU+vCK4PuHU7K491Uad0Es8ai/v6GBCUDWv10Iee+3dz4hzPq6Kj/rq
26 | MfrRAYsEzmolOjjfA4KmkqkiGJ+7aQCg7ru+5E086Z3oAqECHqIOsg==
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/updateDependencies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ./gradlew dependencies --update-locks '*:*'
4 |
--------------------------------------------------------------------------------