diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java index 8bb073249..8917c27a6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java @@ -33,6 +33,8 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.setting.Accounts; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.DialogController; import org.jackhuang.hmcl.ui.construct.PromptDialogPane; @@ -82,9 +84,9 @@ public class AccountListItem extends RadioButton { return new AccountListItemSkin(this); } - public void refresh() { - account.clearCache(); - thread(() -> { + private Task refreshAsync() { + return Task.runAsync(() -> { + account.clearCache(); try { account.logIn(); } catch (CredentialExpiredException e) { @@ -94,13 +96,19 @@ public class AccountListItem extends RadioButton { // ignore cancellation } catch (Exception e1) { LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1); + throw e1; } } catch (AuthenticationException e) { LOG.log(Level.WARNING, "Failed to refresh " + account + " with token", e); + throw e; } }); } + public void refresh() { + refreshAsync().whenComplete(e -> {}).start(); + } + public boolean canUploadSkin() { return account instanceof YggdrasilAccount && !(account instanceof AuthlibInjectorAccount); } @@ -121,12 +129,15 @@ public class AccountListItem extends RadioButton { Controllers.prompt(new PromptDialogPane.Builder(i18n("account.skin.upload"), (questions, resolve, reject) -> { PromptDialogPane.Builder.CandidatesQuestion q = (PromptDialogPane.Builder.CandidatesQuestion) questions.get(0); String model = q.getValue() == 0 ? "" : "slim"; - try { - ((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath()); - resolve.run(); - } catch (AuthenticationException e) { - reject.accept(AddAccountPane.accountException(e)); - } + refreshAsync() + .thenRunAsync(() -> ((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath())) + .thenComposeAsync(this::refreshAsync) + .thenRunAsync(Schedulers.javafx(), resolve::run) + .whenComplete(Schedulers.javafx(), e -> { + if (e != null) { + reject.accept(AddAccountPane.accountException(e)); + } + }).start(); }).addQuestion(new PromptDialogPane.Builder.CandidatesQuestion(i18n("account.skin.model"), i18n("account.skin.model.default"), i18n("account.skin.model.slim")))); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java index 8200b52d0..dbc13a937 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java @@ -191,7 +191,7 @@ public class YggdrasilAccount extends Account { } public void uploadSkin(String model, Path file) throws AuthenticationException, UnsupportedOperationException { - service.uploadSkin(characterUUID, model, file); + service.uploadSkin(characterUUID, session.getAccessToken(), model, file); } private static String randomClientToken() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java index 3eebb77c1..ed8dd1f01 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java @@ -28,6 +28,7 @@ import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.HttpMultipartRequest; +import org.jackhuang.hmcl.util.io.IOUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache; @@ -151,16 +152,21 @@ public class YggdrasilService { requireEmpty(request(provider.getInvalidationURL(), createRequestWithCredentials(accessToken, clientToken))); } - public void uploadSkin(UUID uuid, String model, Path file) throws AuthenticationException, UnsupportedOperationException { + public void uploadSkin(UUID uuid, String accessToken, String model, Path file) throws AuthenticationException, UnsupportedOperationException { try { HttpURLConnection con = NetworkUtils.createHttpConnection(provider.getSkinUploadURL(uuid)); con.setRequestMethod("PUT"); + con.setRequestProperty("Authorization", "Bearer " + accessToken); con.setDoOutput(true); try (HttpMultipartRequest request = new HttpMultipartRequest(con)) { + request.param("model", model); try (InputStream fis = Files.newInputStream(file)) { request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis); } - request.param("model", model); + } + String response = IOUtils.readFullyAsString(con.getInputStream()); + if (response.startsWith("{")) { + handleErrorMessage(fromJson(response, ErrorResponse.class)); } } catch (IOException e) { throw new AuthenticationException(e); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpMultipartRequest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpMultipartRequest.java index dfc03ac6f..19f24c0a6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpMultipartRequest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpMultipartRequest.java @@ -1,3 +1,20 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package org.jackhuang.hmcl.util.io; import java.io.ByteArrayOutputStream; @@ -17,6 +34,8 @@ public class HttpMultipartRequest implements Closeable { public HttpMultipartRequest(HttpURLConnection urlConnection) throws IOException { this.urlConnection = urlConnection; + urlConnection.setDoOutput(true); + urlConnection.setUseCaches(false); urlConnection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); stream = new ByteArrayOutputStream(); @@ -31,9 +50,9 @@ public class HttpMultipartRequest implements Closeable { addLine("--" + boundary); addLine(String.format("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"", name, filename)); addLine("Content-Type: " + contentType); - addLine("Content-Transfer-Encoding: binary"); addLine(""); IOUtils.copyTo(inputStream, stream); + addLine(""); return this; }