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.commons.http;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021/**
022 * Shared, dependency-free predicates for validating <b>same-origin</b> request paths declared by higher layers, plus
023 * the single {@link #MIN_POLL_INTERVAL_MS polling floor} those layers clamp declared poll intervals to.
024 *
025 * <p>
026 * This is the toolkit's one home for "is this app-declared endpoint safe to fetch from the browser" so the widgets
027 * and views modules can both <b>call</b> it without either depending on the other.  The same-origin core
028 * ({@link #isSameOriginPath(String)}) rejects an absolute URL, a protocol-relative {@code //host} prefix, a
029 * scheme (colon-before-slash), and any {@code ..} path segment &mdash; the base rule every declarative endpoint
030 * shares.  {@link #isNonTemplatedPath(String)} adds the stricter "no {@code {…}} placeholder" rule an endpoint that
031 * is <b>not</b> row/parameter-scoped requires (a card field-list, for example, is fixed &mdash; it is not fetched
032 * per-row, so a {@code {id}} template is meaningless and rejected).  A path-templated caller (a calendar that
033 * legitimately needs {@code {year}}/{@code {month}}) layers {@link #isSafeTemplate(String, String...)} on top of
034 * {@link #isSameOriginPath(String)} rather than duplicating the same-origin core.  Document links (an event
035 * {@code href}) use {@link #isSafeDocumentUrl(String)}, which is the same-origin core plus query/fragment.
036 *
037 * @since 10.0.0
038 */
039public final class SafePathTemplate {
040
041   /**
042    * The minimum honored polling interval, in milliseconds.
043    *
044    * <p>
045    * A declared poll interval below this floor is clamped up to it rather than honored as configured &mdash; a
046    * declarable interval with no floor lets a consumer configure a self-inflicted load problem on a server-side
047    * endpoint.  This is the single, easily-tested source of truth for the floor shared by every polling widget
048    * (view tables, card field-lists, calendars), so a stale/cached client script cannot be tricked into honoring a
049    * sub-floor value the server never actually declared.
050    */
051   public static final long MIN_POLL_INTERVAL_MS = 5_000L;
052
053   private SafePathTemplate() {}
054
055   /**
056    * Whether {@code path} is a same-origin path: not blank, no {@code ://}, no {@code //} prefix, no scheme
057    * (colon before the first slash), and no {@code ..} path segments.
058    *
059    * <p>
060    * The check rejects, and never normalises and then accepts, the characters a browser's URL parser rewrites: a
061    * backslash (read as a slash, so {@code \\host} is protocol-relative), any C0 control character or DEL, and a
062    * first or last character at or below U+0020 (stripped, so {@code " //host"} resolves cross-host).  Internal
063    * spaces and non-ASCII characters are still accepted.  The JS guards in the console runtimes apply the same
064    * rule, and {@code same-origin-path-corpus.json} pins them to it.
065    *
066    * <p>
067    * This is the shared same-origin core.  It deliberately says nothing about {@code {…}} template placeholders:
068    * a per-row/per-parameter caller (which needs {@code {id}}) uses this as-is, and a caller that forbids
069    * templating layers {@link #isNonTemplatedPath(String)} on top.
070    *
071    * @param path The candidate path.  Can be <jk>null</jk>.
072    * @return <jk>true</jk> if the string is a same-origin path.
073    */
074   @SuppressWarnings({
075      "java:S3776" // Flat sequence of independent rejection checks; splitting it would obscure the security rules.
076   })
077   public static boolean isSameOriginPath(String path) {
078      if (ib(path))
079         return false;
080      if (path.charAt(0) <= ' ' || path.charAt(path.length() - 1) <= ' ')
081         return false;
082      for (var i = 0; i < path.length(); i++) {
083         var c = path.charAt(i);
084         if (c == '\\' || c < ' ' || c == 0x7f)
085            return false;
086      }
087      if (path.contains("://"))
088         return false;
089      if (path.startsWith("//"))
090         return false;
091      var colon = path.indexOf(':');
092      var slash = path.indexOf('/');
093      if (colon >= 0 && (slash < 0 || colon < slash))
094         return false;
095      for (var seg : path.split("/", -1)) {
096         if (eq(seg, ".."))
097            return false;
098      }
099      return true;
100   }
101
102   /**
103    * Whether {@code path} is a same-origin path (per {@link #isSameOriginPath(String)}) that also carries <b>no</b>
104    * {@code {…}} template placeholder.
105    *
106    * <p>
107    * A fixed, non-parameterized endpoint (a card field-list refresh, for example) must be exactly the path it
108    * declares; a {@code {id}}-style placeholder has no substitution source and is rejected.
109    *
110    * @param path The candidate path.  Can be <jk>null</jk>.
111    * @return <jk>true</jk> if the string is a same-origin, non-templated path.
112    */
113   public static boolean isNonTemplatedPath(String path) {
114      return isSameOriginPath(path) && path.indexOf('{') < 0;
115   }
116
117   /**
118    * Clamps a declared poll interval up to {@link #MIN_POLL_INTERVAL_MS}.
119    *
120    * @param intervalMs The declared interval, in milliseconds.
121    * @return {@code max(intervalMs, }{@link #MIN_POLL_INTERVAL_MS}{@code )}.
122    */
123   public static long clampPollInterval(long intervalMs) {
124      return Math.max(intervalMs, MIN_POLL_INTERVAL_MS);
125   }
126
127   /**
128    * Whether {@code template} is a same-origin path template (per {@link #isSameOriginPath(String)}) that also
129    * contains every required substitution token.
130    *
131    * <p>
132    * A per-month calendar GET like {@code /events/{year}/{month}} is same-origin <em>and</em> must actually
133    * declare the tokens the emitter will substitute.  Built on {@link #isSameOriginPath(String)} rather than a
134    * second same-origin implementation.
135    *
136    * @param template The candidate template.  Can be <jk>null</jk>.
137    * @param requiredTokens The literal tokens (for example {@code "{year}"}, {@code "{month}"}) that must all be
138    *    present.  May be empty (same-origin only).
139    * @return <jk>true</jk> if {@code template} is same-origin and contains every token.
140    */
141   public static boolean isSafeTemplate(String template, String...requiredTokens) {
142      if (!isSameOriginPath(template))
143         return false;
144      if (requiredTokens != null)
145         for (var t : requiredTokens)
146            if (t != null && !template.contains(t))
147               return false;
148      return true;
149   }
150
151   /**
152    * Whether {@code url} is a same-origin <b>document</b> URL (query and fragment allowed).
153    *
154    * <p>
155    * Mirrors the client-side {@code isSafeDetailUrl} check.  Query/hash never trip the absolute/scheme/{@code ..}
156    * rejections in {@link #isSameOriginPath(String)}; a {@code ?} value containing {@code ://} is still rejected.
157    *
158    * @param url The candidate document URL.  Can be <jk>null</jk>.
159    * @return <jk>true</jk> if the URL is a safe same-origin document link.
160    */
161   public static boolean isSafeDocumentUrl(String url) {
162      return isSameOriginPath(url);
163   }
164}