Skip to content

Commit 96bea0e

Browse files
HTTP(S) proxy support (#160)
Co-authored-by: Player <[email protected]>
1 parent cf13ef8 commit 96bea0e

File tree

6 files changed

+329
-45
lines changed

6 files changed

+329
-45
lines changed

src/main/java/net/fabricmc/installer/Main.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public static void main(String[] args) throws IOException {
4141
System.setProperty("javax.net.ssl.trustStoreType", "WINDOWS-ROOT");
4242
}
4343

44+
System.setProperty("java.net.useSystemProxies", "true");
45+
4446
System.out.println("Loading Fabric Installer: " + Main.class.getPackage().getImplementationVersion());
4547

4648
HANDLERS.add(new ClientHandler());

src/main/java/net/fabricmc/installer/server/MinecraftServerDownloader.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.nio.file.Path;
2323
import java.nio.file.StandardCopyOption;
2424

25+
import net.fabricmc.installer.util.HttpClient;
2526
import net.fabricmc.installer.util.LauncherMeta;
2627
import net.fabricmc.installer.util.Utils;
2728
import net.fabricmc.installer.util.VersionMeta;
@@ -41,7 +42,7 @@ public void downloadMinecraftServer(Path serverJar) throws IOException {
4142

4243
Path serverJarTmp = serverJar.resolveSibling(serverJar.getFileName().toString() + ".tmp");
4344
Files.deleteIfExists(serverJar);
44-
Utils.downloadFile(new URL(getServerDownload().url), serverJarTmp);
45+
HttpClient.downloadFile(new URL(getServerDownload().url), serverJarTmp);
4546

4647
if (!isServerJarValid(serverJarTmp)) {
4748
throw new IOException("Failed to validate downloaded server jar");

src/main/java/net/fabricmc/installer/util/FabricService.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,35 +33,35 @@ public final class FabricService {
3333
* Query Fabric Meta path and decode as JSON.
3434
*/
3535
public static Json queryMetaJson(String path) throws IOException {
36-
return invokeWithFallbacks((service, arg) -> Json.read(Utils.readString(new URL(service.meta + arg))), path);
36+
return invokeWithFallbacks((service, arg) -> Json.read(HttpClient.readString(new URL(service.meta + arg))), path);
3737
}
3838

3939
/**
4040
* Query and decode JSON from url, substituting Fabric Maven with fallbacks or overrides.
4141
*/
4242
public static Json queryJsonSubstitutedMaven(String url) throws IOException {
4343
if (!url.startsWith(Reference.DEFAULT_MAVEN_SERVER)) {
44-
return Json.read(Utils.readString(new URL(url)));
44+
return Json.read(HttpClient.readString(new URL(url)));
4545
}
4646

4747
String path = url.substring(Reference.DEFAULT_MAVEN_SERVER.length());
4848

49-
return invokeWithFallbacks((service, arg) -> Json.read(Utils.readString(new URL(service.maven + arg))), path);
49+
return invokeWithFallbacks((service, arg) -> Json.read(HttpClient.readString(new URL(service.maven + arg))), path);
5050
}
5151

5252
/**
5353
* Download url to file, substituting Fabric Maven with fallbacks or overrides.
5454
*/
5555
public static void downloadSubstitutedMaven(String url, Path out) throws IOException {
5656
if (!url.startsWith(Reference.DEFAULT_MAVEN_SERVER)) {
57-
Utils.downloadFile(new URL(url), out);
57+
HttpClient.downloadFile(new URL(url), out);
5858
return;
5959
}
6060

6161
String path = url.substring(Reference.DEFAULT_MAVEN_SERVER.length());
6262

6363
invokeWithFallbacks((service, arg) -> {
64-
Utils.downloadFile(new URL(service.maven + arg), out);
64+
HttpClient.downloadFile(new URL(service.maven + arg), out);
6565
return null;
6666
}, path);
6767
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package net.fabricmc.installer.util;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.net.HttpURLConnection;
22+
import java.net.InetSocketAddress;
23+
import java.net.Proxy;
24+
import java.net.ProxySelector;
25+
import java.net.URI;
26+
import java.net.URISyntaxException;
27+
import java.net.URL;
28+
import java.nio.file.Files;
29+
import java.nio.file.Path;
30+
import java.nio.file.StandardCopyOption;
31+
import java.util.ArrayList;
32+
import java.util.Arrays;
33+
import java.util.Collections;
34+
import java.util.HashSet;
35+
import java.util.List;
36+
import java.util.Set;
37+
38+
public final class HttpClient {
39+
// When we successfully connect to a proxy, we store it here so that we can try it first for subsequent requests.
40+
private static volatile Proxy lastSuccessfulProxy;
41+
42+
private static final List<ProxySupplier> PROXIES = Arrays.asList(
43+
uri -> Collections.singletonList(Proxy.NO_PROXY), // Direct connect without proxy
44+
uri -> ProxySelector.getDefault().select(uri), // Common Java proxy system properties and system configured proxies See: sun.net.spi.DefaultProxySelector
45+
HttpClient::getEnvironmentProxies // CURL environment variables
46+
);
47+
48+
private static final int HTTP_TIMEOUT_MS = 8000;
49+
50+
private HttpClient() {
51+
}
52+
53+
public static String readString(URL url) throws IOException {
54+
return tryWithProxies(url, Utils::readString);
55+
}
56+
57+
public static void downloadFile(URL url, Path path) throws IOException {
58+
try {
59+
tryWithProxies(url, (Handler<Void>) in -> {
60+
Files.createDirectories(path.getParent());
61+
Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
62+
return null;
63+
});
64+
} catch (Throwable t) {
65+
try {
66+
Files.deleteIfExists(path);
67+
} catch (Throwable t2) {
68+
t.addSuppressed(t2);
69+
}
70+
71+
throw t;
72+
}
73+
}
74+
75+
private static InputStream openUrl(URL url, Proxy proxy) throws IOException {
76+
HttpURLConnection conn = (HttpURLConnection) url.openConnection(proxy);
77+
78+
conn.setConnectTimeout(HTTP_TIMEOUT_MS);
79+
conn.setReadTimeout(HTTP_TIMEOUT_MS);
80+
conn.connect();
81+
82+
int responseCode = conn.getResponseCode();
83+
if (responseCode < 200 || responseCode >= 300) throw new IOException("HTTP request to "+url+" failed: "+responseCode);
84+
85+
return conn.getInputStream();
86+
}
87+
88+
// Returns the list of proxies set via environment variables.
89+
// This reads the de-facto standard environment variables used by CURL, see https://superuser.com/a/1166790
90+
private static List<Proxy> getEnvironmentProxies(URI uri) {
91+
ArrayList<Proxy> proxies = new ArrayList<>();
92+
93+
String httpProxy = System.getenv("HTTP_PROXY");
94+
95+
if (httpProxy == null) {
96+
// CURL only supports the lowercase environment variable
97+
httpProxy = System.getenv("http_proxy");
98+
}
99+
100+
if (httpProxy != null) {
101+
try {
102+
proxies.add(parseProxy(httpProxy));
103+
} catch (URISyntaxException e) {
104+
System.err.println("Invalid HTTP_PROXY environment variable: " + httpProxy);
105+
}
106+
}
107+
108+
String httpsProxy = System.getenv("HTTPS_PROXY");
109+
110+
if (httpsProxy != null) {
111+
try {
112+
proxies.add(parseProxy(httpsProxy));
113+
} catch (URISyntaxException e) {
114+
System.err.println("Invalid HTTPS_PROXY environment variable: " + httpsProxy);
115+
}
116+
}
117+
118+
return proxies;
119+
}
120+
121+
private static Proxy parseProxy(String str) throws URISyntaxException {
122+
URI uri = new URI(str);
123+
String host = uri.getHost();
124+
int port = uri.getPort() == -1 ? 80 : uri.getPort(); // Default to port 80 if not specified
125+
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port));
126+
}
127+
128+
private static <T> T tryWithProxies(URL url, Handler<T> handler) throws IOException {
129+
URI uri;
130+
131+
try {
132+
uri = url.toURI();
133+
} catch (URISyntaxException e) {
134+
throw new IOException(e.getMessage(), e);
135+
}
136+
137+
Set<Proxy> attemptedProxies = new HashSet<>();
138+
IOException exception = null;
139+
140+
// try lastSuccessfulProxy first, if available
141+
if (lastSuccessfulProxy != null) {
142+
attemptedProxies.add(lastSuccessfulProxy);
143+
144+
try (InputStream is = openUrl(url, lastSuccessfulProxy)) {
145+
return handler.read(is);
146+
} catch (IOException e) {
147+
HttpClient.lastSuccessfulProxy = null; // failed, remove priority for the specific proxy
148+
exception = e;
149+
}
150+
}
151+
152+
// try all other proxies
153+
154+
for (ProxySupplier proxySupplier : PROXIES) {
155+
List<Proxy> proxies = proxySupplier.getProxies(uri);
156+
157+
// Try each proxy in the list
158+
for (Proxy proxy : proxies) {
159+
if (proxy == null) {
160+
continue;
161+
}
162+
163+
// Don't attempt to use a proxy that we have already tried
164+
if (!attemptedProxies.add(proxy)) {
165+
continue;
166+
}
167+
168+
try {
169+
T value;
170+
171+
try (InputStream is = openUrl(url, proxy)) {
172+
value = handler.read(is);
173+
}
174+
175+
HttpClient.lastSuccessfulProxy = proxy; // Store the last used proxy so we can try it first next time
176+
177+
return value;
178+
} catch (IOException e) {
179+
IOException ioe = new IOException(String.format("Request to %s using %s failed: %s", uri, proxy, e.getMessage()), e);
180+
181+
if (exception == null) {
182+
exception = ioe;
183+
} else {
184+
exception.addSuppressed(ioe);
185+
}
186+
}
187+
}
188+
}
189+
190+
if (exception != null) {
191+
throw exception;
192+
} else {
193+
// Should never happen, as we always try to connect directly first
194+
throw new IllegalStateException("Did not attempt http connection");
195+
}
196+
}
197+
198+
private interface ProxySupplier {
199+
// Returns a list of proxies for the given URI, or a null list if no proxy should be used.
200+
// A null proxy entry in the list is skipped.
201+
List<Proxy> getProxies(URI uri) throws IOException;
202+
}
203+
204+
private interface Handler<T> {
205+
T read(InputStream in) throws IOException;
206+
}
207+
}

src/main/java/net/fabricmc/installer/util/Utils.java

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,10 @@
1919
import java.io.IOException;
2020
import java.io.InputStream;
2121
import java.io.InputStreamReader;
22-
import java.net.HttpURLConnection;
23-
import java.net.URL;
2422
import java.nio.charset.StandardCharsets;
2523
import java.nio.file.Files;
2624
import java.nio.file.Path;
2725
import java.nio.file.Paths;
28-
import java.nio.file.StandardCopyOption;
2926
import java.security.MessageDigest;
3027
import java.security.NoSuchAlgorithmException;
3128
import java.text.DateFormat;
@@ -86,12 +83,6 @@ public static Path findDefaultInstallDir() {
8683
return dir.toAbsolutePath().normalize();
8784
}
8885

89-
public static String readString(URL url) throws IOException {
90-
try (InputStream is = openUrl(url)) {
91-
return readString(is);
92-
}
93-
}
94-
9586
public static String readString(Path path) throws IOException {
9687
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
9788
}
@@ -120,36 +111,6 @@ public static void writeToFile(Path path, String string) throws IOException {
120111
Files.write(path, string.getBytes(StandardCharsets.UTF_8));
121112
}
122113

123-
public static void downloadFile(URL url, Path path) throws IOException {
124-
try (InputStream in = openUrl(url)) {
125-
Files.createDirectories(path.getParent());
126-
Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
127-
} catch (Throwable t) {
128-
try {
129-
Files.deleteIfExists(path);
130-
} catch (Throwable t2) {
131-
t.addSuppressed(t2);
132-
}
133-
134-
throw t;
135-
}
136-
}
137-
138-
private static final int HTTP_TIMEOUT_MS = 8000;
139-
140-
private static InputStream openUrl(URL url) throws IOException {
141-
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
142-
143-
conn.setConnectTimeout(HTTP_TIMEOUT_MS);
144-
conn.setReadTimeout(HTTP_TIMEOUT_MS);
145-
conn.connect();
146-
147-
int responseCode = conn.getResponseCode();
148-
if (responseCode < 200 || responseCode >= 300) throw new IOException("HTTP request to "+url+" failed: "+responseCode);
149-
150-
return conn.getInputStream();
151-
}
152-
153114
public static String getProfileIcon() {
154115
try (InputStream is = Utils.class.getClassLoader().getResourceAsStream("profile_icon.png")) {
155116
byte[] ret = new byte[4096];

0 commit comments

Comments
 (0)