package org.openautonomousconnection.webserver.utils; import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; 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; 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; import java.util.LinkedHashMap; import java.util.Map; /** * Simple HTTPS -> OAC proxy helper. * *

v1.0.1 entry returns {@link WebResourceResponsePacket} and mirrors correlation header fields * from {@link WebPageContext#request}.

* *

v1.0.0 method is kept for older call sites.

*/ 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() { } /** * 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 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 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 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 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 out, String name) { String v = con.getHeaderField(name); if (v != null && !v.isBlank()) out.put(name, v); } private static String getHeaderIgnoreCase(Map headers, String key) { if (headers == null || headers.isEmpty() || key == null) return null; String needle = key.trim().toLowerCase(java.util.Locale.ROOT); for (Map.Entry 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}. * * @param ctx the current web page context (for optional user-agent forwarding) * @param url the target HTTPS URL * @return proxied response * @deprecated v1.0.1 code should call {@link #proxyGet(WebPageContext, String)} returning {@link WebResourceResponsePacket}. */ @Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1") public static WebResponsePacket proxyGetV100B(WebPageContext ctx, String url) { try { return proxyGetInternalV100B(ctx, url, 0); } catch (Exception e) { return new WebResponsePacket( 502, "text/plain; charset=utf-8", HeaderMaps.mutable(), ("Bad Gateway: " + e.getClass().getName() + ": " + e.getMessage()).getBytes(StandardCharsets.UTF_8) ); } } private static WebResponsePacket proxyGetInternalV100B(WebPageContext ctx, String url, int depth) throws Exception { if (depth > MAX_REDIRECTS) { return new WebResponsePacket( 508, "text/plain; charset=utf-8", HeaderMaps.mutable(), "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) { ua = getHeaderIgnoreCase(ctx.request.getHeaders(), "user-agent"); } 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", HeaderMaps.mutable(), ("Bad Gateway: redirect without Location (code=" + code + ")").getBytes(StandardCharsets.UTF_8) ); } URL resolved = new URL(target, location); con.disconnect(); return proxyGetInternalV100B(ctx, resolved.toString(), depth + 1); } String contentType = con.getContentType(); if (contentType == null || contentType.isBlank()) contentType = "application/octet-stream"; byte[] body; try (InputStream in = (code >= 400 ? con.getErrorStream() : con.getInputStream())) { body = readAllBytes(in); } finally { con.disconnect(); } return new WebResponsePacket(code, contentType, HeaderMaps.mutable(), body); } }