commit 54cbf4cccfa1b2982dfcebf88bcdd7676d8819a1 from: Omar Polo date: Tue Oct 12 21:07:06 2021 UTC add a simple gemini client commit - b11fcb801f7dbbcd9d175dabdfb37f2152849600 commit + 54cbf4cccfa1b2982dfcebf88bcdd7676d8819a1 blob - /dev/null blob + 1ecc5d408ab20f8d8457d1ebdc28afe9dd08a032 (mode 644) --- /dev/null +++ java/com/omarpolo/gemini/Gemini.java @@ -0,0 +1,144 @@ +package com.omarpolo.gemini; + +import javax.net.ssl.*; +import java.io.*; +import java.net.Socket; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.NoSuchElementException; +import java.util.Scanner; + +public class Gemini { + public static class DummyManager extends X509ExtendedTrustManager { + + @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; + } + } + + public static class MalformedResponse extends Exception { + public MalformedResponse() {} + public MalformedResponse(String msg) { + super(msg); + } + } + + public static class Response implements AutoCloseable { + private final BufferedReader in; + private final PrintWriter out; + private final SSLSocket sock; + + private final int code; + private final String meta; + + private Response(PrintWriter out, SSLSocket sock) throws IOException, MalformedResponse { + var inStream = sock.getInputStream(); + this.in = new BufferedReader(new InputStreamReader(inStream)); + this.out = out; + this.sock = sock; + + var reply = in.readLine(); + if (reply.length() > 1027) { + throw new MalformedResponse("reply header too long"); + } + + var s = new Scanner(new StringReader(reply)); + try { + code = s.nextInt(); + s.skip(" "); + meta = s.nextLine(); + } catch (NoSuchElementException e) { + throw new MalformedResponse(); + } + } + + public int getCode() { + return code; + } + + public String getMeta() { + return meta; + } + + public BufferedReader body() { + return in; + } + + public void close() throws IOException { + in.close(); + out.close(); + sock.close(); + } + } + + public static SSLSocket connect(String host, int port) throws IOException { + try { + var params = new SSLParameters(); + params.setServerNames(Collections.singletonList(new SNIHostName(host))); + + var ctx = SSLContext.getInstance("TLS"); + ctx.init(null, new DummyManager[]{new DummyManager()}, new SecureRandom()); + var factory = (SSLSocketFactory) ctx.getSocketFactory(); + + var socket = (SSLSocket) factory.createSocket(host, port); + socket.setSSLParameters(params); + socket.startHandshake(); + return socket; + } + catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException("Unexpected failure", e); + } + } + + public static Response get(URL url) throws IOException, MalformedResponse { + int port = url.getPort(); + if (port == -1) { + port = 1965; + } + + String req = url.toString() + "\r\n"; + return get(url.getHost(), port, req); + } + + public static Response get(String host, int port, String req) throws IOException, MalformedResponse { + var sock = connect(host, port); + + var outStream = sock.getOutputStream(); + var out = new PrintWriter( + new BufferedWriter(new OutputStreamWriter(outStream))); + + out.print(req); + out.flush(); + + return new Response(out, sock); + } +}