|  | 
|  | 1 | +//usr/bin/env jbang "$0" "$@" ; exit $? | 
|  | 2 | +//DEPS info.picocli:picocli:4.2.0 | 
|  | 3 | +//DEPS org.jboss.resteasy:resteasy-client:4.4.1.Final | 
|  | 4 | +//DEPS com.fasterxml.jackson.core:jackson-databind:2.2.3 | 
|  | 5 | + | 
|  | 6 | +import com.fasterxml.jackson.databind.JsonNode; | 
|  | 7 | +import com.fasterxml.jackson.databind.ObjectMapper; | 
|  | 8 | +import com.fasterxml.jackson.databind.node.ObjectNode; | 
|  | 9 | +import org.apache.http.HttpRequest; | 
|  | 10 | +import org.apache.http.client.methods.CloseableHttpResponse; | 
|  | 11 | +import org.apache.http.client.methods.HttpPost; | 
|  | 12 | +import org.apache.http.entity.StringEntity; | 
|  | 13 | +import org.apache.http.impl.client.CloseableHttpClient; | 
|  | 14 | +import org.apache.http.impl.client.HttpClientBuilder; | 
|  | 15 | +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; | 
|  | 16 | +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; | 
|  | 17 | +import org.jboss.resteasy.spi.ResteasyConfiguration; | 
|  | 18 | +import picocli.CommandLine; | 
|  | 19 | +import picocli.CommandLine.Command; | 
|  | 20 | +import picocli.CommandLine.Parameters; | 
|  | 21 | + | 
|  | 22 | +import javax.ws.rs.HttpMethod; | 
|  | 23 | +import javax.ws.rs.client.*; | 
|  | 24 | +import javax.ws.rs.core.Configuration; | 
|  | 25 | +import javax.ws.rs.core.Request; | 
|  | 26 | +import javax.ws.rs.core.UriBuilder; | 
|  | 27 | +import javax.ws.rs.sse.InboundSseEvent; | 
|  | 28 | +import javax.ws.rs.sse.SseEventSource; | 
|  | 29 | +import java.io.IOException; | 
|  | 30 | +import java.net.*; | 
|  | 31 | +import java.util.concurrent.Callable; | 
|  | 32 | +import java.util.concurrent.TimeUnit; | 
|  | 33 | +import java.util.logging.Logger; | 
|  | 34 | + | 
|  | 35 | +import static picocli.CommandLine.*; | 
|  | 36 | + | 
|  | 37 | +/** | 
|  | 38 | + * A pure java implementation of smee-client. | 
|  | 39 | + */ | 
|  | 40 | +@Command(name = "smee", mixinStandardHelpOptions = true, version = "smee 0.1", | 
|  | 41 | +        description = "smee made with jbang", showDefaultValues = true) | 
|  | 42 | +class smee implements Callable<Integer> { | 
|  | 43 | + | 
|  | 44 | +    private static final Logger log; | 
|  | 45 | + | 
|  | 46 | +    static { | 
|  | 47 | +        System.setProperty("java.util.logging.SimpleFormatter.format", | 
|  | 48 | +                "%5$s %n"); | 
|  | 49 | +        log = Logger.getLogger(smee.class.getName()); | 
|  | 50 | +    } | 
|  | 51 | +    @Option(names = {"-u","--url"}, description = "URL of the webhook proxy service\n  Default: Fetch new from https://smee.io/new") | 
|  | 52 | +    private String url; | 
|  | 53 | + | 
|  | 54 | +    @Option(names={"-t", "--target"}, | 
|  | 55 | +            description = "Full URL (including protocol and path) of the target service the events will forwarded to\n  Default: http://127.0.0.1:PORT/PATH") | 
|  | 56 | +    private String target; | 
|  | 57 | + | 
|  | 58 | +    @Option(names={"-p","--port"}, defaultValue = "${PORT:-3000}", description = "Local HTTP server port") | 
|  | 59 | +    int port; | 
|  | 60 | + | 
|  | 61 | +    @Option(names={"-P", "--path"}, defaultValue = "/", description = "URL path to post proxied requests to") | 
|  | 62 | +    String path; | 
|  | 63 | + | 
|  | 64 | +    public static void main(String... args) { | 
|  | 65 | +        int exitCode = new CommandLine(new smee()).execute(args); | 
|  | 66 | +        System.exit(exitCode); | 
|  | 67 | +    } | 
|  | 68 | + | 
|  | 69 | +    @Override | 
|  | 70 | +    public Integer call() throws Exception { // your business logic goes here... | 
|  | 71 | + | 
|  | 72 | +        if(target==null) { | 
|  | 73 | +            target = String.format("http://127.0.0.1:%s%s", port, path); | 
|  | 74 | +        } | 
|  | 75 | + | 
|  | 76 | +        if(url==null) { | 
|  | 77 | +            url = createChannel(); | 
|  | 78 | +        } | 
|  | 79 | + | 
|  | 80 | +        URI uri = new URI(url); | 
|  | 81 | + | 
|  | 82 | +        Client client = ClientBuilder.newBuilder() | 
|  | 83 | +                                     .build(); | 
|  | 84 | +                                     //.register(HTTPLoggingFilter.class); | 
|  | 85 | + | 
|  | 86 | +        final var events = SseEventSource.target(client.target(uri)) | 
|  | 87 | +                                          // Reconnect immediately | 
|  | 88 | +                                         .reconnectingEvery(0, TimeUnit.MILLISECONDS).build(); | 
|  | 89 | + | 
|  | 90 | +        events.register(this::onMessage, this::onError); | 
|  | 91 | + | 
|  | 92 | +        log.info("Forwarding " + url + " to " + target); | 
|  | 93 | +        events.open(); | 
|  | 94 | + | 
|  | 95 | +        if(events.isOpen()) { | 
|  | 96 | +            log.info("Connected " + url); | 
|  | 97 | +        } | 
|  | 98 | + | 
|  | 99 | +        while(true) { | 
|  | 100 | +            Thread.sleep(50000); | 
|  | 101 | +        } | 
|  | 102 | + | 
|  | 103 | +        //return 0; | 
|  | 104 | +    } | 
|  | 105 | + | 
|  | 106 | +    private void onError(Throwable error) { | 
|  | 107 | +        log.severe(error.getMessage()); | 
|  | 108 | +    } | 
|  | 109 | + | 
|  | 110 | +    private void onMessage(InboundSseEvent event) { | 
|  | 111 | +        if("ping".equals(event.getName()) || "ready".equals(event.getName())) { | 
|  | 112 | +            return; | 
|  | 113 | +        } | 
|  | 114 | +        ObjectMapper mapper = new ObjectMapper(); | 
|  | 115 | +        try { | 
|  | 116 | +            ObjectNode data = (ObjectNode) mapper.readTree(event.readData()); | 
|  | 117 | + | 
|  | 118 | +            var urib = UriBuilder.fromUri(this.target); | 
|  | 119 | + | 
|  | 120 | +            if(data.has("query")) { | 
|  | 121 | +                urib.replaceQuery(URLEncoder.encode(data.get("query").asText(), "UTF-8")); | 
|  | 122 | +                data.remove("query"); | 
|  | 123 | +            } | 
|  | 124 | + | 
|  | 125 | +            try(CloseableHttpClient client = HttpClientBuilder.create().disableCookieManagement().build()) { | 
|  | 126 | + | 
|  | 127 | +                var request = new HttpPost(urib.build()); | 
|  | 128 | + | 
|  | 129 | +                if(data.has("body")) { | 
|  | 130 | +                    request.setEntity(new StringEntity(data.get("body").asText())); | 
|  | 131 | +                    data.remove("body"); | 
|  | 132 | +                } | 
|  | 133 | + | 
|  | 134 | +                data.fieldNames().forEachRemaining(s -> | 
|  | 135 | +                { | 
|  | 136 | +                    if(!s.equalsIgnoreCase("content-length")) { | 
|  | 137 | +                    request.setHeader(s, data.get(s).asText()); | 
|  | 138 | +                }}); | 
|  | 139 | + | 
|  | 140 | + | 
|  | 141 | +                CloseableHttpResponse response = client.execute(request); | 
|  | 142 | + | 
|  | 143 | +                if(response.getStatusLine().getStatusCode()!=200) { | 
|  | 144 | +                    log.severe(response.getStatusLine().toString()); | 
|  | 145 | +                } else { | 
|  | 146 | +                    log.info(request.getMethod() + " " + request.getURI() + " - " + response.getStatusLine().getStatusCode()); | 
|  | 147 | +                } | 
|  | 148 | +            } | 
|  | 149 | + | 
|  | 150 | +        } catch (IOException e) { | 
|  | 151 | +            log.warning("Could not parse event data: " + e.getMessage()); | 
|  | 152 | +            e.printStackTrace(); | 
|  | 153 | +        } catch (RuntimeException re) { | 
|  | 154 | +            log.warning("Could not parse event data: " + re.getMessage()); | 
|  | 155 | +            re.printStackTrace(); | 
|  | 156 | +        } | 
|  | 157 | + | 
|  | 158 | +    } | 
|  | 159 | + | 
|  | 160 | +    private String createChannel() throws IOException { | 
|  | 161 | +        HttpURLConnection con = (HttpURLConnection)(new URL( "https://smee.io/new" ).openConnection()); | 
|  | 162 | +        con.setInstanceFollowRedirects( false ); | 
|  | 163 | +        con.connect(); | 
|  | 164 | +        return con.getHeaderField( "Location" ); | 
|  | 165 | +    } | 
|  | 166 | + | 
|  | 167 | +    static public class HTTPLoggingFilter implements ClientRequestFilter, ClientResponseFilter { | 
|  | 168 | + | 
|  | 169 | +        private static final Logger logger = Logger.getLogger(HTTPLoggingFilter.class.getName()); | 
|  | 170 | + | 
|  | 171 | +        @Override | 
|  | 172 | +        public void filter(ClientRequestContext requestContext) throws IOException { | 
|  | 173 | +            logger.info("request:" + requestContext.getUri().toString()); | 
|  | 174 | +            logger.info("request:" + requestContext.getHeaders().toString()); | 
|  | 175 | + | 
|  | 176 | +        } | 
|  | 177 | + | 
|  | 178 | +        @Override | 
|  | 179 | +        public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { | 
|  | 180 | +            // | 
|  | 181 | +            // logger.info(responseContext.getUri().toString()); | 
|  | 182 | +            logger.info("response: " + responseContext.getHeaders().toString()); | 
|  | 183 | +        } | 
|  | 184 | +    } | 
|  | 185 | +} | 
0 commit comments