diff --git a/README.md b/README.md
index c8714db..d0395ad 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Simple and easy to use Minecraft microsoft authentication library (Java and Bedr
## Features
- Full support for Minecraft: Java Edition and Minecraft: Bedrock Edition
- Basic support for Minecraft: Education Edition
-- Login using device code, credentials, a JavaFX WebView window or a local webserver
+- Login using device code, credentials, a local webserver
- Refreshing and validating token chains
- Serializing and deserializing token chains to and from json
- Customizable login flows (Client ID, scopes, ...)
@@ -22,7 +22,7 @@ If you just want the latest jar file you can download it from [GitHub Actions](h
MinecraftAuth provides most of its functionality through the ``MinecraftAuth`` class.
It contains predefined login flows for Minecraft: Java Edition and Minecraft: Bedrock Edition using the official client ids and scopes.
-To customize/configure a login flow in order to change application details (like client id, scope or client secret) or to use alternative login ways (like JavaFX WebView window or local webserver) you can use the ``MinecraftAuth.builder()`` method.
+To customize/configure a login flow in order to change application details (like client id, scope or client secret) or to use alternative login ways (like local webserver) you can use the ``MinecraftAuth.builder()`` method.
For examples, you can look at the predefined login flows in the ``MinecraftAuth`` class.
Here is an example of how to manage a Minecraft: Java Edition account (For Minecraft: Bedrock Edition you can use pretty much the same code, but replace ``Java`` with ``Bedrock``):
diff --git a/buildSrc/src/main/groovy/minecraftauth.publishing-conventions.gradle b/buildSrc/src/main/groovy/minecraftauth.publishing-conventions.gradle
index 7c07e02..c1f309e 100644
--- a/buildSrc/src/main/groovy/minecraftauth.publishing-conventions.gradle
+++ b/buildSrc/src/main/groovy/minecraftauth.publishing-conventions.gradle
@@ -8,13 +8,13 @@ publishing {
publications {
mavenJava {
pom {
- name = "MinecraftAuth"
+ name = "MinecraftAuthHeadless"
description = "Simple and easy to use Minecraft microsoft authentication library (Java and Bedrock)"
- url = "https://github.com/RaphiMC/MinecraftAuth"
+ url = "https://github.com/mcio-dev/MinecraftAuthHeadless"
licenses {
license {
name = "LGPL-3.0 License"
- url = "https://github.com/RaphiMC/MinecraftAuth/blob/main/LICENSE"
+ url = "https://github.com/mcio-dev/MinecraftAuthHeadless/blob/main/LICENSE"
}
}
developers {
@@ -23,9 +23,9 @@ publishing {
}
}
scm {
- connection = "scm:git:git://github.com/RaphiMC/MinecraftAuth.git"
- developerConnection = "scm:git:ssh://github.com/RaphiMC/MinecraftAuth.git"
- url = "https://github.com/RaphiMC/MinecraftAuth.git"
+ connection = "scm:git:git://github.com/mcio-dev/MinecraftAuthHeadless.git"
+ developerConnection = "scm:git:ssh://github.com/mcio-dev/MinecraftAuthHeadless.git"
+ url = "https://github.com/mcio-dev/MinecraftAuthHeadless.git"
}
}
}
diff --git a/gradle.properties b/gradle.properties
index c8cd3b1..69dc35a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,5 +4,5 @@ org.gradle.configuration-cache=true
java_version=8
maven_group=net.raphimc
-maven_name=MinecraftAuth
+maven_name=MinecraftAuthHeadless
maven_version=4.1.2-SNAPSHOT
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 9128c7d..1af9e09 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=845952a9d6afa783db70bb3b0effaae45ae5542ca2bb7929619e8af49cb634cf
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/jitpack.yml b/jitpack.yml
new file mode 100644
index 0000000..727c9ab
--- /dev/null
+++ b/jitpack.yml
@@ -0,0 +1,2 @@
+jdk:
+ - openjdk21
diff --git a/settings.gradle b/settings.gradle
index 62759a0..8aa5653 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -9,4 +9,4 @@ plugins {
id "org.gradle.toolchains.foojay-resolver-convention" version "1.0.0"
}
-rootProject.name = "MinecraftAuth"
+rootProject.name = "MinecraftAuthHeadless"
diff --git a/src/javaFxStub/java/javafx/application/Platform.java b/src/javaFxStub/java/javafx/application/Platform.java
deleted file mode 100644
index 66b7913..0000000
--- a/src/javaFxStub/java/javafx/application/Platform.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * This file is part of MinecraftAuth - https://github.com/RaphiMC/MinecraftAuth
- * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package javafx.application;
-
-public class Platform {
-
- public static native void runLater(final Runnable runnable);
-
-}
diff --git a/src/javaFxStub/java/javafx/beans/value/ChangeListener.java b/src/javaFxStub/java/javafx/beans/value/ChangeListener.java
deleted file mode 100644
index 5d774ba..0000000
--- a/src/javaFxStub/java/javafx/beans/value/ChangeListener.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * This file is part of MinecraftAuth - https://github.com/RaphiMC/MinecraftAuth
- * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package javafx.beans.value;
-
-public interface ChangeListener {
-
- void changed(final ObservableValue extends T> observable, final T oldValue, final T newValue);
-
-}
diff --git a/src/javaFxStub/java/javafx/beans/value/ObservableValue.java b/src/javaFxStub/java/javafx/beans/value/ObservableValue.java
deleted file mode 100644
index 8b56ee6..0000000
--- a/src/javaFxStub/java/javafx/beans/value/ObservableValue.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * This file is part of MinecraftAuth - https://github.com/RaphiMC/MinecraftAuth
- * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package javafx.beans.value;
-
-public interface ObservableValue {
-
- void addListener(final ChangeListener super T> listener);
-
-}
diff --git a/src/javaFxStub/java/javafx/embed/swing/JFXPanel.java b/src/javaFxStub/java/javafx/embed/swing/JFXPanel.java
deleted file mode 100644
index 63cd762..0000000
--- a/src/javaFxStub/java/javafx/embed/swing/JFXPanel.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * This file is part of MinecraftAuth - https://github.com/RaphiMC/MinecraftAuth
- * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package javafx.embed.swing;
-
-import javafx.scene.Scene;
-
-import javax.swing.*;
-
-public class JFXPanel extends JComponent {
-
- public native Scene getScene();
-
- public native void setScene(final Scene scene);
-
-}
diff --git a/src/javaFxStub/java/javafx/scene/Parent.java b/src/javaFxStub/java/javafx/scene/Parent.java
deleted file mode 100644
index d8801d1..0000000
--- a/src/javaFxStub/java/javafx/scene/Parent.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * This file is part of MinecraftAuth - https://github.com/RaphiMC/MinecraftAuth
- * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package javafx.scene;
-
-public abstract class Parent {
-}
diff --git a/src/javaFxStub/java/javafx/scene/Scene.java b/src/javaFxStub/java/javafx/scene/Scene.java
deleted file mode 100644
index 14f11c2..0000000
--- a/src/javaFxStub/java/javafx/scene/Scene.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * This file is part of MinecraftAuth - https://github.com/RaphiMC/MinecraftAuth
- * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package javafx.scene;
-
-public class Scene {
-
- public Scene(final Parent root, final double width, final double height) {
- }
-
- public native Parent getRoot();
-
-}
diff --git a/src/javaFxStub/java/javafx/scene/web/WebEngine.java b/src/javaFxStub/java/javafx/scene/web/WebEngine.java
deleted file mode 100644
index 6f51f41..0000000
--- a/src/javaFxStub/java/javafx/scene/web/WebEngine.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * This file is part of MinecraftAuth - https://github.com/RaphiMC/MinecraftAuth
- * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package javafx.scene.web;
-
-import javafx.beans.value.ObservableValue;
-
-public final class WebEngine {
-
- public native void setUserAgent(final String value);
-
- public native void load(final String url);
-
- public native ObservableValue locationProperty();
-
-}
diff --git a/src/javaFxStub/java/javafx/scene/web/WebView.java b/src/javaFxStub/java/javafx/scene/web/WebView.java
deleted file mode 100644
index 3112ca4..0000000
--- a/src/javaFxStub/java/javafx/scene/web/WebView.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * This file is part of MinecraftAuth - https://github.com/RaphiMC/MinecraftAuth
- * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package javafx.scene.web;
-
-import javafx.scene.Parent;
-
-public final class WebView extends Parent {
-
- public native void setContextMenuEnabled(final boolean value);
-
- public native WebEngine getEngine();
-
-}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/HeaderStore.java b/src/main/java/net/lenni0451/commons/httpclient/HeaderStore.java
new file mode 100644
index 0000000..edabe10
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/HeaderStore.java
@@ -0,0 +1,194 @@
+package net.lenni0451.commons.httpclient;
+
+import net.lenni0451.commons.httpclient.model.HttpHeader;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public abstract class HeaderStore> {
+
+ private final Map> headers = new HashMap<>();
+
+ public HeaderStore() {
+ }
+
+ public HeaderStore(final Map> headers) {
+ headers.forEach((k, v) -> this.headers.put(k.toLowerCase(Locale.ROOT), new ArrayList<>(v)));
+ }
+
+ /**
+ * @return The headers
+ */
+ public Map> getHeaders() {
+ return Collections.unmodifiableMap(
+ this.headers
+ .entrySet()
+ .stream()
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ e -> new ArrayList<>(e.getValue())
+ ))
+ );
+ }
+
+ /**
+ * Get a header.
+ *
+ * @param name The name of the header
+ * @return The header or null if not set
+ */
+ public List getHeader(final String name) {
+ return this.headers.get(name.toLowerCase());
+ }
+
+ /**
+ * Get the first header with the given name.
+ *
+ * @param name The name of the header
+ * @return The response header
+ */
+ public Optional getFirstHeader(final String name) {
+ List values = this.headers.get(name.toLowerCase(Locale.ROOT));
+ if (values == null || values.isEmpty()) return Optional.empty();
+ return Optional.of(values.get(0));
+ }
+
+ /**
+ * Get the last header with the given name.
+ *
+ * @param name The name of the header
+ * @return The response header
+ */
+ public Optional getLastHeader(final String name) {
+ List values = this.headers.get(name.toLowerCase(Locale.ROOT));
+ if (values == null || values.isEmpty()) return Optional.empty();
+ return Optional.of(values.get(values.size() - 1));
+ }
+
+ /**
+ * Append a header. If the header already exists it will be appended to the list.
+ *
+ * @param name The name of the header
+ * @param value The value of the header
+ * @return This instance for chaining
+ */
+ public T appendHeader(final String name, final String value) {
+ this.headers.computeIfAbsent(name.toLowerCase(Locale.ROOT), n -> new ArrayList<>()).add(value);
+ return (T) this;
+ }
+
+ /**
+ * Append a header. If the header already exists it will be appended to the list.
+ *
+ * @param headers The headers to add
+ * @return This instance for chaining
+ */
+ public T appendHeader(final HttpHeader... headers) {
+ for (HttpHeader header : headers) this.appendHeader(header.getName(), header.getValue());
+ return (T) this;
+ }
+
+ /**
+ * Append a header. If the header already exists it will be appended to the list.
+ *
+ * @param headers The headers to add
+ * @return This instance for chaining
+ */
+ public T appendHeader(final Collection headers) {
+ for (HttpHeader header : headers) this.appendHeader(header.getName(), header.getValue());
+ return (T) this;
+ }
+
+ /**
+ * Set a header. If the header already exists it will be overwritten.
+ *
+ * @param name The name of the header
+ * @param value The value of the header
+ * @return This instance for chaining
+ */
+ public T setHeader(final String name, final String value) {
+ List values = new ArrayList<>();
+ values.add(value);
+ this.headers.put(name.toLowerCase(Locale.ROOT), values);
+ return (T) this;
+ }
+
+ /**
+ * Set a header. If the header already exists it will be overwritten.
+ *
+ * @param headers The headers to set
+ * @return This instance for chaining
+ */
+ public T setHeader(final HttpHeader... headers) {
+ for (HttpHeader h : headers) {
+ this.setHeader(h.getName(), h.getValue());
+ }
+ return (T) this;
+ }
+
+ /**
+ * Set a header. If the header already exists it will be overwritten.
+ *
+ * @param headers The headers to set
+ * @return This instance for chaining
+ */
+ public T setHeader(final Collection headers) {
+ for (HttpHeader h : headers) {
+ this.setHeader(h.getName(), h.getValue());
+ }
+ return (T) this;
+ }
+
+ /**
+ * Remove a header.
+ *
+ * @param name The name of the header
+ * @return This instance for chaining
+ */
+ public T removeHeader(final String name) {
+ this.headers.remove(name.toLowerCase(Locale.ROOT));
+ return (T) this;
+ }
+
+ /**
+ * Clear all headers.
+ *
+ * @return This instance for chaining
+ */
+ public T clearHeaders() {
+ this.headers.clear();
+ return (T) this;
+ }
+
+ /**
+ * Check if a header is set.
+ *
+ * @param name The name of the header
+ * @return Whether the header is set
+ */
+ public boolean hasHeader(final String name) {
+ return this.headers.containsKey(name.toLowerCase(Locale.ROOT));
+ }
+
+ /**
+ * Check if a header is set.
+ *
+ * @param name The name of the header
+ * @param value The value of the header
+ * @return Whether the header is set
+ */
+ public boolean hasHeader(final String name, final String value) {
+ return this.headers.get(name.toLowerCase(Locale.ROOT)).contains(value);
+ }
+
+ /**
+ * Check if a header is set.
+ *
+ * @param header The header to check
+ * @return Whether the header is set
+ */
+ public boolean hasHeader(final HttpHeader header) {
+ return this.hasHeader(header.getName(), header.getValue());
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/HttpClient.java b/src/main/java/net/lenni0451/commons/httpclient/HttpClient.java
new file mode 100644
index 0000000..39f3a24
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/HttpClient.java
@@ -0,0 +1,266 @@
+package net.lenni0451.commons.httpclient;
+
+import net.lenni0451.commons.httpclient.constants.Headers;
+import net.lenni0451.commons.httpclient.exceptions.RetryExceededException;
+import net.lenni0451.commons.httpclient.executor.ExecutorType;
+import net.lenni0451.commons.httpclient.executor.RequestExecutor;
+import net.lenni0451.commons.httpclient.handler.HttpResponseHandler;
+import net.lenni0451.commons.httpclient.proxy.ProxyHandler;
+import net.lenni0451.commons.httpclient.requests.HttpRequest;
+import net.lenni0451.commons.httpclient.utils.HttpRequestUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.net.ssl.SSLException;
+import java.io.IOException;
+import java.net.CookieManager;
+import java.net.ProtocolException;
+import java.net.UnknownHostException;
+import java.util.Optional;
+import java.util.function.Function;
+
+public class HttpClient extends HeaderStore implements HttpRequestBuilder {
+
+ private RequestExecutor executor;
+ @Nullable
+ private CookieManager cookieManager = new CookieManager();
+ private boolean followRedirects = true;
+ private int connectTimeout = 10_000;
+ private int readTimeout = 10_000;
+ private RetryHandler retryHandler = new RetryHandler();
+ private ProxyHandler proxyHandler = new ProxyHandler();
+ private boolean ignoreInvalidSSL = false;
+
+ /**
+ * Create a new http client with the default executor.
+ */
+ public HttpClient() {
+ this(ExecutorType.AUTO);
+ }
+
+ /**
+ * Create a new http client with the given executor type.
+ * Some executor types may not be supported by your Java version. Make sure to check {@link ExecutorType#isAvailable()} before using them.
+ *
+ * @param executorType The executor type to use
+ */
+ public HttpClient(@NotNull final ExecutorType executorType) {
+ this(executorType::makeExecutor);
+ }
+
+ /**
+ * Create a new http client with the given executor.
+ * You'll only need this if you want to use a custom executor.
+ * For using an official executor use {@link ExecutorType} with {@link #HttpClient(ExecutorType)}.
+ *
+ * @param executorSupplier The supplier for the executor to use
+ */
+ public HttpClient(@NotNull final Function executorSupplier) {
+ this.setExecutor(executorSupplier);
+ }
+
+ /**
+ * Set the executor to use for all requests.
+ *
+ * @param executorSupplier The supplier for the executor to use
+ * @return This instance for chaining
+ */
+ public HttpClient setExecutor(@NotNull final Function executorSupplier) {
+ this.executor = executorSupplier.apply(this);
+ if (this.executor == null) throw new IllegalArgumentException("Unsupported executor type");
+ return this;
+ }
+
+ /**
+ * @return The cookie manager
+ */
+ @Nullable
+ public CookieManager getCookieManager() {
+ return this.cookieManager;
+ }
+
+ /**
+ * Set the cookie manager to use for all requests.
+ * If this is null no cookies will be used.
+ *
+ * @param cookieManager The cookie manager to use
+ * @return This instance for chaining
+ */
+ public HttpClient setCookieManager(@Nullable final CookieManager cookieManager) {
+ this.cookieManager = cookieManager;
+ return this;
+ }
+
+ /**
+ * @return Whether redirects should be followed
+ */
+ public boolean isFollowRedirects() {
+ return this.followRedirects;
+ }
+
+ /**
+ * Set whether redirects should be followed.
+ *
+ * @param followRedirects Whether redirects should be followed
+ * @return This instance for chaining
+ */
+ public HttpClient setFollowRedirects(final boolean followRedirects) {
+ this.followRedirects = followRedirects;
+ return this;
+ }
+
+ /**
+ * @return The connect timeout for all requests in milliseconds
+ */
+ public int getConnectTimeout() {
+ return this.connectTimeout;
+ }
+
+ /**
+ * Set the connect timeout for all requests.
+ *
+ * @param connectTimeout The connect timeout in milliseconds
+ * @return This instance for chaining
+ */
+ public HttpClient setConnectTimeout(final int connectTimeout) {
+ this.connectTimeout = connectTimeout;
+ return this;
+ }
+
+ /**
+ * @return The read timeout for all requests in milliseconds
+ */
+ public int getReadTimeout() {
+ return this.readTimeout;
+ }
+
+ /**
+ * Set the read timeout for all requests.
+ *
+ * @param readTimeout The read timeout in milliseconds
+ * @return This instance for chaining
+ */
+ public HttpClient setReadTimeout(final int readTimeout) {
+ this.readTimeout = readTimeout;
+ return this;
+ }
+
+ /**
+ * @return The retry handler
+ */
+ @NotNull
+ public RetryHandler getRetryHandler() {
+ return this.retryHandler;
+ }
+
+ /**
+ * Set the retry handler for all requests.
+ *
+ * @param retryHandler The retry handler
+ * @return This instance for chaining
+ */
+ public HttpClient setRetryHandler(@NotNull final RetryHandler retryHandler) {
+ this.retryHandler = retryHandler;
+ return this;
+ }
+
+ /**
+ * @return The proxy handler
+ */
+ @NotNull
+ public ProxyHandler getProxyHandler() {
+ return this.proxyHandler;
+ }
+
+ /**
+ * Set the proxy handler for all requests.
+ *
+ * @param proxyHandler The proxy handler
+ * @return This instance for chaining
+ */
+ public HttpClient setProxyHandler(@NotNull final ProxyHandler proxyHandler) {
+ this.proxyHandler = proxyHandler;
+ return this;
+ }
+
+ /**
+ * @return Whether invalid SSL certificates should be ignored
+ */
+ public boolean isIgnoreInvalidSSL() {
+ return this.ignoreInvalidSSL;
+ }
+
+ /**
+ * Set whether invalid SSL certificates should be ignored.
+ *
+ * @param ignoreInvalidSSL Whether invalid SSL certificates should be ignored
+ * @return This instance for chaining
+ */
+ public HttpClient setIgnoreInvalidSSL(final boolean ignoreInvalidSSL) {
+ this.ignoreInvalidSSL = ignoreInvalidSSL;
+ return this;
+ }
+
+ /**
+ * Execute a request and pass the response to the response handler.
+ * The return value of the response handler will be returned.
+ *
+ * @param request The request to execute
+ * @param responseHandler The response handler
+ * @param The return type of the response handler
+ * @return The return value of the response handler
+ * @throws IOException If an I/O error occurs
+ */
+ public R execute(final HttpRequest request, final HttpResponseHandler responseHandler) throws IOException {
+ return responseHandler.handle(this.execute(request));
+ }
+
+ /**
+ * Execute a request and return the response.
+ *
+ * @param request The request to execute
+ * @return The response
+ * @throws IOException If an I/O error occurs
+ * @throws RetryExceededException If the maximum header retry count was exceeded
+ * @throws IllegalStateException If the maximum retry count was exceeded but no exception was thrown
+ */
+ public HttpResponse execute(final HttpRequest request) throws IOException {
+ RetryHandler retryHandler = request.isRetryHandlerSet() ? request.getRetryHandler() : this.retryHandler;
+
+ for (int connects = 0; connects <= retryHandler.getMaxConnectRetries(); connects++) {
+ try {
+ HttpResponse response = null;
+ for (int headers = 0; headers <= retryHandler.getMaxHeaderRetries(); headers++) {
+ response = this.executor.execute(request);
+ Optional retryAfter = response.getFirstHeader(Headers.RETRY_AFTER);
+ if (retryAfter.isPresent()) {
+ if (headers >= retryHandler.getMaxHeaderRetries()) break;
+ Long delay = HttpRequestUtils.parseSecondsOrHttpDate(retryAfter.get());
+ if (delay == null) return response; //An invalid retry after header. Treat as no retry
+ if (delay > 0) Thread.sleep(delay);
+ } else {
+ return response;
+ }
+ }
+ if (response == null) throw new IllegalStateException("Response not received but no exception was thrown");
+ if (retryHandler.getMaxHeaderRetries() == 0) return response;
+ else throw new RetryExceededException(response);
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ } catch (UnknownHostException | SSLException | ProtocolException e) {
+ //No need to retry these as they are not going to change
+ throw e;
+ } catch (IOException e) {
+ if (connects >= retryHandler.getMaxConnectRetries()) throw e;
+ }
+ }
+ throw new IllegalStateException("Connect retry failed but no exception was thrown");
+ }
+
+ @Override
+ public T bind(T request) {
+ request.bind(this);
+ return request;
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/HttpRequestBuilder.java b/src/main/java/net/lenni0451/commons/httpclient/HttpRequestBuilder.java
new file mode 100644
index 0000000..210ad14
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/HttpRequestBuilder.java
@@ -0,0 +1,176 @@
+package net.lenni0451.commons.httpclient;
+
+import net.lenni0451.commons.httpclient.requests.HttpContentRequest;
+import net.lenni0451.commons.httpclient.requests.HttpRequest;
+import net.lenni0451.commons.httpclient.requests.impl.*;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public interface HttpRequestBuilder {
+
+ /**
+ * Bind the given request to a client.
+ *
+ * @param request The request to bind
+ * @param The type of the request
+ * @return The bound request
+ */
+ default T bind(final T request) {
+ return request;
+ }
+
+ /**
+ * Create a new GET request with the given url.
+ *
+ * @param url The url to send the request to
+ * @return The created request
+ * @throws MalformedURLException If the url is invalid
+ */
+ default GetRequest get(final String url) throws MalformedURLException {
+ return this.bind(new GetRequest(url));
+ }
+
+ /**
+ * Create a new GET request with the given url.
+ *
+ * @param url The url to send the request to
+ * @return The created request
+ */
+ default GetRequest get(final URL url) {
+ return this.bind(new GetRequest(url));
+ }
+
+ /**
+ * Create a new HEAD request with the given url.
+ *
+ * @param url The url to send the request to
+ * @return The created request
+ * @throws MalformedURLException If the url is invalid
+ */
+ default HeadRequest head(final String url) throws MalformedURLException {
+ return this.bind(new HeadRequest(url));
+ }
+
+ /**
+ * Create a new HEAD request with the given url.
+ *
+ * @param url The url to send the request to
+ * @return The created request
+ */
+ default HeadRequest head(final URL url) {
+ return this.bind(new HeadRequest(url));
+ }
+
+ /**
+ * Create a new DELETE request with the given url.
+ *
+ * @param url The url to send the request to
+ * @return The created request
+ * @throws MalformedURLException If the url is invalid
+ */
+ default DeleteRequest delete(final String url) throws MalformedURLException {
+ return this.bind(new DeleteRequest(url));
+ }
+
+ /**
+ * Create a new DELETE request with the given url.
+ *
+ * @param url The url to send the request to
+ * @return The created request
+ */
+ default DeleteRequest delete(final URL url) {
+ return this.bind(new DeleteRequest(url));
+ }
+
+ /**
+ * Create a new POST request with the given url.
+ *
+ * @param url The url to send the request to
+ * @return The created request
+ * @throws MalformedURLException If the url is invalid
+ */
+ default PostRequest post(final String url) throws MalformedURLException {
+ return this.bind(new PostRequest(url));
+ }
+
+ /**
+ * Create a new POST request with the given url.
+ *
+ * @param url The url to send the request to
+ * @return The created request
+ */
+ default PostRequest post(final URL url) {
+ return this.bind(new PostRequest(url));
+ }
+
+ /**
+ * Create a new PUT request with the given url.
+ *
+ * @param url The url to send the request to
+ * @return The created request
+ * @throws MalformedURLException If the url is invalid
+ */
+ default PutRequest put(final String url) throws MalformedURLException {
+ return this.bind(new PutRequest(url));
+ }
+
+ /**
+ * Create a new PUT request with the given url.
+ *
+ * @param url The url to send the request to
+ * @return The created request
+ */
+ default PutRequest put(final URL url) {
+ return this.bind(new PutRequest(url));
+ }
+
+ /**
+ * Create a new request with the given method and url.
+ *
+ * @param method The method to use
+ * @param url The url to send the request to
+ * @return The created request
+ * @throws MalformedURLException If the url is invalid
+ */
+ default HttpRequest request(final String method, final String url) throws MalformedURLException {
+ return this.bind(new HttpRequest(method, url));
+ }
+
+ /**
+ * Create a new request with the given method and url.
+ *
+ * @param method The method to use
+ * @param url The url to send the request to
+ * @return The created request
+ */
+ default HttpRequest request(final String method, final URL url) {
+ return this.bind(new HttpRequest(method, url));
+ }
+
+ /**
+ * Create a new request with the given method and url.
+ * This request will send content to the server if specified.
+ *
+ * @param method The method to use
+ * @param url The url to send the request to
+ * @return The created request
+ * @throws MalformedURLException If the url is invalid
+ */
+ default HttpContentRequest contentRequest(final String method, final String url) throws MalformedURLException {
+ return this.bind(new HttpContentRequest(method, url));
+ }
+
+ /**
+ * Create a new request with the given method and url.
+ * This request will send content to the server if specified.
+ *
+ * @param method The method to use
+ * @param url The url to send the request to
+ * @return The created request
+ */
+ default HttpContentRequest contentRequest(final String method, final URL url) {
+ return this.bind(new HttpContentRequest(method, url));
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/HttpResponse.java b/src/main/java/net/lenni0451/commons/httpclient/HttpResponse.java
new file mode 100644
index 0000000..b38c339
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/HttpResponse.java
@@ -0,0 +1,111 @@
+package net.lenni0451.commons.httpclient;
+
+import lombok.SneakyThrows;
+import net.lenni0451.commons.httpclient.constants.StatusCodes;
+import net.lenni0451.commons.httpclient.model.ContentType;
+import net.lenni0451.commons.httpclient.utils.HttpRequestUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public class HttpResponse extends HeaderStore {
+
+ private final URL url;
+ private final int statusCode;
+ private byte[] content;
+ private InputStream inputStream;
+
+ public HttpResponse(final URL url, final int statusCode, final byte[] content, final Map> headers) {
+ super(headers);
+ this.url = url;
+ this.statusCode = statusCode;
+ this.content = content;
+ }
+
+ public HttpResponse(final URL url, final int statusCode, final InputStream inputStream, final Map> headers) {
+ super(headers);
+ this.url = url;
+ this.statusCode = statusCode;
+ this.inputStream = inputStream;
+ }
+
+ /**
+ * @return The request url
+ */
+ public URL getURL() {
+ return this.url;
+ }
+
+ /**
+ * @return The status code of the response
+ */
+ public int getStatusCode() {
+ return this.statusCode;
+ }
+
+ /**
+ * @return The message of the status code
+ */
+ public String getStatusMessage() {
+ return StatusCodes.STATUS_CODES.getOrDefault(this.statusCode, "Unknown");
+ }
+
+ /**
+ * @return The response body as a stream
+ */
+ public InputStream getInputStream() {
+ if (this.inputStream == null) {
+ return new ByteArrayInputStream(this.content);
+ } else {
+ return this.inputStream;
+ }
+ }
+
+ /**
+ * @return The response body
+ */
+ @SneakyThrows
+ public byte[] getContent() {
+ if (this.content == null) {
+ //If the content is null, the response is streamed
+ //Since the user wants the entire content, we have to read the stream into memory
+ this.content = HttpRequestUtils.readFromStream(this.inputStream);
+ //Close and null the stream to free resources
+ this.inputStream.close();
+ this.inputStream = null;
+ }
+ return this.content;
+ }
+
+ /**
+ * @return The response body as a string
+ */
+ public String getContentAsString() {
+ return this.getContentAsString(this.getContentType().flatMap(ContentType::getCharset).orElse(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Get the response body as a string with the given charset.
+ *
+ * @param charset The charset to use
+ * @return The response body as a string
+ */
+ public String getContentAsString(final Charset charset) {
+ return new String(this.getContent(), charset);
+ }
+
+ /**
+ * @return The content type of the response
+ */
+ public Optional getContentType() {
+ return this.getFirstHeader("Content-Type").map(ContentType::parse);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/RetryHandler.java b/src/main/java/net/lenni0451/commons/httpclient/RetryHandler.java
new file mode 100644
index 0000000..e81b22d
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/RetryHandler.java
@@ -0,0 +1,62 @@
+package net.lenni0451.commons.httpclient;
+
+import net.lenni0451.commons.httpclient.constants.Headers;
+
+public class RetryHandler {
+
+ private int maxConnectRetries = 0;
+ private int maxHeaderRetries = 0;
+
+ public RetryHandler() {
+ }
+
+ public RetryHandler(final int maxConnectRetries, final int maxHeaderRetries) {
+ this.maxConnectRetries = maxConnectRetries;
+ this.maxHeaderRetries = maxHeaderRetries;
+ }
+
+ /**
+ * Get the maximum amount of connect retries.
+ * A connect attempt is counted when the connection times out.
+ *
+ * @return The maximum amount of connect retries
+ */
+ public int getMaxConnectRetries() {
+ return this.maxConnectRetries;
+ }
+
+ /**
+ * Set the maximum amount of connect retries.
+ *
+ * @param maxConnectRetries The maximum amount of connect retries
+ * @return This instance for chaining
+ */
+ public RetryHandler setMaxConnectRetries(final int maxConnectRetries) {
+ if (maxConnectRetries < 0) throw new IllegalArgumentException("maxConnectRetries must be >= 0");
+ this.maxConnectRetries = maxConnectRetries;
+ return this;
+ }
+
+ /**
+ * Get the maximum amount of header retries.
+ * A header retry is counted when the {@link Headers#RETRY_AFTER} header is present.
+ *
+ * @return The maximum amount of header retries
+ */
+ public int getMaxHeaderRetries() {
+ return this.maxHeaderRetries;
+ }
+
+ /**
+ * Set the maximum amount of header retries.
+ *
+ * @param maxHeaderRetries The maximum amount of header retries
+ * @return This instance for chaining
+ */
+ public RetryHandler setMaxHeaderRetries(final int maxHeaderRetries) {
+ if (maxHeaderRetries < 0) throw new IllegalArgumentException("maxHeaderRetries must be >= 0");
+ this.maxHeaderRetries = maxHeaderRetries;
+ return this;
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/constants/ContentTypes.java b/src/main/java/net/lenni0451/commons/httpclient/constants/ContentTypes.java
new file mode 100644
index 0000000..f21ebde
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/constants/ContentTypes.java
@@ -0,0 +1,32 @@
+package net.lenni0451.commons.httpclient.constants;
+
+import lombok.experimental.UtilityClass;
+import net.lenni0451.commons.httpclient.model.ContentType;
+
+import java.nio.charset.StandardCharsets;
+
+@UtilityClass
+public class ContentTypes {
+
+ public static final ContentType WILDCARD = new ContentType("*/*");
+ public static final ContentType APPLICATION_ATOM_XML = new ContentType("application/atom+xml", StandardCharsets.ISO_8859_1);
+ public static final ContentType APPLICATION_FORM_URLENCODED = new ContentType("application/x-www-form-urlencoded", StandardCharsets.ISO_8859_1);
+ public static final ContentType APPLICATION_JSON = new ContentType("application/json", StandardCharsets.UTF_8);
+ public static final ContentType APPLICATION_OCTET_STREAM = new ContentType("application/octet-stream");
+ public static final ContentType APPLICATION_SOAP_XML = new ContentType("application/soap+xml", StandardCharsets.UTF_8);
+ public static final ContentType APPLICATION_SVG_XML = new ContentType("application/svg+xml", StandardCharsets.ISO_8859_1);
+ public static final ContentType APPLICATION_XHTML_XML = new ContentType("application/xhtml+xml", StandardCharsets.ISO_8859_1);
+ public static final ContentType APPLICATION_XML = new ContentType("application/xml", StandardCharsets.ISO_8859_1);
+ public static final ContentType IMAGE_BMP = new ContentType("image/bmp");
+ public static final ContentType IMAGE_GIF = new ContentType("image/gif");
+ public static final ContentType IMAGE_JPEG = new ContentType("image/jpeg");
+ public static final ContentType IMAGE_PNG = new ContentType("image/png");
+ public static final ContentType IMAGE_SVG = new ContentType("image/svg+xml");
+ public static final ContentType IMAGE_TIFF = new ContentType("image/tiff");
+ public static final ContentType IMAGE_WEBP = new ContentType("image/webp");
+ public static final ContentType MULTIPART_FORM_DATA = new ContentType("multipart/form-data", StandardCharsets.ISO_8859_1);
+ public static final ContentType TEXT_HTML = new ContentType("text/html", StandardCharsets.ISO_8859_1);
+ public static final ContentType TEXT_PLAIN = new ContentType("text/plain", StandardCharsets.ISO_8859_1);
+ public static final ContentType TEXT_XML = new ContentType("text/xml", StandardCharsets.ISO_8859_1);
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/constants/Headers.java b/src/main/java/net/lenni0451/commons/httpclient/constants/Headers.java
new file mode 100644
index 0000000..9dde082
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/constants/Headers.java
@@ -0,0 +1,513 @@
+package net.lenni0451.commons.httpclient.constants;
+
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class Headers {
+
+ /**
+ * Defines the authentication method that should be used to access a resource.
+ */
+ public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+ /**
+ * Contains the credentials to authenticate a user-agent with a server.
+ */
+ public static final String AUTHORIZATION = "Authorization";
+ /**
+ * Defines the authentication method that should be used to access a resource behind a proxy server.
+ */
+ public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
+ /**
+ * Contains the credentials to authenticate a user agent with a proxy server.
+ */
+ public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
+ /**
+ * The time, in seconds, that the object has been in a proxy cache.
+ */
+ public static final String AGE = "Age";
+ /**
+ * Directives for caching mechanisms in both requests and responses.
+ */
+ public static final String CACHE_CONTROL = "Cache-Control";
+ /**
+ * Clears browsing data (e.g. cookies, storage, cache) associated with the requesting website.
+ */
+ public static final String CLEAR_SITE_DATA = "Clear-Site-Data";
+ /**
+ * The date/time after which the response is considered stale.
+ */
+ public static final String EXPIRES = "Expires";
+ /**
+ * The last modification date of the resource, used to compare several versions of the same resource. It is less accurate than {@link #ETAG}, but easier to calculate in some environments.
+ * Conditional requests using {@link #IF_MODIFIED_SINCE} and {@link #IF_UNMODIFIED_SINCE} use this value to change the behavior of the request.
+ */
+ public static final String LAST_MODIFIED = "Last-Modified";
+ /**
+ * A unique string identifying the version of the resource. Conditional requests using {@link #IF_MATCH} and {@link #IF_NONE_MATCH} use this value to change the behavior of the request.
+ */
+ public static final String ETAG = "ETag";
+ /**
+ * Makes the request conditional, and applies the method only if the stored resource matches one of the given ETags.
+ */
+ public static final String IF_MATCH = "If-Match";
+ /**
+ * Makes the request conditional, and applies the method only if the stored resource doesn't match any of the given ETags.
+ * This is used to update caches (for safe requests), or to prevent uploading a new resource when one already exists.
+ */
+ public static final String IF_NONE_MATCH = "If-None-Match";
+ /**
+ * Makes the request conditional, and expects the resource to be transmitted only if it has been modified after the given date.
+ * This is used to transmit data only when the cache is out of date.
+ */
+ public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
+ /**
+ * Makes the request conditional, and expects the resource to be transmitted only if it has not been modified after the given date.
+ * This ensures the coherence of a new fragment of a specific range with previous ones, or to implement an optimistic concurrency control system when modifying existing documents.
+ */
+ public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
+ /**
+ * Determines how to match request headers to decide whether a cached response can be used rather than requesting a fresh one from the origin server.
+ */
+ public static final String VARY = "Vary";
+ /**
+ * Controls whether the network connection stays open after the current transaction finishes.
+ */
+ public static final String CONNECTION = "Connection";
+ /**
+ * Controls how long a persistent connection should stay open.
+ */
+ public static final String KEEP_ALIVE = "Keep-Alive";
+ /**
+ * Informs the server about the mime types of data that can be sent back.
+ */
+ public static final String ACCEPT = "Accept";
+ /**
+ * The encoding algorithm, usually a compression algorithm, that can be used on the resource sent back.
+ */
+ public static final String ACCEPT_ENCODING = "Accept-Encoding";
+ /**
+ * Informs the server about the human language the server is expected to send back. This is a hint and is not necessarily under the full control of the user: the server should always pay attention not to override an explicit user choice (like selecting a language from a dropdown).
+ */
+ public static final String ACCEPT_LANGUAGE = "Accept-Language";
+ /**
+ * Indicates expectations that need to be fulfilled by the server to properly handle the request.
+ */
+ public static final String EXPECT = "Expect";
+ /**
+ * When using https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE, indicates the maximum number of hops the request can do before being reflected to the sender.
+ */
+ public static final String MAX_FORWARDS = "Max-Forwards";
+ /**
+ * Contains stored HTTP cookies previously sent by the server with the {@link #SET_COOKIE} header.
+ */
+ public static final String COOKIE = "Cookie";
+ /**
+ * Send cookies from the server to the user-agent.
+ */
+ public static final String SET_COOKIE = "Set-Cookie";
+ /**
+ * Indicates whether the response to the request can be exposed when the credentials flag is true.
+ */
+ public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
+ /**
+ * Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request.
+ */
+ public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
+ /**
+ * Specifies the methods allowed when accessing the resource in response to a preflight request.
+ */
+ public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
+ /**
+ * Indicates whether the response can be shared.
+ */
+ public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
+ /**
+ * Indicates which headers can be exposed as part of the response by listing their names.
+ */
+ public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
+ /**
+ * Indicates how long the results of a preflight request can be cached.
+ */
+ public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
+ /**
+ * Used when issuing a preflight request to let the server know which HTTP headers will be used when the actual request is made.
+ */
+ public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
+ /**
+ * Used when issuing a preflight request to let the server know which HTTP method will be used when the actual request is made.
+ */
+ public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
+ /**
+ * Indicates where a fetch originates from.
+ */
+ public static final String ORIGIN = "Origin";
+ /**
+ * Specifies origins that are allowed to see values of attributes retrieved via features of the Resource Timing API, which would otherwise be reported as zero due to cross-origin restrictions.
+ */
+ public static final String TIMING_ALLOW_ORIGIN = "Timing-Allow-Origin";
+ /**
+ * Indicates if the resource transmitted should be displayed inline (default behavior without the header), or if it should be handled like a download and the browser should present a "Save As" dialog.
+ */
+ public static final String CONTENT_DISPOSITION = "Content-Disposition";
+ /**
+ * The size of the resource, in decimal number of bytes.
+ */
+ public static final String CONTENT_LENGTH = "Content-Length";
+ /**
+ * Indicates the media type of the resource.
+ */
+ public static final String CONTENT_TYPE = "Content-Type";
+ /**
+ * Used to specify the compression algorithm.
+ */
+ public static final String CONTENT_ENCODING = "Content-Encoding";
+ /**
+ * Describes the human language(s) intended for the audience, so that it allows a user to differentiate according to the users' own preferred language.
+ */
+ public static final String CONTENT_LANGUAGE = "Content-Language";
+ /**
+ * Indicates an alternate location for the returned data.
+ */
+ public static final String CONTENT_LOCATION = "Content-Location";
+ /**
+ * Contains information from the client-facing side of proxy servers that is altered or lost when a proxy is involved in the path of the request.
+ */
+ public static final String FORWARDED = "Forwarded";
+ /**
+ * Added by proxies, both forward and reverse proxies, and can appear in the request headers and the response headers.
+ */
+ public static final String VIA = "Via";
+ /**
+ * Indicates the URL to redirect a page to.
+ */
+ public static final String LOCATION = "Location";
+ /**
+ * Directs the browser to reload the page or redirect to another. Takes the same value as the `meta` element with refresh.
+ */
+ public static final String REFRESH = "Refresh";
+ /**
+ * Contains an Internet email address for a human user who controls the requesting user agent.
+ */
+ public static final String FROM = "From";
+ /**
+ * Specifies the domain name of the server (for virtual hosting), and (optionally) the TCP port number on which the server is listening.
+ */
+ public static final String HOST = "Host";
+ /**
+ * The address of the previous web page from which a link to the currently requested page was followed.
+ */
+ public static final String REFERER = "Referer";
+ /**
+ * Governs which referrer information sent in the {@link #REFERER} header should be included with requests made.
+ */
+ public static final String REFERRER_POLICY = "Referrer-Policy";
+ /**
+ * Contains a characteristic string that allows the network protocol peers to identify the application type, operating system, software vendor or software version of the requesting software user agent.
+ */
+ public static final String USER_AGENT = "User-Agent";
+ /**
+ * Lists the set of HTTP request methods supported by a resource.
+ */
+ public static final String ALLOW = "Allow";
+ /**
+ * Contains information about the software used by the origin server to handle the request.
+ */
+ public static final String SERVER = "Server";
+ /**
+ * Indicates if the server supports range requests, and if so in which unit the range can be expressed.
+ */
+ public static final String ACCEPT_RANGES = "Accept-Ranges";
+ /**
+ * Indicates the part of a document that the server should return.
+ */
+ public static final String RANGE = "Range";
+ /**
+ * Creates a conditional range request that is only fulfilled if the given etag or date matches the remote resource. Used to prevent downloading two ranges from incompatible version of the resource.
+ */
+ public static final String IF_RANGE = "If-Range";
+ /**
+ * Indicates where in a full body message a partial message belongs.
+ */
+ public static final String CONTENT_RANGE = "Content-Range";
+ /**
+ * Allows a server to declare an embedder policy for a given document.
+ */
+ public static final String CROSS_ORIGIN_EMBEDDER_POLICY = "Cross-Origin-Embedder-Policy";
+ /**
+ * Prevents other domains from opening/controlling a window.
+ */
+ public static final String CROSS_ORIGIN_OPENER_POLICY = "Cross-Origin-Opener-Policy";
+ /**
+ * Prevents other domains from reading the response of the resources to which this header is applied.
+ *
+ * @see CORP explainer article
+ */
+ public static final String CROSS_ORIGIN_RESOURCE_POLICY = "Cross-Origin-Resource-Policy";
+ /**
+ * Controls resources the user agent is allowed to load for a given page.
+ */
+ public static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
+ /**
+ * Allows web developers to experiment with policies by monitoring, but not enforcing, their effects. These violation reports consist of JSON documents sent via an HTTP {@code POST} request to the specified URI.
+ */
+ public static final String CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only";
+ /**
+ * Provides a mechanism to allow and deny the use of browser features in a website's own frame, and in {@code iframe}s that it embeds.
+ */
+ public static final String PERMISSIONS_POLICY = "Permissions-Policy";
+ /**
+ * Force communication using HTTPS instead of HTTP.
+ */
+ public static final String STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security";
+ /**
+ * Sends a signal to the server expressing the client's preference for an encrypted and authenticated response, and that it can successfully handle the {@code upgrade-insecure-requests} directive.
+ */
+ public static final String UPGRADE_INSECURE_REQUESTS = "Upgrade-Insecure-Requests";
+ /**
+ * Disables MIME sniffing and forces browser to use the type given in {@link #CONTENT_TYPE}.
+ */
+ public static final String X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options";
+ /**
+ * Indicates whether a browser should be allowed to render a page in a {@code frame}, {@code iframe}, {@code embed} or {@code object}.
+ */
+ public static final String X_FRAME_OPTIONS = "X-Frame-Options";
+ /**
+ * Specifies if a cross-domain policy file ({@code crossdomain.xml}) is allowed.
+ * The file may define a policy to grant clients, such as Adobe's Flash Player (now obsolete), Adobe Acrobat, Microsoft Silverlight (now obsolete), or Apache Flex,
+ * permission to handle data across domains that would otherwise be restricted due to the Same-Origin Policy.
+ *
+ */
+ public static final String X_PERMITTED_CROSS_DOMAIN_POLICIES = "X-Permitted-Cross-Domain-Policies";
+ /**
+ * May be set by hosting environments or other frameworks and contains information about them while not providing any usefulness to the application or its visitors.
+ * Unset this header to avoid exposing potential vulnerabilities.
+ */
+ public static final String X_POWERED_BY = "X-Powered-By";
+ /**
+ * Enables cross-site scripting filtering.
+ */
+ public static final String X_XSS_PROTECTION = "X-XSS-Protection";
+ /**
+ * Indicates the relationship between a request initiator's origin and its target's origin. It is a Structured Header whose value is a token with possible values {@code cross-site}, {@code same-origin}, {@code same-site}, and {@code none}.
+ */
+ public static final String SEC_FETCH_SITE = "Sec-Fetch-Site";
+ /**
+ * Indicates the request's mode to a server. It is a Structured Header whose value is a token with possible values {@code cors}, {@code navigate}, {@code no-cors}, {@code same-origin}, and {@code websocket}.
+ */
+ public static final String SEC_FETCH_MODE = "Sec-Fetch-Mode";
+ /**
+ * Indicates whether or not a navigation request was triggered by user activation. It is a Structured Header whose value is a boolean so possible values are {@code ?0} for false and {@code ?1} for true.
+ */
+ public static final String SEC_FETCH_USER = "Sec-Fetch-User";
+ /**
+ * Indicates the request's destination. It is a Structured Header whose value is a token with possible values {@code audio}, {@code audioworklet}, {@code document}, {@code embed}, {@code empty}, {@code font}, {@code image}, {@code manifest}, {@code object}, {@code paintworklet}, {@code report}, {@code script}, {@code serviceworker}, {@code sharedworker}, {@code style}, {@code track}, {@code video}, {@code worker}, and {@code xslt}.
+ */
+ public static final String SEC_FETCH_DEST = "Sec-Fetch-Dest";
+ /**
+ * Indicates the purpose of the request, when the purpose is something other than immediate use by the user-agent.
+ * The header currently has one possible value, {@code prefetch}, which indicates that the resource is being fetched preemptively for a possible future navigation.
+ */
+ public static final String SEC_PURPOSE = "Sec-Purpose";
+ /**
+ * A request header sent in preemptive request to {@code fetch()} a resource during service worker boot.
+ * The value, which is set with {@code NavigationPreloadManager.setHeaderValue()"}, can be used to inform a server that a different resource should be returned than in a normal {@code fetch()} operation.
+ */
+ public static final String SERVICE_WORKER_NAVIGATION_PRELOAD = "Service-Worker-Navigation-Preload";
+ /**
+ * Used to specify a server endpoint for the browser to send warning and error reports to.
+ */
+ public static final String REPORT_TO = "Report-To";
+ /**
+ * Specifies the form of encoding used to safely transfer the resource to the user.
+ */
+ public static final String TRANSFER_ENCODING = "Transfer-Encoding";
+ /**
+ * Specifies the transfer encodings the user agent is willing to accept.
+ */
+ public static final String TE = "TE";
+ /**
+ * Allows the sender to include additional fields at the end of chunked message.
+ */
+ public static final String TRAILER = "Trailer";
+ /**
+ * Used to list alternate ways to reach this service.
+ */
+ public static final String ALT_SVC = "Alt-Svc";
+ /**
+ * Used to identify the alternative service in use.
+ */
+ public static final String ALT_USED = "Alt-Used";
+ /**
+ * Contains the date and time at which the message was originated.
+ */
+ public static final String DATE = "Date";
+ /**
+ * This entity-header field provides a means for serializing one or more links in HTTP headers. It is semantically equivalent to the HTML {@code link} element.
+ */
+ public static final String LINK = "Link";
+ /**
+ * Indicates how long the user agent should wait before making a follow-up request.
+ */
+ public static final String RETRY_AFTER = "Retry-After";
+ /**
+ * Communicates one or more metrics and descriptions for the given request-response cycle.
+ */
+ public static final String SERVER_TIMING = "Server-Timing";
+ /**
+ * Used to remove the path restriction by including this header in the response of the Service Worker script.
+ */
+ public static final String SERVICE_WORKER_ALLOWED = "Service-Worker-Allowed";
+ /**
+ * Links generated code to a source map.
+ */
+ public static final String SOURCEMAP = "SourceMap";
+ /**
+ * This HTTP/1.1 (only) header can be used to upgrade an already established client/server connection to a different protocol (over the same transport protocol).
+ * For example, it can be used by a client to upgrade a connection from HTTP 1.1 to HTTP 2.0, or an HTTP or HTTPS connection into a WebSocket.
+ */
+ public static final String UPGRADE = "Upgrade";
+ /**
+ * Servers can advertise support for Client Hints using the {@code Accept-CH} header field or an equivalent HTML {@code } element with http-equiv attribute.
+ */
+ public static final String ACCEPT_CH = "Accept-CH";
+ /**
+ * Servers use {@code Critical-CH} along with {@link #ACCEPT_CH} to specify that accepted client hints are also critical client hints.
+ */
+ public static final String CRITICAL_CH = "Critical-CH";
+ /**
+ * User agent's branding and version.
+ */
+ public static final String SEC_CH_UA = "Sec-CH-UA";
+ /**
+ * User agent's underlying platform architecture.
+ */
+ public static final String SEC_CH_UA_ARCH = "Sec-CH-UA-Arch";
+ /**
+ * User agent's underlying CPU architecture bitness (for example "64" bit).
+ */
+ public static final String SEC_CH_UA_BITNESS = "Sec-CH-UA-Bitness";
+ /**
+ * Full version for each brand in the user agent's brand list.
+ */
+ public static final String SEC_CH_UA_FULL_VERSION_LIST = "Sec-CH-UA-Full-Version-List";
+ /**
+ * User agent is running on a mobile device or, more generally, prefers a "mobile" user experience.
+ */
+ public static final String SEC_CH_UA_MOBILE = "Sec-CH-UA-Mobile";
+ /**
+ * User agent's device model.
+ */
+ public static final String SEC_CH_UA_MODEL = "Sec-CH-UA-Model";
+ /**
+ * User agent's underlying operation system/platform.
+ */
+ public static final String SEC_CH_UA_PLATFORM = "Sec-CH-UA-Platform";
+ /**
+ * User agent's underlying operation system version.
+ */
+ public static final String SEC_CH_UA_PLATFORM_VERSION = "Sec-CH-UA-Platform-Version";
+ /**
+ * User's preference of dark or light color scheme.
+ */
+ public static final String SEC_CH_UA_PREFERS_COLOR_SCHEME = "Sec-CH-UA-Prefers-Color-Scheme";
+ /**
+ * User's preference to see fewer animations and content layout shifts.
+ */
+ public static final String SEC_CH_UA_PREFERS_REDUCED_MOTION = "Sec-CH-UA-Prefers-Reduced-Motion";
+ /**
+ * Approximate amount of available client RAM memory.
+ * This is part of the Device Memory API.
+ */
+ public static final String DEVICE_MEMORY = "Device-Memory";
+ /**
+ * Approximate bandwidth of the client's connection to the server, in Mbps.
+ * This is part of the Network Information API.
+ */
+ public static final String DOWNLINK = "Downlink";
+ /**
+ * The {@code effective connection type} ("network profile") that best matches the connection's latency and bandwidth.
+ * This is part of the Network Information API.
+ */
+ public static final String ECT = "ECT";
+ /**
+ * Application layer round trip time (RTT) in milliseconds, which includes the server processing time.
+ * This is part of the Network Information API.
+ */
+ public static final String RTT = "RTT";
+ /**
+ * A string {@code on} that indicates the user agent's preference for reduced data usage.
+ */
+ public static final String SAVE_DATA = "Save-Data";
+ /**
+ * Indicates whether the user consents to a website or service selling or sharing their personal information with third parties.
+ */
+ public static final String SEC_GPC = "Sec-GPC";
+ /**
+ * Provides a mechanism to allow web applications to isolate their origins.
+ */
+ public static final String ORIGIN_ISOLATION = "Origin-Isolation";
+ /**
+ * Defines a mechanism that enables developers to declare a network error reporting policy.
+ */
+ public static final String NEL = "NEL";
+ /**
+ * A client can express the desired push policy for a request by sending an Accept-Push-Policy header field in the request.
+ */
+ public static final String ACCEPT_PUSH_POLICY = "Accept-Push-Policy";
+ /**
+ * A client can send the Accept-Signature header field to indicate intention to take advantage of any available signatures and to indicate what kinds of signatures it supports.
+ */
+ public static final String ACCEPT_SIGNATURE = "Accept-Signature";
+ /**
+ * Indicates that the request has been conveyed in TLS early data.
+ */
+ public static final String EARLY_DATA = "Early-Data";
+ /**
+ * A Push-Policy defines the server behavior regarding push when processing a request.
+ */
+ public static final String PUSH_POLICY = "Push-Policy";
+ /**
+ * The Signature header field conveys a list of signatures for an exchange, each one accompanied by information about how to determine the authority of and refresh that signature.
+ */
+ public static final String SIGNATURE = "Signature";
+ /**
+ * The Signed-Headers header field identifies an ordered list of response header fields to include in a signature.
+ */
+ public static final String SIGNED_HEADERS = "Signed-Headers";
+ /**
+ * Set by a navigation target to opt-in to using various higher-risk loading modes.
+ * For example, cross-origin, same-site prerendering requires a {@code Supports-Loading-Mode} value of {@code credentialed-prerender}.
+ */
+ public static final String SUPPORTS_LOADING_MODE = "Supports-Loading-Mode";
+ /**
+ * Identifies the originating IP addresses of a client connecting to a web server through an HTTP proxy or a load balancer.
+ */
+ public static final String X_FORWARDED_FOR = "X-Forwarded-For";
+ /**
+ * Identifies the original host requested that a client used to connect to your proxy or load balancer.
+ */
+ public static final String X_FORWARDED_HOST = "X-Forwarded-Host";
+ /**
+ * Identifies the protocol (HTTP or HTTPS) that a client used to connect to your proxy or load balancer.
+ */
+ public static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
+ /**
+ * Controls DNS prefetching, a feature by which browsers proactively perform domain name resolution on both links that the user may choose to follow as well as URLs for items referenced by the document, including images, CSS, JavaScript, and so forth.
+ */
+ public static final String X_DNS_PREFETCH_CONTROL = "X-DNS-Prefetch-Control";
+ /**
+ * The X-Robots-Tag HTTP header is used to indicate how a web page is to be indexed within public search engine results.
+ * The header is effectively equivalent to {@code }.
+ */
+ public static final String X_ROBOTS_TAG = "X-Robots-Tag";
+ /**
+ * Implementation-specific header that may have various effects anywhere along the request-response chain.
+ * Used for backwards compatibility with HTTP/1.0 caches where the {@code Cache-Control} header is not yet present.
+ */
+ public static final String PRAGMA = "Pragma";
+ /**
+ * General warning information about possible problems.
+ */
+ public static final String WARNING = "Warning";
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/constants/RequestMethods.java b/src/main/java/net/lenni0451/commons/httpclient/constants/RequestMethods.java
new file mode 100644
index 0000000..9dbbdc2
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/constants/RequestMethods.java
@@ -0,0 +1,45 @@
+package net.lenni0451.commons.httpclient.constants;
+
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class RequestMethods {
+
+ /**
+ * The {@code GET} method requests a representation of the specified resource. Requests using {@code GET} should only retrieve data.
+ */
+ public static final String GET = "GET";
+ /**
+ * The {@code HEAD} method asks for a response identical to a {@link #GET} request, but without the response body.
+ */
+ public static final String HEAD = "HEAD";
+ /**
+ * The {@code POST} method submits an entity to the specified resource, often causing a change in state or side effects on the server.
+ */
+ public static final String POST = "POST";
+ /**
+ * The {@code PUT} method replaces all current representations of the target resource with the request payload.
+ */
+ public static final String PUT = "PUT";
+ /**
+ * The {@code DELETE} method deletes the specified resource.
+ */
+ public static final String DELETE = "DELETE";
+ /**
+ * The {@code CONNECT} method establishes a tunnel to the server identified by the target resource.
+ */
+ public static final String CONNECT = "CONNECT";
+ /**
+ * The {@code OPTIONS} method describes the communication options for the target resource.
+ */
+ public static final String OPTIONS = "OPTIONS";
+ /**
+ * The {@code TRACE} method performs a message loop-back test along the path to the target resource.
+ */
+ public static final String TRACE = "TRACE";
+ /**
+ * The {@code PATCH} method applies partial modifications to a resource.
+ */
+ public static final String PATCH = "PATCH";
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/constants/StatusCodes.java b/src/main/java/net/lenni0451/commons/httpclient/constants/StatusCodes.java
new file mode 100644
index 0000000..64546b5
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/constants/StatusCodes.java
@@ -0,0 +1,377 @@
+package net.lenni0451.commons.httpclient.constants;
+
+import lombok.experimental.UtilityClass;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@UtilityClass
+public class StatusCodes {
+
+ /**
+ * This interim response indicates that the client should continue the request or ignore the response if the request is already finished.
+ */
+ public static final int CONTINUE = 100;
+ /**
+ * This code is sent in response to an {@code Upgrade} request header from the client and indicates the protocol the server is switching to.
+ */
+ public static final int SWITCHING_PROTOCOLS = 101;
+ /**
+ * This code indicates that the server has received and is processing the request, but no response is available yet.
+ */
+ public static final int PROCESSING = 102;
+ /**
+ * This status code is primarily intended to be used with the {@code Link} header, letting the user agent start preloading
+ * resources while the server prepares a response or preconnect to an origin from which the page will need resources.
+ */
+ public static final int EARLY_HINTS = 103;
+ /**
+ * The request succeeded. The result meaning of "success" depends on the HTTP method:
+ */
+ public static final int OK = 200;
+ /**
+ * The request succeeded, and a new resource was created as a result.
+ * This is typically the response sent after {@code POST} requests, or some {@code PUT} requests.
+ */
+ public static final int CREATED = 201;
+ /**
+ * The request has been received but not yet acted upon.
+ * It is noncommittal, since there is no way in HTTP to later send an asynchronous response indicating the outcome of the request.
+ * It is intended for cases where another process or server handles the request, or for batch processing.
+ */
+ public static final int ACCEPTED = 202;
+ /**
+ * This response code means the returned metadata is not exactly the same as is available from the origin server, but is collected from a local or a third-party copy.
+ * This is mostly used for mirrors or backups of another resource.
+ * Except for that specific case, the {@code 200 OK} response is preferred to this status.
+ */
+ public static final int NON_AUTHORITATIVE_INFORMATION = 203;
+ /**
+ * There is no content to send for this request, but the headers may be useful.
+ * The user agent may update its cached headers for this resource with the new ones.
+ */
+ public static final int NO_CONTENT = 204;
+ /**
+ * Tells the user agent to reset the document which sent this request.
+ */
+ public static final int RESET_CONTENT = 205;
+ /**
+ * This response code is used when the {@code Range} header is sent from the client to request only part of a resource.
+ */
+ public static final int PARTIAL_CONTENT = 206;
+ /**
+ * Conveys information about multiple resources, for situations where multiple status codes might be appropriate.
+ */
+ public static final int MULTI_STATUS = 207;
+ /**
+ * Used inside a {@code WebDAV} response element to avoid repeatedly enumerating the internal members of multiple bindings to the same collection.
+ */
+ public static final int ALREADY_REPORTED = 208;
+ /**
+ * The server has fulfilled a {@code GET} request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance.
+ */
+ public static final int IM_USED = 226;
+ /**
+ * The request has more than one possible response. The user agent or user should choose one of them. (There is no standardized way of choosing one of the responses, but HTML links to the possibilities are recommended so the user can pick.)
+ */
+ public static final int MULTIPLE_CHOICES = 300;
+ /**
+ * The URL of the requested resource has been changed permanently. The new URL is given in the response.
+ */
+ public static final int MOVED_PERMANENTLY = 301;
+ /**
+ * This response code means that the URI of requested resource has been changed temporarily.
+ * Further changes in the URI might be made in the future. Therefore, this same URI should be used by the client in future requests.
+ */
+ public static final int MOVED_TEMPORARILY = 302;
+ /**
+ * The server sent this response to direct the client to get the requested resource at another URI with a GET request.
+ */
+ public static final int SEE_OTHER = 303;
+ /**
+ * This is used for caching purposes.
+ * It tells the client that the response has not been modified, so the client can continue to use the same cached version of the response.
+ */
+ public static final int NOT_MODIFIED = 304;
+ /**
+ * Defined in a previous version of the HTTP specification to indicate that a requested response must be accessed by a proxy.
+ * It has been deprecated due to security concerns regarding in-band configuration of a proxy.
+ */
+ public static final int USE_PROXY = 305;
+ /**
+ * This response code is no longer used; it is just reserved. It was used in a previous version of the HTTP/1.1 specification.
+ */
+ public static final int UNUSED = 306;
+ /**
+ * The server sends this response to direct the client to get the requested resource at another URI with the same method that was used in the prior request.
+ * This has the same semantics as the {@code 302 Found} HTTP response code, with the exception that the user agent must not change the HTTP method used:
+ * if a {@code POST} was used in the first request, a {@code POST} must be used in the second request.
+ */
+ public static final int TEMPORARY_REDIRECT = 307;
+ /**
+ * This means that the resource is now permanently located at another URI, specified by the {@code Location:} HTTP Response header.
+ * This has the same semantics as the {@code 301 Moved Permanently} HTTP response code, with the exception that the user agent must not change the HTTP method used:
+ * if a {@code POST} was used in the first request, a {@code POST} must be used in the second request.
+ */
+ public static final int PERMANENT_REDIRECT = 308;
+ /**
+ * The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
+ */
+ public static final int BAD_REQUEST = 400;
+ /**
+ * Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated".
+ * That is, the client must authenticate itself to get the requested response.
+ */
+ public static final int UNAUTHORIZED = 401;
+ /**
+ * This response code is reserved for future use.
+ * The initial aim for creating this code was using it for digital payment systems, however this status code is used very rarely and no standard convention exists.
+ */
+ public static final int PAYMENT_REQUIRED = 402;
+ /**
+ * The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource.
+ * Unlike {@code 401 Unauthorized}, the client's identity is known to the server.
+ */
+ public static final int FORBIDDEN = 403;
+ /**
+ * The server cannot find the requested resource.
+ * In the browser, this means the URL is not recognized.
+ * In an API, this can also mean that the endpoint is valid but the resource itself does not exist.
+ * Servers may also send this response instead of {@code 403 Forbidden} to hide the existence of a resource from an unauthorized client.
+ * This response code is probably the most well known due to its frequent occurrence on the web.
+ */
+ public static final int NOT_FOUND = 404;
+ /**
+ * The request method is known by the server but is not supported by the target resource.
+ * For example, an API may not allow calling {@code DELETE} to remove a resource.
+ */
+ public static final int METHOD_NOT_ALLOWED = 405;
+ /**
+ * This response is sent when the web server, after performing server-driven content negotiation,
+ * doesn't find any content that conforms to the criteria given by the user agent.
+ */
+ public static final int NOT_ACCEPTABLE = 406;
+ /**
+ * This is similar to {@code 401 Unauthorized} but authentication is needed to be done by a proxy.
+ */
+ public static final int PROXY_AUTHENTICATION_REQUIRED = 407;
+ /**
+ * This response is sent on an idle connection by some servers, even without any previous request by the client.
+ * It means that the server would like to shut down this unused connection.
+ * This response is used much more since some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection mechanisms to speed up surfing.
+ * Also note that some servers merely shut down the connection without sending this message.
+ */
+ public static final int REQUEST_TIMEOUT = 408;
+ /**
+ * This response is sent when a request conflicts with the current state of the server.
+ */
+ public static final int CONFLICT = 409;
+ /**
+ * This response is sent when the requested content has been permanently deleted from server, with no forwarding address.
+ * Clients are expected to remove their caches and links to the resource.
+ * The HTTP specification intends this status code to be used for "limited-time, promotional services".
+ * APIs should not feel compelled to indicate resources that have been deleted with this status code.
+ */
+ public static final int GONE = 410;
+ /**
+ * Server rejected the request because the {@code Content-Length} header field is not defined and the server requires it.
+ */
+ public static final int LENGTH_REQUIRED = 411;
+ /**
+ * The client has indicated preconditions in its headers which the server does not meet.
+ */
+ public static final int PRECONDITION_FAILED = 412;
+ /**
+ * Request entity is larger than limits defined by server.
+ * The server might close the connection or return an {@code Retry-After} header field.
+ */
+ public static final int PAYLOAD_TOO_LARGE = 413;
+ /**
+ * The URI requested by the client is longer than the server is willing to interpret.
+ */
+ public static final int URI_TOO_LONG = 414;
+ /**
+ * The media format of the requested data is not supported by the server, so the server is rejecting the request.
+ */
+ public static final int UNSUPPORTED_MEDIA_TYPE = 415;
+ /**
+ * The range specified by the {@code Range} header field in the request cannot be fulfilled.
+ * It's possible that the range is outside the size of the target URI's data.
+ */
+ public static final int RANGE_NOT_SATISFIABLE = 416;
+ /**
+ * This response code means the expectation indicated by the {@code Expect} request header field cannot be met by the server.
+ */
+ public static final int EXPECTATION_FAILED = 417;
+ /**
+ * The server refuses the attempt to brew coffee with a teapot.
+ */
+ public static final int IM_A_TEAPOT = 418;
+ /**
+ * The request was directed at a server that is not able to produce a response.
+ * This can be sent by a server that is not configured to produce responses for the combination of scheme and authority that are included in the request URI.
+ */
+ public static final int MISDIRECTED_REQUEST = 421;
+ /**
+ * The request was well-formed but was unable to be followed due to semantic errors.
+ */
+ public static final int UNPROCESSABLE_CONTENT = 422;
+ /**
+ * The resource that is being accessed is locked.
+ */
+ public static final int LOCKED = 423;
+ /**
+ * The request failed due to failure of a previous request.
+ */
+ public static final int FAILED_DEPENDENCY = 424;
+ /**
+ * Indicates that the server is unwilling to risk processing a request that might be replayed.
+ */
+ public static final int TOO_EARLY = 425;
+ /**
+ * The server refuses to perform the request using the current protocol but might be willing to do so after the client upgrades to a different protocol.
+ * The server sends an {@code Upgrade} header in a 426 response to indicate the required protocol(s).
+ */
+ public static final int UPGRADE_REQUIRED = 426;
+ /**
+ * The origin server requires the request to be conditional.
+ * This response is intended to prevent the {@code lost update} problem, where a client {@code GET}s a resource's state, modifies it and {@code PUT}s it back to the server, when meanwhile a third party has modified the state on the server, leading to a conflict.
+ */
+ public static final int PRECONDITION_REQUIRED = 428;
+ /**
+ * The user has sent too many requests in a given amount of time ("rate limiting").
+ */
+ public static final int TOO_MANY_REQUESTS = 429;
+ /**
+ * The server is unwilling to process the request because its header fields are too large.
+ * The request may be resubmitted after reducing the size of the request header fields.
+ */
+ public static final int REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
+ /**
+ * The user agent requested a resource that cannot legally be provided, such as a web page censored by a government.
+ */
+ public static final int UNAVAILABLE_FOR_LEGAL_REASONS = 451;
+ /**
+ * The server has encountered a situation it does not know how to handle.
+ */
+ public static final int INTERNAL_SERVER_ERROR = 500;
+ /**
+ * The request method is not supported by the server and cannot be handled. The only methods that servers are required to support (and therefore that must not return this code) are {@code GET} and {@code HEAD}.
+ */
+ public static final int NOT_IMPLEMENTED = 501;
+ /**
+ * This error response means that the server, while working as a gateway to get a response needed to handle the request, got an invalid response.
+ */
+ public static final int BAD_GATEWAY = 502;
+ /**
+ * The server is not ready to handle the request.
+ * Common causes are a server that is down for maintenance or that is overloaded.
+ * Note that together with this response, a user-friendly page explaining the problem should be sent.
+ * This response should be used for temporary conditions and the {@code Retry-After} HTTP header should, if possible, contain the estimated time before the recovery of the service.
+ * The webmaster must also take care about the caching-related headers that are sent along with this response, as these temporary condition responses should usually not be cached.
+ */
+ public static final int SERVICE_UNAVAILABLE = 503;
+ /**
+ * This error response is given when the server is acting as a gateway and cannot get a response in time.
+ */
+ public static final int GATEWAY_TIMEOUT = 504;
+ /**
+ * The HTTP version used in the request is not supported by the server.
+ */
+ public static final int HTTP_VERSION_NOT_SUPPORTED = 505;
+ /**
+ * The server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.
+ */
+ public static final int VARIANT_ALSO_NEGOTIATES = 506;
+ /**
+ * The method could not be performed on the resource because the server is unable to store the representation needed to successfully complete the request.
+ */
+ public static final int INSUFFICIENT_STORAGE = 507;
+ /**
+ * The server detected an infinite loop while processing the request.
+ */
+ public static final int LOOP_DETECTED = 508;
+ /**
+ * Further extensions to the request are required for the server to fulfill it.
+ */
+ public static final int NOT_EXTENDED = 510;
+ /**
+ * Indicates that the client needs to authenticate to gain network access.
+ */
+ public static final int NETWORK_AUTHENTICATION_REQUIRED = 511;
+
+ /**
+ * A map of all status codes and their messages.
+ */
+ public static final Map STATUS_CODES;
+
+ static {
+ Map statusCodes = new HashMap<>();
+ statusCodes.put(CONTINUE, "Continue");
+ statusCodes.put(SWITCHING_PROTOCOLS, "Switching Protocols");
+ statusCodes.put(PROCESSING, "Processing");
+ statusCodes.put(EARLY_HINTS, "Early Hints");
+ statusCodes.put(OK, "OK");
+ statusCodes.put(CREATED, "Created");
+ statusCodes.put(ACCEPTED, "Accepted");
+ statusCodes.put(NON_AUTHORITATIVE_INFORMATION, "Non-Authoritative Information");
+ statusCodes.put(NO_CONTENT, "No Content");
+ statusCodes.put(RESET_CONTENT, "Reset Content");
+ statusCodes.put(PARTIAL_CONTENT, "Partial Content");
+ statusCodes.put(MULTI_STATUS, "Multi-Status");
+ statusCodes.put(ALREADY_REPORTED, "Already Reported");
+ statusCodes.put(IM_USED, "IM Used");
+ statusCodes.put(MULTIPLE_CHOICES, "Multiple Choices");
+ statusCodes.put(MOVED_PERMANENTLY, "Moved Permanently");
+ statusCodes.put(MOVED_TEMPORARILY, "Moved Temporarily");
+ statusCodes.put(SEE_OTHER, "See Other");
+ statusCodes.put(NOT_MODIFIED, "Not Modified");
+ statusCodes.put(USE_PROXY, "Use Proxy");
+ statusCodes.put(UNUSED, "Unused");
+ statusCodes.put(TEMPORARY_REDIRECT, "Temporary Redirect");
+ statusCodes.put(PERMANENT_REDIRECT, "Permanent Redirect");
+ statusCodes.put(BAD_REQUEST, "Bad Request");
+ statusCodes.put(UNAUTHORIZED, "Unauthorized");
+ statusCodes.put(PAYMENT_REQUIRED, "Payment Required");
+ statusCodes.put(FORBIDDEN, "Forbidden");
+ statusCodes.put(NOT_FOUND, "Not Found");
+ statusCodes.put(METHOD_NOT_ALLOWED, "Method Not Allowed");
+ statusCodes.put(NOT_ACCEPTABLE, "Not Acceptable");
+ statusCodes.put(PROXY_AUTHENTICATION_REQUIRED, "Proxy Authentication Required");
+ statusCodes.put(REQUEST_TIMEOUT, "Request Timeout");
+ statusCodes.put(CONFLICT, "Conflict");
+ statusCodes.put(GONE, "Gone");
+ statusCodes.put(LENGTH_REQUIRED, "Length Required");
+ statusCodes.put(PRECONDITION_FAILED, "Precondition Failed");
+ statusCodes.put(PAYLOAD_TOO_LARGE, "Payload Too Large");
+ statusCodes.put(URI_TOO_LONG, "URI Too Long");
+ statusCodes.put(UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type");
+ statusCodes.put(RANGE_NOT_SATISFIABLE, "Range Not Satisfiable");
+ statusCodes.put(EXPECTATION_FAILED, "Expectation Failed");
+ statusCodes.put(IM_A_TEAPOT, "I'm a teapot");
+ statusCodes.put(MISDIRECTED_REQUEST, "Misdirected Request");
+ statusCodes.put(UNPROCESSABLE_CONTENT, "Unprocessable Content");
+ statusCodes.put(LOCKED, "Locked");
+ statusCodes.put(FAILED_DEPENDENCY, "Failed Dependency");
+ statusCodes.put(TOO_EARLY, "Too Early");
+ statusCodes.put(UPGRADE_REQUIRED, "Upgrade Required");
+ statusCodes.put(PRECONDITION_REQUIRED, "Precondition Required");
+ statusCodes.put(TOO_MANY_REQUESTS, "Too Many Requests");
+ statusCodes.put(REQUEST_HEADER_FIELDS_TOO_LARGE, "Request Header Fields Too Large");
+ statusCodes.put(UNAVAILABLE_FOR_LEGAL_REASONS, "Unavailable For Legal Reasons");
+ statusCodes.put(INTERNAL_SERVER_ERROR, "Internal Server Error");
+ statusCodes.put(NOT_IMPLEMENTED, "Not Implemented");
+ statusCodes.put(BAD_GATEWAY, "Bad Gateway");
+ statusCodes.put(SERVICE_UNAVAILABLE, "Service Unavailable");
+ statusCodes.put(GATEWAY_TIMEOUT, "Gateway Timeout");
+ statusCodes.put(HTTP_VERSION_NOT_SUPPORTED, "HTTP Version Not Supported");
+ statusCodes.put(VARIANT_ALSO_NEGOTIATES, "Variant Also Negotiates");
+ statusCodes.put(INSUFFICIENT_STORAGE, "Insufficient Storage");
+ statusCodes.put(LOOP_DETECTED, "Loop Detected");
+ statusCodes.put(NOT_EXTENDED, "Not Extended");
+ statusCodes.put(NETWORK_AUTHENTICATION_REQUIRED, "Network Authentication Required");
+ STATUS_CODES = Collections.unmodifiableMap(statusCodes);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/content/HttpContent.java b/src/main/java/net/lenni0451/commons/httpclient/content/HttpContent.java
new file mode 100644
index 0000000..363d69b
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/content/HttpContent.java
@@ -0,0 +1,161 @@
+package net.lenni0451.commons.httpclient.content;
+
+import net.lenni0451.commons.httpclient.content.impl.ByteArrayContent;
+import net.lenni0451.commons.httpclient.content.impl.FileContent;
+import net.lenni0451.commons.httpclient.content.impl.StringContent;
+import net.lenni0451.commons.httpclient.content.impl.URLEncodedFormContent;
+import net.lenni0451.commons.httpclient.model.ContentType;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+public abstract class HttpContent {
+
+ /**
+ * Create a new content from the given bytes.
+ *
+ * @param content The bytes to send
+ * @return The created content
+ */
+ public static HttpContent bytes(final byte[] content) {
+ return new ByteArrayContent(content);
+ }
+
+ /**
+ * Create a new content from the given bytes.
+ *
+ * @param content The bytes to send
+ * @param offset The offset to start reading from
+ * @param length The length of the bytes to read
+ * @return The created content
+ */
+ public static HttpContent bytes(final byte[] content, final int offset, final int length) {
+ return new ByteArrayContent(content, offset, length);
+ }
+
+ /**
+ * Create a new content from the given string.
+ *
+ * @param content The string to send
+ * @return The created content
+ */
+ public static HttpContent string(final String content) {
+ return new StringContent(content);
+ }
+
+ /**
+ * Create a new content from the given string.
+ *
+ * @param content The string to send
+ * @param charset The charset to use
+ * @return The created content
+ */
+ public static HttpContent string(final String content, final Charset charset) {
+ return new StringContent(content, charset);
+ }
+
+ /**
+ * Create a new content from the given file.
+ *
+ * @param file The file to send
+ * @return The created content
+ */
+ public static HttpContent file(final File file) {
+ return new FileContent(file);
+ }
+
+ /**
+ * Create a new content from the given form data.
+ *
+ * @param key The key
+ * @param value The value
+ * @return The created content
+ */
+ public static HttpContent form(final String key, final String value) {
+ return new URLEncodedFormContent().put(key, value);
+ }
+
+ /**
+ * Create a new content from the given form data.
+ *
+ * @param form The form data
+ * @return The created content
+ */
+ public static HttpContent form(final Map form) {
+ return new URLEncodedFormContent(form);
+ }
+
+ /**
+ * Create a new streamed content from the given input stream.
+ *
+ * @param contentType The content type
+ * @param inputStream The input stream
+ * @param contentLength The content length
+ * @return The created content
+ */
+ public static StreamedHttpContent streamed(final ContentType contentType, final InputStream inputStream, final int contentLength) {
+ return new StreamedHttpContent(contentType, inputStream, contentLength);
+ }
+
+
+ private final ContentType contentType;
+ protected byte[] content;
+
+ public HttpContent(final ContentType contentType) {
+ this.contentType = contentType;
+ }
+
+ /**
+ * @return The content type
+ */
+ public ContentType getContentType() {
+ return this.contentType;
+ }
+
+ /**
+ * @return The content as bytes
+ * @throws IOException If an I/O error occurs
+ */
+ public byte @NotNull [] getAsBytes() throws IOException {
+ if (this.content == null) this.content = this.compute();
+ return this.content;
+ }
+
+ /**
+ * @return The content as a UTF-8 string
+ * @throws IOException If an I/O error occurs
+ */
+ @NotNull
+ public String getAsString() throws IOException {
+ return this.getAsString(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Get the content as a string with the given charset.
+ *
+ * @param charset The charset to use
+ * @return The content as a string
+ * @throws IOException If an I/O error occurs
+ */
+ @NotNull
+ public String getAsString(final Charset charset) throws IOException {
+ return new String(this.getAsBytes(), charset);
+ }
+
+ /**
+ * @return The content length
+ */
+ public abstract int getContentLength();
+
+ /**
+ * @return The content
+ * @throws IOException If an I/O error occurs
+ */
+ protected abstract byte @NotNull [] compute() throws IOException;
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/content/StreamedHttpContent.java b/src/main/java/net/lenni0451/commons/httpclient/content/StreamedHttpContent.java
new file mode 100644
index 0000000..0515d0b
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/content/StreamedHttpContent.java
@@ -0,0 +1,64 @@
+package net.lenni0451.commons.httpclient.content;
+
+import net.lenni0451.commons.httpclient.model.ContentType;
+import net.lenni0451.commons.httpclient.utils.HttpRequestUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Represents a streamed http content which is not fully loaded into memory.
+ * It requires special handling in the executor to support this type of content.
+ * It is fully backwards compatible if the executor does not support this feature by reading the entire stream into memory.
+ * All built-in executors support this type of content.
+ */
+public class StreamedHttpContent extends HttpContent {
+
+ private final InputStream inputStream;
+ private final int contentLength;
+ private int bufferSize = 1024;
+
+ public StreamedHttpContent(final ContentType contentType, final InputStream inputStream, final int contentLength) {
+ super(contentType);
+ this.inputStream = inputStream;
+ this.contentLength = contentLength;
+ }
+
+ /**
+ * @return The input stream
+ */
+ public InputStream getInputStream() {
+ return this.inputStream;
+ }
+
+ /**
+ * @return The buffer size for reading the stream
+ */
+ public int getBufferSize() {
+ return this.bufferSize;
+ }
+
+ /**
+ * Set the buffer size for reading the stream.
+ * This option may be ignored depending on the executor implementation.
+ *
+ * @param bufferSize The buffer size
+ * @return This instance for chaining
+ */
+ public StreamedHttpContent setBufferSize(final int bufferSize) {
+ this.bufferSize = bufferSize;
+ return this;
+ }
+
+ @Override
+ public int getContentLength() {
+ return this.contentLength;
+ }
+
+ @Override
+ protected byte @NotNull [] compute() throws IOException {
+ return HttpRequestUtils.readFromStream(this.inputStream);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/content/impl/ByteArrayContent.java b/src/main/java/net/lenni0451/commons/httpclient/content/impl/ByteArrayContent.java
new file mode 100644
index 0000000..ee2205d
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/content/impl/ByteArrayContent.java
@@ -0,0 +1,48 @@
+package net.lenni0451.commons.httpclient.content.impl;
+
+import net.lenni0451.commons.httpclient.constants.ContentTypes;
+import net.lenni0451.commons.httpclient.content.HttpContent;
+import net.lenni0451.commons.httpclient.model.ContentType;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+
+public class ByteArrayContent extends HttpContent {
+
+ private final byte[] content;
+ private final int start;
+ private final int length;
+
+ public ByteArrayContent(final byte[] content) {
+ this(content, 0, content.length);
+ }
+
+ public ByteArrayContent(final byte[] content, final int start, final int length) {
+ super(ContentTypes.APPLICATION_OCTET_STREAM);
+ this.content = content;
+ this.start = start;
+ this.length = length;
+ }
+
+ public ByteArrayContent(final ContentType contentType, final byte[] content) {
+ this(contentType, content, 0, content.length);
+ }
+
+ public ByteArrayContent(final ContentType contentType, final byte[] content, final int start, final int length) {
+ super(contentType);
+ this.content = content;
+ this.start = start;
+ this.length = length;
+ }
+
+ @Override
+ public int getContentLength() {
+ return this.content.length;
+ }
+
+ @Override
+ protected byte @NotNull [] compute() {
+ return Arrays.copyOfRange(this.content, this.start, this.start + this.length);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/content/impl/FileContent.java b/src/main/java/net/lenni0451/commons/httpclient/content/impl/FileContent.java
new file mode 100644
index 0000000..5018b3f
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/content/impl/FileContent.java
@@ -0,0 +1,38 @@
+package net.lenni0451.commons.httpclient.content.impl;
+
+import net.lenni0451.commons.httpclient.constants.ContentTypes;
+import net.lenni0451.commons.httpclient.content.HttpContent;
+import net.lenni0451.commons.httpclient.model.ContentType;
+import net.lenni0451.commons.httpclient.utils.HttpRequestUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+public class FileContent extends HttpContent {
+
+ private final File file;
+
+ public FileContent(final File file) {
+ this(ContentTypes.APPLICATION_OCTET_STREAM, file);
+ }
+
+ public FileContent(final ContentType contentType, final File file) {
+ super(contentType);
+ this.file = file;
+ }
+
+ @Override
+ public int getContentLength() {
+ return (int) this.file.length();
+ }
+
+ @Override
+ protected byte @NotNull [] compute() throws IOException {
+ try (FileInputStream fis = new FileInputStream(this.file)) {
+ return HttpRequestUtils.readFromStream(fis);
+ }
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/content/impl/StringContent.java b/src/main/java/net/lenni0451/commons/httpclient/content/impl/StringContent.java
new file mode 100644
index 0000000..e3bbd38
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/content/impl/StringContent.java
@@ -0,0 +1,26 @@
+package net.lenni0451.commons.httpclient.content.impl;
+
+import net.lenni0451.commons.httpclient.model.ContentType;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+public class StringContent extends ByteArrayContent {
+
+ public StringContent(final String content) {
+ this(content, StandardCharsets.UTF_8);
+ }
+
+ public StringContent(final String content, final Charset charset) {
+ super(content.getBytes(charset));
+ }
+
+ public StringContent(final ContentType contentType, final String content) {
+ this(contentType, content, StandardCharsets.UTF_8);
+ }
+
+ public StringContent(final ContentType contentType, final String content, final Charset charset) {
+ super(contentType, content.getBytes(charset));
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/content/impl/URLEncodedFormContent.java b/src/main/java/net/lenni0451/commons/httpclient/content/impl/URLEncodedFormContent.java
new file mode 100644
index 0000000..a556b12
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/content/impl/URLEncodedFormContent.java
@@ -0,0 +1,99 @@
+package net.lenni0451.commons.httpclient.content.impl;
+
+import lombok.SneakyThrows;
+import net.lenni0451.commons.httpclient.constants.ContentTypes;
+import net.lenni0451.commons.httpclient.content.HttpContent;
+import net.lenni0451.commons.httpclient.utils.URLCoder;
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class URLEncodedFormContent extends HttpContent {
+
+ private final List entries;
+ private final Charset charset;
+
+ public URLEncodedFormContent() {
+ this(StandardCharsets.UTF_8);
+ }
+
+ public URLEncodedFormContent(final Map entries) {
+ this(entries, StandardCharsets.UTF_8);
+ }
+
+ public URLEncodedFormContent(final Charset charset) {
+ super(ContentTypes.APPLICATION_FORM_URLENCODED);
+ this.entries = new ArrayList<>();
+ this.charset = charset;
+ }
+
+ public URLEncodedFormContent(final Map entries, final Charset charset) {
+ super(ContentTypes.APPLICATION_FORM_URLENCODED);
+ this.entries = entries.entrySet().stream().map(e -> new FormEntry(e.getKey(), e.getValue())).collect(Collectors.toList());
+ this.charset = charset;
+ }
+
+ /**
+ * Add a new entry to the form.
+ *
+ * @param key The key
+ * @param value The value
+ * @return This instance for chaining
+ */
+ public URLEncodedFormContent put(final String key, final String value) {
+ this.entries.add(new FormEntry(key, value));
+ this.content = null;
+ return this;
+ }
+
+ @Override
+ @SneakyThrows
+ public int getContentLength() {
+ return this.getAsBytes().length;
+ }
+
+ @Override
+ protected byte @NotNull [] compute() {
+ StringBuilder builder = new StringBuilder();
+ for (FormEntry entry : this.entries) {
+ if (builder.length() != 0) builder.append("&");
+ builder
+ .append(entry.encodeKey(this.charset))
+ .append("=")
+ .append(entry.encodeValue(this.charset));
+ }
+ return builder.toString().getBytes(this.charset);
+ }
+
+ private static class FormEntry {
+ private final String key;
+ private final String value;
+
+ private FormEntry(final String key, final String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ private String getKey() {
+ return this.key;
+ }
+
+ private String encodeKey(Charset charset) {
+ return URLCoder.encode(this.key, charset);
+ }
+
+ private String getValue() {
+ return this.value;
+ }
+
+ private String encodeValue(Charset charset) {
+ return URLCoder.encode(this.value, charset);
+ }
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/exceptions/HttpRequestException.java b/src/main/java/net/lenni0451/commons/httpclient/exceptions/HttpRequestException.java
new file mode 100644
index 0000000..4d7fdf9
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/exceptions/HttpRequestException.java
@@ -0,0 +1,27 @@
+package net.lenni0451.commons.httpclient.exceptions;
+
+import net.lenni0451.commons.httpclient.HttpResponse;
+
+import java.io.IOException;
+
+public class HttpRequestException extends IOException {
+
+ private final HttpResponse response;
+
+ public HttpRequestException(final HttpResponse response) {
+ this(response, "Request failed: " + response.getStatusCode() + " " + response.getStatusMessage());
+ }
+
+ public HttpRequestException(final HttpResponse response, final String message) {
+ super(message);
+ this.response = response;
+ }
+
+ /**
+ * @return The response of the request
+ */
+ public HttpResponse getResponse() {
+ return this.response;
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/exceptions/RetryExceededException.java b/src/main/java/net/lenni0451/commons/httpclient/exceptions/RetryExceededException.java
new file mode 100644
index 0000000..385f0a9
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/exceptions/RetryExceededException.java
@@ -0,0 +1,15 @@
+package net.lenni0451.commons.httpclient.exceptions;
+
+import net.lenni0451.commons.httpclient.HttpResponse;
+
+public class RetryExceededException extends HttpRequestException {
+
+ public RetryExceededException(final HttpResponse response) {
+ super(response, "Maximum retry count exceeded");
+ }
+
+ public RetryExceededException(final HttpResponse response, final String message) {
+ super(response, message);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/executor/ExecutorType.java b/src/main/java/net/lenni0451/commons/httpclient/executor/ExecutorType.java
new file mode 100644
index 0000000..42976ca
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/executor/ExecutorType.java
@@ -0,0 +1,89 @@
+package net.lenni0451.commons.httpclient.executor;
+
+import net.lenni0451.commons.httpclient.HttpClient;
+
+import java.lang.reflect.Constructor;
+
+public enum ExecutorType {
+
+ /**
+ * Automatically choose the best executor type for the current Java version.
+ * This will try to use the other types in reverse order and use the first one that is available.
+ * e.g. if {@link #HTTP_CLIENT} is available it will be used, otherwise {@link #URL_CONNECTION} will be used.
+ */
+ AUTO {
+ @Override
+ public RequestExecutor initExecutor(HttpClient client) {
+ for (int i = values().length - 1; i >= 0; i--) {
+ ExecutorType type = values()[i];
+ if (AUTO.equals(type)) continue;
+ if (type.isAvailable()) {
+ RequestExecutor executor = type.makeExecutor(client);
+ if (executor != null) return executor;
+ }
+ }
+ throw new IllegalStateException("Failed to find a suitable executor. This should never happen. Please report this to the developer.");
+ }
+ },
+ /**
+ * Use the default URLConnection executor.
+ * This is the default executor for Java 10 and below.
+ */
+ URL_CONNECTION {
+ @Override
+ public RequestExecutor initExecutor(HttpClient client) {
+ return new URLConnectionExecutor(client);
+ }
+ },
+ /**
+ * Use the new HttpClient executor which was added in Java 11.
+ * This implementation is faster than the URLConnection executor and supports HTTP/2.
+ * Sadly the only supported proxy type is HTTP, SOCKS is not supported.
+ */
+ HTTP_CLIENT {
+ private Constructor> constructor;
+
+ @Override
+ protected void init() throws Throwable {
+ Class.forName("java.net.http.HttpClient");
+ Class> executorClass = Class.forName("net.lenni0451.commons.httpclient.executor.HttpClientExecutor");
+ this.constructor = executorClass.getDeclaredConstructor(HttpClient.class);
+ }
+
+ @Override
+ protected RequestExecutor initExecutor(HttpClient client) throws Throwable {
+ return (RequestExecutor) this.constructor.newInstance(client);
+ }
+ },
+ ;
+
+
+ private boolean available;
+
+ ExecutorType() {
+ try {
+ this.init();
+ this.available = true;
+ } catch (Throwable ignored) {
+ this.available = false;
+ }
+ }
+
+ public final boolean isAvailable() {
+ return this.available;
+ }
+
+ public final RequestExecutor makeExecutor(final HttpClient client) {
+ try {
+ return this.initExecutor(client);
+ } catch (Throwable ignored) {
+ }
+ return null;
+ }
+
+ protected void init() throws Throwable {
+ }
+
+ protected abstract RequestExecutor initExecutor(final HttpClient client) throws Throwable;
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/executor/RequestExecutor.java b/src/main/java/net/lenni0451/commons/httpclient/executor/RequestExecutor.java
new file mode 100644
index 0000000..1f9b16f
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/executor/RequestExecutor.java
@@ -0,0 +1,58 @@
+package net.lenni0451.commons.httpclient.executor;
+
+import net.lenni0451.commons.httpclient.HttpClient;
+import net.lenni0451.commons.httpclient.HttpResponse;
+import net.lenni0451.commons.httpclient.constants.Headers;
+import net.lenni0451.commons.httpclient.content.HttpContent;
+import net.lenni0451.commons.httpclient.requests.HttpContentRequest;
+import net.lenni0451.commons.httpclient.requests.HttpRequest;
+import net.lenni0451.commons.httpclient.utils.HttpRequestUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.net.CookieManager;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public abstract class RequestExecutor {
+
+ @NotNull
+ protected final HttpClient client;
+
+ public RequestExecutor(@NotNull final HttpClient client) {
+ this.client = client;
+ }
+
+ @NotNull
+ public abstract HttpResponse execute(@NotNull final HttpRequest request) throws IOException, InterruptedException;
+
+ @Nullable
+ protected final CookieManager getCookieManager(@NotNull final HttpRequest request) {
+ return request.isCookieManagerSet() ? request.getCookieManager() : this.client.getCookieManager();
+ }
+
+ protected final boolean isIgnoreInvalidSSL(@NotNull final HttpRequest request) {
+ return request.isIgnoreInvalidSSLSet() ? request.getIgnoreInvalidSSL() : this.client.isIgnoreInvalidSSL();
+ }
+
+ protected final Map> getHeaders(@NotNull final HttpRequest request, @Nullable final CookieManager cookieManager) throws IOException {
+ Map> headers = new HashMap<>();
+ if (request instanceof HttpContentRequest) {
+ HttpContent content = ((HttpContentRequest) request).getContent();
+ if (content != null) {
+ headers.put(Headers.CONTENT_TYPE, Collections.singletonList(content.getContentType().toString()));
+ headers.put(Headers.CONTENT_LENGTH, Collections.singletonList(String.valueOf(content.getContentLength())));
+ }
+ }
+ return HttpRequestUtils.mergeHeaders(
+ HttpRequestUtils.getCookieHeaders(cookieManager, request.getURL()),
+ headers,
+ this.client.getHeaders(),
+ request.getHeaders()
+ );
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/executor/URLConnectionExecutor.java b/src/main/java/net/lenni0451/commons/httpclient/executor/URLConnectionExecutor.java
new file mode 100644
index 0000000..26857fd
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/executor/URLConnectionExecutor.java
@@ -0,0 +1,141 @@
+package net.lenni0451.commons.httpclient.executor;
+
+import net.lenni0451.commons.httpclient.HttpClient;
+import net.lenni0451.commons.httpclient.HttpResponse;
+import net.lenni0451.commons.httpclient.content.HttpContent;
+import net.lenni0451.commons.httpclient.content.StreamedHttpContent;
+import net.lenni0451.commons.httpclient.proxy.ProxyHandler;
+import net.lenni0451.commons.httpclient.proxy.SingleProxySelector;
+import net.lenni0451.commons.httpclient.requests.HttpContentRequest;
+import net.lenni0451.commons.httpclient.requests.HttpRequest;
+import net.lenni0451.commons.httpclient.utils.HttpRequestUtils;
+import net.lenni0451.commons.httpclient.utils.IgnoringTrustManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.net.ssl.HttpsURLConnection;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CookieManager;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class URLConnectionExecutor extends RequestExecutor {
+
+ public URLConnectionExecutor(final HttpClient client) {
+ super(client);
+ }
+
+ @NotNull
+ @Override
+ public HttpResponse execute(@NotNull final HttpRequest request) throws IOException {
+ CookieManager cookieManager = this.getCookieManager(request);
+ HttpURLConnection connection = this.openConnection(request, cookieManager);
+ return this.executeRequest(connection, cookieManager, request);
+ }
+
+ private HttpURLConnection openConnection(final HttpRequest request, final CookieManager cookieManager) throws IOException {
+ SingleProxySelector proxySelector = null;
+ if (this.client.getProxyHandler().isProxySet()) proxySelector = this.client.getProxyHandler().getProxySelector();
+ try {
+ if (proxySelector != null) proxySelector.set();
+ URL url = request.getURL();
+ HttpURLConnection connection;
+ if (this.client.getProxyHandler().isProxySet()) {
+ ProxyHandler proxy = this.client.getProxyHandler();
+ connection = (HttpURLConnection) url.openConnection(proxy.toJavaProxy());
+ if (proxy.getUsername() != null && proxy.getPassword() != null) {
+ String proxyAuth = proxy.getUsername() + ":" + proxy.getPassword();
+ String encodedAuth = Base64.getEncoder().encodeToString(proxyAuth.getBytes());
+ connection.setRequestProperty("Proxy-Authorization", "Basic " + encodedAuth);
+ }
+ } else {
+ connection = (HttpURLConnection) url.openConnection();
+ }
+ if (this.isIgnoreInvalidSSL(request) && connection instanceof HttpsURLConnection) {
+ HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
+ httpsConnection.setSSLSocketFactory(IgnoringTrustManager.makeIgnoringSSLContext().getSocketFactory());
+ }
+ this.setupConnection(connection, cookieManager, request);
+ connection.connect();
+ return connection;
+ } finally {
+ if (proxySelector != null) proxySelector.reset();
+ }
+ }
+
+ private void setupConnection(final HttpURLConnection connection, @Nullable final CookieManager cookieManager, final HttpRequest request) throws IOException {
+ HttpRequestUtils.setHeaders(connection, this.getHeaders(request, cookieManager));
+ HttpContentRequest contentRequest = request instanceof HttpContentRequest ? (HttpContentRequest) request : null;
+ HttpContent content = contentRequest != null ? contentRequest.getContent() : null;
+
+ connection.setConnectTimeout(this.client.getConnectTimeout());
+ connection.setReadTimeout(this.client.getReadTimeout());
+ connection.setRequestMethod(request.getMethod());
+ connection.setDoInput(true);
+ if (contentRequest != null && content != null) {
+ connection.setDoOutput(true);
+ if (content instanceof StreamedHttpContent) connection.setFixedLengthStreamingMode(content.getContentLength());
+ } else {
+ connection.setDoOutput(false);
+ }
+ switch (request.getFollowRedirects()) {
+ case NOT_SET:
+ connection.setInstanceFollowRedirects(this.client.isFollowRedirects());
+ break;
+ case FOLLOW:
+ connection.setInstanceFollowRedirects(true);
+ break;
+ case IGNORE:
+ connection.setInstanceFollowRedirects(false);
+ break;
+ }
+ }
+
+ private HttpResponse executeRequest(final HttpURLConnection connection, @Nullable final CookieManager cookieManager, final HttpRequest request) throws IOException {
+ boolean closeConnection = true;
+ try {
+ if (connection.getDoOutput()) {
+ HttpContent content = ((HttpContentRequest) request).getContent();
+ OutputStream os = connection.getOutputStream();
+ if (content instanceof StreamedHttpContent) {
+ StreamedHttpContent streamedContent = (StreamedHttpContent) content;
+ InputStream is = streamedContent.getInputStream();
+ byte[] buffer = new byte[streamedContent.getBufferSize()];
+ int read;
+ while ((read = is.read(buffer)) != -1) os.write(buffer, 0, read);
+ is.close();
+ } else {
+ os.write(content.getAsBytes());
+ }
+ os.flush();
+ }
+
+ Map> headers = connection
+ .getHeaderFields()
+ .entrySet()
+ .stream()
+ .filter(e -> e.getKey() != null)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ HttpResponse response;
+ if (request.isStreamedResponse()) {
+ InputStream body = HttpRequestUtils.getInputStream(connection);
+ response = new HttpResponse(request.getURL(), connection.getResponseCode(), body, headers);
+ closeConnection = false; //The connection needs to remain open for streamed responses
+ } else {
+ byte[] body = HttpRequestUtils.readBody(connection);
+ response = new HttpResponse(request.getURL(), connection.getResponseCode(), body, headers);
+ }
+ HttpRequestUtils.updateCookies(cookieManager, request.getURL(), connection.getHeaderFields());
+ return response;
+ } finally {
+ if (closeConnection) connection.disconnect();
+ }
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/handler/HttpResponseHandler.java b/src/main/java/net/lenni0451/commons/httpclient/handler/HttpResponseHandler.java
new file mode 100644
index 0000000..6e83adc
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/handler/HttpResponseHandler.java
@@ -0,0 +1,28 @@
+package net.lenni0451.commons.httpclient.handler;
+
+import net.lenni0451.commons.httpclient.HttpResponse;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+@FunctionalInterface
+public interface HttpResponseHandler {
+
+ /**
+ * @return A handler that returns the response
+ */
+ static HttpResponseHandler identity() {
+ return response -> response;
+ }
+
+
+ /**
+ * Handle the response and return the result.
+ *
+ * @param response The response to handle
+ * @return The result
+ * @throws IOException If an I/O error occurs
+ */
+ R handle(@NotNull final HttpResponse response) throws IOException;
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/handler/ThrowingResponseHandler.java b/src/main/java/net/lenni0451/commons/httpclient/handler/ThrowingResponseHandler.java
new file mode 100644
index 0000000..55c0c60
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/handler/ThrowingResponseHandler.java
@@ -0,0 +1,17 @@
+package net.lenni0451.commons.httpclient.handler;
+
+import net.lenni0451.commons.httpclient.HttpResponse;
+import net.lenni0451.commons.httpclient.exceptions.HttpRequestException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+public class ThrowingResponseHandler implements HttpResponseHandler {
+
+ @Override
+ public HttpResponse handle(@NotNull HttpResponse response) throws IOException {
+ if (response.getStatusCode() >= 300) throw new HttpRequestException(response);
+ return response;
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/model/ContentType.java b/src/main/java/net/lenni0451/commons/httpclient/model/ContentType.java
new file mode 100644
index 0000000..d48c606
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/model/ContentType.java
@@ -0,0 +1,101 @@
+package net.lenni0451.commons.httpclient.model;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+
+public class ContentType {
+
+ /**
+ * Parse a content type string.
+ *
+ * @param contentType The content type string
+ * @return The parsed content type
+ */
+ public static ContentType parse(final String contentType) {
+ if (!contentType.contains(";")) return new ContentType(contentType.toLowerCase(Locale.ROOT), null, null);
+ String[] parts = contentType.split(";");
+ String type = parts[0].toLowerCase(Locale.ROOT);
+ Charset charset = null;
+ String boundary = null;
+ for (int i = 1; i < parts.length; i++) {
+ String part = parts[i].trim();
+ if (part.startsWith("charset=")) {
+ try {
+ charset = Charset.forName(part.substring(8));
+ } catch (UnsupportedCharsetException ignored) {
+ }
+ } else if (part.startsWith("boundary=")) {
+ boundary = part.substring(9);
+ }
+ }
+ return new ContentType(type, charset, boundary);
+ }
+
+
+ private final String mimeType;
+ private final Charset charset;
+ private final String boundary;
+
+ public ContentType(final String mimeType) {
+ this(mimeType, null, null);
+ }
+
+ public ContentType(final String mimeType, @Nullable final Charset charset) {
+ this(mimeType, charset, null);
+ }
+
+ public ContentType(final String mimeType, @Nullable final String boundary) {
+ this(mimeType, null, boundary);
+ }
+
+ public ContentType(final String mimeType, @Nullable final Charset charset, @Nullable final String boundary) {
+ this.mimeType = mimeType;
+ this.charset = charset;
+ this.boundary = boundary;
+ }
+
+ /**
+ * @return The mime type of the content type
+ */
+ public String getMimeType() {
+ return this.mimeType;
+ }
+
+ /**
+ * @return The charset of the content type
+ */
+ public Optional getCharset() {
+ return Optional.ofNullable(this.charset);
+ }
+
+ /**
+ * @return The boundary of the content type
+ */
+ public Optional getBoundary() {
+ return Optional.ofNullable(this.boundary);
+ }
+
+ @Override
+ public String toString() {
+ return this.mimeType + (this.charset != null ? "; charset=" + this.charset.name() : "") + (this.boundary != null ? "; boundary=" + this.boundary : "");
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ContentType that = (ContentType) o;
+ return Objects.equals(this.mimeType, that.mimeType) && Objects.equals(this.charset, that.charset) && Objects.equals(this.boundary, that.boundary);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.mimeType, this.charset, this.boundary);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/model/HttpHeader.java b/src/main/java/net/lenni0451/commons/httpclient/model/HttpHeader.java
new file mode 100644
index 0000000..f983d54
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/model/HttpHeader.java
@@ -0,0 +1,50 @@
+package net.lenni0451.commons.httpclient.model;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+public class HttpHeader {
+
+ @NotNull
+ private final String name;
+ @NotNull
+ private final String value;
+
+ public HttpHeader(@NotNull final String name, @NotNull final String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @NotNull
+ public String getName() {
+ return this.name;
+ }
+
+ @NotNull
+ public String getValue() {
+ return this.value;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpHeader{" +
+ "name='" + this.name + '\'' +
+ ", value='" + this.value + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ HttpHeader that = (HttpHeader) o;
+ return Objects.equals(this.name, that.name) && Objects.equals(this.value, that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.name, this.value);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/proxy/ProxyHandler.java b/src/main/java/net/lenni0451/commons/httpclient/proxy/ProxyHandler.java
new file mode 100644
index 0000000..64e86cf
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/proxy/ProxyHandler.java
@@ -0,0 +1,210 @@
+package net.lenni0451.commons.httpclient.proxy;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.Method;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.SocketAddress;
+
+public class ProxyHandler {
+
+ private ProxyType proxyType;
+ private SocketAddress address;
+ private String username;
+ private String password;
+
+ public ProxyHandler() {
+ }
+
+ public ProxyHandler(final ProxyType proxyType, final String host, final int port) {
+ this(proxyType, host, port, null, null);
+ }
+
+ public ProxyHandler(final ProxyType proxyType, final String host, final int port, @Nullable final String username, @Nullable final String password) {
+ this(proxyType, new InetSocketAddress(host, port), username, password);
+ }
+
+ public ProxyHandler(final ProxyType proxyType, final SocketAddress address) {
+ this(proxyType, address, null, null);
+ }
+
+ public ProxyHandler(final ProxyType proxyType, final SocketAddress address, @Nullable final String username, @Nullable final String password) {
+ this.proxyType = proxyType;
+ this.address = address;
+ this.username = username;
+ this.password = password;
+ }
+
+ /**
+ * Set the proxy to use.
+ *
+ * @param type The type of the proxy
+ * @param host The host of the proxy
+ * @param port The port of the proxy
+ * @return This instance for chaining
+ */
+ public ProxyHandler setProxy(final ProxyType type, final String host, final int port) {
+ return this.setProxy(type, new InetSocketAddress(host, port));
+ }
+
+ /**
+ * Set the proxy to use.
+ *
+ * @param type The type of the proxy
+ * @param address The address of the proxy
+ * @return This instance for chaining
+ */
+ public ProxyHandler setProxy(final ProxyType type, final SocketAddress address) {
+ this.proxyType = type;
+ this.address = address;
+ return this;
+ }
+
+ /**
+ * Unset the proxy.
+ *
+ * @return This instance for chaining
+ */
+ public ProxyHandler unsetProxy() {
+ this.proxyType = null;
+ this.address = null;
+ return this;
+ }
+
+ /**
+ * @return If the proxy is set
+ */
+ public boolean isProxySet() {
+ return this.proxyType != null && this.address != null;
+ }
+
+ /**
+ * @return The type of the proxy
+ */
+ @Nullable
+ public ProxyType getProxyType() {
+ return this.proxyType;
+ }
+
+ /**
+ * Set the type of the proxy.
+ *
+ * @param type The type of the proxy
+ * @return This instance for chaining
+ */
+ public ProxyHandler setProxyType(@NotNull final ProxyType type) {
+ this.proxyType = type;
+ return this;
+ }
+
+ /**
+ * @return The proxy address
+ */
+ @Nullable
+ public SocketAddress getAddress() {
+ return this.address;
+ }
+
+ /**
+ * Set the proxy address.
+ *
+ * @param address The proxy address
+ * @return This instance for chaining
+ */
+ public ProxyHandler setAddress(@NotNull final SocketAddress address) {
+ this.address = address;
+ return this;
+ }
+
+ /**
+ * @return If the authentication is set
+ */
+ public boolean isAuthenticationSet() {
+ return this.username != null && this.password != null;
+ }
+
+ /**
+ * @return The username for the proxy
+ */
+ @Nullable
+ public String getUsername() {
+ return this.username;
+ }
+
+ /**
+ * Set the username for the proxy.
+ *
+ * @param username The username for the proxy
+ * @return This instance for chaining
+ */
+ public ProxyHandler setUsername(@Nullable final String username) {
+ this.username = username;
+ return this;
+ }
+
+ /**
+ * @return The password for the proxy
+ */
+ @Nullable
+ public String getPassword() {
+ return this.password;
+ }
+
+ /**
+ * Set the password for the proxy.
+ *
+ * @param password The password for the proxy
+ * @return This instance for chaining
+ */
+ public ProxyHandler setPassword(@Nullable final String password) {
+ this.password = password;
+ return this;
+ }
+
+ /**
+ * @return The SingleProxySelector for this proxy
+ * @throws IllegalStateException If the proxy is not set
+ */
+ public SingleProxySelector getProxySelector() {
+ if (!this.isProxySet()) throw new IllegalStateException("Proxy is not set");
+ return new SingleProxySelector(this.toJavaProxy(), this.username, this.password);
+ }
+
+ /**
+ * @return The SingleProxyAuthenticator for this proxy
+ * @throws IllegalStateException If the proxy or the username/password is not set
+ */
+ public SingleProxyAuthenticator getProxyAuthenticator() {
+ if (!this.isProxySet()) throw new IllegalStateException("Proxy is not set");
+ if (!this.isAuthenticationSet()) throw new IllegalStateException("Username or password is not set");
+ return new SingleProxyAuthenticator(this.username, this.password);
+ }
+
+ /**
+ * Create a {@link Proxy} object from this proxy.
+ * The {@link Proxy} might not support all proxy types.
+ *
+ * @return The created {@link Proxy} object
+ */
+ public Proxy toJavaProxy() {
+ switch (this.proxyType) {
+ case HTTP:
+ return new Proxy(Proxy.Type.HTTP, this.address);
+ case SOCKS4:
+ try {
+ Class> clazz = Class.forName("sun.net.SocksProxy");
+ Method createMethod = clazz.getDeclaredMethod("create", SocketAddress.class, int.class);
+ return (Proxy) createMethod.invoke(null, this.address, 4);
+ } catch (Throwable t) {
+ throw new UnsupportedOperationException("SOCKS4 proxy type is not supported", t);
+ }
+ case SOCKS5:
+ return new Proxy(Proxy.Type.SOCKS, this.address);
+ default:
+ throw new IllegalStateException("Unknown proxy type: " + this.proxyType.name());
+ }
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/proxy/ProxyType.java b/src/main/java/net/lenni0451/commons/httpclient/proxy/ProxyType.java
new file mode 100644
index 0000000..b6e75a3
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/proxy/ProxyType.java
@@ -0,0 +1,30 @@
+package net.lenni0451.commons.httpclient.proxy;
+
+import java.net.Proxy;
+
+public enum ProxyType {
+
+ HTTP,
+ SOCKS4,
+ SOCKS5;
+
+ /**
+ * Get the {@link ProxyType} from a {@link Proxy.Type}.
+ * If the type is {@link Proxy.Type#DIRECT} an IllegalArgumentException will be thrown.
+ * If the type is {@link Proxy.Type#SOCKS} {@link ProxyType#SOCKS5} will be returned.
+ *
+ * @param type The type to convert
+ * @return The converted type
+ */
+ public static ProxyType from(final Proxy.Type type) {
+ switch (type) {
+ case HTTP:
+ return HTTP;
+ case SOCKS:
+ return SOCKS5;
+ default:
+ throw new IllegalArgumentException("Unknown proxy type: " + type.name());
+ }
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/proxy/SingleProxyAuthenticator.java b/src/main/java/net/lenni0451/commons/httpclient/proxy/SingleProxyAuthenticator.java
new file mode 100644
index 0000000..05bd46b
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/proxy/SingleProxyAuthenticator.java
@@ -0,0 +1,19 @@
+package net.lenni0451.commons.httpclient.proxy;
+
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+
+public class SingleProxyAuthenticator extends Authenticator {
+
+ private final PasswordAuthentication passwordAuthentication;
+
+ public SingleProxyAuthenticator(final String username, final String password) {
+ this.passwordAuthentication = new PasswordAuthentication(username, password.toCharArray());
+ }
+
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return this.passwordAuthentication;
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/proxy/SingleProxySelector.java b/src/main/java/net/lenni0451/commons/httpclient/proxy/SingleProxySelector.java
new file mode 100644
index 0000000..7908535
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/proxy/SingleProxySelector.java
@@ -0,0 +1,56 @@
+package net.lenni0451.commons.httpclient.proxy;
+
+import java.io.IOException;
+import java.net.*;
+import java.util.Collections;
+import java.util.List;
+
+public class SingleProxySelector extends ProxySelector {
+
+ private final Proxy proxy;
+ private final String username;
+ private final String password;
+ private final ProxySelector defaultProxySelector;
+ private final Authenticator defaultAuthenticator;
+
+ public SingleProxySelector(final Proxy proxy, final String username, final String password) {
+ this.proxy = proxy;
+ this.username = username;
+ this.password = password;
+
+ this.defaultProxySelector = ProxySelector.getDefault();
+ this.defaultAuthenticator = null;
+ }
+
+ /**
+ * Set this proxy selector as default.
+ * This also sets the authenticator if username and password are set.
+ */
+ public void set() {
+ ProxySelector.setDefault(this);
+ if (this.username != null && this.password != null) {
+ Authenticator.setDefault(new SingleProxyAuthenticator(this.username, this.password));
+ }
+ }
+
+ /**
+ * Reset the default proxy selector and authenticator.
+ */
+ public void reset() {
+ ProxySelector.setDefault(this.defaultProxySelector);
+ if (this.username != null && this.password != null) {
+ Authenticator.setDefault(this.defaultAuthenticator);
+ }
+ }
+
+ @Override
+ public List select(URI uri) {
+ return Collections.singletonList(this.proxy);
+ }
+
+ @Override
+ public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
+ //Do nothing
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/requests/HttpContentRequest.java b/src/main/java/net/lenni0451/commons/httpclient/requests/HttpContentRequest.java
new file mode 100644
index 0000000..5fb3ebd
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/requests/HttpContentRequest.java
@@ -0,0 +1,48 @@
+package net.lenni0451.commons.httpclient.requests;
+
+import net.lenni0451.commons.httpclient.content.HttpContent;
+import org.jetbrains.annotations.Nullable;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class HttpContentRequest extends HttpRequest {
+
+ @Nullable
+ private HttpContent content;
+
+ public HttpContentRequest(final String method, final String url) throws MalformedURLException {
+ super(method, url);
+ }
+
+ public HttpContentRequest(final String method, final URL url) {
+ super(method, url);
+ }
+
+ /**
+ * @return If this request has content
+ */
+ public boolean hasContent() {
+ return this.content != null;
+ }
+
+ /**
+ * @return The content of this request
+ */
+ @Nullable
+ public HttpContent getContent() {
+ return this.content;
+ }
+
+ /**
+ * Set the content of this request.
+ *
+ * @param content The content to set
+ * @return This instance for chaining
+ */
+ public HttpContentRequest setContent(@Nullable final HttpContent content) {
+ this.content = content;
+ return this;
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/requests/HttpRequest.java b/src/main/java/net/lenni0451/commons/httpclient/requests/HttpRequest.java
new file mode 100644
index 0000000..e98d671
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/requests/HttpRequest.java
@@ -0,0 +1,260 @@
+package net.lenni0451.commons.httpclient.requests;
+
+import net.lenni0451.commons.httpclient.HeaderStore;
+import net.lenni0451.commons.httpclient.HttpClient;
+import net.lenni0451.commons.httpclient.HttpResponse;
+import net.lenni0451.commons.httpclient.RetryHandler;
+import net.lenni0451.commons.httpclient.handler.HttpResponseHandler;
+import net.lenni0451.commons.httpclient.utils.ResettableStorage;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.CookieManager;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class HttpRequest extends HeaderStore {
+
+ private final String method;
+ private final URL url;
+ private boolean streamedResponse;
+ private FollowRedirects followRedirects = FollowRedirects.NOT_SET;
+ private final ResettableStorage cookieManager = new ResettableStorage<>();
+ private final ResettableStorage retryHandler = new ResettableStorage<>();
+ private final ResettableStorage ignoreInvalidSSL = new ResettableStorage<>();
+ private WeakReference boundClient;
+
+ public HttpRequest(final String method, final String url) throws MalformedURLException {
+ this(method, new URL(url));
+ }
+
+ public HttpRequest(final String method, final URL url) {
+ this.method = method;
+ this.url = url;
+ }
+
+ /**
+ * @return The request method
+ */
+ public String getMethod() {
+ return this.method;
+ }
+
+ /**
+ * @return The request url
+ */
+ public URL getURL() {
+ return this.url;
+ }
+
+ /**
+ * @return If the response should be streamed
+ */
+ public boolean isStreamedResponse() {
+ return this.streamedResponse;
+ }
+
+ /**
+ * Set if the response should be streamed.
+ * Streaming the response will not buffer the content and will allow you to read the content while it is being downloaded.
+ * Use the {@link HttpResponse#getInputStream()} method to get the stream.
+ * Some executors may not support streaming responses. All built-in executors support this feature.
+ *
+ * @param streamedResponse If the response should be streamed
+ * @return This instance for chaining
+ */
+ public HttpRequest setStreamedResponse(final boolean streamedResponse) {
+ this.streamedResponse = streamedResponse;
+ return this;
+ }
+
+ /**
+ * @return If redirects should be followed
+ */
+ public FollowRedirects getFollowRedirects() {
+ return this.followRedirects;
+ }
+
+ /**
+ * Set if redirects should be followed.
+ *
+ * @param followRedirects If redirects should be followed
+ * @return This instance for chaining
+ */
+ public HttpRequest setFollowRedirects(final boolean followRedirects) {
+ return this.setFollowRedirects(followRedirects ? FollowRedirects.FOLLOW : FollowRedirects.IGNORE);
+ }
+
+ /**
+ * Set if redirects should be followed.
+ *
+ * @param followRedirects If redirects should be followed
+ * @return This instance for chaining
+ */
+ public HttpRequest setFollowRedirects(@NotNull final FollowRedirects followRedirects) {
+ this.followRedirects = followRedirects;
+ return this;
+ }
+
+ /**
+ * @return If the cookie manager is set
+ */
+ public boolean isCookieManagerSet() {
+ return this.cookieManager.isSet();
+ }
+
+ /**
+ * Unset the cookie manager.
+ *
+ * @return This instance for chaining
+ */
+ public HttpRequest unsetCookieManager() {
+ this.cookieManager.unset();
+ return this;
+ }
+
+ /**
+ * @return The set cookie manager
+ * @throws IllegalStateException If the cookie manager is not set
+ */
+ @Nullable
+ public CookieManager getCookieManager() {
+ return this.cookieManager.get();
+ }
+
+ /**
+ * Set the cookie manager to use for this request.
+ *
+ * @param cookieManager The cookie manager to use
+ * @return This instance for chaining
+ */
+ public HttpRequest setCookieManager(@Nullable final CookieManager cookieManager) {
+ this.cookieManager.set(cookieManager);
+ return this;
+ }
+
+ /**
+ * @return If the retry handler is set
+ */
+ public boolean isRetryHandlerSet() {
+ return this.retryHandler.isSet();
+ }
+
+ /**
+ * Unset the retry handler.
+ *
+ * @return This instance for chaining
+ */
+ public HttpRequest unsetRetryHandler() {
+ this.retryHandler.unset();
+ return this;
+ }
+
+ /**
+ * @return The set retry handler
+ * @throws IllegalStateException If the retry handler is not set
+ */
+ @NotNull
+ public RetryHandler getRetryHandler() {
+ return this.retryHandler.get();
+ }
+
+ /**
+ * Set the retry handler to use for this request.
+ *
+ * @param retryHandler The retry handler to use
+ * @return This instance for chaining
+ */
+ public HttpRequest setRetryHandler(@NotNull final RetryHandler retryHandler) {
+ this.retryHandler.set(retryHandler);
+ return this;
+ }
+
+ /**
+ * @return If the ignore invalid SSL flag is set
+ */
+ public boolean isIgnoreInvalidSSLSet() {
+ return this.ignoreInvalidSSL.isSet();
+ }
+
+ /**
+ * Unset the ignore invalid SSL flag.
+ *
+ * @return This instance for chaining
+ */
+ public HttpRequest unsetIgnoreInvalidSSL() {
+ this.ignoreInvalidSSL.unset();
+ return this;
+ }
+
+ /**
+ * @return The set ignore invalid SSL flag
+ * @throws IllegalStateException If the ignore invalid SSL flag is not set
+ */
+ public boolean getIgnoreInvalidSSL() {
+ return this.ignoreInvalidSSL.get();
+ }
+
+ /**
+ * Set the ignore invalid SSL flag to use for this request.
+ *
+ * @param ignoreInvalidSSL The ignore invalid SSL flag to use
+ * @return This instance for chaining
+ */
+ public HttpRequest setIgnoreInvalidSSL(final boolean ignoreInvalidSSL) {
+ this.ignoreInvalidSSL.set(ignoreInvalidSSL);
+ return this;
+ }
+
+ /**
+ * Bind this request to a client for execution.
+ *
+ * @param client The client to bind to
+ * @return This instance for chaining
+ */
+ public HttpRequest bind(@Nullable final HttpClient client) {
+ if (client == null) this.boundClient = null;
+ else this.boundClient = new WeakReference<>(client);
+ return this;
+ }
+
+ /**
+ * Execute this request and return the response.
+ * If a client is bound to this request it will be used for execution.
+ * If no client is bound a new client will be created.
+ *
+ * @return The response of the request
+ * @throws IOException If an I/O error occurs
+ */
+ public HttpResponse execute() throws IOException {
+ HttpClient client = null;
+ if (this.boundClient != null) client = this.boundClient.get();
+ if (client == null) client = new HttpClient();
+ return client.execute(this);
+ }
+
+ /**
+ * Execute this request and pass the response to the response handler.
+ * If a client is bound to this request it will be used for execution.
+ * If no client is bound a new client will be created.
+ *
+ * @param responseHandler The response handler
+ * @param The return type of the response handler
+ * @return The return value of the response handler
+ * @throws IOException If an I/O error occurs
+ */
+ public R execute(final HttpResponseHandler responseHandler) throws IOException {
+ HttpClient client = null;
+ if (this.boundClient != null) client = this.boundClient.get();
+ if (client == null) client = new HttpClient();
+ return client.execute(this, responseHandler);
+ }
+
+
+ public enum FollowRedirects {
+ NOT_SET, FOLLOW, IGNORE
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/requests/impl/DeleteRequest.java b/src/main/java/net/lenni0451/commons/httpclient/requests/impl/DeleteRequest.java
new file mode 100644
index 0000000..ea2f706
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/requests/impl/DeleteRequest.java
@@ -0,0 +1,19 @@
+package net.lenni0451.commons.httpclient.requests.impl;
+
+import net.lenni0451.commons.httpclient.constants.RequestMethods;
+import net.lenni0451.commons.httpclient.requests.HttpRequest;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class DeleteRequest extends HttpRequest {
+
+ public DeleteRequest(final String url) throws MalformedURLException {
+ super(RequestMethods.DELETE, url);
+ }
+
+ public DeleteRequest(final URL url) {
+ super(RequestMethods.DELETE, url);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/requests/impl/GetRequest.java b/src/main/java/net/lenni0451/commons/httpclient/requests/impl/GetRequest.java
new file mode 100644
index 0000000..1f60b6e
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/requests/impl/GetRequest.java
@@ -0,0 +1,19 @@
+package net.lenni0451.commons.httpclient.requests.impl;
+
+import net.lenni0451.commons.httpclient.constants.RequestMethods;
+import net.lenni0451.commons.httpclient.requests.HttpRequest;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class GetRequest extends HttpRequest {
+
+ public GetRequest(final String url) throws MalformedURLException {
+ super(RequestMethods.GET, url);
+ }
+
+ public GetRequest(final URL url) {
+ super(RequestMethods.GET, url);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/requests/impl/HeadRequest.java b/src/main/java/net/lenni0451/commons/httpclient/requests/impl/HeadRequest.java
new file mode 100644
index 0000000..061bd75
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/requests/impl/HeadRequest.java
@@ -0,0 +1,19 @@
+package net.lenni0451.commons.httpclient.requests.impl;
+
+import net.lenni0451.commons.httpclient.constants.RequestMethods;
+import net.lenni0451.commons.httpclient.requests.HttpRequest;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class HeadRequest extends HttpRequest {
+
+ public HeadRequest(final String url) throws MalformedURLException {
+ super(RequestMethods.HEAD, url);
+ }
+
+ public HeadRequest(final URL url) {
+ super(RequestMethods.HEAD, url);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/requests/impl/PostRequest.java b/src/main/java/net/lenni0451/commons/httpclient/requests/impl/PostRequest.java
new file mode 100644
index 0000000..13b2df8
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/requests/impl/PostRequest.java
@@ -0,0 +1,19 @@
+package net.lenni0451.commons.httpclient.requests.impl;
+
+import net.lenni0451.commons.httpclient.constants.RequestMethods;
+import net.lenni0451.commons.httpclient.requests.HttpContentRequest;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class PostRequest extends HttpContentRequest {
+
+ public PostRequest(final String url) throws MalformedURLException {
+ super(RequestMethods.POST, url);
+ }
+
+ public PostRequest(final URL url) {
+ super(RequestMethods.POST, url);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/requests/impl/PutRequest.java b/src/main/java/net/lenni0451/commons/httpclient/requests/impl/PutRequest.java
new file mode 100644
index 0000000..b39f801
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/requests/impl/PutRequest.java
@@ -0,0 +1,19 @@
+package net.lenni0451.commons.httpclient.requests.impl;
+
+import net.lenni0451.commons.httpclient.constants.RequestMethods;
+import net.lenni0451.commons.httpclient.requests.HttpContentRequest;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class PutRequest extends HttpContentRequest {
+
+ public PutRequest(final String url) throws MalformedURLException {
+ super(RequestMethods.PUT, url);
+ }
+
+ public PutRequest(final URL url) {
+ super(RequestMethods.PUT, url);
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/utils/HttpRequestUtils.java b/src/main/java/net/lenni0451/commons/httpclient/utils/HttpRequestUtils.java
new file mode 100644
index 0000000..01354b6
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/utils/HttpRequestUtils.java
@@ -0,0 +1,159 @@
+package net.lenni0451.commons.httpclient.utils;
+
+import lombok.experimental.UtilityClass;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.CookieManager;
+import java.net.HttpURLConnection;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.*;
+
+@UtilityClass
+public class HttpRequestUtils {
+
+ /**
+ * Merge multiple headers into one map.
+ *
+ * @param maps The headers to merge
+ * @return The merged headers
+ */
+ @SafeVarargs
+ public static Map> mergeHeaders(final Map>... maps) {
+ Map> headers = new HashMap<>();
+ for (Map> map : maps) {
+ for (Map.Entry> entry : map.entrySet()) {
+ if (entry.getValue().isEmpty()) continue; //Skip empty headers
+ headers.put(entry.getKey().toLowerCase(Locale.ROOT), entry.getValue());
+ }
+ }
+ return headers;
+ }
+
+ /**
+ * Get the cookie headers for a URL.
+ *
+ * @param cookieManager The cookie manager to use
+ * @param url The URL to get the cookies for
+ * @return The cookie headers
+ * @throws IOException If an I/O error occurs
+ */
+ public static Map> getCookieHeaders(@Nullable final CookieManager cookieManager, final URL url) throws IOException {
+ try {
+ if (cookieManager == null) return Collections.emptyMap();
+ return cookieManager.get(url.toURI(), Collections.emptyMap());
+ } catch (URISyntaxException e) {
+ throw new IOException("Failed to parse URL as URI", e);
+ }
+ }
+
+ /**
+ * Update the cookies for a URL.
+ *
+ * @param cookieManager The cookie manager to use
+ * @param url The URL to update the cookies for
+ * @param headers The headers to update the cookies from
+ * @throws IOException If an I/O error occurs
+ */
+ public static void updateCookies(@Nullable final CookieManager cookieManager, final URL url, final Map> headers) throws IOException {
+ if (cookieManager == null) return;
+ try {
+ cookieManager.put(url.toURI(), headers);
+ } catch (URISyntaxException e) {
+ throw new IOException("Failed to parse URL as URI", e);
+ }
+ }
+
+ /**
+ * Set the headers for a connection.
+ *
+ * @param connection The connection to set the headers for
+ * @param headers The headers to set
+ */
+ public static void setHeaders(final HttpURLConnection connection, final Map> headers) {
+ for (Map.Entry> entry : headers.entrySet()) {
+ connection.setRequestProperty(entry.getKey(), String.join("; ", entry.getValue()));
+ }
+ }
+
+ /**
+ * Read the body of a connection.
+ *
+ * @param connection The connection to read the body from
+ * @return The body of the connection
+ * @throws IOException If an I/O error occurs
+ */
+ public static byte[] readBody(final HttpURLConnection connection) throws IOException {
+ return readFromStream(getInputStream(connection));
+ }
+
+ /**
+ * Get the input stream of a connection.
+ *
+ * @param connection The connection to get the input stream from
+ * @return The input stream of the connection
+ * @throws IOException If an I/O error occurs
+ */
+ public static InputStream getInputStream(final HttpURLConnection connection) throws IOException {
+ InputStream is;
+ if (connection.getResponseCode() >= 400) is = connection.getErrorStream();
+ else is = connection.getInputStream();
+ if (is == null) is = new ByteArrayInputStream(new byte[0]);
+ return is;
+ }
+
+ /**
+ * Read the body of a connection.
+ *
+ * @param is The input stream to read the body from
+ * @return The body of the connection
+ * @throws IOException If an I/O error occurs
+ */
+ public static byte[] readFromStream(final InputStream is) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] buf = new byte[1024];
+ int len;
+ while ((len = is.read(buf)) != -1) baos.write(buf, 0, len);
+ return baos.toByteArray();
+ }
+
+ /**
+ * Parse a HTTP date.
+ *
+ * @param httpDate The HTTP date to parse
+ * @return The parsed date
+ * @throws DateTimeParseException If the date could not be parsed
+ */
+ public static Instant parseHttpDate(final String httpDate) throws DateTimeParseException {
+ return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(httpDate));
+ }
+
+ /**
+ * Parse an HTTP date or seconds string as milliseconds until the date.
+ *
+ * @param value The value to parse
+ * @return The parsed value in milliseconds
+ */
+ @Nullable
+ public static Long parseSecondsOrHttpDate(final String value) {
+ try {
+ Instant date = HttpRequestUtils.parseHttpDate(value);
+ return date.toEpochMilli() - Instant.now().toEpochMilli();
+ } catch (DateTimeParseException ignored) {
+ }
+ try {
+ int seconds = Integer.parseInt(value);
+ return (long) seconds * 1000;
+ } catch (NumberFormatException ignored) {
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/utils/IgnoringTrustManager.java b/src/main/java/net/lenni0451/commons/httpclient/utils/IgnoringTrustManager.java
new file mode 100644
index 0000000..2ac6ea7
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/utils/IgnoringTrustManager.java
@@ -0,0 +1,54 @@
+package net.lenni0451.commons.httpclient.utils;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509ExtendedTrustManager;
+import java.io.IOException;
+import java.net.Socket;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+
+public class IgnoringTrustManager extends X509ExtendedTrustManager {
+
+ public static SSLContext makeIgnoringSSLContext() throws IOException {
+ try {
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, new TrustManager[]{new IgnoringTrustManager()}, new SecureRandom());
+ return sslContext;
+ } catch (Throwable t) {
+ throw new IOException("Failed to create ignoring SSL socket factory", t);
+ }
+ }
+
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) {
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/utils/ResettableStorage.java b/src/main/java/net/lenni0451/commons/httpclient/utils/ResettableStorage.java
new file mode 100644
index 0000000..ce362e5
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/utils/ResettableStorage.java
@@ -0,0 +1,34 @@
+package net.lenni0451.commons.httpclient.utils;
+
+public class ResettableStorage {
+
+ private boolean set = false;
+ private T value;
+
+ public ResettableStorage() {
+ }
+
+ public ResettableStorage(final T defaultValue) {
+ this.set(defaultValue);
+ }
+
+ public boolean isSet() {
+ return this.set;
+ }
+
+ public void unset() {
+ this.set = false;
+ this.value = null;
+ }
+
+ public T get() {
+ if (!this.set) throw new IllegalStateException("Value is not set");
+ return this.value;
+ }
+
+ public void set(final T value) {
+ this.value = value;
+ this.set = true;
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/utils/URLCoder.java b/src/main/java/net/lenni0451/commons/httpclient/utils/URLCoder.java
new file mode 100644
index 0000000..eec3b27
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/utils/URLCoder.java
@@ -0,0 +1,62 @@
+package net.lenni0451.commons.httpclient.utils;
+
+import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
+
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+@UtilityClass
+public class URLCoder {
+
+ /**
+ * Encode a string to be used in a URL.
+ * This method uses {@link StandardCharsets#UTF_8} as charset.
+ *
+ * @param s The string to encode
+ * @return The encoded string
+ */
+ @SneakyThrows
+ public static String encode(final String s) {
+ return encode(s, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Encode a string to be used in a URL.
+ *
+ * @param s The string to encode
+ * @param charset The charset to use
+ * @return The encoded string
+ */
+ @SneakyThrows
+ public static String encode(final String s, final Charset charset) {
+ return URLEncoder.encode(s, charset.name());
+ }
+
+ /**
+ * Decode a string from a URL.
+ * This method uses {@link StandardCharsets#UTF_8} as charset.
+ *
+ * @param s The string to decode
+ * @return The decoded string
+ */
+ @SneakyThrows
+ public static String decode(final String s) {
+ return decode(s, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Decode a string from a URL.
+ *
+ * @param s The string to decode
+ * @param charset The charset to use
+ * @return The decoded string
+ */
+ @SneakyThrows
+ public static String decode(final String s, final Charset charset) {
+ return URLDecoder.decode(s, charset.name());
+ }
+
+}
diff --git a/src/main/java/net/lenni0451/commons/httpclient/utils/URLWrapper.java b/src/main/java/net/lenni0451/commons/httpclient/utils/URLWrapper.java
new file mode 100644
index 0000000..a37b045
--- /dev/null
+++ b/src/main/java/net/lenni0451/commons/httpclient/utils/URLWrapper.java
@@ -0,0 +1,361 @@
+package net.lenni0451.commons.httpclient.utils;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class URLWrapper {
+
+ /**
+ * Create a new empty URLWrapper.
+ *
+ * @return The URLWrapper
+ */
+ public static URLWrapper empty() {
+ return new URLWrapper();
+ }
+
+ /**
+ * Create a new URLWrapper from a {@link String}.
+ *
+ * @param url The URL to wrap
+ * @return The URLWrapper
+ * @throws MalformedURLException If the URL is invalid
+ */
+ public static URLWrapper of(final String url) throws MalformedURLException {
+ return new URLWrapper(url);
+ }
+
+ /**
+ * Create a new URLWrapper from an {@link URL}.
+ *
+ * @param url The URL to wrap
+ * @return The URLWrapper
+ */
+ public static URLWrapper of(final URL url) {
+ return new URLWrapper(url);
+ }
+
+ /**
+ * Create a new URLWrapper from an {@link URI}.
+ *
+ * @param uri The URI to wrap
+ * @return The URLWrapper
+ */
+ public static URLWrapper of(final URI uri) {
+ return new URLWrapper(uri);
+ }
+
+
+ private String protocol;
+ private String host;
+ private int port = -1;
+ private String path;
+ private String query;
+ private String userInfo;
+ private String reference;
+
+ public URLWrapper() {
+ }
+
+ public URLWrapper(final String url) throws MalformedURLException {
+ this(new URL(url));
+ }
+
+ public URLWrapper(final URL url) {
+ this.protocol = url.getProtocol();
+ this.host = url.getHost();
+ this.port = url.getPort();
+ this.path = url.getPath();
+ this.query = url.getQuery();
+ this.userInfo = url.getUserInfo();
+ this.reference = url.getRef();
+ }
+
+ public URLWrapper(final URI uri) {
+ this.protocol = uri.getScheme();
+ this.host = uri.getHost();
+ this.port = uri.getPort();
+ this.path = uri.getPath();
+ this.query = uri.getQuery();
+ this.userInfo = uri.getUserInfo();
+ this.reference = uri.getFragment();
+ }
+
+ /**
+ * @return The protocol of the URL. e.g. {@code https}
+ */
+ public String getProtocol() {
+ return this.protocol;
+ }
+
+ /**
+ * Set the protocol of the URL.
+ * e.g. {@code https}
+ *
+ * @param protocol The new protocol
+ * @return The URLWrapper
+ */
+ public URLWrapper setProtocol(final String protocol) {
+ this.protocol = protocol;
+ return this;
+ }
+
+ /**
+ * @return The host of the URL. e.g. {@code www.example.com}
+ */
+ public String getHost() {
+ return this.host;
+ }
+
+ /**
+ * Set the host of the URL.
+ * e.g. {@code www.example.com}
+ *
+ * @param host The new host
+ * @return The URLWrapper
+ */
+ public URLWrapper setHost(final String host) {
+ this.host = host;
+ return this;
+ }
+
+ /**
+ * @return The port of the URL. e.g. {@code 443}
+ */
+ public int getPort() {
+ return this.port;
+ }
+
+ /**
+ * Set the port of the URL.
+ * e.g. {@code 443}
+ *
+ * @param port The new port
+ * @return The URLWrapper
+ */
+ public URLWrapper setPort(final int port) {
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * @return The file of the URL. e.g. {@code /search}
+ */
+ public String getPath() {
+ return this.path;
+ }
+
+ /**
+ * Set the file of the URL.
+ * e.g. {@code /search}
+ *
+ * @param path The new file
+ * @return The URLWrapper
+ */
+ public URLWrapper setPath(final String path) {
+ this.path = path;
+ return this;
+ }
+
+ /**
+ * @return The query of the URL. e.g. {@code q=hello}
+ */
+ public String getQuery() {
+ return this.query;
+ }
+
+ /**
+ * Set the query of the URL.
+ * e.g. {@code q=hello}
+ *
+ * @param query The new query
+ * @return The URLWrapper
+ */
+ public URLWrapper setQuery(final String query) {
+ this.query = query;
+ return this;
+ }
+
+ /**
+ * Get a wrapper for the query parameters.
+ *
+ * @return The query wrapper
+ */
+ public QueryWrapper wrapQuery() {
+ return new QueryWrapper();
+ }
+
+ /**
+ * @return The user info of the URL. e.g. {@code user:password}
+ */
+ public String getUserInfo() {
+ return this.userInfo;
+ }
+
+ /**
+ * Set the user info of the URL.
+ * e.g. {@code user:password}
+ *
+ * @param userInfo The new user info
+ * @return The URLWrapper
+ */
+ public URLWrapper setUserInfo(final String userInfo) {
+ this.userInfo = userInfo;
+ return this;
+ }
+
+ /**
+ * @return The reference of the URL. e.g. {@code reference}
+ */
+ public String getReference() {
+ return this.reference;
+ }
+
+ /**
+ * Set the reference of the URL.
+ * e.g. {@code reference}
+ *
+ * @param reference The new reference
+ * @return The URLWrapper
+ */
+ public URLWrapper setReference(final String reference) {
+ this.reference = reference;
+ return this;
+ }
+
+ /**
+ * @return The wrapped URL
+ * @throws MalformedURLException If the URL is invalid
+ */
+ public URL toURL() throws MalformedURLException {
+ return new URL(this.toString());
+ }
+
+ /**
+ * @return The wrapped URI
+ */
+ public URI toURI() {
+ return URI.create(this.toString());
+ }
+
+ @Override
+ public String toString() {
+ String url = this.protocol + "://";
+ if (this.userInfo != null) url += this.userInfo + "@";
+ url += this.host;
+ if (this.port >= 0) url += ":" + this.port;
+ if (this.path != null) url += this.path;
+ if (this.query != null) url += "?" + this.query;
+ if (this.reference != null) url += "#" + this.reference;
+ return url;
+ }
+
+ public class QueryWrapper {
+ private final Map queries = new HashMap<>();
+
+ private QueryWrapper() {
+ String query = URLWrapper.this.getQuery();
+ if (query != null) {
+ for (String queryPart : query.split("&")) {
+ String[] split = queryPart.split("=", 2);
+ if (split.length == 2) {
+ this.queries.put(URLCoder.decode(split[0]), URLCoder.decode(split[1]));
+ } else {
+ this.queries.put(URLCoder.decode(split[0]), "");
+ }
+ }
+ }
+ }
+
+ /**
+ * @return A map of all query parameters
+ */
+ public Map getQueries() {
+ return Collections.unmodifiableMap(this.queries);
+ }
+
+ /**
+ * Get a query parameter by its key.
+ *
+ * @param key The key of the query parameter
+ * @return The value of the query parameter or null if it does not exist
+ */
+ public Optional getQuery(final String key) {
+ return Optional.ofNullable(this.queries.get(key));
+ }
+
+ /**
+ * Set a query parameter.
+ *
+ * @param key The key of the query parameter
+ * @param value The value of the query parameter
+ * @return The URLWrapper
+ */
+ public QueryWrapper setQuery(final String key, final String value) {
+ this.queries.put(key, value);
+ return this;
+ }
+
+ /**
+ * Add multiple query parameters.
+ *
+ * @param queries The query parameters to add
+ * @return The URLWrapper
+ */
+ public QueryWrapper addQueries(final Map queries) {
+ this.queries.putAll(queries);
+ return this;
+ }
+
+ /**
+ * Remove a query parameter.
+ *
+ * @param key The key of the query parameter
+ * @return The URLWrapper
+ */
+ public QueryWrapper removeQuery(final String key) {
+ this.queries.remove(key);
+ return this;
+ }
+
+ /**
+ * Check if a query parameter exists.
+ *
+ * @param key The key of the query parameter
+ * @return True if the query parameter exists
+ */
+ public boolean hasQuery(final String key) {
+ return this.queries.containsKey(key);
+ }
+
+ /**
+ * Apply the changes to the URL.
+ *
+ * @return The URLWrapper
+ */
+ public URLWrapper apply() {
+ StringBuilder query = new StringBuilder();
+ for (Map.Entry entry : this.queries.entrySet()) {
+ query.append(URLCoder.encode(entry.getKey())).append("=").append(URLCoder.encode(entry.getValue())).append("&");
+ }
+ if (query.length() > 0) query.deleteCharAt(query.length() - 1);
+ URLWrapper.this.setQuery(query.toString());
+ return URLWrapper.this;
+ }
+
+ /**
+ * Discard the changes to the URL.
+ *
+ * @return The URLWrapper
+ */
+ public URLWrapper discard() {
+ return URLWrapper.this;
+ }
+ }
+
+}
diff --git a/src/main/java/net/raphimc/minecraftauth/MinecraftAuth.java b/src/main/java/net/raphimc/minecraftauth/MinecraftAuth.java
index 5513a2e..5061e70 100644
--- a/src/main/java/net/raphimc/minecraftauth/MinecraftAuth.java
+++ b/src/main/java/net/raphimc/minecraftauth/MinecraftAuth.java
@@ -17,7 +17,6 @@
*/
package net.raphimc.minecraftauth;
-import lombok.SneakyThrows;
import net.lenni0451.commons.httpclient.HttpClient;
import net.lenni0451.commons.httpclient.RetryHandler;
import net.lenni0451.commons.httpclient.constants.ContentTypes;
@@ -41,10 +40,9 @@
import net.raphimc.minecraftauth.util.OAuthEnvironment;
import net.raphimc.minecraftauth.util.logging.ILogger;
import net.raphimc.minecraftauth.util.logging.LazyLogger;
-import net.raphimc.minecraftauth.util.logging.Slf4jConsoleLogger;
+import net.raphimc.minecraftauth.util.logging.JavaConsoleLogger;
import org.jetbrains.annotations.ApiStatus;
-import java.lang.reflect.Constructor;
import java.util.function.Function;
public class MinecraftAuth {
@@ -52,7 +50,7 @@ public class MinecraftAuth {
public static final String VERSION = "${version}";
public static final String IMPL_VERSION = "${version}+${commit_hash}";
- public static ILogger LOGGER = new LazyLogger(Slf4jConsoleLogger::new);
+ public static ILogger LOGGER = new LazyLogger(JavaConsoleLogger::new);
public static String USER_AGENT = "MinecraftAuth/" + VERSION;
public static final AbstractStep, StepFullJavaSession.FullJavaSession> JAVA_DEVICE_CODE_LOGIN = builder()
@@ -118,9 +116,12 @@ public static MsaTokenBuilder builder() {
return new MsaTokenBuilder();
}
+
public static HttpClient createHttpClient() {
- final int timeout = 5000;
+ return createHttpClient(5000);
+ }
+ public static HttpClient createHttpClient(int timeout) {
return new HttpClient()
.setConnectTimeout(timeout)
.setReadTimeout(timeout * 2)
@@ -251,26 +252,6 @@ public InitialXblSessionBuilder credentials() {
return new InitialXblSessionBuilder(this);
}
- /**
- * Opens a JavaFX WebView window to get an MSA token. The window closes when the user logged in.
- * Optionally accepts a {@link StepJfxWebViewMsaCode.JavaFxWebView} as input when calling {@link AbstractStep#getFromInput(HttpClient, AbstractStep.InitialInput)}.
- *
- * @return The builder
- */
- @SneakyThrows
- public InitialXblSessionBuilder javaFxWebView() {
- if (this.applicationDetails.getRedirectUri() == null) {
- this.applicationDetails = this.applicationDetails.withRedirectUri(this.applicationDetails.getOAuthEnvironment().getNativeClientUrl());
- }
-
- // Don't reference the constructor directly to prevent Spigot from loading JavaFX classes when not needed
- // Spigot's class remapper is crappy and loads classes even when the method isn't ever called
- final Constructor> constructor = StepJfxWebViewMsaCode.class.getConstructor(AbstractStep.ApplicationDetails.class, int.class);
- this.msaCodeStep = (AbstractStep, MsaCodeStep.MsaCode>) constructor.newInstance(this.applicationDetails, this.timeout * 1000);
-
- return new InitialXblSessionBuilder(this);
- }
-
/**
* Generates a URL to open in the browser to get an MSA token. The browser redirects to a localhost URL with the token as a parameter when the user logged in.
* Needs instance of {@link StepLocalWebServer.LocalWebServerCallback} as input when calling {@link AbstractStep#getFromInput(HttpClient, AbstractStep.InitialInput)}.
diff --git a/src/main/java/net/raphimc/minecraftauth/step/msa/StepJfxWebViewMsaCode.java b/src/main/java/net/raphimc/minecraftauth/step/msa/StepJfxWebViewMsaCode.java
deleted file mode 100644
index 13d0df7..0000000
--- a/src/main/java/net/raphimc/minecraftauth/step/msa/StepJfxWebViewMsaCode.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * This file is part of MinecraftAuth - https://github.com/RaphiMC/MinecraftAuth
- * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package net.raphimc.minecraftauth.step.msa;
-
-import javafx.application.Platform;
-import javafx.embed.swing.JFXPanel;
-import javafx.scene.Scene;
-import javafx.scene.web.WebView;
-import lombok.AllArgsConstructor;
-import lombok.EqualsAndHashCode;
-import lombok.SneakyThrows;
-import lombok.Value;
-import net.lenni0451.commons.httpclient.HttpClient;
-import net.lenni0451.commons.httpclient.HttpResponse;
-import net.lenni0451.commons.httpclient.constants.HttpHeaders;
-import net.lenni0451.commons.httpclient.utils.URLWrapper;
-import net.raphimc.minecraftauth.responsehandler.exception.MsaRequestException;
-import net.raphimc.minecraftauth.step.AbstractStep;
-import net.raphimc.minecraftauth.util.logging.ILogger;
-
-import javax.swing.*;
-import java.awt.event.WindowAdapter;
-import java.awt.event.WindowEvent;
-import java.net.URL;
-import java.util.Collections;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-
-public class StepJfxWebViewMsaCode extends MsaCodeStep {
-
- private final int timeout;
-
- public StepJfxWebViewMsaCode(final ApplicationDetails applicationDetails, final int timeout) {
- super(applicationDetails);
-
- this.timeout = timeout;
- }
-
- @Override
- @SneakyThrows
- protected MsaCode execute(final ILogger logger, final HttpClient httpClient, final JavaFxWebView javaFxWebViewCallback) throws Exception {
- logger.info(this, "Opening JavaFX WebView window for MSA login...");
-
- final URL authenticationUrl = new URLWrapper(this.applicationDetails.getOAuthEnvironment().getAuthorizeUrl()).wrapQuery().addQueries(this.applicationDetails.getOAuthParameters()).apply().toURL();
- final CompletableFuture msaCodeFuture = new CompletableFuture<>();
-
- final JFXPanel jfxPanel = new JFXPanel();
- final JFrame window = new JFrame("MinecraftAuth - Microsoft Login");
- window.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
- window.setSize(800, 600);
- window.setLocationRelativeTo(null);
- window.setResizable(false);
- window.setContentPane(jfxPanel);
- window.addWindowListener(new WindowAdapter() {
- @Override
- public void windowClosing(WindowEvent e) {
- if (!msaCodeFuture.isDone()) {
- msaCodeFuture.completeExceptionally(new UserClosedWindowException());
- }
- }
- });
-
- Platform.runLater(() -> {
- final WebView webView = new WebView();
- webView.setContextMenuEnabled(false);
- httpClient.getFirstHeader(HttpHeaders.USER_AGENT).ifPresent(s -> webView.getEngine().setUserAgent(s));
- webView.getEngine().load(authenticationUrl.toString());
- webView.getEngine().locationProperty().addListener((observable, oldValue, newValue) -> {
- try {
- if (newValue.startsWith(this.applicationDetails.getRedirectUri())) {
- final Map parameters = new URLWrapper(newValue).wrapQuery().getQueries();
- if (parameters.containsKey("error") && parameters.containsKey("error_description")) {
- final HttpResponse fakeResponse = new HttpResponse(null, 500, new byte[0], Collections.emptyMap());
- throw new MsaRequestException(fakeResponse, parameters.get("error"), parameters.get("error_description"));
- }
- if (!parameters.containsKey("code")) {
- throw new IllegalStateException("Could not extract MSA Code from response url");
- }
-
- msaCodeFuture.complete(new MsaCode(parameters.get("code")));
- }
- } catch (Throwable e) {
- msaCodeFuture.completeExceptionally(e);
- }
- });
- jfxPanel.setScene(new Scene(webView, window.getWidth(), window.getHeight()));
-
- if (javaFxWebViewCallback == null) {
- window.setVisible(true);
- } else {
- javaFxWebViewCallback.openCallback.accept(window);
- }
- });
-
- try {
- final MsaCode msaCode = msaCodeFuture.get(this.timeout, TimeUnit.MILLISECONDS);
- logger.info(this, "Got MSA Code");
- return msaCode;
- } catch (TimeoutException e) {
- throw new TimeoutException("MSA login timed out");
- } catch (ExecutionException e) {
- if (e.getCause() != null) {
- throw e.getCause();
- } else {
- throw e;
- }
- } finally {
- if (javaFxWebViewCallback == null) {
- window.dispose();
- } else {
- javaFxWebViewCallback.closeCallback.accept(window);
- }
- }
- }
-
- @Value
- @AllArgsConstructor
- @EqualsAndHashCode(callSuper = false)
- public static class JavaFxWebView extends AbstractStep.InitialInput {
-
- Consumer openCallback;
- Consumer closeCallback;
-
- public JavaFxWebView() {
- this.openCallback = window -> window.setVisible(true);
- this.closeCallback = JFrame::dispose;
- }
-
- @Deprecated
- public JavaFxWebView(final BiConsumer openCallback, final Consumer closeCallback) {
- this.openCallback = window -> {
- final WebView webView = (WebView) ((JFXPanel) window.getContentPane()).getScene().getRoot();
- openCallback.accept(window, webView);
- };
- this.closeCallback = closeCallback;
- }
-
- }
-
- public static class UserClosedWindowException extends Exception {
-
- public UserClosedWindowException() {
- super("User closed login window");
- }
-
- }
-
-}
diff --git a/src/main/java/net/raphimc/minecraftauth/util/logging/ConsoleLogger.java b/src/main/java/net/raphimc/minecraftauth/util/logging/NOPLogger.java
similarity index 74%
rename from src/main/java/net/raphimc/minecraftauth/util/logging/ConsoleLogger.java
rename to src/main/java/net/raphimc/minecraftauth/util/logging/NOPLogger.java
index 26ceb0d..f70a7d0 100644
--- a/src/main/java/net/raphimc/minecraftauth/util/logging/ConsoleLogger.java
+++ b/src/main/java/net/raphimc/minecraftauth/util/logging/NOPLogger.java
@@ -17,31 +17,17 @@
*/
package net.raphimc.minecraftauth.util.logging;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Deprecated
-public class ConsoleLogger implements ILogger {
-
- @Deprecated
- public static final Logger LOGGER = LoggerFactory.getLogger("MinecraftAuth");
-
- @Deprecated
+public class NOPLogger implements ILogger {
+ public static final NOPLogger INSTANCE = new NOPLogger();
+ private NOPLogger() {
+ }
@Override
public void info(String message) {
- LOGGER.info(message);
}
-
- @Deprecated
@Override
public void warn(String message) {
- LOGGER.warn(message);
}
-
- @Deprecated
@Override
public void error(String message) {
- LOGGER.error(message);
}
-
}