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 { private final int limit; private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); private final CompletableFuture result = new CompletableFuture<>(); private Flow.Subscription subscription; LimitedBody(int limit) { if (limit < 1 || limit > 131_072) throw new IllegalArgumentException(); this.limit = limit; } public CompletionStage getBody() { return result; } public void onSubscribe(Flow.Subscription value) { subscription = value; value.request(1); } public void onNext(List 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()); } } }