package com.mojang.realmsclient.client; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.mojang.logging.LogUtils; import com.mojang.realmsclient.client.worldupload.RealmsUploadCanceledException; import com.mojang.realmsclient.dto.UploadInfo; import com.mojang.realmsclient.gui.screens.UploadResult; import com.mojang.realmsclient.gui.screens.UploadResult.Builder; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.time.Duration; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.Util; import net.minecraft.client.User; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.InputStreamEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.Args; import org.apache.http.util.EntityUtils; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class FileUpload { private static final Logger LOGGER = LogUtils.getLogger(); private static final int MAX_RETRIES = 5; private static final String UPLOAD_PATH = "/upload"; private final File file; private final long realmId; private final int slotId; private final UploadInfo uploadInfo; private final String sessionId; private final String username; private final String clientVersion; private final String worldVersion; private final UploadStatus uploadStatus; final AtomicBoolean cancelled = new AtomicBoolean(false); @Nullable private CompletableFuture uploadTask; private final RequestConfig requestConfig = RequestConfig.custom() .setSocketTimeout((int)TimeUnit.MINUTES.toMillis(10L)) .setConnectTimeout((int)TimeUnit.SECONDS.toMillis(15L)) .build(); public FileUpload(File file, long realmId, int slotId, UploadInfo uploadInfo, User user, String clientVersiob, String worldVersion, UploadStatus uploadStatus) { this.file = file; this.realmId = realmId; this.slotId = slotId; this.uploadInfo = uploadInfo; this.sessionId = user.getSessionId(); this.username = user.getName(); this.clientVersion = clientVersiob; this.worldVersion = worldVersion; this.uploadStatus = uploadStatus; } public UploadResult upload() { if (this.uploadTask != null) { return new Builder().build(); } else { this.uploadTask = CompletableFuture.supplyAsync(() -> this.requestUpload(0), Util.backgroundExecutor()); if (this.cancelled.get()) { this.cancel(); return new Builder().build(); } else { return (UploadResult)this.uploadTask.join(); } } } public void cancel() { this.cancelled.set(true); } /** * @param retries The number of times this upload has already been attempted */ private UploadResult requestUpload(int retries) { Builder builder = new Builder(); if (this.cancelled.get()) { return builder.build(); } else { this.uploadStatus.setTotalBytes(this.file.length()); HttpPost httpPost = new HttpPost(this.uploadInfo.getUploadEndpoint().resolve("/upload/" + this.realmId + "/" + this.slotId)); CloseableHttpClient closeableHttpClient = HttpClientBuilder.create().setDefaultRequestConfig(this.requestConfig).build(); UploadResult var8; try { this.setupRequest(httpPost); HttpResponse httpResponse = closeableHttpClient.execute(httpPost); long l = this.getRetryDelaySeconds(httpResponse); if (!this.shouldRetry(l, retries)) { this.handleResponse(httpResponse, builder); return builder.build(); } var8 = this.retryUploadAfter(l, retries); } catch (Exception var12) { if (!this.cancelled.get()) { LOGGER.error("Caught exception while uploading: ", (Throwable)var12); return builder.build(); } throw new RealmsUploadCanceledException(); } finally { this.cleanup(httpPost, closeableHttpClient); } return var8; } } private void cleanup(HttpPost post, @Nullable CloseableHttpClient httpClient) { post.releaseConnection(); if (httpClient != null) { try { httpClient.close(); } catch (IOException var4) { LOGGER.error("Failed to close Realms upload client"); } } } private void setupRequest(HttpPost post) throws FileNotFoundException { post.setHeader( "Cookie", "sid=" + this.sessionId + ";token=" + this.uploadInfo.getToken() + ";user=" + this.username + ";version=" + this.clientVersion + ";worldVersion=" + this.worldVersion ); FileUpload.CustomInputStreamEntity customInputStreamEntity = new FileUpload.CustomInputStreamEntity( new FileInputStream(this.file), this.file.length(), this.uploadStatus ); customInputStreamEntity.setContentType("application/octet-stream"); post.setEntity(customInputStreamEntity); } private void handleResponse(HttpResponse response, Builder uploadResult) throws IOException { int i = response.getStatusLine().getStatusCode(); if (i == 401) { LOGGER.debug("Realms server returned 401: {}", response.getFirstHeader("WWW-Authenticate")); } uploadResult.withStatusCode(i); if (response.getEntity() != null) { String string = EntityUtils.toString(response.getEntity(), "UTF-8"); if (string != null) { try { JsonParser jsonParser = new JsonParser(); JsonElement jsonElement = jsonParser.parse(string).getAsJsonObject().get("errorMsg"); Optional optional = Optional.ofNullable(jsonElement).map(JsonElement::getAsString); uploadResult.withErrorMessage((String)optional.orElse(null)); } catch (Exception var8) { } } } } private boolean shouldRetry(long retryDelaySeconds, int retries) { return retryDelaySeconds > 0L && retries + 1 < 5; } private UploadResult retryUploadAfter(long seconds, int retries) throws InterruptedException { Thread.sleep(Duration.ofSeconds(seconds).toMillis()); return this.requestUpload(retries + 1); } private long getRetryDelaySeconds(HttpResponse httpResponse) { return (Long)Optional.ofNullable(httpResponse.getFirstHeader("Retry-After")).map(NameValuePair::getValue).map(Long::valueOf).orElse(0L); } public boolean isFinished() { return this.uploadTask.isDone() || this.uploadTask.isCancelled(); } @Environment(EnvType.CLIENT) class CustomInputStreamEntity extends InputStreamEntity { private final long length; private final InputStream content; private final UploadStatus uploadStatus; public CustomInputStreamEntity(final InputStream content, final long length, final UploadStatus uploadStatus) { super(content); this.content = content; this.length = length; this.uploadStatus = uploadStatus; } @Override public void writeTo(OutputStream outputStream) throws IOException { Args.notNull(outputStream, "Output stream"); InputStream inputStream = this.content; try { byte[] bs = new byte[4096]; int i; if (this.length < 0L) { while ((i = inputStream.read(bs)) != -1) { if (FileUpload.this.cancelled.get()) { throw new RealmsUploadCanceledException(); } outputStream.write(bs, 0, i); this.uploadStatus.onWrite(i); } } else { long l = this.length; while (l > 0L) { i = inputStream.read(bs, 0, (int)Math.min(4096L, l)); if (i == -1) { break; } if (FileUpload.this.cancelled.get()) { throw new RealmsUploadCanceledException(); } outputStream.write(bs, 0, i); this.uploadStatus.onWrite(i); l -= i; outputStream.flush(); } } } catch (Throwable var8) { if (inputStream != null) { try { inputStream.close(); } catch (Throwable var7) { var8.addSuppressed(var7); } } throw var8; } if (inputStream != null) { inputStream.close(); } } } }