Volver al índice
src/main/java/ar/com/companeros/identity/SkinHttp.java
package ar.com.companeros.identity;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Flow;
/** Descargas acotadas, sin redirecciones. Sólo hosts de Mojang y su CDN. */
public final class SkinHttp {
private static final HttpClient CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NEVER).build();
private SkinHttp() { }
public static byte[] get(URI uri, int limit) throws IOException, InterruptedException {
if (!"https".equals(uri.getScheme()) || uri.getPort() != -1 || uri.getUserInfo() != null
|| (uri.getQuery() != null && !("sessionserver.mojang.com".equals(uri.getHost()) && "unsigned=false".equals(uri.getQuery())))
|| uri.getFragment() != null
|| !java.util.Set.of("api.mojang.com", "sessionserver.mojang.com", "textures.minecraft.net").contains(uri.getHost()))
throw new IOException("Servicio de skins no permitido");
var request = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(8)).GET().build();
var response = CLIENT.send(request, ignored -> new LimitedBody(limit));
if (response.statusCode() != 200) throw new IOException("Minecraft no devolvió esa skin");
return response.body();
}
// Se cancela la transferencia antes de superar el cupo, incluso sin Content-Length.
private static final class LimitedBody implements HttpResponse.BodySubscriber<byte[]> {
private final int limit;
private final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
private final CompletableFuture<byte[]> result = new CompletableFuture<>();
private Flow.Subscription subscription;
LimitedBody(int limit) { if (limit < 1 || limit > 131_072) throw new IllegalArgumentException(); this.limit = limit; }
public CompletionStage<byte[]> getBody() { return result; }
public void onSubscribe(Flow.Subscription value) { subscription = value; value.request(1); }
public void onNext(List<ByteBuffer> buffers) {
for (ByteBuffer buffer : buffers) {
if (buffer.remaining() > limit - bytes.size()) {
subscription.cancel(); result.completeExceptionally(new IOException("Skin demasiado grande")); return;
}
byte[] part = new byte[buffer.remaining()]; buffer.get(part); bytes.writeBytes(part);
}
subscription.request(1);
}
public void onError(Throwable error) { result.completeExceptionally(error); }
public void onComplete() { result.complete(bytes.toByteArray()); }
}
}