Skip to content

Commit 2d95e92

Browse files
committed
HTTP(S) proxy support
1 parent cf13ef8 commit 2d95e92

File tree

4 files changed

+194
-45
lines changed

4 files changed

+194
-45
lines changed

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: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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.List;
35+
36+
public final class HttpClient {
37+
// When we successfully connect to a proxy, we store it here so that we can try it first for subsequent requests.
38+
private static Proxy lastUsedProxy = null;
39+
40+
private static final List<ProxySupplier> PROXIES = Arrays.asList(
41+
uri -> lastUsedProxy != null
42+
? Collections.singletonList(lastUsedProxy) // First try the last used proxy if we have it from a previous request
43+
: Collections.emptyList(),
44+
uri -> null, // Direct connect without proxy
45+
uri -> ProxySelector.getDefault().select(uri), // Common Java proxy system properties See: sun.net.spi.DefaultProxySelector
46+
HttpClient::getEnvironmentProxies // CURL environment variables
47+
);
48+
// TODO system proxy setting, (Windows/MacOS)
49+
// TODO automatic proxy detection, (WPAD)
50+
51+
private static final int HTTP_TIMEOUT_MS = 8000;
52+
53+
private HttpClient() {
54+
}
55+
56+
public static String readString(URL url) throws IOException {
57+
return tryWithProxies(url, Utils::readString);
58+
}
59+
60+
public static void downloadFile(URL url, Path path) throws IOException {
61+
try {
62+
tryWithProxies(url, (Handler<Void>) in -> {
63+
Files.createDirectories(path.getParent());
64+
Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
65+
return null;
66+
});
67+
} catch (Throwable t) {
68+
try {
69+
Files.deleteIfExists(path);
70+
} catch (Throwable t2) {
71+
t.addSuppressed(t2);
72+
}
73+
74+
throw t;
75+
}
76+
}
77+
78+
private static InputStream openUrl(URL url, Proxy proxy) throws IOException {
79+
HttpURLConnection conn = (HttpURLConnection) (proxy == null ? url.openConnection() : url.openConnection(proxy));
80+
81+
conn.setConnectTimeout(HTTP_TIMEOUT_MS);
82+
conn.setReadTimeout(HTTP_TIMEOUT_MS);
83+
conn.connect();
84+
85+
int responseCode = conn.getResponseCode();
86+
if (responseCode < 200 || responseCode >= 300) throw new IOException("HTTP request to "+url+" failed: "+responseCode);
87+
88+
return conn.getInputStream();
89+
}
90+
91+
// Returns the list of proxies set via environment variables.
92+
// This reads the de-facto standard environment variables used by CURL, see https://superuser.com/a/1166790
93+
private static List<Proxy> getEnvironmentProxies(URI uri) {
94+
ArrayList<Proxy> proxies = new ArrayList<>();
95+
96+
String httpProxy = System.getenv("HTTP_PROXY");
97+
98+
if (httpProxy == null) {
99+
// CURL only supports the lowercase environment variable
100+
httpProxy = System.getenv("http_proxy");
101+
}
102+
103+
if (httpProxy != null) {
104+
try {
105+
proxies.add(parseProxy(httpProxy));
106+
} catch (URISyntaxException e) {
107+
System.err.println("Invalid HTTP_PROXY environment variable: " + httpProxy);
108+
}
109+
}
110+
111+
String httpsProxy = System.getenv("HTTPS_PROXY");
112+
113+
if (httpsProxy != null) {
114+
try {
115+
proxies.add(parseProxy(httpsProxy));
116+
} catch (URISyntaxException e) {
117+
System.err.println("Invalid HTTPS_PROXY environment variable: " + httpsProxy);
118+
}
119+
}
120+
121+
return proxies;
122+
}
123+
124+
private static Proxy parseProxy(String str) throws URISyntaxException {
125+
URI uri = new URI(str);
126+
String host = uri.getHost();
127+
int port = uri.getPort() == -1 ? 80 : uri.getPort(); // Default to port 80 if not specified
128+
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port));
129+
}
130+
131+
private static <T> T tryWithProxies(URL url, Handler<T> handler) throws IOException {
132+
URI uri;
133+
134+
try {
135+
uri = url.toURI();
136+
} catch (URISyntaxException e) {
137+
throw new IOException(e.getMessage(), e);
138+
}
139+
140+
IOException exception = null;
141+
142+
for (ProxySupplier proxySupplier : PROXIES) {
143+
List<Proxy> proxies = proxySupplier.getProxies(uri);
144+
145+
if (proxies == null) {
146+
proxies = Collections.singletonList(null);
147+
}
148+
149+
// Try each proxy in the list
150+
for (Proxy proxy : proxies) {
151+
try {
152+
T value;
153+
154+
try (InputStream is = openUrl(url, proxy)) {
155+
value = handler.read(is);
156+
}
157+
158+
lastUsedProxy = proxy; // Store the last used proxy so we can try it first next time
159+
return value;
160+
} catch (IOException e) {
161+
if (exception == null) {
162+
exception = e;
163+
} else {
164+
exception.addSuppressed(e);
165+
}
166+
}
167+
}
168+
}
169+
170+
if (exception != null) {
171+
throw exception;
172+
} else {
173+
// Should never happen, as we always try to connect directly first
174+
throw new IllegalStateException("Did not attempt http connection");
175+
}
176+
}
177+
178+
private interface ProxySupplier {
179+
// Returns a list of proxies for the given URI, or a null list if no proxy should be used.
180+
// A null proxy entry in the list is skipped.
181+
List<Proxy> getProxies(URI uri) throws IOException;
182+
}
183+
184+
private interface Handler<T> {
185+
T read(InputStream in) throws IOException;
186+
}
187+
}

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)