add: upload skin

This commit is contained in:
huanghongxun
2020-05-28 23:16:56 +08:00
parent 476c41b29b
commit bfb55a3813
14 changed files with 210 additions and 12 deletions

View File

@@ -53,6 +53,11 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
return toURL(apiRoot + "authserver/invalidate");
}
@Override
public URL getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
@Override
public URL getProfilePropertiesURL(UUID uuid) throws AuthenticationException {
return toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));

View File

@@ -21,7 +21,7 @@ import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URL;
import java.util.UUID;
import java.util.*;
public class MojangYggdrasilProvider implements YggdrasilProvider {
@@ -45,6 +45,11 @@ public class MojangYggdrasilProvider implements YggdrasilProvider {
return NetworkUtils.toURL("https://authserver.mojang.com/invalidate");
}
@Override
public URL getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
return NetworkUtils.toURL("https://api.mojang.com/user/profile/" + UUIDTypeAdapter.fromUUID(uuid) + "/skin");
}
@Override
public URL getProfilePropertiesURL(UUID uuid) {
return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));

View File

@@ -17,13 +17,7 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import static java.util.Objects.requireNonNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javafx.beans.binding.ObjectBinding;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
@@ -34,7 +28,10 @@ import org.jackhuang.hmcl.auth.NoCharacterException;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import javafx.beans.binding.ObjectBinding;
import java.nio.file.Path;
import java.util.*;
import static java.util.Objects.requireNonNull;
public class YggdrasilAccount extends Account {
@@ -193,6 +190,10 @@ public class YggdrasilAccount extends Account {
service.getProfileRepository().invalidate(characterUUID);
}
public void uploadSkin(String model, Path file) throws AuthenticationException, UnsupportedOperationException {
service.uploadSkin(characterUUID, model, file);
}
private static String randomClientToken() {
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
}

View File

@@ -35,6 +35,24 @@ public interface YggdrasilProvider {
URL getInvalidationURL() throws AuthenticationException;
/**
* URL to upload skin.
*
* Headers:
* Authentication: Bearer <access token>
*
* Payload:
* The payload for this API consists of multipart form data. There are two parts (order does not matter b/c of boundary):
* model: Empty string for the default model and "slim" for the slim model
* file: Raw image file data
*
* @see <a href="https://wiki.vg/Mojang_API#Upload_Skin">https://wiki.vg/Mojang_API#Upload_Skin</a>
* @return url to upload skin
* @throws AuthenticationException if url cannot be generated. e.g. some parameter or query is malformed.
* @throws UnsupportedOperationException if the Yggdrasil provider does not support third-party skin uploading.
*/
URL getSkinUploadURL(UUID uuid) throws AuthenticationException, UnsupportedOperationException;
URL getProfilePropertiesURL(UUID uuid) throws AuthenticationException;
}

View File

@@ -26,11 +26,17 @@ import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.util.StringUtils;
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.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -145,6 +151,22 @@ public class YggdrasilService {
requireEmpty(request(provider.getInvalidationURL(), createRequestWithCredentials(accessToken, clientToken)));
}
public void uploadSkin(UUID uuid, String model, Path file) throws AuthenticationException, UnsupportedOperationException {
try {
HttpURLConnection con = NetworkUtils.createHttpConnection(provider.getSkinUploadURL(uuid));
con.setRequestMethod("PUT");
con.setDoOutput(true);
try (HttpMultipartRequest request = new HttpMultipartRequest(con)) {
try (InputStream fis = Files.newInputStream(file)) {
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
}
request.param("model", model);
}
} catch (IOException e) {
throw new AuthenticationException(e);
}
}
/**
* Get complete game profile.
*

View File

@@ -0,0 +1,56 @@
package org.jackhuang.hmcl.util.io;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import static java.nio.charset.StandardCharsets.UTF_8;
public class HttpMultipartRequest implements Closeable {
private final String boundary = "*****" + System.currentTimeMillis() + "*****";
private final HttpURLConnection urlConnection;
private final ByteArrayOutputStream stream;
private final String endl = "\r\n";
public HttpMultipartRequest(HttpURLConnection urlConnection) throws IOException {
this.urlConnection = urlConnection;
urlConnection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
stream = new ByteArrayOutputStream();
}
private void addLine(String content) throws IOException {
stream.write(content.getBytes(UTF_8));
stream.write(endl.getBytes(UTF_8));
}
public HttpMultipartRequest file(String name, String filename, String contentType, InputStream inputStream) throws IOException {
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);
return this;
}
public HttpMultipartRequest param(String name, String value) throws IOException {
addLine("--" + boundary);
addLine(String.format("Content-Disposition: form-data; name=\"%s\"", name));
addLine("");
addLine(value);
return this;
}
@Override
public void close() throws IOException {
addLine("--" + boundary + "--");
urlConnection.setRequestProperty("Content-Length", "" + stream.size());
try (OutputStream os = urlConnection.getOutputStream()) {
IOUtils.write(stream.toByteArray(), os);
}
}
}