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.rest.server;
018
019
020// Single-type import, not the usual wildcard: this package declares its own Method type, which would otherwise
021// shadow java.lang.reflect.Method here.
022import java.lang.reflect.Method;
023import java.net.*;
024import java.nio.charset.*;
025import java.util.*;
026import java.util.function.*;
027
028import org.apache.juneau.commons.reflect.*;
029import org.apache.juneau.http.response.*;
030import static org.apache.juneau.commons.utils.Shorts.*;
031
032/**
033 * Which HTTP methods are safe, and the startup check that an operation declared {@link Mutating} is not bound to
034 * one of them.
035 *
036 * <p>
037 * This is the single definition of the safe-method set for this framework.  Anything that needs to know whether a
038 * request may change state &mdash; notably
039 * {@link org.apache.juneau.rest.server.filter.LoopbackBoundary#isStateChanging(String)}, which delegates here
040 * &mdash; reads it from this class, so a security filter and a startup check cannot come to disagree about which
041 * methods are writes.  A divergence between those two would be silent and would defeat both.
042 *
043 * <h5 class='section'>The check</h5>
044 * <p>
045 * {@link #check(List)} runs when a resource's operation table is assembled, from
046 * {@link RestOperations#RestOperations(RestOperations.Builder)} &mdash; the one constructor every operation table
047 * passes through, so there is no registration to remember and no path around it.  It fails the resource when an
048 * operation carrying {@link Mutating @Mutating} resolves to a safe method.
049 * <p>
050 * The resolved method is read from {@link RestOpContext#getHttpMethod()}, which is what dispatch itself uses.  That
051 * matters more than it sounds: the method may have been <b>inferred</b> from the Java method name rather than
052 * written down (see {@link RestOp#method()}), and a check that re-derived it independently could disagree with
053 * dispatch about which operation is bound to what.  A security-relevant check that disagrees with the router is
054 * worse than none.
055 * <p>
056 * A wildcard operation ({@code @RestOp(method="*")}) matches {@code GET} among everything else, so it is a
057 * contradiction too and is reported as one.
058 * <p>
059 * The same check fails an operation declared {@link RestOp#read() read=true} that is bound to a method defined as
060 * unsafe, to {@code "*"} or to {@code "RRPC"}, or that is also {@link Mutating @Mutating}.  A read can be reached by
061 * overriding a safe request, so a misdeclared read would be a write behind a plain link.
062 *
063 * <h5 class='section'>Why the rule is a declared contradiction rather than a detected one</h5>
064 * <p>
065 * See {@link Mutating}, which records why inferring "this handler mutates" is not soundly possible and why a check
066 * that can wrongly refuse to start an application is worse than no check at all.  The short form: this class only
067 * ever reports a contradiction the developer wrote down, so it cannot produce a false positive, and it cannot catch
068 * a handler that mutates and says nothing.
069 *
070 * <h5 class='section'>See Also:</h5><ul>
071 *    <li class='ja'>{@link Mutating}
072 *    <li class='jc'>{@link org.apache.juneau.rest.server.filter.LoopbackBoundary}
073 * </ul>
074 *
075 * @since 10.0.0
076 */
077public class MethodSafety {
078
079   /**
080    * The request methods RFC 9110 defines as safe.
081    *
082    * <p>
083    * A method absent from this set &mdash; including one this framework does not recognize &mdash; is treated as
084    * state-changing, so an unusual or future method fails closed rather than skipping write checks.
085    */
086   private static final Set<String> SAFE_METHODS = Set.of("GET", "HEAD", "OPTIONS", "TRACE");
087
088   /** The standard methods defined as unsafe, which an operation can never declare a read. */
089   private static final Set<String> UNSAFE_METHODS = Set.of("POST", "PUT", "PATCH", "DELETE", "CONNECT");
090
091   /** The query parameter that can claim a request's method. */
092   private static final String OVERRIDE_PARAM = "method";
093
094   /** The headers that can claim a request's method. */
095   private static final List<String> OVERRIDE_HEADERS = List.of("X-Method", "X-HTTP-Method-Override", "X-HTTP-Method", "X-Method-Override");
096
097   /**
098    * Constructor.
099    */
100   protected MethodSafety() {}
101
102   /**
103    * Whether {@code method} is defined as safe, and therefore promises not to change state.
104    *
105    * @param method The request method.  Can be <jk>null</jk>, which is not safe.
106    * @return <jk>true</jk> if the method is one of {@code GET}, {@code HEAD}, {@code OPTIONS} or {@code TRACE}.
107    */
108   public static boolean isSafe(String method) {
109      return method != null && SAFE_METHODS.contains(method.toUpperCase(Locale.ROOT));
110   }
111
112   /**
113    * The method a request is judged on by a write-protection check.
114    *
115    * <p>
116    * A request is judged on the method it <b>claims</b>, not only on its request line.  A {@code method} query
117    * parameter, or an {@code X-Method}, {@code X-HTTP-Method-Override}, {@code X-HTTP-Method} or
118    * {@code X-Method-Override} header, naming anything other than a safe method makes the request a write, whether or
119    * not the resource it reaches is configured to honour that override.  A check that followed dispatch's override
120    * configuration instead would open a hole the day a resource's allowed overrides changed.
121    *
122    * <p>
123    * The rules fail closed:
124    * <ul>
125    *    <li>The request-line method is the floor.  An unsafe request line is returned (upper-cased); an override can raise a
126    *       request's severity but never lower it, so {@code POST ?method=GET} stays {@code POST}.
127    *    <li>Any unsafe claim wins, so {@code ?method=GET&method=DELETE} is {@code DELETE}.
128    *    <li>An unknown method is unsafe, so {@code ?method=FROB} is judged as a write.
129    *    <li>A claim with a malformed percent-escape is judged on its raw text, which is never a safe method name.
130    *    <li>A blank claim is ignored.
131    * </ul>
132    *
133    * @param method The request line's method.  Can be <jk>null</jk>, which is returned as <jk>null</jk> (unsafe).
134    * @param rawQuery The raw, undecoded query string.  Can be <jk>null</jk>.
135    * @param headers Looks up a request header by name, returning <jk>null</jk> when absent.  Must not be <jk>null</jk>.
136    * @return The upper-cased request-line method when it is not safe, else the first unsafe claim (upper-cased), else
137    *    the upper-cased request-line method.
138    */
139   public static String effectiveMethod(String method, String rawQuery, UnaryOperator<String> headers) {
140      reqnn("headers", headers);
141      var m = method == null ? null : method.toUpperCase(Locale.ROOT);
142      if (! isSafe(m))
143         return m;
144      for (var claim : queryClaims(rawQuery)) {
145         var c = unsafeClaim(claim);
146         if (c != null)
147            return c;
148      }
149      for (var h : OVERRIDE_HEADERS) {
150         var c = unsafeClaim(headers.apply(h));
151         if (c != null)
152            return c;
153      }
154      return m;
155   }
156
157   /**
158    * The names of the request headers that can claim a request's method, in precedence order.
159    *
160    * <p>
161    * The order is part of the contract: dispatch honours the first admitted header in this order.
162    *
163    * @return An immutable list of header names.  Never <jk>null</jk>.
164    */
165   public static List<String> overrideHeaders() {
166      return OVERRIDE_HEADERS;
167   }
168
169   /**
170    * Every value of the {@code method} query parameter, in order.
171    *
172    * <p>
173    * The one parser shared by the write-protection filters and by dispatch, so they cannot disagree about what a query
174    * string claims.  The raw query is split on {@code &}, each name and value is percent-decoded, and the name is matched
175    * case-insensitively.  Values are returned untrimmed.  A malformed percent-escape leaves the raw text.
176    *
177    * @param rawQuery The raw, undecoded query string.  Can be <jk>null</jk>.
178    * @return The decoded values, in order.  Never <jk>null</jk>; empty when there is no claim.
179    */
180   public static List<String> queryClaims(String rawQuery) {
181      if (rawQuery == null)
182         return List.of();
183      var l = new ArrayList<String>();
184      for (var pair : rawQuery.split("&")) {
185         var i = pair.indexOf('=');
186         if (i > 0 && OVERRIDE_PARAM.equalsIgnoreCase(decode(pair.substring(0, i))))
187            l.add(decode(pair.substring(i + 1)));
188      }
189      return List.copyOf(l);
190   }
191
192   /** The claimed method upper-cased when it is unsafe, else <jk>null</jk> (absent, blank or safe). */
193   private static String unsafeClaim(String claim) {
194      if (ib(claim))
195         return null;
196      var c = claim.trim().toUpperCase(Locale.ROOT);
197      return isSafe(c) ? null : c;
198   }
199
200   /** Decodes a query-string component; a malformed escape leaves the raw text, which is then judged as is. */
201   private static String decode(String s) {
202      try {
203         return URLDecoder.decode(s, StandardCharsets.UTF_8);
204      } catch (IllegalArgumentException e) {
205         return s;
206      }
207   }
208
209   /**
210    * Fails when any operation declared {@link Mutating @Mutating} is bound to a safe method, or any operation
211    * declared {@link RestOp#read() read=true} is bound to a method that cannot be a read.
212    *
213    * @param ops The operations of one resource.  Must not be <jk>null</jk>.
214    * @throws InternalServerError If a mutating operation is bound to a safe method, or a read declaration is
215    *    misplaced or contradicts the operation's method or a {@link Mutating @Mutating} declaration.
216    */
217   public static void check(List<RestOpContext> ops) {
218      reqnn("ops", ops);
219      for (var op : ops)
220         checkOperation(op.getHttpMethod(), op.getJavaMethod(), op.isRead());
221   }
222
223   /**
224    * Fails when {@code javaMethod} is declared {@link Mutating @Mutating} and {@code httpMethod} is safe.
225    *
226    * <p>
227    * The per-operation half of {@link #check(List)}, taking the two facts it compares rather than a built
228    * {@link RestOpContext}, so the rule can be exercised directly.
229    *
230    * @param httpMethod The resolved HTTP method, as dispatch sees it.  Can be <jk>null</jk>, which is not safe.
231    *    {@code "*"} matches every method and is therefore treated as including a safe one.
232    * @param javaMethod The Java method implementing the operation.  Must not be <jk>null</jk>.
233    * @throws InternalServerError If {@code javaMethod} is declared mutating and {@code httpMethod} is safe.
234    */
235   public static void checkOperation(String httpMethod, Method javaMethod) {
236      checkOperation(httpMethod, javaMethod, false);
237   }
238
239   /**
240    * Fails when {@code javaMethod} is declared {@link Mutating @Mutating} and {@code httpMethod} is safe, or when the
241    * operation is declared {@link RestOp#read() read=true} and {@code httpMethod} cannot be a read.
242    *
243    * <p>
244    * A read declaration is refused on the methods defined as unsafe ({@code POST}, {@code PUT}, {@code PATCH},
245    * {@code DELETE}, {@code CONNECT}), on {@code "*"} (which answers {@code DELETE} too), on {@code "RRPC"} (which
246    * dispatches arbitrary interface methods), and together with {@link Mutating @Mutating}.  It is accepted on a
247    * custom method, and is redundant on a safe one.
248    *
249    * @param httpMethod The resolved HTTP method, as dispatch sees it.  Can be <jk>null</jk>, which is not safe.
250    *    {@code "*"} matches every method and is therefore treated as including a safe one.
251    * @param javaMethod The Java method implementing the operation.  Must not be <jk>null</jk>.
252    * @param read Whether the operation is declared {@link RestOp#read() read=true}.
253    * @throws InternalServerError If the declarations contradict each other or the method.
254    * @since 10.0.0
255    */
256   public static void checkOperation(String httpMethod, Method javaMethod, boolean read) {
257      reqnn("javaMethod", javaMethod);
258
259      // Resolved through MethodInfo rather than Method.getAnnotation, so that an operation inheriting its
260      // declaration from a superclass or interface method is seen. Method.getAnnotation does not walk overrides,
261      // which would let an inherited @Mutating go unchecked; MethodInfo walks matching methods child-to-parent,
262      // the same way the framework resolves every other method annotation.
263      var mutating = MethodInfo.of(javaMethod.getDeclaringClass(), javaMethod).getAnnotations(Mutating.class)
264         .findFirst().map(AnnotationInfo::inner).orElse(null);
265      if (read)
266         checkRead(httpMethod, javaMethod, nn(mutating));
267      if (mutating == null)
268         return;
269
270      // A wildcard operation answers GET along with everything else, so it is bound to a safe method too.
271      var wildcard = eq(httpMethod, "*");
272      if (! wildcard && ! isSafe(httpMethod))
273         return;
274
275      var what = mutating.value().isBlank() ? "" : " It changes: " + mutating.value() + ".";
276      throw new InternalServerError(
277         "Operation '" + javaMethod.getDeclaringClass().getSimpleName() + "." + javaMethod.getName()
278         + "' is annotated @Mutating but is bound to "
279         + (wildcard ? "every method, including safe ones" : "'" + httpMethod + "', which is a safe method")
280         + "." + what
281         + " A safe method promises not to change state, and CSRF and origin checks are applied only to methods"
282         + " that are not safe, so this operation would be reachable without them."
283         + " Bind it to POST/PUT/PATCH/DELETE, or remove @Mutating if it does not actually change state."
284         + " Note that @RestOp with no method= infers the method from the Java method name and defaults to GET.");
285   }
286
287   /** Fails when a read declaration contradicts the operation's method or a {@link Mutating @Mutating} declaration. */
288   private static void checkRead(String httpMethod, Method javaMethod, boolean mutating) {
289      // Normalized as the operation table does, so a direct caller passing "rrpc" or "METHOD" gets the same verdict.
290      var m = httpMethod == null ? null : ucr(httpMethod);
291      if (eq(m, "METHOD"))
292         m = "*";
293      String why;
294      if (mutating)
295         why = "is also annotated @Mutating, which declares the opposite";
296      else if (eq(m, "*"))
297         why = "is bound to every method, including unsafe ones such as DELETE";
298      else if (eq(m, "RRPC"))
299         why = "is a remote-proxy (RRPC) operation, which dispatches arbitrary interface methods";
300      else if (nn(m) && UNSAFE_METHODS.contains(m))
301         why = "is bound to '" + m + "', which is defined as unsafe";
302      else
303         return;
304      throw new InternalServerError(
305         "Operation '" + javaMethod.getDeclaringClass().getSimpleName() + "." + javaMethod.getName()
306         + "' is declared @RestOp(read=true) but " + why + "."
307         + " A read can be reached by overriding a GET, so declaring a write a read would let a plain link change state."
308         + " Remove read=true, or bind the operation to a custom method that does not change state."
309         + " Note that @RestOp with no method= infers the method from the Java method name.");
310   }
311}