2026-01-19 14:23:53 +01:00
|
|
|
package org.openautonomousconnection.webserver.utils;
|
2026-01-18 23:06:39 +01:00
|
|
|
|
|
|
|
|
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
|
2026-02-22 17:26:22 +01:00
|
|
|
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
|
|
|
|
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
|
|
|
|
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
2026-01-18 23:06:39 +01:00
|
|
|
import org.openautonomousconnection.webserver.api.WebPageContext;
|
|
|
|
|
|
|
|
|
|
import javax.net.ssl.HttpsURLConnection;
|
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
|
|
import java.io.InputStream;
|
|
|
|
|
import java.net.URL;
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
2026-02-22 17:26:22 +01:00
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
import java.util.Map;
|
2026-01-18 23:06:39 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Simple HTTPS -> OAC proxy helper.
|
|
|
|
|
*
|
2026-02-22 17:26:22 +01:00
|
|
|
* <p>v1.0.1 entry returns {@link WebResourceResponsePacket} and mirrors correlation header fields
|
|
|
|
|
* from {@link WebPageContext#request}.</p>
|
|
|
|
|
*
|
|
|
|
|
* <p>v1.0.0 method is kept for older call sites.</p>
|
2026-01-18 23:06:39 +01:00
|
|
|
*/
|
|
|
|
|
public final class HttpsProxy {
|
|
|
|
|
|
|
|
|
|
private static final int CONNECT_TIMEOUT_MS = 10_000;
|
|
|
|
|
private static final int READ_TIMEOUT_MS = 25_000;
|
|
|
|
|
private static final int MAX_REDIRECTS = 8;
|
|
|
|
|
|
|
|
|
|
private HttpsProxy() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-22 17:26:22 +01:00
|
|
|
* Fetches an HTTPS URL and returns it as a v1.0.1 {@link WebResourceResponsePacket}.
|
|
|
|
|
*
|
|
|
|
|
* @param ctx the current web page context
|
|
|
|
|
* @param url the target HTTPS URL
|
|
|
|
|
* @return proxied response (never null)
|
|
|
|
|
*/
|
|
|
|
|
public static WebResourceResponsePacket proxyGet(WebPageContext ctx, String url) {
|
|
|
|
|
WebPacketHeader in = (ctx != null && ctx.request != null) ? ctx.request.getHeader() : null;
|
|
|
|
|
WebPacketHeader baseHeader = (in == null)
|
|
|
|
|
? new WebPacketHeader(0, 0, 0, 0, WebPacketFlags.RESOURCE, System.currentTimeMillis())
|
|
|
|
|
: new WebPacketHeader(
|
|
|
|
|
in.getRequestId(),
|
|
|
|
|
in.getTabId(),
|
|
|
|
|
in.getPageId(),
|
|
|
|
|
in.getFrameId(),
|
|
|
|
|
in.getFlags() | WebPacketFlags.RESOURCE,
|
|
|
|
|
System.currentTimeMillis()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return proxyGetInternalV101(ctx, url, baseHeader, 0);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
byte[] body = ("Bad Gateway: " + e.getClass().getName() + ": " + e.getMessage())
|
|
|
|
|
.getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
|
|
|
|
|
Map<String, String> headers = new LinkedHashMap<>();
|
|
|
|
|
headers.put("content-length", String.valueOf(body.length));
|
|
|
|
|
|
|
|
|
|
return new WebResourceResponsePacket(
|
|
|
|
|
baseHeader,
|
|
|
|
|
502,
|
|
|
|
|
"text/plain; charset=utf-8",
|
|
|
|
|
headers,
|
|
|
|
|
body,
|
|
|
|
|
null
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static WebResourceResponsePacket proxyGetInternalV101(WebPageContext ctx, String url, WebPacketHeader header, int depth) throws Exception {
|
|
|
|
|
if (depth > MAX_REDIRECTS) {
|
|
|
|
|
byte[] body = "Too many redirects".getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
Map<String, String> headers = new LinkedHashMap<>();
|
|
|
|
|
headers.put("content-length", String.valueOf(body.length));
|
|
|
|
|
return new WebResourceResponsePacket(header, 508, "text/plain; charset=utf-8", headers, body, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
URL target = new URL(url);
|
|
|
|
|
HttpsURLConnection con = (HttpsURLConnection) target.openConnection();
|
|
|
|
|
con.setInstanceFollowRedirects(false);
|
|
|
|
|
con.setRequestMethod("GET");
|
|
|
|
|
con.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
|
|
|
|
con.setReadTimeout(READ_TIMEOUT_MS);
|
|
|
|
|
|
|
|
|
|
// Forward a user-agent if present (optional).
|
|
|
|
|
String ua = null;
|
|
|
|
|
if (ctx != null && ctx.request != null && ctx.request.getHeaders() != null) {
|
|
|
|
|
ua = getHeaderIgnoreCase(ctx.request.getHeaders(), "user-agent");
|
|
|
|
|
}
|
|
|
|
|
con.setRequestProperty("User-Agent", ua != null ? ua : "OAC-HttpsProxy/1.0.1");
|
|
|
|
|
|
|
|
|
|
int code = con.getResponseCode();
|
|
|
|
|
|
|
|
|
|
// Manual redirect handling
|
|
|
|
|
if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
|
|
|
|
|
String location = con.getHeaderField("Location");
|
|
|
|
|
if (location == null || location.isBlank()) {
|
|
|
|
|
con.disconnect();
|
|
|
|
|
byte[] body = ("Bad Gateway: redirect without Location (code=" + code + ")")
|
|
|
|
|
.getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
Map<String, String> headers = new LinkedHashMap<>();
|
|
|
|
|
headers.put("content-length", String.valueOf(body.length));
|
|
|
|
|
return new WebResourceResponsePacket(header, 502, "text/plain; charset=utf-8", headers, body, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
URL resolved = new URL(target, location);
|
|
|
|
|
con.disconnect();
|
|
|
|
|
return proxyGetInternalV101(ctx, resolved.toString(), header, depth + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String contentType = con.getContentType();
|
|
|
|
|
if (contentType == null || contentType.isBlank()) contentType = "application/octet-stream";
|
|
|
|
|
|
|
|
|
|
byte[] body;
|
|
|
|
|
Map<String, String> outHeaders = new LinkedHashMap<>();
|
|
|
|
|
try (InputStream in = (code >= 400 ? con.getErrorStream() : con.getInputStream())) {
|
|
|
|
|
body = readAllBytes(in);
|
|
|
|
|
outHeaders.put("content-length", String.valueOf(body.length));
|
|
|
|
|
|
|
|
|
|
// Pass through a few useful headers (safe subset)
|
|
|
|
|
copyHeaderIfPresent(con, outHeaders, "cache-control");
|
|
|
|
|
copyHeaderIfPresent(con, outHeaders, "etag");
|
|
|
|
|
copyHeaderIfPresent(con, outHeaders, "last-modified");
|
|
|
|
|
} finally {
|
|
|
|
|
con.disconnect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new WebResourceResponsePacket(
|
|
|
|
|
header,
|
|
|
|
|
code,
|
|
|
|
|
contentType,
|
|
|
|
|
outHeaders,
|
|
|
|
|
body,
|
|
|
|
|
null
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void copyHeaderIfPresent(HttpsURLConnection con, Map<String, String> out, String name) {
|
|
|
|
|
String v = con.getHeaderField(name);
|
|
|
|
|
if (v != null && !v.isBlank()) out.put(name, v);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String getHeaderIgnoreCase(Map<String, String> headers, String key) {
|
|
|
|
|
if (headers == null || headers.isEmpty() || key == null) return null;
|
|
|
|
|
String needle = key.trim().toLowerCase(java.util.Locale.ROOT);
|
|
|
|
|
for (Map.Entry<String, String> e : headers.entrySet()) {
|
|
|
|
|
if (e.getKey() == null) continue;
|
|
|
|
|
if (e.getKey().trim().toLowerCase(java.util.Locale.ROOT).equals(needle)) return e.getValue();
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static byte[] readAllBytes(InputStream in) throws Exception {
|
|
|
|
|
if (in == null) return new byte[0];
|
|
|
|
|
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(1024, in.available()));
|
|
|
|
|
byte[] buf = new byte[32 * 1024];
|
|
|
|
|
int r;
|
|
|
|
|
while ((r = in.read(buf)) != -1) {
|
|
|
|
|
out.write(buf, 0, r);
|
|
|
|
|
}
|
|
|
|
|
return out.toByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetches an HTTPS URL and returns it as a v1.0.0 {@link WebResponsePacket}.
|
2026-01-18 23:06:39 +01:00
|
|
|
*
|
|
|
|
|
* @param ctx the current web page context (for optional user-agent forwarding)
|
|
|
|
|
* @param url the target HTTPS URL
|
|
|
|
|
* @return proxied response
|
2026-02-22 17:26:22 +01:00
|
|
|
* @deprecated v1.0.1 code should call {@link #proxyGet(WebPageContext, String)} returning {@link WebResourceResponsePacket}.
|
2026-01-18 23:06:39 +01:00
|
|
|
*/
|
2026-02-22 17:26:22 +01:00
|
|
|
@Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1")
|
|
|
|
|
public static WebResponsePacket proxyGetV100B(WebPageContext ctx, String url) {
|
2026-01-18 23:06:39 +01:00
|
|
|
try {
|
2026-02-22 17:26:22 +01:00
|
|
|
return proxyGetInternalV100B(ctx, url, 0);
|
2026-01-18 23:06:39 +01:00
|
|
|
} catch (Exception e) {
|
|
|
|
|
return new WebResponsePacket(
|
|
|
|
|
502,
|
|
|
|
|
"text/plain; charset=utf-8",
|
2026-02-08 21:40:23 +01:00
|
|
|
HeaderMaps.mutable(),
|
2026-01-18 23:06:39 +01:00
|
|
|
("Bad Gateway: " + e.getClass().getName() + ": " + e.getMessage()).getBytes(StandardCharsets.UTF_8)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 17:26:22 +01:00
|
|
|
private static WebResponsePacket proxyGetInternalV100B(WebPageContext ctx, String url, int depth) throws Exception {
|
2026-01-18 23:06:39 +01:00
|
|
|
if (depth > MAX_REDIRECTS) {
|
|
|
|
|
return new WebResponsePacket(
|
|
|
|
|
508,
|
|
|
|
|
"text/plain; charset=utf-8",
|
2026-02-08 21:40:23 +01:00
|
|
|
HeaderMaps.mutable(),
|
2026-01-18 23:06:39 +01:00
|
|
|
"Too many redirects".getBytes(StandardCharsets.UTF_8)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
URL target = new URL(url);
|
|
|
|
|
HttpsURLConnection con = (HttpsURLConnection) target.openConnection();
|
|
|
|
|
con.setInstanceFollowRedirects(false);
|
|
|
|
|
con.setRequestMethod("GET");
|
|
|
|
|
con.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
|
|
|
|
con.setReadTimeout(READ_TIMEOUT_MS);
|
|
|
|
|
|
|
|
|
|
String ua = null;
|
|
|
|
|
if (ctx != null && ctx.request != null && ctx.request.getHeaders() != null) {
|
2026-02-22 17:26:22 +01:00
|
|
|
ua = getHeaderIgnoreCase(ctx.request.getHeaders(), "user-agent");
|
2026-01-18 23:06:39 +01:00
|
|
|
}
|
|
|
|
|
con.setRequestProperty("User-Agent", ua != null ? ua : "OAC-HttpsProxy/1.0");
|
|
|
|
|
|
|
|
|
|
int code = con.getResponseCode();
|
|
|
|
|
|
|
|
|
|
if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
|
|
|
|
|
String location = con.getHeaderField("Location");
|
|
|
|
|
if (location == null || location.isBlank()) {
|
|
|
|
|
return new WebResponsePacket(
|
|
|
|
|
502,
|
|
|
|
|
"text/plain; charset=utf-8",
|
2026-02-08 21:40:23 +01:00
|
|
|
HeaderMaps.mutable(),
|
2026-01-18 23:06:39 +01:00
|
|
|
("Bad Gateway: redirect without Location (code=" + code + ")").getBytes(StandardCharsets.UTF_8)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
URL resolved = new URL(target, location);
|
|
|
|
|
con.disconnect();
|
2026-02-22 17:26:22 +01:00
|
|
|
return proxyGetInternalV100B(ctx, resolved.toString(), depth + 1);
|
2026-01-18 23:06:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String contentType = con.getContentType();
|
2026-02-22 17:26:22 +01:00
|
|
|
if (contentType == null || contentType.isBlank()) contentType = "application/octet-stream";
|
2026-01-18 23:06:39 +01:00
|
|
|
|
|
|
|
|
byte[] body;
|
|
|
|
|
try (InputStream in = (code >= 400 ? con.getErrorStream() : con.getInputStream())) {
|
|
|
|
|
body = readAllBytes(in);
|
|
|
|
|
} finally {
|
|
|
|
|
con.disconnect();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 21:40:23 +01:00
|
|
|
return new WebResponsePacket(code, contentType, HeaderMaps.mutable(), body);
|
2026-01-18 23:06:39 +01:00
|
|
|
}
|
2026-02-22 17:26:22 +01:00
|
|
|
}
|