// // Copyright 2018 SenX S.A.S. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // package io.warp10.continuum.egress; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.UUID; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import io.warp10.ThrowableUtils; import io.warp10.WarpConfig; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.server.WebSocketHandler; import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; import org.eclipse.jetty.websocket.servlet.WebSocketCreator; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.warp10.Revision; import io.warp10.WarpURLEncoder; import io.warp10.continuum.BootstrapManager; import io.warp10.continuum.Configuration; import io.warp10.continuum.LogUtil; import io.warp10.continuum.store.Constants; import io.warp10.continuum.store.DirectoryClient; import io.warp10.continuum.store.StoreClient; import io.warp10.continuum.thrift.data.LoggingEvent; import io.warp10.crypto.KeyStore; import io.warp10.script.MemoryWarpScriptStack; import io.warp10.script.WarpScriptLib; import io.warp10.script.WarpScriptStack; import io.warp10.script.WarpScriptStack.StackContext; import io.warp10.script.WarpScriptStackFunction; import io.warp10.script.WarpScriptStackRegistry; import io.warp10.script.WarpScriptStopException; /** * */ public class EgressInteractiveHandler extends WebSocketHandler.Simple implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(EgressInteractiveHandler.class); private static final Logger EVENTLOG = LoggerFactory.getLogger("warpscript.events"); private final KeyStore keyStore; private final StoreClient storeClient; private final DirectoryClient directoryClient; private final BootstrapManager bootstrapManager; private final ServerSocket serverSocket; private final AtomicInteger connections = new AtomicInteger(0); private int capacity = 1; @WebSocket public static class InteractiveWebSocket { private EgressInteractiveHandler handler = null; private InteractiveProcessor processor = null; @OnWebSocketConnect public void onWebSocketConnect(Session session) { if (this.handler.capacity <= this.handler.connections.get()) { try { session.getRemote().sendString("// Maximum server capacity is reached (" + this.handler.capacity + ")."); session.disconnect(); } catch (IOException ioe) { throw new RuntimeException(ioe); } } this.handler.connections.incrementAndGet(); this.processor.init(session); } @OnWebSocketMessage public void onWebSocketMessage(Session session, String message) throws Exception { message = message + "\n"; this.processor.pipe(message.getBytes(StandardCharsets.UTF_8)); } @OnWebSocketClose public void onWebSocketClose(Session session, int statusCode, String reason) { try { this.processor.getPipe().close(); } catch (IOException ioe) { } } @OnWebSocketError public void onWebSocketError(Session session, Throwable t) {} public void setProcessor(InteractiveProcessor processor) { this.processor = processor; } public void setHandler(EgressInteractiveHandler handler) { this.handler = handler; } } public EgressInteractiveHandler(KeyStore keyStore, Properties properties, DirectoryClient directoryClient, StoreClient storeClient) throws IOException { super(InteractiveWebSocket.class); this.keyStore = keyStore; this.storeClient = storeClient; this.directoryClient = directoryClient; // // Check if we have a 'bootstrap' property // if (properties.containsKey(Configuration.CONFIG_WARPSCRIPT_INTERACTIVE_BOOTSTRAP_PATH)) { final String path = properties.getProperty(Configuration.CONFIG_WARPSCRIPT_INTERACTIVE_BOOTSTRAP_PATH); long period = properties.containsKey(Configuration.CONFIG_WARPSCRIPT_INTERACTIVE_BOOTSTRAP_PERIOD) ? Long.parseLong(properties.getProperty(Configuration.CONFIG_WARPSCRIPT_BOOTSTRAP_PERIOD)) : 0L; this.bootstrapManager = new BootstrapManager(path, period); } else { this.bootstrapManager = new BootstrapManager(); } if (properties.containsKey(Configuration.CONFIG_WARPSCRIPT_INTERACTIVE_CAPACITY)) { capacity = Integer.parseInt(properties.getProperty(Configuration.CONFIG_WARPSCRIPT_INTERACTIVE_CAPACITY)); } // // Listen for incoming connections // if (properties.containsKey(Configuration.CONFIG_WARPSCRIPT_INTERACTIVE_TCP_PORT)) { this.serverSocket = new ServerSocket(Integer.parseInt(properties.getProperty(Configuration.CONFIG_WARPSCRIPT_INTERACTIVE_TCP_PORT))); Thread t = new Thread(this); t.setDaemon(true); t.setName("[Interactive TCP Handler]"); t.start(); } else { this.serverSocket = null; } } @Override public void run() { while (true) { try { Socket connectionSocket = this.serverSocket.accept(); if (this.capacity <= this.connections.get()) { PrintWriter pw = new PrintWriter(new OutputStreamWriter(connectionSocket.getOutputStream())); pw.println("// Maximum server capacity is reached (" + this.capacity + ")."); pw.flush(); LOG.error("Maximum server capacity is reached (" + this.capacity + ")."); connectionSocket.close(); continue; } this.connections.incrementAndGet(); InteractiveProcessor processor = new InteractiveProcessor(this, connectionSocket, null, connectionSocket.getInputStream(), connectionSocket.getOutputStream()); } catch (IOException ioe) { } } } @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (Constants.API_ENDPOINT_INTERACTIVE.equals(target)) { baseRequest.setHandled(true); super.handle(target, baseRequest, request, response); } } @Override public void configure(final WebSocketServletFactory factory) { final EgressInteractiveHandler self = this; final WebSocketCreator oldcreator = factory.getCreator(); WebSocketCreator creator = new WebSocketCreator() { @Override public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) { InteractiveWebSocket ws = (InteractiveWebSocket) oldcreator.createWebSocket(req, resp); ws.setHandler(self); try { ws.setProcessor(new InteractiveProcessor(self, null, null, null, null)); } catch (IOException ioe) { throw new RuntimeException(ioe); } return ws; } }; factory.setCreator(creator); super.configure(factory); } public static class InteractiveProcessor extends Thread { private final EgressInteractiveHandler rel; private final Socket socket; private final InputStream in; private final OutputStream out; private Session session; private final PrintWriter pw; private PipedOutputStream pipedOut = null; private MemoryWarpScriptStack stack; private final LinkedBlockingQueue<byte[]> pipeQueue = new LinkedBlockingQueue<byte[]>(2); public InteractiveProcessor(EgressInteractiveHandler rel, Socket socket, Session session, InputStream in, OutputStream out) throws IOException { this.rel = rel; this.socket = socket; if (null != in && null != out) { this.in = in; this.out = out; } else { this.pipedOut = new PipedOutputStream(); this.in = new PipedInputStream(this.pipedOut); this.out = null; // Start the queue runner thread Thread queueRunner = new Thread() { @Override public void run() { while(true) { try { byte[] data = pipeQueue.take(); pipedOut.write(data); pipedOut.flush(); } catch (Exception e) { } } } }; queueRunner.setDaemon(true); queueRunner.start(); } this.session = session; if (null != this.out) { this.pw = new PrintWriter(this.out); } else { this.pw = null; } // Delay start until we have a session when using WebSocket if (null != socket) { this.start(); } } public void init(Session session) { this.session = session; this.start(); } /** * Offer data to the pipe, this will be put * into a queue and dequeues by a stable Thread * so the PipedInputStream does not consider that * the writing Thread died. */ public void pipe(byte[] data) throws InterruptedException { this.pipeQueue.put(data); } public PipedOutputStream getPipe() { return this.pipedOut; } public String getBanner() { StringBuilder sb = new StringBuilder(); sb.append("//"); sb.append("\n"); sb.append("//"); sb.append("\n"); sb.append("// ___ __ ____________"); sb.append("\n"); sb.append("// __ | / /_____ _______________ __< /_ __ \\"); sb.append("\n"); sb.append("// __ | /| / /_ __ `/_ ___/__ __ \\ __ /_ / / /"); sb.append("\n"); sb.append("// __ |/ |/ / / /_/ /_ / __ /_/ / _ / / /_/ /"); sb.append("\n"); sb.append("// ____/|__/ \\__,_/ /_/ _ .___/ /_/ \\____/"); sb.append("\n"); sb.append("// /_/"); sb.append("\n"); sb.append("//"); sb.append("\n"); sb.append("// Revision "); sb.append(Revision.REVISION); sb.append("// "); sb.append("\n"); return sb.toString(); } public String getPrompt() { StringBuilder sb = new StringBuilder(); sb.append("WS"); if (stack.isInSecureScript()) { sb.append("S"); } if (stack.isInComment()) { sb.append("#"); } if (stack.isInMultiline()) { sb.append("'"); } if (stack.getMacroDepth() > 0) { sb.append("%"); sb.append(stack.getMacroDepth()); } if (stack.depth() > 0) { sb.append("<"); sb.append(stack.depth()); } sb.append("> "); return sb.toString(); } private static class PrintWriterWrapper extends PrintWriter { private final PrintWriter writer; private final Session session; public PrintWriterWrapper(PrintWriter writer, Session session) { super(System.out); this.writer = writer; this.session = session; } @Override public void print(String s) { if (null != this.writer) { this.writer.print(s); } else if (null != this.session) { this.session.getRemote().sendStringByFuture(s); } } @Override public void println(String s) { print(s + "\n"); } @Override public void print(Object obj) { print(obj.toString()); } @Override public void println(Object x) { println(x.toString()); } @Override public void print(int i) { print(Integer.toString(i)); } @Override public void println(int i) { println(Integer.toString(i)); } @Override public void flush() { if (null != this.writer) { this.writer.flush(); } } } @Override public void run() { try { BufferedReader in = new BufferedReader(new InputStreamReader(this.in)); PrintWriter out = new PrintWriterWrapper(null != this.out ? new PrintWriter(this.out) : null, session); out.print(getBanner()); this.stack = new MemoryWarpScriptStack(this.rel.storeClient, this.rel.directoryClient); this.stack.setAttribute(WarpScriptStack.ATTRIBUTE_NAME, "[EgressInteractiveHandler " + Thread.currentThread().getName() + "]"); WarpConfig.setThreadProperty(WarpConfig.THREAD_PROPERTY_SESSION, UUID.randomUUID().toString()); // // Store PrintWriter // stack.setAttribute(WarpScriptStack.ATTRIBUTE_INTERACTIVE_WRITER, out); String uuid = UUID.randomUUID().toString(); StackContext context = this.rel.bootstrapManager.getBootstrapContext(); try { if (null != context) { stack.push(context); stack.restore(); } // // Execute the bootstrap code // stack.exec(WarpScriptLib.BOOTSTRAP); } catch (Throwable t) { out.print("// ERROR "); out.println(WarpURLEncoder.encode(ThrowableUtils.getErrorMessage(t), StandardCharsets.UTF_8).replaceAll("%20", " ").replaceAll("%27", "'")); if (null != this.socket) { this.socket.close(); } if (null != this.session) { this.session.disconnect(); } return; } List<Long> times = new ArrayList<Long>(1); long seqno = 0; while(true) { // Output prompt out.print(getPrompt()); out.flush(); String line = in.readLine(); seqno++; if (null == line) { if (null != this.socket) { this.socket.close(); } if (null != this.session) { this.session.disconnect(); } return; } Throwable t = null; long nano = System.nanoTime(); long time = 0L; try { stack.exec(line); time = System.nanoTime() - nano; if (stack.depth() > 0 && null != stack.getAttribute(WarpScriptStack.ATTRIBUTE_INTERACTIVE_ECHO)) { WarpScriptStackFunction npeek = (WarpScriptStackFunction) WarpScriptLib.getFunction("NPEEK"); stack.push(stack.getAttribute(WarpScriptStack.ATTRIBUTE_INTERACTIVE_ECHO)); out.println(" "); npeek.apply(stack); out.println(" "); } } catch (WarpScriptStopException ese) { continue; } catch (Throwable te) { t = te; out.print("// ERROR "); out.println(WarpURLEncoder.encode(ThrowableUtils.getErrorMessage(t), StandardCharsets.UTF_8).replaceAll("%20", " ").replaceAll("%27", "'")); } finally { times.clear(); times.add(System.nanoTime() - nano); LoggingEvent event = LogUtil.setLoggingEventAttribute(null, LogUtil.WARPSCRIPT_SCRIPT, line); event = LogUtil.setLoggingEventAttribute(event, LogUtil.WARPSCRIPT_TIMES, times); event = LogUtil.setLoggingEventAttribute(event, LogUtil.WARPSCRIPT_UUID, uuid); event = LogUtil.setLoggingEventAttribute(event, LogUtil.WARPSCRIPT_SEQNO, seqno); if (stack.isAuthenticated()) { event = LogUtil.setLoggingEventAttribute(event, WarpScriptStack.ATTRIBUTE_TOKEN, stack.getAttribute(WarpScriptStack.ATTRIBUTE_TOKEN).toString()); } if (null != t) { event = LogUtil.setLoggingEventStackTrace(event, LogUtil.STACK_TRACE, t); } String msg = LogUtil.serializeLoggingEvent(this.rel.keyStore, event); if (null != t) { EVENTLOG.error(msg); } else { EVENTLOG.info(msg); } if (Boolean.TRUE.equals(stack.getAttribute(WarpScriptStack.ATTRIBUTE_INTERACTIVE_TIME))) { out.print("// TIME "); out.println(time + " ns"); } } } } catch (IOException ioe) { return; } finally { WarpConfig.clearThreadProperties(); WarpScriptStackRegistry.unregister(stack); this.rel.connections.decrementAndGet(); try { if (null != this.socket) { this.socket.close(); } if (null != this.session) { this.session.disconnect(); } } catch (IOException ioe) { } } } } }