001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.terminal.bus;
018
019import static java.nio.charset.StandardCharsets.*;
020
021import java.util.*;
022import java.util.concurrent.atomic.*;
023import java.util.logging.*;
024
025import org.apache.juneau.marshall.collections.*;
026import org.apache.juneau.console.bus.*;
027import org.apache.juneau.terminal.report.*;
028import org.apache.juneau.terminal.region.*;
029
030/**
031 * The {@code term.input} upstream handler: hands keystrokes from a browser terminal card to a {@link TerminalSink}.
032 *
033 * <p>
034 * Wiring.  The handler answers through the bus, but the bus is built from the policy that holds the handler, so the
035 * bus is bound afterwards, once:
036 * <p class='bjava'>
037 *    TerminalInput <jv>input</jv> = TerminalInput.<jsm>create</jsm>(<jv>access</jv>);
038 *    ServerBus <jv>bus</jv> = ServerBus.<jsm>create</jsm>(BusPolicy.<jsm>create</jsm>()
039 *       .upstream(TerminalInput.<jsf>TOPIC</jsf>, <jv>input</jv>)
040 *       .downstream(TerminalInput.<jsf>STATUS_TOPIC</jsf>, <jk>false</jk>)
041 *       .build());
042 *    <jv>input</jv>.bind(<jv>bus</jv>);
043 * </p>
044 *
045 * <p>
046 * A frame's payload is <c>{"id": &lt;terminal id&gt;, "data": &lt;string&gt;}</c>; an empty {@code data} is a probe.
047 * Every probe, and every refused data frame, is answered on {@link #STATUS_TOPIC} to the sending session only, with
048 * <c>{"id", "state"}</c>, where the state is {@code live}, {@code denied}, {@code closed}, {@code busy} or {@code bad}.
049 * An accepted data frame gets no answer.  The id is echoed only when it is a valid terminal id; otherwise the answer has
050 * no {@code id} key.
051 *
052 * <p>
053 * Typing needs both {@link TerminalAccess#canWatch(TerminalCaller, String)} and a present
054 * {@link TerminalAccess#inputSink(TerminalCaller, String)}.  The bus's {@code maxUpstreamPerSecond} policy (20 by
055 * default) bounds the frame rate per session, which is why the client batches keystrokes.
056 *
057 * <p>
058 * Each accepted non-empty write is audited at INFO on the {@code juneau.terminal.input} logger as
059 * <c>principal=&lt;name or -&gt; terminal=&lt;id&gt; bytes=&lt;n&gt; keys=&lt;list&gt;</c>, where the list names the
060 * control keys present (for example <c>keys=^C,ESC,CR</c>), comma-separated, in order of first appearance, at most 8.
061 * The typed content is never logged.  Refusals log at FINE with the state only.
062 */
063public final class TerminalInput implements UpstreamHandler {
064
065   /** The upstream topic carrying keystrokes. */
066   public static final String TOPIC = "term.input";
067
068   /** The downstream topic carrying the per-session answers.  Never retained. */
069   public static final String STATUS_TOPIC = "term.input-status";
070
071   private static final Logger AUDIT = Logger.getLogger("juneau.terminal.input");
072   private static final int MAX_KEYS = 8;
073   private static final Set<String> KEYS = Set.of("id", "data");
074
075   private final TerminalAccess access;
076   private final AtomicReference<ServerBus> bus = new AtomicReference<>();
077
078   private TerminalInput(TerminalAccess access) {
079      this.access = access;
080   }
081
082   /**
083    * Creates a handler.
084    *
085    * @param access Who may watch and who may type.  Must not be <jk>null</jk>.
086    * @return A new, unbound handler.
087    * @throws IllegalArgumentException If {@code access} is <jk>null</jk>.
088    */
089   public static TerminalInput create(TerminalAccess access) {
090      if (access == null)
091         throw new IllegalArgumentException("TerminalInput.create: access must not be null");
092      return new TerminalInput(access);
093   }
094
095   /**
096    * Binds the bus the answers are published on.  May be called once.
097    *
098    * @param bus The bus whose policy holds this handler.  Must not be <jk>null</jk>.
099    * @return This object.
100    * @throws IllegalArgumentException If {@code bus} is <jk>null</jk>.
101    * @throws IllegalStateException If a bus is already bound.
102    */
103   public TerminalInput bind(ServerBus bus) {
104      if (bus == null)
105         throw new IllegalArgumentException("TerminalInput.bind: bus must not be null");
106      if (! this.bus.compareAndSet(null, bus))
107         throw new IllegalStateException("TerminalInput.bind: already bound");
108      return this;
109   }
110
111   /**
112    * Handles one {@code term.input} frame.
113    *
114    * @param session The sending session.  Answers go to it only.
115    * @param topic The topic, {@link #TOPIC}.
116    * @param payload The frame payload, <c>{"id", "data"}</c>.
117    * @throws IllegalStateException If no bus is bound.
118    * @throws Exception Whatever the sink's {@code write} throws; the bus answers {@code bus:upstream-failed}.
119    */
120   @Override /* UpstreamHandler */
121   public void handle(BusSession session, String topic, JsonMap payload) throws Exception {
122      var b = bus.get();
123      if (b == null)
124         throw new IllegalStateException("TerminalInput: handle called before bind");
125      var caller = new SessionCaller(session);
126
127      var rawId = payload.get("id");
128      var id = rawId instanceof String s && TerminalViewChecks.isRunId(s) ? s : null;
129      if (id == null || ! (payload.get("data") instanceof String data) || ! KEYS.equals(payload.keySet())) {
130         refuse(b, session, caller, id, "bad");
131         return;
132      }
133
134      var sink = access.canWatch(caller, id) ? access.inputSink(caller, id).orElse(null) : null;
135      if (sink == null) {
136         refuse(b, session, caller, id, "denied");
137         return;
138      }
139      if (! sink.acceptsInput()) {
140         refuse(b, session, caller, id, "closed");
141         return;
142      }
143      if (data.isEmpty()) {
144         answer(b, session, id, "live");
145         return;
146      }
147      var bytes = data.getBytes(UTF_8);
148      if (! sink.write(bytes)) {
149         // A refused write is "busy" unless the process stopped accepting input since the check above.
150         refuse(b, session, caller, id, sink.acceptsInput() ? "busy" : "closed");
151         return;
152      }
153      AUDIT.info(() -> String.format(Locale.ROOT, "principal=%s terminal=%s bytes=%d keys=%s", principal(caller), id, bytes.length, keys(bytes)));
154   }
155
156   private static void refuse(ServerBus b, BusSession session, TerminalCaller caller, String id, String state) {
157      AUDIT.fine(() -> String.format(Locale.ROOT, "principal=%s terminal=%s state=%s", principal(caller), id == null ? "-" : id, state));
158      answer(b, session, id, state);
159   }
160
161   private static void answer(ServerBus b, BusSession session, String id, String state) {
162      // The bus serializer writes null map values, so an id that failed the grammar is left out rather than nulled.
163      var payload = id == null ? JsonMap.of("state", state) : JsonMap.of("id", id, "state", state);
164      b.publishTo(s -> s == session, STATUS_TOPIC, payload);
165   }
166
167   /**
168    * The principal for an audit record: {@code -} when anonymous, otherwise the name with every character
169    * {@code <= 0x20}, every ISO control (0x7F, 0x80-0x9F), U+2028, U+2029, {@code '='}, {@code '\'} and {@code '%'}
170    * percent-encoded as its UTF-8 bytes ({@code %XX} per byte, upper-case hex; for example U+2028 is {@code %E2%80%A8}),
171    * so a name can neither split the record nor forge a {@code key=value} field.
172    */
173   private static String principal(TerminalCaller c) {
174      var name = c.principalName();
175      if (name == null)
176         return "-";
177      var sb = new StringBuilder(name.length());
178      for (var i = 0; i < name.length(); i++) {
179         var ch = name.charAt(i);
180         if (ch <= 0x20 || Character.isISOControl(ch) || ch == '\u2028' || ch == '\u2029' || ch == '=' || ch == '\\' || ch == '%')
181            for (var x : String.valueOf(ch).getBytes(UTF_8))
182               sb.append('%').append(String.format(Locale.ROOT, "%02X", x & 0xFF));
183         else
184            sb.append(ch);
185      }
186      return sb.toString();
187   }
188
189   /** The control keys present, comma-separated, in order of first appearance, at most {@value #MAX_KEYS}. */
190   private static String keys(byte[] bytes) {
191      var names = new LinkedHashSet<String>();
192      for (var x : bytes) {
193         var name = keyName(x & 0xFF);
194         if (name != null) {
195            names.add(name);
196            if (names.size() == MAX_KEYS)
197               break;
198         }
199      }
200      return String.join(",", names);
201   }
202
203   /**
204    * The audit name of a control byte, or <jk>null</jk> for any other byte.
205    *
206    * <p>
207    * {@code ^C}, {@code ^D}, {@code ^Z}, {@code ESC}, {@code CR}, {@code TAB} and {@code BS} (0x08 and 0x7F) are named;
208    * every other C0 byte uses standard caret notation ({@code b + 0x40}): {@code ^A}..{@code ^Y} for the letters,
209    * {@code ^J} for LF, and {@code ^@}, {@code ^\}, {@code ^]}, {@code ^^}, {@code ^_} for 0x00 and 0x1C-0x1F.
210    */
211   private static String keyName(int b) {
212      return switch (b) {
213         case 0x03 -> "^C";
214         case 0x04 -> "^D";
215         case 0x1A -> "^Z";
216         case 0x1B -> "ESC";
217         case 0x0D -> "CR";
218         case 0x09 -> "TAB";
219         case 0x08 -> "BS";
220         case 0x7F -> "BS";  // xterm's Backspace sends DEL (0x7F) by default.
221         default -> b < 0x20 ? "^" + (char)(b + 0x40) : null;
222      };
223   }
224}