├── .gitignore
├── .m2
└── settings.xml
├── .travis.yml
├── .vscode
└── settings.json
├── AUTHORS.txt
├── LICENSE.txt
├── README.md
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── orange
│ │ └── common
│ │ └── springboot
│ │ └── autoconfigure
│ │ └── proxy
│ │ ├── HostnameMatcher.java
│ │ ├── MultiProxySelector.java
│ │ ├── MultiServerAuthenticator.java
│ │ ├── NetworkProxyAutoConfiguration.java
│ │ ├── NetworkProxyProperties.java
│ │ └── ProxySettingsFromEnv.java
└── resources
│ └── META-INF
│ └── spring.factories
└── test
└── java
└── com
└── orange
└── common
└── springboot
└── autoconfigure
└── proxy
├── HostnameMatcherTest.java
├── NetworkProxyAutoConfigurationTest.java
└── ProxySettingsFromEnvTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 | .sts4-cache
12 |
13 | ### IntelliJ IDEA ###
14 | .idea
15 | *.iws
16 | *.iml
17 | *.ipr
18 |
19 | ### NetBeans ###
20 | /nbproject/private/
21 | /build/
22 | /nbbuild/
23 | /dist/
24 | /nbdist/
25 | /.nb-gradle/
--------------------------------------------------------------------------------
/.m2/settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | github
10 | ${env.GITHUB_USERNAME}
11 | ${env.GITHUB_TOKEN}
12 |
13 |
14 |
15 |
16 | github
17 |
18 |
19 |
20 |
21 | github
22 |
23 |
24 | central
25 | https://repo1.maven.org/maven2
26 |
27 |
28 | github
29 | https://maven.pkg.github.com/Orange-OpenSource/*
30 |
31 | true
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 | jdk:
3 | - oraclejdk8
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "java.configuration.updateBuildConfiguration": "automatic"
3 | }
--------------------------------------------------------------------------------
/AUTHORS.txt:
--------------------------------------------------------------------------------
1 | Pierre Smeyers
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
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 {yyyy} {name of copyright owner}
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 | # Spring Boot Auto Configure: Proxy
2 | [](https://travis-ci.org/Orange-OpenSource/spring-boot-autoconfigure-proxy)
3 |
4 |
5 | This Spring Boot library provides network proxy auto-configuration.
6 |
7 |
8 |
9 |
10 | ## Purpose
11 |
12 | There are several caveats when managing network proxies in Java:
13 | 1. authentication management \
14 | _Java doesn't support any property such as `(http|https|ftp).proxyHost` or `(http|https|ftp).proxyPort` to define the username &
15 | password to use with an http proxy that requires basic authentication_
16 | 2. multiple proxies \
17 | _if you run you app in an environment with **several** proxies (say one for the intranet, one for the internet),
18 | Java properties are not enough_
19 | 3. no standard environment support \
20 | _on Linux, `http_proxy`, `https_proxy`, `ftp_proxy` and `no_proxy` environment variables are quite a standard...
21 | too bad Java doesn't support them natively_
22 |
23 | Well, this Spring Boot auto-configurer is all about those 3 points.
24 |
25 | From the running context, it will try to discover and setup network proxy(ies) in the following way and order:
26 | 1. _explicit_ configuration from **Spring Boot configuration** (`yaml`, properties or else),
27 | 2. _implicit_ configuration from **environment variables**,
28 | 3. _implicit_ configuration from **Java properties**.
29 |
30 |
31 |
32 |
33 | ## Usage
34 |
35 | ### Include it in your project
36 |
37 | Maven style (`pom.xml`):
38 |
39 | ```xml
40 |
41 |
42 |
43 | github-orange
44 | https://maven.pkg.github.com/Orange-OpenSource/spring-boot-autoconfigure-proxy
45 |
46 | true
47 |
48 |
49 | false
50 |
51 |
52 |
53 |
54 |
55 | ...
56 |
57 | com.orange.common
58 | spring-boot-autoconfigure-proxy
59 | 1.0.2
60 |
61 | ...
62 |
63 | ```
64 |
65 | ### Using environment variables
66 |
67 | Proxy configuration can be implicitly set using the following **environment variables**:
68 |
69 | variable | description
70 | ------------------------------ | ------------------------------------------------
71 | `http_proxy` or `HTTP_PROXY` | The proxy URL to use for `http` connections. Format: `://:` or `://:@:`
72 | `https_proxy` or `HTTPS_PROXY` | The proxy URL to use for `https` connections. Format: `://:` or `://:@:`
73 | `ftp_proxy` or `FTP_PROXY` | The proxy URL to use for `ftp` connections. Format: `://:` or `://:@:`
74 | `no_proxy` or `NO_PROXY` | Comma-separated list of domain extensions proxy should _not_ be used for.
75 |
76 | See: [wget documentation](https://www.gnu.org/software/wget/manual/html_node/Proxies.html)
77 |
78 |
79 | ### Using Java properties
80 |
81 | Proxy configuration can be implicitly set using the following **Java properties**:
82 |
83 | property | description
84 | --------------------- | ------------------------------------------------
85 | `http.proxyHost` | Standard Java property defining the proxy host to use for `http` connections.
86 | `http.proxyPort` | Standard Java property defining the proxy port to use for `http` connections.
87 | `http.nonProxyHosts` | Standard Java property defining list of domain extensions proxy should _not_ be used for `http` connections.
88 | `http.proxyUser` | Non-standard Java property defining the basic authent username to use for `http` connections.
89 | `http.proxyPassword` | Non-standard Java property defining the basic authent password to use for `http` connections.
90 | `https.proxyHost` | Standard Java property defining the proxy host to use for `http`s connections.
91 | `https.proxyPort` | Standard Java property defining the proxy port to use for `https` connections.
92 | `https.nonProxyHosts` | Standard Java property defining list of domain extensions proxy should _not_ be used for `https` connections.
93 | `https.proxyUser` | Non-standard Java property defining the basic authent username to use for `https` connections.
94 | `https.proxyPassword` | Non-standard Java property defining the basic authent password to use for `https` connections.
95 | `ftp.proxyHost` | Standard Java property defining the proxy host to use for `ftp` connections.
96 | `ftp.proxyPort` | Standard Java property defining the proxy port to use for `ftp` connections.
97 | `ftp.nonProxyHosts` | Standard Java property defining list of domain extensions proxy should _not_ be used for `ftp` connections.
98 | `ftp.proxyUser` | Non-standard Java property defining the basic authent username to use for `ftp` connections.
99 | `ftp.proxyPassword` | Non-standard Java property defining the basic authent password to use for `ftp` connections.
100 |
101 | See: [Java Networking and Proxies documentation](https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html)
102 |
103 |
104 | ### Using Spring Boot configuration
105 |
106 | Unless you have multiple proxy servers to manage, `spring-boot-autoconfigure-proxy` can always be used either the
107 | **environment variables** way or the **Java properties** way.
108 |
109 | But in the other case or if you just prefer controlling your configuration, go the **Spring Boot configuration** way!
110 |
111 | Here is a configuration example with a proxy for intranet addresses, and another one for internet addresses:
112 |
113 | ```yaml
114 | network:
115 | proxy:
116 | enable: true # allows disabling auto-config; enabled by default
117 |
118 | # explicit list of proxy servers with settings
119 | servers:
120 | # 1: the intranet proxy (doesn't require any authentication)
121 | -
122 | host: intranet.proxy.acme.com
123 | port: 3128
124 | # list of hostname matchers
125 | for-hosts:
126 | # support basic hostname
127 | - portal.acme.com
128 | # or domain name
129 | - .intranet.acme.com
130 | # or wildcards
131 | - "*.intranet.acme.*"
132 | # or even regular expressions
133 | - /10\.236\.\d+\.\d+/
134 |
135 | # 2: the internet proxy (requires an authentication)
136 | -
137 | host: internet.proxy.acme.com
138 | port: 8080
139 | username: pismy
140 | password: let.me.out
141 | # list of non-hostname matchers
142 | not-for-hosts:
143 | - localhost
144 | - 127.0.0.1
145 | - /10\.99\.\d+\.\d+/
146 | ```
147 |
148 | When the application is trying to access a network resource, the URI hostname will be tested against every configured proxy
149 | one by one, if none matches then a direct connection will be used (no proxy).
150 |
151 | Examples:
152 | - `https://www.google.com` would match the **internet** proxy in the above configuration,
153 | - `http://10.99.101.5/path/to/a/resource` would'nt match any configured proxy and would use direct connection (matches the last regex non-matcher from internet proxy),
154 | - `http://billing.intranet.acme.fr/api` would match the **intranet** proxy (matches the 3rd wildcard matcher).
155 |
156 |
157 |
158 | ## License
159 |
160 | This code is under [Apache-2.0 License](LICENSE.txt)
161 |
162 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | org.springframework.boot
6 | spring-boot-starter-parent
7 | 2.1.9.RELEASE
8 |
9 |
10 | com.orange.common
11 | spring-boot-autoconfigure-proxy
12 | 1.0.2
13 | spring-boot-autoconfigure-proxy
14 | Spring Boot AutoConfigure Proxy
15 |
16 |
17 | github
18 | UTF-8
19 | 1.8
20 |
21 |
22 |
23 |
24 |
25 | org.springframework.boot
26 | spring-boot
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter
31 |
32 |
33 | javax.validation
34 | validation-api
35 | 2.0.1.Final
36 |
37 |
38 | org.springframework.boot
39 | spring-boot-autoconfigure-processor
40 | true
41 |
42 |
43 | org.springframework.boot
44 | spring-boot-devtools
45 | runtime
46 |
47 |
48 | org.springframework.boot
49 | spring-boot-configuration-processor
50 | true
51 |
52 |
53 | org.springframework.boot
54 | spring-boot-starter-test
55 | test
56 |
57 |
58 |
59 |
60 |
61 |
62 | maven-release-plugin
63 | 2.5.3
64 |
65 | false
66 | release
67 | true
68 |
69 |
70 |
71 |
72 |
73 |
74 | https://github.com/Orange-OpenSource/spring-boot-autoconfigure-proxy.git
75 | scm:git:https://github.com/Orange-OpenSource/spring-boot-autoconfigure-proxy.git
76 | scm:git:https://github.com/Orange-OpenSource/spring-boot-autoconfigure-proxy.git
77 | HEAD
78 |
79 |
80 |
81 |
82 | github
83 | GitHub Orange-OpenSource Apache Maven Packages
84 | https://maven.pkg.github.com/Orange-OpenSource/spring-boot-autoconfigure-proxy
85 |
86 |
87 | github
88 | GitHub Orange-OpenSource Apache Maven Packages
89 | https://maven.pkg.github.com/Orange-OpenSource/spring-boot-autoconfigure-proxy
90 |
91 |
92 |
93 |
94 |
95 | Apache License Version 2.0
96 | http://www.apache.org/licenses/LICENSE-2.0
97 |
98 |
99 |
100 |
101 | Orange
102 | http://www.orange.com
103 |
104 |
105 |
106 |
107 | release
108 |
109 |
110 |
111 | maven-source-plugin
112 |
113 |
114 | attach-sources
115 |
116 | jar
117 |
118 |
119 |
120 |
121 |
122 | maven-javadoc-plugin
123 |
124 |
125 | attach-javadocs
126 |
127 | jar
128 |
129 |
130 | false
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/src/main/java/com/orange/common/springboot/autoconfigure/proxy/HostnameMatcher.java:
--------------------------------------------------------------------------------
1 | package com.orange.common.springboot.autoconfigure.proxy;
2 |
3 | import java.util.regex.Pattern;
4 |
5 | public abstract class HostnameMatcher {
6 |
7 | public abstract boolean matches(String hostname);
8 |
9 | public static HostnameMatcher parse(String matcher) {
10 | if (matcher.startsWith("/") && matcher.endsWith("/")) {
11 | // the matcher is a regexp
12 | return new PatternMatcher(Pattern.compile(matcher.substring(1, matcher.length() - 1)));
13 | } else {
14 | // replace '*' wilcards, quote all the rest
15 | StringBuilder regex = new StringBuilder(matcher.length());
16 | int cur = 0;
17 | int next = 0;
18 | while (next < matcher.length() && (next = matcher.indexOf('*', cur)) >= 0) {
19 | if (next - cur > 0) {
20 | regex.append(Pattern.quote(matcher.substring(cur, next)));
21 | }
22 | regex.append(".*");
23 | cur = next + 1;
24 | }
25 | if (cur == 0) {
26 | // no star in the matcher
27 | return new HostOrDomainMatcher(matcher);
28 | } else {
29 | // append tail
30 | if (matcher.length() - cur > 0) {
31 | regex.append(Pattern.quote(matcher.substring(cur)));
32 | }
33 | return new PatternMatcher(Pattern.compile(regex.toString()));
34 | }
35 | }
36 | }
37 |
38 | static class HostOrDomainMatcher extends HostnameMatcher {
39 | private final String hostOrDomain;
40 |
41 | private HostOrDomainMatcher(String hostOrDomain) {
42 | this.hostOrDomain = hostOrDomain;
43 | }
44 |
45 | @Override
46 | public boolean matches(String hostname) {
47 | return hostname.endsWith(hostOrDomain)
48 | && (
49 | hostOrDomain.startsWith(".") // hostOrDomain is explicitly a domain
50 | || hostname.length() == hostOrDomain.length() // hostOrDomain is a hostname
51 | || hostname.charAt(hostname.length() - hostOrDomain.length() - 1) == '.' // hostOrDomain is maybe a domain name without leading '.'
52 | );
53 | }
54 | }
55 |
56 | static class PatternMatcher extends HostnameMatcher {
57 | private final Pattern pattern;
58 |
59 | private PatternMatcher(Pattern pattern) {
60 | this.pattern = pattern;
61 | }
62 |
63 | @Override
64 | public boolean matches(String hostname) {
65 | return pattern.matcher(hostname).matches();
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/com/orange/common/springboot/autoconfigure/proxy/MultiProxySelector.java:
--------------------------------------------------------------------------------
1 | package com.orange.common.springboot.autoconfigure.proxy;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | import java.io.IOException;
7 | import java.net.*;
8 | import java.util.*;
9 | import java.util.regex.PatternSyntaxException;
10 | import java.util.stream.Collectors;
11 |
12 | /**
13 | * This is a {@link ProxySelector} implementation able to manage several {@link Proxy} depending on the uri scheme and host
14 | */
15 | class MultiProxySelector extends ProxySelector {
16 | private static final Logger LOGGER = LoggerFactory.getLogger(MultiProxySelector.class);
17 |
18 | static class ProxyEntry {
19 | private final NetworkProxyProperties.ProxyServerConfig cfg;
20 | private final Proxy proxy;
21 | private final List positiveMatchers;
22 | private final List negativeMatchers;
23 |
24 | private ProxyEntry(NetworkProxyProperties.ProxyServerConfig cfg, Proxy proxy, List positiveMatchers, List negativeMatchers) {
25 | this.cfg = cfg;
26 | this.proxy = proxy;
27 | this.positiveMatchers = positiveMatchers;
28 | this.negativeMatchers = negativeMatchers;
29 | }
30 |
31 | Proxy getProxy() {
32 | return proxy;
33 | }
34 |
35 | boolean matches(String protocol, String host) {
36 | // test protocol matches
37 | if (!cfg.getForProtocols().contains(protocol)) {
38 | return false;
39 | }
40 | // test matchers
41 | if (positiveMatchers.isEmpty()) {
42 | return !negativeMatchers.stream().anyMatch(matcher -> matcher.matches(host));
43 | } else {
44 | return positiveMatchers.stream().anyMatch(matcher -> matcher.matches(host));
45 | }
46 | }
47 |
48 | @Override
49 | public String toString() {
50 | return cfg.toString();
51 | }
52 | }
53 |
54 | static class SchemeAndHost {
55 | final String protocol;
56 | final String host;
57 |
58 | SchemeAndHost(String protocol, String host) {
59 | this.protocol = protocol;
60 | this.host = host;
61 | }
62 |
63 | @Override
64 | public String toString() {
65 | return "{" +
66 | "protocol='" + protocol + '\'' +
67 | ", host='" + host + '\'' +
68 | '}';
69 | }
70 |
71 | @Override
72 | public boolean equals(Object o) {
73 | if (this == o) return true;
74 | if (o == null || getClass() != o.getClass()) return false;
75 | SchemeAndHost that = (SchemeAndHost) o;
76 | return Objects.equals(protocol, that.protocol) &&
77 | Objects.equals(host, that.host);
78 | }
79 |
80 | @Override
81 | public int hashCode() {
82 | return Objects.hash(protocol, host);
83 | }
84 | }
85 |
86 | private final List proxies;
87 |
88 | private Map> hostname2Proxies = new HashMap<>();
89 |
90 | private MultiProxySelector(List proxies) {
91 | this.proxies = proxies;
92 | }
93 |
94 | @Override
95 | public List select(URI uri) {
96 | if (uri == null) {
97 | throw new IllegalArgumentException("URI can't be null.");
98 | }
99 | String protocol = uri.getScheme();
100 | String host = uri.getHost();
101 |
102 | if (host == null) {
103 | // This is a hack to ensure backward compatibility in two
104 | // cases: 1. hostnames contain non-ascii characters,
105 | // internationalized domain names. in which case, URI will
106 | // return null, see BugID 4957669; 2. Some hostnames can
107 | // contain '_' chars even though it's not supposed to be
108 | // legal, in which case URI will return null for getHost,
109 | // but not for getAuthority() See BugID 4913253
110 | String auth = uri.getAuthority();
111 | if (auth != null) {
112 | int i;
113 | i = auth.indexOf('@');
114 | if (i >= 0) {
115 | auth = auth.substring(i + 1);
116 | }
117 | i = auth.lastIndexOf(':');
118 | if (i >= 0) {
119 | auth = auth.substring(0, i);
120 | }
121 | host = auth;
122 | }
123 | }
124 |
125 | if (protocol == null || host == null) {
126 | throw new IllegalArgumentException("protocol = " + protocol + " host = " + host);
127 | }
128 |
129 | return hostname2Proxies.computeIfAbsent(new SchemeAndHost(protocol, host), this::doGetProxies);
130 | }
131 |
132 | private List doGetProxies(SchemeAndHost schemeAndHost) {
133 | Proxy proxy = proxies.stream()
134 | .filter(e -> e.matches(schemeAndHost.protocol, schemeAndHost.host))
135 | .map(ProxyEntry::getProxy)
136 | .findFirst()
137 | .orElse(Proxy.NO_PROXY);
138 | LOGGER.info("Proxies for [{}] : {}", schemeAndHost, proxy);
139 | return Collections.singletonList(proxy);
140 | }
141 |
142 | @Override
143 | public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
144 | LOGGER.info("connect failed: {}", uri, ioe);
145 | }
146 |
147 | @Override
148 | public String toString() {
149 | return "MultiProxySelector{" +
150 | "proxies=" + proxies +
151 | '}';
152 | }
153 |
154 | static MultiProxySelector build(List proxies) {
155 | List proxyEntries = new ArrayList<>();
156 | for (int i = 0; i < proxies.size(); i++) {
157 | NetworkProxyProperties.ProxyServerConfig cfg = proxies.get(i);
158 |
159 | if (cfg.getHost() == null || cfg.getHost().length() == 0) {
160 | throw new IllegalArgumentException("network.proxy.servers[" + i + "].host can't be null or empty.");
161 | }
162 |
163 | if (cfg.getPort() == null) {
164 | throw new IllegalArgumentException("network.proxy.servers[" + i + "].port can't be null.");
165 | }
166 |
167 | int countMatchers = (cfg.getForHosts().isEmpty() ? 0 : 1) + (cfg.getNotForHosts().isEmpty() ? 0 : 1);
168 | if (countMatchers == 0) {
169 | throw new IllegalArgumentException("network.proxy.servers[" + i + "] must define either '.forHosts' or '.notForHosts' matchers in configuration.");
170 | } else if (countMatchers > 1) {
171 | throw new IllegalArgumentException("network.proxy.servers[" + i + "] you can't specify both '.forHosts' and '.notForHosts' matchers in configuration.");
172 | }
173 |
174 | // parse matchers
175 | List positiveMatchers;
176 | try {
177 | positiveMatchers = cfg.getForHosts().stream().map(HostnameMatcher::parse).collect(Collectors.toList());
178 | } catch (PatternSyntaxException pte) {
179 | throw new IllegalArgumentException("network.proxy.servers[" + i + "].for-hosts contains an invalid pattern.", pte);
180 | }
181 | List negativeMatchers;
182 | try {
183 | negativeMatchers = cfg.getNotForHosts().stream().map(HostnameMatcher::parse).collect(Collectors.toList());
184 | } catch (PatternSyntaxException pte) {
185 | throw new IllegalArgumentException("network.proxy.servers[" + i + "].not-for-hosts contains an invalid pattern.", pte);
186 | }
187 |
188 | // make proxy
189 | Proxy proxy = new Proxy(cfg.getType() == NetworkProxyProperties.ProxyServerConfig.Type.http ? Proxy.Type.HTTP : Proxy.Type.SOCKS, new InetSocketAddress(cfg.getHost(), cfg.getPort()));
190 | proxyEntries.add(new ProxyEntry(cfg, proxy, positiveMatchers, negativeMatchers));
191 | }
192 |
193 | return new MultiProxySelector(proxyEntries);
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/main/java/com/orange/common/springboot/autoconfigure/proxy/MultiServerAuthenticator.java:
--------------------------------------------------------------------------------
1 | package com.orange.common.springboot.autoconfigure.proxy;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | import java.net.Authenticator;
7 | import java.net.PasswordAuthentication;
8 | import java.util.HashMap;
9 | import java.util.Map;
10 |
11 | /**
12 | * This is an {@link Authenticator} implementation able to manage several servers
13 | */
14 | public class MultiServerAuthenticator extends Authenticator {
15 | private static final Logger LOGGER = LoggerFactory.getLogger(MultiServerAuthenticator.class);
16 |
17 | private Map host2Authent = new HashMap<>();
18 |
19 | public void add(String host, String user, String password) {
20 | host2Authent.put(host, new PasswordAuthentication(user, password.toCharArray()));
21 | }
22 |
23 | @Override
24 | protected PasswordAuthentication getPasswordAuthentication() {
25 | String host = "" + getRequestingHost() + ":" + getRequestingPort();
26 | PasswordAuthentication passwordAuthentication = host2Authent.get(host);
27 | LOGGER.trace("using proxy authentication for <{}>: {}", host, passwordAuthentication == null ? "none" : passwordAuthentication.getUserName() + "/***");
28 | return passwordAuthentication;
29 | }
30 |
31 | public int size() {
32 | return host2Authent.size();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/orange/common/springboot/autoconfigure/proxy/NetworkProxyAutoConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.orange.common.springboot.autoconfigure.proxy;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.boot.autoconfigure.AutoConfigureAfter;
6 | import org.springframework.boot.autoconfigure.AutoConfigureOrder;
7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
8 | import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
9 | import org.springframework.boot.context.properties.EnableConfigurationProperties;
10 | import org.springframework.context.annotation.Configuration;
11 | import org.springframework.core.Ordered;
12 |
13 | import javax.annotation.PostConstruct;
14 | import java.net.Authenticator;
15 | import java.net.ProxySelector;
16 |
17 | @Configuration
18 | @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
19 | @AutoConfigureAfter(PropertyPlaceholderAutoConfiguration.class)
20 | @ConditionalOnProperty(prefix = "network.proxy", name = "enabled", havingValue = "true", matchIfMissing = true)
21 | @EnableConfigurationProperties(NetworkProxyProperties.class)
22 | public class NetworkProxyAutoConfiguration {
23 | private static final Logger LOGGER = LoggerFactory.getLogger(NetworkProxyAutoConfiguration.class);
24 | private static final String[] PROTOCOLS = {"http", "https", "ftp"};
25 |
26 | private final NetworkProxyProperties properties;
27 |
28 | public NetworkProxyAutoConfiguration(NetworkProxyProperties properties) {
29 | this.properties = properties;
30 | }
31 |
32 | @PostConstruct
33 | public void setupProxyConfiguration() {
34 | MultiServerAuthenticator msa = new MultiServerAuthenticator();
35 |
36 | if (!properties.getServers().isEmpty()) {
37 | // CASE 1: explicit proxies configuration
38 | LOGGER.info("Configuring proxies from Spring Boot configuration");
39 |
40 | // install proxy selector
41 | ProxySelector.setDefault(MultiProxySelector.build(properties.getServers()));
42 |
43 | // set password authentication for every proxy that need one
44 | for (NetworkProxyProperties.ProxyServerConfig cfg : properties.getServers()) {
45 | if (cfg.getUsername() != null && cfg.getPassword() != null) {
46 | msa.add(cfg.getHost() + ":" + cfg.getPort(), cfg.getUsername(), cfg.getPassword());
47 | }
48 | }
49 | } else {
50 | for (String protocol : PROTOCOLS) {
51 | ProxySettingsFromEnv proxySettings = ProxySettingsFromEnv.read(protocol);
52 | if (proxySettings != null) {
53 | // CASE 2: auto-conf from ENV
54 | LOGGER.info("Configuring proxy for {} from env '{}': {}", protocol, proxySettings.getEnvName(), proxySettings);
55 |
56 | // set password authent if specified
57 | if (proxySettings.getUsername() != null && proxySettings.getPassword() != null) {
58 | msa.add(proxySettings.getHost() + ":" + proxySettings.getPort(), proxySettings.getUsername(), proxySettings.getPassword());
59 | }
60 |
61 | // set proxy properties
62 | System.setProperty(protocol + ".proxyHost", proxySettings.getHost());
63 | System.setProperty(protocol + ".proxyPort", String.valueOf(proxySettings.getPort()));
64 | if (proxySettings.getNoProxyHosts() != null && proxySettings.getNoProxyHosts().length > 0) {
65 | System.setProperty(protocol + ".nonProxyHosts", String.join("|", proxySettings.getNoProxyHosts()));
66 | }
67 | } else {
68 | // CASE 3: auto-conf from Java properties (support http.proxyUser & http.proxyPassword)
69 | String host = System.getProperty(protocol + ".proxyHost");
70 | String port = System.getProperty(protocol + ".proxyPort");
71 | String username = System.getProperty(protocol + ".proxyUser");
72 | String password = System.getProperty(protocol + ".proxyPassword");
73 | if (host != null && port != null && username != null && password != null) {
74 | LOGGER.info("Configuring proxy authent for {} from Java properties '{}' & '{}'", protocol, protocol + ".proxyUser", protocol + ".proxyPassword");
75 | msa.add(host + ":" + port, username, password);
76 | } else {
77 | // no proxy configuration
78 | LOGGER.info("No proxy configuration found for {}", protocol);
79 | }
80 | }
81 | }
82 | }
83 |
84 | // install default authenticator (if not empty)
85 | if (msa.size() > 0) {
86 | // see: https://www.oracle.com/technetwork/java/javase/8u111-relnotes-3124969.html
87 | System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
88 | Authenticator.setDefault(msa);
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/main/java/com/orange/common/springboot/autoconfigure/proxy/NetworkProxyProperties.java:
--------------------------------------------------------------------------------
1 | package com.orange.common.springboot.autoconfigure.proxy;
2 |
3 | import org.springframework.boot.context.properties.ConfigurationProperties;
4 | import org.springframework.validation.Errors;
5 | import org.springframework.validation.Validator;
6 | import org.springframework.validation.annotation.Validated;
7 |
8 | import javax.validation.Valid;
9 | import javax.validation.constraints.NotEmpty;
10 | import javax.validation.constraints.NotNull;
11 | import java.util.Arrays;
12 | import java.util.Collections;
13 | import java.util.List;
14 | import java.util.regex.PatternSyntaxException;
15 |
16 | @ConfigurationProperties(prefix = "network.proxy")
17 | @Validated
18 | public class NetworkProxyProperties implements Validator {
19 | /**
20 | * Whether to enable network proxy auto configuration.
21 | */
22 | private boolean enabled = true;
23 |
24 | /**
25 | * Explicit network proxy servers configuration
26 | */
27 | @Valid
28 | private List servers = Collections.emptyList();
29 |
30 | public boolean isEnabled() {
31 | return enabled;
32 | }
33 |
34 | public void setEnabled(boolean enabled) {
35 | this.enabled = enabled;
36 | }
37 |
38 | public List getServers() {
39 | return servers;
40 | }
41 |
42 | public void setServers(List servers) {
43 | this.servers = servers;
44 | }
45 |
46 | @Override
47 | public String toString() {
48 | return "NetworkProxyProperties{" +
49 | "enabled=" + enabled +
50 | ", servers=" + servers +
51 | '}';
52 | }
53 |
54 | @Validated
55 | public static class ProxyServerConfig {
56 | enum Type {
57 | http, socks
58 | }
59 | /**
60 | * The proxy type ({@code http} or {@code socks}). Default: {@code http}.
61 | */
62 | private Type type = Type.http;
63 | /**
64 | * The proxy host
65 | */
66 | @NotEmpty
67 | private String host;
68 | /**
69 | * The proxy port
70 | */
71 | @NotNull
72 | private Integer port;
73 | /**
74 | * The proxy username
75 | */
76 | private String username;
77 | /**
78 | * The proxy password
79 | */
80 | private String password;
81 | /**
82 | * For hosts matchers
83 | */
84 | private List forHosts = Collections.emptyList();
85 | /**
86 | * Not for hosts matchers
87 | */
88 | private List notForHosts = Collections.emptyList();
89 | /**
90 | * Protocols. Default: {@code ["http", "https", "ftp"]}
91 | */
92 | private List forProtocols = Arrays.asList("http", "https", "ftp");
93 |
94 | public String getHost() {
95 | return host;
96 | }
97 |
98 | public void setHost(String host) {
99 | this.host = host;
100 | }
101 |
102 | public List getNotForHosts() {
103 | return notForHosts;
104 | }
105 |
106 | public void setNotForHosts(List notForHosts) {
107 | this.notForHosts = notForHosts;
108 | }
109 |
110 | public Integer getPort() {
111 | return port;
112 | }
113 |
114 | public void setPort(Integer port) {
115 | this.port = port;
116 | }
117 |
118 | public String getUsername() {
119 | return username;
120 | }
121 |
122 | public void setUsername(String username) {
123 | this.username = username;
124 | }
125 |
126 | public String getPassword() {
127 | return password;
128 | }
129 |
130 | public void setPassword(String password) {
131 | this.password = password;
132 | }
133 |
134 | public List getForHosts() {
135 | return forHosts;
136 | }
137 |
138 | public void setForHosts(List forHosts) {
139 | this.forHosts = forHosts;
140 | }
141 |
142 | public List getForProtocols() {
143 | return forProtocols;
144 | }
145 |
146 | public void setForProtocols(List forProtocols) {
147 | this.forProtocols = forProtocols;
148 | }
149 |
150 | public Type getType() {
151 | return type;
152 | }
153 |
154 | public void setType(Type type) {
155 | this.type = type;
156 | }
157 |
158 | @Override
159 | public String toString() {
160 | return "ProxyServerConfig{" +
161 | "type=" + type +
162 | ", host='" + host + '\'' +
163 | ", port=" + port +
164 | ", username='" + username + '\'' +
165 | ", password='" + (password == null ? "(none)" : "***") + '\'' +
166 | ", forHosts=" + forHosts +
167 | ", notForHosts=" + notForHosts +
168 | ", forProtocols=" + forProtocols +
169 | '}';
170 | }
171 | }
172 |
173 | // ================================================================================================================
174 | // === Validator impl
175 | // ================================================================================================================
176 |
177 | @Override
178 | public boolean supports(Class> clazz) {
179 | return clazz.equals(NetworkProxyProperties.class);
180 | }
181 |
182 | @Override
183 | public void validate(Object target, Errors errors) {
184 | NetworkProxyProperties properties = (NetworkProxyProperties) target;
185 | List proxies = properties.getServers();
186 | for (int i = 0; i < proxies.size(); i++) {
187 | NetworkProxyProperties.ProxyServerConfig cfg = proxies.get(i);
188 |
189 | int countMatchers = (cfg.getForHosts().isEmpty() ? 0 : 1) + (cfg.getNotForHosts().isEmpty() ? 0 : 1);
190 | if(countMatchers == 0) {
191 | errors.rejectValue("servers[" + i + "]", "nomatcher", "you must specify either 'forHosts' or 'notForHosts' matchers");
192 | } else if(countMatchers > 1) {
193 | errors.rejectValue("servers[" + i + "]", "toomanymatchers", "you can't specify both 'forHosts' and 'notForHosts' matchers");
194 | }
195 |
196 | // check patterns
197 | for(int j=0; j
13 | * Uses {@code HTTP_PROXY}, {@code HTTPS_PROXY}, {@code FTP_PROXY} and {@code NO_PROXY} variables.
14 | */
15 | public class ProxySettingsFromEnv {
16 | private static final Logger LOGGER = LoggerFactory.getLogger(MultiServerAuthenticator.class);
17 |
18 | private final String forProtocol;
19 | private final String protocol;
20 | private final String host;
21 | private final int port;
22 | private final String[] noProxyHosts;
23 | private final String username;
24 | private final String password;
25 |
26 | public ProxySettingsFromEnv(String forProtocol, String protocol, String host, int port, String[] noProxyHosts, String username, String password) {
27 | this.forProtocol = forProtocol;
28 | this.protocol = protocol;
29 | this.username = username;
30 | this.password = password;
31 | this.host = host;
32 | this.noProxyHosts = noProxyHosts;
33 | this.port = port;
34 | }
35 |
36 | /**
37 | * Returns the proxy protocol (one of {@code http}, {@code socks} or {@code socks5})
38 | */
39 | public String getProtocol() {
40 | return protocol;
41 | }
42 |
43 | /**
44 | * Returns the protocol (scheme) this proxy setting applies to
45 | */
46 | public String getForProtocol() {
47 | return forProtocol;
48 | }
49 |
50 | /**
51 | * Returns the proxy server host
52 | */
53 | public String getHost() {
54 | return host;
55 | }
56 |
57 | /**
58 | * Returns the proxy server port
59 | */
60 | public int getPort() {
61 | return port;
62 | }
63 |
64 | /**
65 | * Returns the list of no-proxy server hosts (matchers)
66 | */
67 | public String[] getNoProxyHosts() {
68 | return noProxyHosts;
69 | }
70 |
71 | /**
72 | * Returns the proxy username (if requires authentication)
73 | */
74 | public String getUsername() {
75 | return username;
76 | }
77 |
78 | /**
79 | * Returns the proxy password (if requires authentication)
80 | */
81 | public String getPassword() {
82 | return password;
83 | }
84 |
85 | /**
86 | * Returns the proxy setting environment variable name
87 | */
88 | public String getEnvName() {
89 | return forProtocol + "_proxy";
90 | }
91 |
92 | /**
93 | * Returns the proxy setting environment variable value (hides the password)
94 | */
95 | public String getEnvVal() {
96 | return protocol + "://" + (username == null ? "" : username + ":***@") + host + ":" + port;
97 | }
98 |
99 | @Override
100 | public String toString() {
101 | return "ProxySettingsFromEnv{" +
102 | "protocol='" + protocol + '\'' +
103 | ", host='" + host + '\'' +
104 | ", port=" + port +
105 | ", forProtocol=" + forProtocol +
106 | ", noProxyHosts=" + Arrays.toString(noProxyHosts) +
107 | ", username='" + (username == null ? "(none)" : username) + '\'' +
108 | ", password='" + (password == null ? "(none)" : "***") + '\'' +
109 | '}';
110 | }
111 |
112 | /**
113 | * Reads and parses the proxy settings from system environment
114 | *
115 | * @param protocol determines for which forProtocol the proxy settings shall be read
116 | * @return parsed setting, or {@code null} if not set
117 | */
118 | public static ProxySettingsFromEnv read(String protocol) {
119 | return parse(protocol, getEnvIgnoreCase(protocol + "_proxy"), getEnvIgnoreCase("no_proxy"));
120 | }
121 |
122 | static ProxySettingsFromEnv parse(String protocol, String proxyUrl, String noProxy) {
123 | if (proxyUrl == null) {
124 | return null;
125 | }
126 | try {
127 | URI url = new URI(proxyUrl);
128 | if (url.getHost() == null) {
129 | LOGGER.error("Invalid proxy configuration URL for {}: {} - host not specified", protocol, proxyUrl);
130 | return null;
131 | }
132 | if (url.getPort() == -1) {
133 | LOGGER.error("Invalid proxy configuration URL for {}: {} - port not specified", protocol, proxyUrl);
134 | return null;
135 | }
136 | // scheme is optional (defaults to http)
137 | String scheme = url.getScheme() == null ? "http" : url.getScheme();
138 |
139 | // read login/password
140 | String username = null;
141 | String password = null;
142 | String userInfo = url.getUserInfo();
143 | if (userInfo != null) {
144 | int idx = userInfo.indexOf(':');
145 | username = userInfo.substring(0, idx);
146 | password = userInfo.substring(idx + 1);
147 | }
148 | // add no proxy hosts
149 | String[] noProxyHosts = null;
150 | if (noProxy != null) {
151 | noProxyHosts = noProxy.split("\\s*,\\s*");
152 | }
153 | return new ProxySettingsFromEnv(protocol, scheme, url.getHost(), url.getPort(), noProxyHosts, username, password);
154 | } catch (URISyntaxException e) {
155 | LOGGER.error("Could not decode proxy configuration for {}: {}", protocol, proxyUrl, e);
156 | return null;
157 | }
158 | }
159 |
160 |
161 | static String getEnvIgnoreCase(String name) {
162 | String val = System.getenv(name.toLowerCase());
163 | return val != null ? val : System.getenv(name.toUpperCase());
164 | }
165 |
166 | }
167 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2 | com.orange.common.springboot.autoconfigure.proxy.NetworkProxyAutoConfiguration
--------------------------------------------------------------------------------
/src/test/java/com/orange/common/springboot/autoconfigure/proxy/HostnameMatcherTest.java:
--------------------------------------------------------------------------------
1 | package com.orange.common.springboot.autoconfigure.proxy;
2 |
3 | import org.junit.Test;
4 |
5 | import static com.orange.common.springboot.autoconfigure.proxy.HostnameMatcher.parse;
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | public class HostnameMatcherTest {
9 | @Test
10 | public void parse_domain_or_hostname_should_work() {
11 | HostnameMatcher matcher = parse("orange.com");
12 | assertThat(matcher)
13 | .isInstanceOf(HostnameMatcher.HostOrDomainMatcher.class);
14 | assertThat(matcher.matches("orange.com")).isTrue();
15 | assertThat(matcher.matches("portal.orange.com")).isTrue();
16 | assertThat(matcher.matches("myorange.com")).isFalse();
17 | }
18 |
19 | @Test
20 | public void parse_domain_should_work() {
21 | HostnameMatcher matcher = parse(".orange.com");
22 | assertThat(matcher)
23 | .isInstanceOf(HostnameMatcher.HostOrDomainMatcher.class);
24 | assertThat(matcher.matches("orange.com")).isFalse();
25 | assertThat(matcher.matches("portal.orange.com")).isTrue();
26 | assertThat(matcher.matches("myorange.com")).isFalse();
27 | }
28 |
29 | @Test
30 | public void parse_matcher_with_leading_star_should_work() {
31 | HostnameMatcher matcher = parse("*.orange.com");
32 | assertThat(matcher)
33 | .isInstanceOf(HostnameMatcher.PatternMatcher.class)
34 | .extracting("pattern")
35 | .extracting("pattern")
36 | .containsExactly(".*\\Q.orange.com\\E");
37 | assertThat(matcher.matches("orange.com")).isFalse();
38 | assertThat(matcher.matches("portal.orange.com")).isTrue();
39 | assertThat(matcher.matches("myorange.com")).isFalse();
40 | }
41 |
42 | @Test
43 | public void parse_matcher_with_trailing_star_should_work() {
44 | HostnameMatcher matcher = parse("*.orange.*");
45 | assertThat(matcher)
46 | .isInstanceOf(HostnameMatcher.PatternMatcher.class)
47 | .extracting("pattern")
48 | .extracting("pattern")
49 | .containsExactly(".*\\Q.orange.\\E.*");
50 | assertThat(matcher.matches("orange.com")).isFalse();
51 | assertThat(matcher.matches("portal.orange.com")).isTrue();
52 | assertThat(matcher.matches("portal.orange.fr")).isTrue();
53 | assertThat(matcher.matches("myorange.com")).isFalse();
54 | }
55 |
56 | @Test
57 | public void parse_regex_matcher_should_work() {
58 | assertThat(parse("/.*\\.orange\\.com/"))
59 | .isInstanceOf(HostnameMatcher.PatternMatcher.class)
60 | .extracting("pattern")
61 | .extracting("pattern")
62 | .containsExactly(".*\\.orange\\.com");
63 | }
64 | }
--------------------------------------------------------------------------------
/src/test/java/com/orange/common/springboot/autoconfigure/proxy/NetworkProxyAutoConfigurationTest.java:
--------------------------------------------------------------------------------
1 | package com.orange.common.springboot.autoconfigure.proxy;
2 |
3 | import org.assertj.core.api.Condition;
4 | import org.junit.Ignore;
5 | import org.junit.Test;
6 | import org.springframework.beans.factory.BeanCreationException;
7 | import org.springframework.boot.autoconfigure.AutoConfigurations;
8 | import org.springframework.boot.context.properties.bind.validation.BindValidationException;
9 | import org.springframework.boot.test.context.runner.ApplicationContextRunner;
10 | import org.springframework.validation.FieldError;
11 | import org.springframework.validation.ObjectError;
12 |
13 | import java.net.Proxy;
14 | import java.net.ProxySelector;
15 | import java.net.URI;
16 |
17 | import static org.assertj.core.api.Assertions.assertThat;
18 | import static org.assertj.core.api.Assertions.fail;
19 |
20 | @Ignore
21 | public class NetworkProxyAutoConfigurationTest {
22 | private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
23 | .withConfiguration(AutoConfigurations.of(NetworkProxyAutoConfiguration.class));
24 |
25 | @Test
26 | public void basic_conf_should_work() {
27 | this.contextRunner.withPropertyValues(
28 | "network.proxy.enable=true",
29 | "network.proxy.servers.0.host=proxyhost",
30 | "network.proxy.servers.0.port=3128",
31 | "network.proxy.servers.0.for-hosts.0=*"
32 | )
33 | .run((context) -> {
34 | assertThat(context.isRunning()).isTrue();
35 | assertThat(ProxySelector.getDefault()).isInstanceOf(MultiProxySelector.class);
36 | });
37 | }
38 |
39 | @Test
40 | public void basic_conf2_should_work() {
41 | this.contextRunner.withPropertyValues(
42 | "network.proxy.enable=true",
43 | "network.proxy.servers.0.host=proxyhost",
44 | "network.proxy.servers.0.port=3128",
45 | "network.proxy.servers.0.username=login",
46 | "network.proxy.servers.0.password=password",
47 | "network.proxy.servers.0.not-for-hosts.0=intranet.fr"
48 | )
49 | .run((context) -> {
50 | assertThat(context.isRunning()).isTrue();
51 | assertThat(ProxySelector.getDefault()).isInstanceOf(MultiProxySelector.class);
52 | });
53 | }
54 |
55 | @Test
56 | public void uppercase_type_should_work() {
57 | this.contextRunner.withPropertyValues(
58 | "network.proxy.enable=true",
59 | "network.proxy.servers.0.type=HTTP",
60 | "network.proxy.servers.0.host=proxyhost",
61 | "network.proxy.servers.0.port=3128",
62 | "network.proxy.servers.0.for-hosts.0=*"
63 | )
64 | .run((context) -> {
65 | assertThat(context.isRunning()).isTrue();
66 | assertThat(ProxySelector.getDefault()).isInstanceOf(MultiProxySelector.class);
67 | });
68 | }
69 |
70 | @Test
71 | public void invalid_type_should_fail() {
72 | this.contextRunner.withPropertyValues(
73 | "network.proxy.enable=true",
74 | "network.proxy.servers.0.type=nosuchtype",
75 | "network.proxy.servers.0.host=proxyhost",
76 | "network.proxy.servers.0.port=3128",
77 | "network.proxy.servers.0.for-hosts.0=*"
78 | )
79 | .run((context) -> {
80 | Throwable failure = context.getStartupFailure();
81 | assertIsBeanCreationException(failure, "networkProxyAutoConfiguration");
82 | // ConversionFailedException
83 | });
84 | }
85 |
86 | @Test
87 | public void missing_host_should_fail() {
88 | this.contextRunner.withPropertyValues(
89 | "network.proxy.enable=true",
90 | "network.proxy.servers.0.port=3128",
91 | "network.proxy.servers.0.for-hosts.0=*"
92 | )
93 | .run((context) -> {
94 | Throwable failure = context.getStartupFailure();
95 | assertIsBeanCreationException(failure, "networkProxyAutoConfiguration");
96 | assertIsValidationError(failure, "network.proxy", "servers[0].host", "NotEmpty");
97 | });
98 | }
99 |
100 | @Test
101 | public void missing_port_should_fail() {
102 | this.contextRunner.withPropertyValues(
103 | "network.proxy.enable=true",
104 | "network.proxy.servers.0.host=proxyhost",
105 | "network.proxy.servers.0.for-hosts.0=*"
106 | )
107 | .run((context) -> {
108 | Throwable failure = context.getStartupFailure();
109 | assertIsBeanCreationException(failure, "networkProxyAutoConfiguration");
110 | assertIsValidationError(failure, "network.proxy", "servers[0].port", "NotNull");
111 | });
112 | }
113 |
114 | @Test
115 | public void invalid_regex_matcher_should_fail() {
116 | this.contextRunner.withPropertyValues(
117 | "network.proxy.enable=true",
118 |
119 | "network.proxy.servers.0.host=INTRANET",
120 | "network.proxy.servers.0.port=3128",
121 | // faulty regex
122 | "network.proxy.servers.0.for-hosts.0=/+/",
123 | "network.proxy.servers.0.for-hosts.1=*.intranet.fr",
124 | "network.proxy.servers.0.for-hosts.2=some.host.on.intranet"
125 | )
126 | .run((context) -> {
127 | Throwable failure = context.getStartupFailure();
128 | assertIsBeanCreationException(failure, "networkProxyAutoConfiguration");
129 | assertIsValidationError(failure, "network.proxy", "servers[0].forHosts[0]", "invalid");
130 | });
131 | }
132 |
133 | @Test
134 | public void missing_matchers_should_fail() {
135 | this.contextRunner.withPropertyValues(
136 | "network.proxy.enable=true",
137 |
138 | "network.proxy.servers.0.host=INTRANET",
139 | "network.proxy.servers.0.port=3128"
140 | )
141 | .run((context) -> {
142 | Throwable failure = context.getStartupFailure();
143 | assertIsBeanCreationException(failure, "networkProxyAutoConfiguration");
144 | assertIsValidationError(failure, "network.proxy", "servers[0]", "nomatcher");
145 | });
146 | }
147 |
148 | @Test
149 | public void two_matchers_should_fail() {
150 | this.contextRunner.withPropertyValues(
151 | "network.proxy.enable=true",
152 | "network.proxy.servers.0.host=proxyhost",
153 | "network.proxy.servers.0.port=3128",
154 | "network.proxy.servers.0.for-hosts.0=*",
155 | "network.proxy.servers.0.not-for-hosts.0=*"
156 | )
157 | .run((context) -> {
158 | Throwable failure = context.getStartupFailure();
159 | assertIsBeanCreationException(failure, "networkProxyAutoConfiguration");
160 | assertIsValidationError(failure, "network.proxy", "servers[0]", "toomanymatchers");
161 | });
162 | }
163 |
164 | @Test
165 | public void several_proxies_should_work() {
166 | this.contextRunner.withPropertyValues(
167 | "network.proxy.enable=true",
168 |
169 | "network.proxy.servers.0.host=INTRANET",
170 | "network.proxy.servers.0.port=3128",
171 | "network.proxy.servers.0.for-hosts.0=/10\\.236\\.\\d+\\.\\d+/",
172 | "network.proxy.servers.0.for-hosts.1=*.intranet.fr",
173 | "network.proxy.servers.0.for-hosts.2=app.intranet",
174 |
175 | "network.proxy.servers.1.host=INTERNET",
176 | "network.proxy.servers.1.port=3128",
177 | "network.proxy.servers.1.not-for-hosts.0=/10\\.99\\.\\d+\\.\\d+/",
178 | "network.proxy.servers.1.not-for-hosts.1=localhost",
179 | "network.proxy.servers.1.not-for-hosts.2=127.0.0.1"
180 | )
181 | .run((context) -> {
182 | assertThat(context.isRunning()).isTrue();
183 | ProxySelector selector = ProxySelector.getDefault();
184 | assertThat(selector).isInstanceOf(MultiProxySelector.class);
185 |
186 | // test intranet addresses
187 | assertThat(selector.select(new URI("http://host1.intranet.fr/a/b/c"))).hasSize(1)
188 | .element(0).has(proxy(Proxy.Type.HTTP, "INTRANET:3128"));
189 | assertThat(selector.select(new URI("socket://host1.intranet.fr/a/b/c"))).hasSize(1)
190 | .element(0).has(proxy(Proxy.Type.DIRECT, null));
191 | assertThat(selector.select(new URI("http://10.236.1.1/a/b/c"))).hasSize(1)
192 | .element(0).has(proxy(Proxy.Type.HTTP, "INTRANET:3128"));
193 | assertThat(selector.select(new URI("http://some.app.intranet/a/b/c"))).hasSize(1)
194 | .element(0).has(proxy(Proxy.Type.HTTP, "INTRANET:3128"));
195 | assertThat(selector.select(new URI("http://app.intranet/a/b/c"))).hasSize(1)
196 | .element(0).has(proxy(Proxy.Type.HTTP, "INTRANET:3128"));
197 | // test local addresses
198 | assertThat(selector.select(new URI("https://localhost/a/b/c"))).hasSize(1)
199 | .element(0).has(proxy(Proxy.Type.DIRECT, null));
200 | assertThat(selector.select(new URI("https://127.0.0.1/a/b/c"))).hasSize(1)
201 | .element(0).has(proxy(Proxy.Type.DIRECT, null));
202 | assertThat(selector.select(new URI("https://10.99.1.1/a/b/c"))).hasSize(1)
203 | .element(0).has(proxy(Proxy.Type.DIRECT, null));
204 | // test internet addresses
205 | assertThat(selector.select(new URI("https://www.google.com/"))).hasSize(1)
206 | .element(0).has(proxy(Proxy.Type.HTTP, "INTERNET:3128"));
207 | assertThat(selector.select(new URI("http://www.google.com/"))).hasSize(1)
208 | .element(0).has(proxy(Proxy.Type.HTTP, "INTERNET:3128"));
209 | assertThat(selector.select(new URI("http://172.3.12.5/a/b/c"))).hasSize(1)
210 | .element(0).has(proxy(Proxy.Type.HTTP, "INTERNET:3128"));
211 | assertThat(selector.select(new URI("http://someapp.intranet/a/b/c"))).hasSize(1)
212 | .element(0).has(proxy(Proxy.Type.HTTP, "INTERNET:3128"));
213 | assertThat(selector.select(new URI("socket://www.google.com/"))).hasSize(1)
214 | .element(0).has(proxy(Proxy.Type.DIRECT, null));
215 |
216 | });
217 | }
218 |
219 | private Condition proxy(Proxy.Type type, String address) {
220 | return new Condition<>(proxy -> {
221 | assertThat(proxy).extracting("type").containsExactly(type);
222 | if (address != null) {
223 | assertThat(proxy.address().toString()).isEqualTo(address);
224 | }
225 | return true;
226 | }, "proxy condition");
227 | }
228 |
229 | void assertIsBeanCreationException(Throwable error, String beanName) {
230 | assertThat(error).isInstanceOf(BeanCreationException.class)
231 | .extracting("beanName").containsExactly(beanName);
232 | }
233 |
234 | void assertIsValidationError(Throwable error, String objectName, String field, String code) {
235 | while (error != null && !(error instanceof BindValidationException) && error.getCause() != error) {
236 | error = error.getCause();
237 | }
238 | assertThat(error).isInstanceOf(BindValidationException.class);
239 | BindValidationException bve = (BindValidationException) error;
240 | for (ObjectError err : bve.getValidationErrors().getAllErrors()) {
241 | if (err instanceof FieldError) {
242 | if (err.getObjectName().equals(objectName) && ((FieldError) err).getField().equals(field)) {
243 | assertThat(err.getCode()).isEqualTo(code);
244 | return;
245 | }
246 | }
247 | }
248 | fail("expected field " + field + " error not found");
249 | }
250 |
251 | }
--------------------------------------------------------------------------------
/src/test/java/com/orange/common/springboot/autoconfigure/proxy/ProxySettingsFromEnvTest.java:
--------------------------------------------------------------------------------
1 | package com.orange.common.springboot.autoconfigure.proxy;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | public class ProxySettingsFromEnvTest {
8 | @Test
9 | public void simple_http_proxy_should_be_parsed() {
10 | ProxySettingsFromEnv proxy = ProxySettingsFromEnv.parse("http", "http://proxy:8080", null);
11 | assertThat(proxy).isNotNull();
12 | assertThat(proxy.getProtocol()).isEqualTo("http");
13 | assertThat(proxy.getHost()).isEqualTo("proxy");
14 | assertThat(proxy.getPort()).isEqualTo(8080);
15 | assertThat(proxy.getForProtocol()).isEqualTo("http");
16 | assertThat(proxy.getUsername()).isNull();
17 | assertThat(proxy.getPassword()).isNull();
18 | assertThat(proxy.getNoProxyHosts()).isNullOrEmpty();
19 | }
20 |
21 | @Test
22 | public void simple_socks_proxy_should_be_parsed() {
23 | ProxySettingsFromEnv proxy = ProxySettingsFromEnv.parse("http", "socks://proxy:8080", null);
24 | assertThat(proxy).isNotNull();
25 | assertThat(proxy.getProtocol()).isEqualTo("socks");
26 | assertThat(proxy.getHost()).isEqualTo("proxy");
27 | assertThat(proxy.getPort()).isEqualTo(8080);
28 | assertThat(proxy.getForProtocol()).isEqualTo("http");
29 | assertThat(proxy.getUsername()).isNull();
30 | assertThat(proxy.getPassword()).isNull();
31 | assertThat(proxy.getNoProxyHosts()).isNullOrEmpty();
32 | }
33 |
34 | @Test
35 | public void simple_socks5_proxy_should_be_parsed() {
36 | ProxySettingsFromEnv proxy = ProxySettingsFromEnv.parse("http", "socks5://proxy:8080", null);
37 | assertThat(proxy).isNotNull();
38 | assertThat(proxy.getProtocol()).isEqualTo("socks5");
39 | assertThat(proxy.getHost()).isEqualTo("proxy");
40 | assertThat(proxy.getPort()).isEqualTo(8080);
41 | assertThat(proxy.getForProtocol()).isEqualTo("http");
42 | assertThat(proxy.getUsername()).isNull();
43 | assertThat(proxy.getPassword()).isNull();
44 | assertThat(proxy.getNoProxyHosts()).isNullOrEmpty();
45 | }
46 |
47 | @Test
48 | public void http_with_authent_proxy_should_be_parsed() {
49 | ProxySettingsFromEnv proxy = ProxySettingsFromEnv.parse("http", "http://user:password@proxy:8080", null);
50 | assertThat(proxy).isNotNull();
51 | assertThat(proxy.getProtocol()).isEqualTo("http");
52 | assertThat(proxy.getHost()).isEqualTo("proxy");
53 | assertThat(proxy.getPort()).isEqualTo(8080);
54 | assertThat(proxy.getForProtocol()).isEqualTo("http");
55 | assertThat(proxy.getUsername()).isEqualTo("user");
56 | assertThat(proxy.getPassword()).isEqualTo("password");
57 | assertThat(proxy.getNoProxyHosts()).isNullOrEmpty();
58 | }
59 |
60 | @Test
61 | public void http_proxy_with_nohosts_should_be_parsed() {
62 | ProxySettingsFromEnv proxy = ProxySettingsFromEnv.parse("http", "http://proxy:8080", "localhost, 127.0.0.1, *.intranet.fr");
63 | assertThat(proxy).isNotNull();
64 | assertThat(proxy.getProtocol()).isEqualTo("http");
65 | assertThat(proxy.getHost()).isEqualTo("proxy");
66 | assertThat(proxy.getPort()).isEqualTo(8080);
67 | assertThat(proxy.getForProtocol()).isEqualTo("http");
68 | assertThat(proxy.getUsername()).isNull();
69 | assertThat(proxy.getPassword()).isNull();
70 | assertThat(proxy.getNoProxyHosts()).containsExactly("localhost", "127.0.0.1", "*.intranet.fr");
71 | }
72 |
73 | }
--------------------------------------------------------------------------------