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.report;
018
019import java.net.*;
020import java.util.regex.*;
021
022import org.apache.juneau.commons.http.*;
023
024/**
025 * Shared validation grammar; not intended for application use.
026 *
027 * <p>
028 * The terminal-view grammars live in one place so the server types, the report readers and the client module
029 * (which mirrors them, pinned by a shared vectors file) cannot drift.
030 */
031public final class TerminalViewChecks {
032
033   private static final Pattern RUN_ID = Pattern.compile("^[A-Za-z0-9_-]{1,128}$");
034   private static final Pattern TOKEN = Pattern.compile("^[A-Za-z0-9._~-]{1,128}$");
035   private static final Pattern STEP_ID = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$");
036   private static final Pattern FW = Pattern.compile("^[a-z0-9][a-z0-9._-]{0,31}$");
037   private static final Pattern TAB_CR_LF = Pattern.compile("[\t\r\n]");
038   private static final Pattern FRAGMENT_HREF = Pattern.compile("^#[A-Za-z0-9._:~-]{0,128}$");
039
040   /** The byte-offset placeholder; a raw-href template contains it exactly once. */
041   public static final String OFFSET_PLACEHOLDER = "{offset}";
042
043   /** The refresh floor, in milliseconds, for terminal-view and terminal poll intervals. */
044   public static final long MIN_REFRESH_MS = 1000;
045
046   private TerminalViewChecks() {}
047
048   /**
049    * Whether the string is a valid run id.
050    *
051    * @param s The candidate.  May be <jk>null</jk>.
052    * @return <jk>true</jk> if valid.
053    */
054   public static boolean isRunId(String s) {
055      return s != null && RUN_ID.matcher(s).matches();
056   }
057
058   /**
059    * Whether the string is a valid opaque page token.
060    *
061    * @param s The candidate.  May be <jk>null</jk>.
062    * @return <jk>true</jk> if valid.
063    */
064   public static boolean isToken(String s) {
065      return s != null && TOKEN.matcher(s).matches();
066   }
067
068   /**
069    * Whether the string is a valid step id.
070    *
071    * @param s The candidate.  May be <jk>null</jk>.
072    * @return <jk>true</jk> if valid.
073    */
074   public static boolean isStepId(String s) {
075      return s != null && STEP_ID.matcher(s).matches();
076   }
077
078   /**
079    * Whether the string is a valid framework key.
080    *
081    * @param s The candidate.  May be <jk>null</jk>.
082    * @return <jk>true</jk> if valid.
083    */
084   public static boolean isFw(String s) {
085      return s != null && FW.matcher(s).matches();
086   }
087
088   /**
089    * Whether the string is a safe note href.
090    *
091    * <p>
092    * Either an absolute URL whose scheme is exactly <c>http</c> or <c>https</c> and which has a non-empty host, or
093    * anything that {@link #isSafeLineHref(String)} accepts.  TAB, CR and LF are stripped first.
094    *
095    * @param s The candidate.  May be <jk>null</jk>.
096    * @return <jk>true</jk> if safe.
097    */
098   public static boolean isNoteHref(String s) {
099      if (s == null)
100         return false;
101      var t = TAB_CR_LF.matcher(s).replaceAll("");
102      if (t.isEmpty())
103         return false;
104      if (t.startsWith("http://") || t.startsWith("https://")) {
105         if (t.contains("\\"))
106            return false;
107         try {
108            var host = new URI(t).getHost();
109            return host != null && ! host.isEmpty();
110         } catch (URISyntaxException e) {
111            return false;
112         }
113      }
114      return isSafeLineHref(t);
115   }
116
117   /**
118    * Whether the string is a valid raw-href template.
119    *
120    * <p>
121    * It must contain exactly one <c>{offset}</c> placeholder and, with <c>1</c>
122    * substituted, be a safe line href.
123    *
124    * @param s The candidate.  May be <jk>null</jk>.
125    * @return <jk>true</jk> if valid.
126    */
127   public static boolean isRawHrefTemplate(String s) {
128      if (s == null)
129         return false;
130      if (count(s, OFFSET_PLACEHOLDER) != 1)
131         return false;
132      return isSafeLineHref(s.replace(OFFSET_PLACEHOLDER, "1"));
133   }
134
135   private static int count(String s, String token) {
136      var n = 0;
137      for (var i = s.indexOf(token); i >= 0; i = s.indexOf(token, i + token.length()))
138         n++;
139      return n;
140   }
141
142   /**
143    * Whether a line href is safe: a bare fragment, or a same-origin path accepted by
144    * {@link #isSameOriginPath(String)}.
145    *
146    * <p>
147    * TAB, CR and LF are stripped first.  A backslash anywhere refuses the href.
148    *
149    * @param s The candidate href.  May be <jk>null</jk>.
150    * @return <jk>true</jk> if the href is safe.
151    */
152   public static boolean isSafeLineHref(String s) {
153      if (s == null)
154         return false;
155      var t = TAB_CR_LF.matcher(s).replaceAll("");
156      if (t.startsWith("#"))
157         return FRAGMENT_HREF.matcher(t).matches();
158      return isSameOriginPath(t);
159   }
160
161   /**
162    * Whether the string is a same-origin path template, by the one shared rule in
163    * {@link SafePathTemplate#isSameOriginPath(String)}: no <c>://</c>, no <c>//</c> prefix, no scheme
164    * colon-before-slash, no <c>..</c> path segment, and no backslash, control character, or leading or trailing space.
165    *
166    * @param path The candidate template.  May be <jk>null</jk>.
167    * @return <jk>true</jk> if the string is a same-origin path template.
168    */
169   public static boolean isSameOriginPath(String path) {
170      return SafePathTemplate.isSameOriginPath(path);
171   }
172
173   /**
174    * Truncates a value for an error message: 64 characters plus an ellipsis.
175    *
176    * @param s The value.  May be <jk>null</jk>.
177    * @return The clipped text, or <js>"null"</js> for <jk>null</jk>.
178    */
179   public static String clip(String s) {
180      if (s == null)
181         return "null";
182      return s.length() > 64 ? s.substring(0, 64) + "\u2026" : s;
183   }
184
185   /**
186    * Returns the stripped first line of a string.
187    *
188    * @param s The string.  May be <jk>null</jk>.
189    * @return The first line, or an empty string for <jk>null</jk>.
190    */
191   public static String firstLine(String s) {
192      if (s == null)
193         return "";
194      var i = s.indexOf('\n');
195      return (i < 0 ? s : s.substring(0, i)).strip();
196   }
197}