foul-magics/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java

372 lines
No EOL
12 KiB
Java

package mage.client.remote;
import mage.client.dialog.PreferencesDialog;
import mage.remote.Connection;
import mage.util.DebugUtil;
import mage.utils.MageVersion;
import org.apache.log4j.Logger;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
/**
* Network: proxy class to set up and use network connections like URLConnection
* <p>
* It used stream logic for data access
* <p>
* For text:
* - download text data by XmageURLConnection.downloadText
* <p>
* For binary data (e.g. file download):
* - get stream by XmageURLConnection.downloadBinary
* - save stream to file or process
* <p>
* TODO: no needs in POST requests (support only GET), but can be added later for another third party APIs
*
* @author JayDi85
*/
public class XmageURLConnection {
private static final MageVersion version = new MageVersion(XmageURLConnection.class);
private static final Logger logger = Logger.getLogger(XmageURLConnection.class);
private static final int CONNECTION_STARTING_TIMEOUT_MS = 10000;
private static final int CONNECTION_READING_TIMEOUT_MS = 60000;
private static final AtomicLong debugLastRequestTimeMs = new AtomicLong(0);
private static final ReentrantLock debugLogsWriterlock = new ReentrantLock();
static {
// add Authority Information Access (AIA) Extension support for certificates from Windows servers like gatherer website
// fix download errors like sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
System.setProperty("com.sun.security.enableAIAcaIssuers", "true");
}
final String url;
Proxy proxy = null;
HttpURLConnection connection = null;
HttpLoggingType loggingType = HttpLoggingType.ERRORS;
boolean forceGZipEncoding = false;
public XmageURLConnection(String url) {
this.url = url;
}
// example: 404 Not Found xxx
enum HttpLoggingType {
NONE,
ERRORS,
ALL
}
/**
* Add additional headers like non standard user agent, etc
*/
public void setRequestHeaders(Map<String, String> additionalHeaders) {
makeSureConnectionStarted();
for (String key : additionalHeaders.keySet()) {
this.connection.setRequestProperty(key, additionalHeaders.get(key));
}
}
public void setForceGZipEncoding(boolean enable) {
this.forceGZipEncoding = enable;
}
/**
* Connect to server
*/
public void startConnection() {
initDefaultProxy();
try {
URL url = new URL(this.url);
// proxy settings
if (this.proxy != null) {
this.connection = (HttpURLConnection) url.openConnection(this.proxy);
} else {
this.connection = (HttpURLConnection) url.openConnection();
}
// additional settings
this.connection.setConnectTimeout(CONNECTION_STARTING_TIMEOUT_MS);
this.connection.setReadTimeout(CONNECTION_READING_TIMEOUT_MS);
initDefaultHeaders();
} catch (IOException e) {
this.connection = null;
}
}
public void initDefaultProxy() {
Connection.ProxyType configProxyType = Connection.ProxyType.valueByText(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_TYPE, "None"));
Proxy.Type type;
switch (configProxyType) {
case HTTP:
type = Proxy.Type.HTTP;
break;
case SOCKS:
type = Proxy.Type.SOCKS;
break;
case NONE:
default:
type = Proxy.Type.DIRECT;
break;
}
this.proxy = Proxy.NO_PROXY;
if (!PreferencesDialog.NETWORK_ENABLE_PROXY_SUPPORT) {
return;
}
if (type != Proxy.Type.DIRECT) {
try {
String address = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_ADDRESS, "");
int port = Integer.parseInt(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_PORT, "80"));
this.proxy = new Proxy(type, new InetSocketAddress(address, port));
} catch (Exception e) {
throw new RuntimeException("Network: can't create proxy, check your settings or reset it - " + e, e);
}
}
}
private void initDefaultHeaders() {
// warning, Accept-Encoding processing inside URLConnection for http/https links (trying to use gzip by default)
// use force encoding for special use cases (example: download big text file as zip file)
if (forceGZipEncoding) {
this.connection.setRequestProperty("Accept-Encoding", "gzip");
}
this.connection.setRequestProperty("User-Agent", getDefaultUserAgent());
}
public static String getDefaultUserAgent() {
// user agent due standard notation User-Agent: <product> / <product-version> <comment>
// warning, dot not add os, language and other details
return String.format("XMage/%s build: %s", version.toString(false), version.getBuildTime());
}
/**
* Connect to server's resource
*/
public void connect() throws IOException {
makeSureConnectionStarted();
// debug: take time before send real request
long diffTime = 0;
if (DebugUtil.NETWORK_PROFILE_REQUESTS) {
long currentTime = System.currentTimeMillis();
long oldTime = debugLastRequestTimeMs.getAndSet(currentTime);
if (oldTime > 0) {
diffTime = currentTime - oldTime;
}
}
// send request
this.connection.connect();
// wait response
this.connection.getResponseCode();
// debug: save stats, can be called from diff threads
if (DebugUtil.NETWORK_PROFILE_REQUESTS) {
String debugInfo = String.format("+%d %d %s %s",
diffTime,
this.connection.getResponseCode(),
this.connection.getResponseMessage(),
this.url
) + System.lineSeparator();
debugLogsWriterlock.lock();
try {
// it's simple and slow save without write buffer, but it's ok for images download process
Files.write(Paths.get(DebugUtil.NETWORK_PROFILE_REQUESTS_DUMP_FILE_NAME), debugInfo.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} finally {
debugLogsWriterlock.unlock();
}
}
// print error logs
printHttpResult();
}
public boolean isConnected() {
return this.connection != null;
}
/**
* Get http status code from a web server (200 for ok, -1 for not connect, 400 and other for errors)
*/
public int getResponseCode() {
makeSureConnectionStarted();
try {
return this.connection.getResponseCode();
} catch (IOException ignore) {
return -1;
}
}
/**
* Get total file size to download (call it after start connection)
* -1 for unknown size
* 0 for text or small files
* <p>
* Warning, result depends on Accept-Encoding, so use it for information only
*/
public int getContentLength() {
makeSureConnectionStarted();
return this.connection.getContentLength();
}
/**
* Get http status message from a web server like Not Found
*/
public String getResponseMessage() {
makeSureConnectionStarted();
try {
return this.connection.getResponseMessage();
} catch (IOException ignore) {
return "";
}
}
/**
* Get returned html from a web server
*/
public String getErrorResponseAsString() {
makeSureConnectionStarted();
java.util.Scanner s = new java.util.Scanner(this.connection.getErrorStream()).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
/**
* Get non-text data as stream, e.g. binary file content
*/
public InputStream getGoodResponseAsStream() throws IOException {
makeSureConnectionStarted();
return this.connection.getInputStream();
}
/**
* Get text data as string, e.g. html document
*/
public String getGoodResponseAsString() {
makeSureConnectionStarted();
StringBuffer tmp = new StringBuffer();
BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(this.getGoodResponseAsStream()));
String line;
while ((line = in.readLine()) != null) {
tmp.append(line);
}
} catch (IOException e) {
throw new RuntimeException("Network: can't get text data from " + this.url + " - " + e, e);
}
return String.valueOf(tmp);
}
private void makeSureConnectionStarted() {
if (!isConnected()) {
throw new IllegalArgumentException("Wrong code usage: must call startConnection first", new Throwable());
}
}
public static String downloadText(String resourceUrl) {
return downloadText(resourceUrl, null);
}
/**
* Fast download of text data
*
* @param additionalHeaders set extra headers like application/json
*
* @return downloaded text on OK 200 response or empty on any other errors
*/
public static String downloadText(String resourceUrl, Map<String, String> additionalHeaders) {
XmageURLConnection con = new XmageURLConnection(resourceUrl);
con.startConnection();
if (con.isConnected()) {
try {
if (additionalHeaders != null) {
con.setRequestHeaders(additionalHeaders);
}
con.connect();
if (con.getResponseCode() == 200) {
return con.getGoodResponseAsString();
}
} catch (IOException e) {
logger.error(e, e);
}
}
return "";
}
public static InputStream downloadBinary(String resourceUrl) {
return downloadBinary(resourceUrl, false);
}
/**
* Fast download of binary data
*
* @return stream on OK 200 response or null on any other errors
*/
public static InputStream downloadBinary(String resourceUrl, boolean downloadAsGZip) {
XmageURLConnection con = new XmageURLConnection(resourceUrl);
con.setForceGZipEncoding(downloadAsGZip);
con.startConnection();
if (con.isConnected()) {
try {
con.connect();
if (con.getResponseCode() == 200) {
return con.getGoodResponseAsStream();
}
} catch (IOException e) {
logger.error(e, e);
}
}
return null;
}
private void printHttpResult() {
if (this.connection == null) {
return;
}
boolean needPrint;
switch (this.loggingType) {
case NONE:
needPrint = false;
break;
case ERRORS:
needPrint = getResponseCode() != HttpURLConnection.HTTP_OK;
break;
case ALL:
default:
needPrint = true;
}
if (needPrint) {
logger.info(String.format("http request %d %s %s", this.getResponseCode(), this.getResponseMessage(), this.url));
}
}
}