package io.gitlab.jfronny.libjf; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Type; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; public class HttpUtils { private static final HttpClient CLIENT = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build(); private enum Method { GET, POST } public static class Request { private static final Pattern CURSEFORGE_API = Pattern.compile("(?:http(s)?://)?addons-ecs\\.forgesvc\\.net/api/+"); private final String url; private final HttpRequest.Builder builder; private Method method; private int sent = 0; public Request(Method method, String url) throws URISyntaxException { this.url = url.replace(" ", "%20"); this.builder = HttpRequest.newBuilder() .uri(new URI(this.url)) .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"); this.method = method; } public Request bearer(String token) { builder.header("Authorization", "Bearer " + token); return this; } public Request header(String name, String value) { builder.header(name, value); return this; } public Request bodyString(String string) { builder.header("Content-Type", "text/plain"); builder.method(method.name(), HttpRequest.BodyPublishers.ofString(string)); method = null; return this; } public Request bodyForm(String string) { builder.header("Content-Type", "application/x-www-form-urlencoded"); builder.method(method.name(), HttpRequest.BodyPublishers.ofString(string)); method = null; return this; } public Request bodyForm(Map entries) { return bodyForm(entries.entrySet() .stream() .map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + '=' + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)) .collect(Collectors.joining("&"))); } public Request bodyJson(String string) { builder.header("Content-Type", "application/json"); builder.method(method.name(), HttpRequest.BodyPublishers.ofString(string)); method = null; return this; } public Request bodyJson(Object object) { builder.header("Content-Type", "application/json"); builder.method(method.name(), HttpRequest.BodyPublishers.ofString(LibJf.GSON.toJson(object))); method = null; return this; } private T _send(String accept, HttpResponse.BodyHandler responseBodyHandler) throws IOException { sent++; if (sent > 3) throw new IOException("Attempted third redirect, stopping"); builder.header("Accept", accept); if (method != null) builder.method(method.name(), HttpRequest.BodyPublishers.noBody()); HttpResponse res; try { res = CLIENT.send(builder.build(), responseBodyHandler); } catch (InterruptedException e) { throw new IOException("Could not send request", e); } if (res.statusCode() == 200) return res.body(); Optional location = res.headers().firstValue("location"); // Redirect if (location.isPresent() && (res.statusCode() == 302 || res.statusCode() == 307) && method == Method.GET) { try { return HttpUtils.get(location.get())._send(accept, responseBodyHandler); } catch (URISyntaxException e) { throw new IOException("Could not follow redirect", e); } } // CurseForge serverside error if (CURSEFORGE_API.matcher(url).matches() && res.statusCode() >= 500 && res.statusCode() < 600) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new IOException("Could not sleep before resending request" + e); } return _send(accept, responseBodyHandler); } throw new IOException("Unexpected return method: " + res.statusCode() + " (URL=" + url + ")"); } public void send() throws IOException { _send("*/*", HttpResponse.BodyHandlers.discarding()); } public InputStream sendInputStream() throws IOException { return _send("*/*", HttpResponse.BodyHandlers.ofInputStream()); } public String sendString() throws IOException { return _send("*/*", HttpResponse.BodyHandlers.ofString()); } public Stream sendLines() throws IOException { return _send("*/*", HttpResponse.BodyHandlers.ofLines()); } public T sendJson(Type type) throws IOException { InputStream in = _send("application/json", HttpResponse.BodyHandlers.ofInputStream()); return in == null ? null : LibJf.GSON.fromJson(new InputStreamReader(in), type); } private String getString(Object a) throws IOException { if (a instanceof InputStream s) return new String(s.readAllBytes()); if (a instanceof String s) return s; if (a instanceof Stream s) return ((Stream)s).collect(Collectors.joining()); return ""; } } public static Request get(String url) throws URISyntaxException { return new Request(Method.GET, url); } public static Request post(String url) throws URISyntaxException { return new Request(Method.POST, url); } }