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}