001package gudusoft.gsqlparser.demos.sqlguard;
002
003import com.sun.net.httpserver.HttpExchange;
004import com.sun.net.httpserver.HttpHandler;
005import com.sun.net.httpserver.HttpServer;
006
007import java.io.ByteArrayOutputStream;
008import java.io.IOException;
009import java.io.InputStream;
010import java.io.OutputStream;
011import java.net.HttpURLConnection;
012import java.net.InetAddress;
013import java.net.InetSocketAddress;
014import java.net.URL;
015import java.nio.charset.StandardCharsets;
016import java.util.concurrent.ArrayBlockingQueue;
017import java.util.concurrent.ThreadPoolExecutor;
018import java.util.concurrent.TimeUnit;
019
020public class SqlGuardHttpServer {
021    public static final int MAX_BODY_BYTES = 1572864;
022    private final String host;
023    private final int port;
024    private final SqlGuardService service;
025    private HttpServer server;
026
027    public SqlGuardHttpServer(String host, int port, SqlGuardService service) {
028        this.host = host == null ? "127.0.0.1" : host;
029        this.port = port;
030        this.service = service;
031    }
032
033    public void start() throws IOException {
034        InetAddress bindAddress = InetAddress.getByName(host);
035        if (!bindAddress.isLoopbackAddress()) {
036            throw new IOException("SQL Guard worker refuses non-loopback bind address: " + host);
037        }
038        server = HttpServer.create(new InetSocketAddress(bindAddress, port), 16);
039        server.createContext("/healthz", new Health());
040        server.createContext("/check", new Check());
041        server.setExecutor(new ThreadPoolExecutor(
042                4, 4, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(32)));
043        server.start();
044    }
045
046    public void stop(int delay) {
047        if (server != null) server.stop(delay);
048    }
049
050    public InetSocketAddress address() {
051        return server.getAddress();
052    }
053
054    class Health implements HttpHandler {
055        public void handle(HttpExchange x) throws IOException {
056            if (!"GET".equals(x.getRequestMethod())) {
057                send(x, 405, "{\"ok\":false}");
058                return;
059            }
060            send(x, 200, "{\"ok\":true,\"service\":\"sql-guard-worker\"}");
061        }
062    }
063
064    class Check implements HttpHandler {
065        public void handle(HttpExchange x) throws IOException {
066            if (!"POST".equals(x.getRequestMethod())) {
067                send(x, 405, "{\"ok\":false}");
068                return;
069            }
070            String body;
071            try {
072                body = readLimited(x.getRequestBody(), MAX_BODY_BYTES);
073            } catch (IOException e) {
074                send(x, 413, SqlGuardResponse.error(null, "REQUEST_TOO_LARGE", "Request body too large.").toJson());
075                return;
076            }
077            SqlGuardResponse r;
078            try {
079                r = service.check(SqlGuardRequest.fromJson(body));
080            } catch (Exception e) {
081                r = SqlGuardResponse.error(null, "INVALID_REQUEST", "Invalid JSON request.");
082            }
083            send(x, r.ok ? 200 : 400, r.toJson());
084        }
085    }
086
087    /**
088     * Reads at most {@code max} bytes from {@code in}, refusing input that exceeds the limit before it is buffered.
089     *
090     * <p>PUBLIC ON PURPOSE: every shipped jar is ProGuard-obfuscated under a
091     * compatibility-first policy that preserves public and protected members
092     * and renames everything else. A package-private method is therefore
093     * unreachable from the released jar, which is what the test suite runs
094     * against in the nightly obfuscated-jar gate. Keeping this public is what
095     * lets that gate execute the real shipped bytes.</p>
096     *
097     * @since 4.2.6
098     */
099    public static String readLimited(InputStream in, int max) throws IOException {
100        ByteArrayOutputStream out = new ByteArrayOutputStream();
101        byte[] buf = new byte[8192];
102        int total = 0;
103        int n;
104        while ((n = in.read(buf)) != -1) {
105            if (total + n > max) {
106                throw new IOException("too large");
107            }
108            total += n;
109            out.write(buf, 0, n);
110        }
111        return new String(out.toByteArray(), StandardCharsets.UTF_8);
112    }
113
114    static void send(HttpExchange x, int status, String body) throws IOException {
115        byte[] b = body.getBytes(StandardCharsets.UTF_8);
116        x.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
117        x.sendResponseHeaders(status, b.length);
118        OutputStream os = x.getResponseBody();
119        try {
120            os.write(b);
121        } finally {
122            os.close();
123        }
124    }
125
126    public static String httpGet(String url) throws IOException {
127        HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection();
128        c.setRequestMethod("GET");
129        return readAll(c);
130    }
131
132    public static String httpPost(String url, String body) throws IOException {
133        HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection();
134        c.setRequestMethod("POST");
135        c.setDoOutput(true);
136        c.setRequestProperty("Content-Type", "application/json");
137        OutputStream os = c.getOutputStream();
138        try {
139            os.write(body.getBytes(StandardCharsets.UTF_8));
140        } finally {
141            os.close();
142        }
143        return readAll(c);
144    }
145
146    private static String readAll(HttpURLConnection c) throws IOException {
147        InputStream in = c.getResponseCode() >= 400 ? c.getErrorStream() : c.getInputStream();
148        if (in == null) {
149            return "";
150        }
151        try {
152            ByteArrayOutputStream out = new ByteArrayOutputStream();
153            byte[] buf = new byte[4096];
154            int n;
155            while ((n = in.read(buf)) != -1) out.write(buf, 0, n);
156            return new String(out.toByteArray(), StandardCharsets.UTF_8);
157        } finally {
158            in.close();
159        }
160    }
161}