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()); }
    }
}