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.petstore.console;
018
019import java.io.*;
020import java.util.*;
021import java.util.concurrent.atomic.*;
022
023import org.apache.juneau.commons.settings.*;
024import org.apache.juneau.rest.server.MethodSafety;
025import org.apache.juneau.rest.server.filter.*;
026
027import jakarta.servlet.*;
028import jakarta.servlet.http.*;
029
030/**
031 * The console's CSRF boundary: a {@link LoopbackBoundaryFilter} applied to {@code /console} and everything under it.
032 *
033 * <p>
034 * On console requests the boundary pins the {@code Host}, and on console writes it also requires a same-origin
035 * {@code Origin}, a JSON body and the per-process {@link SynchronizerToken}.  An allowed console request carries
036 * the token to the page renderer, which stamps it on {@code <body data-juneau-csrf>} for the console toolkit to send
037 * back.
038 *
039 * <h5 class='section'>The effective method</h5>
040 * <p>
041 * A request is judged on the method it claims, not only on its request line.  A {@code method} query parameter or
042 * an {@code X-Method}, {@code X-HTTP-Method-Override}, {@code X-HTTP-Method} or {@code X-Method-Override} header
043 * naming anything other than a safe method makes the request a write, whether or not the resource is configured to
044 * honour that override.  So {@code GET /console/pets/1?method=DELETE} needs the same Origin and token a real
045 * {@code DELETE} does, and a later change to a resource's allowed method overrides cannot open a hole here.
046 *
047 * <h5 class='section'>The API's token</h5>
048 * <p>
049 * The {@code /petstore} API is not covered by this boundary: it sits behind its own, {@link PetstoreApiBoundary},
050 * which has a different token.  The console's one write to the API, its photo upload
051 * ({@code PUT /petstore/pets/{id}/photo}), carries the API's token, not this one.  So on every console request it
052 * allows, this boundary also publishes a fresh API token as a {@link String} under
053 * {@link PetstoreApiBoundary#TOKEN_ATTRIBUTE}, for the upload form to stamp.  A refused request publishes nothing.
054 *
055 * <p>
056 * A runner must build this boundary with the {@link PetstoreApiBoundary} instance it mounts on the API.  The no-arg
057 * constructor builds a private one that nobody mounts, so the console's uploads are then refused: visibly, and
058 * closed rather than open.
059 *
060 * <p>
061 * This boundary does not mark the response {@code no-store} for the API's token: the console page shell already
062 * does so for every page carrying the console's token.  When the response is already committed, the shell drops
063 * only its own token attributes, so on that rare path the API's token can still be emitted, possibly without
064 * {@code no-store}.  That is accepted, as {@link PetstoreApiBoundary} describes.
065 *
066 * <h5 class='section'>What is outside</h5>
067 * <p>
068 * The {@code /petstore-ui} app (which only reads) is covered by neither boundary.  The Jetty deployment puts the
069 * microservice admin resources behind a third boundary of their own.
070 *
071 * <h5 class='section'>The authority</h5>
072 * <p>
073 * The single accepted {@code Host} is the {@value #AUTHORITY_PROPERTY} setting when set, else
074 * {@code localhost:<port>} with the port the request arrived on.  It is fixed on the first console request, after
075 * the server has bound, so a runner that falls back to a random port still gets the right value.  Any other
076 * spelling of the same server ({@code 127.0.0.1}, a LAN host name) is answered with {@code 421} on console paths.
077 *
078 * <p>
079 * Set {@value #AUTHORITY_PROPERTY} whenever the browser's {@code Host} differs from the bound port: behind a Docker
080 * port mapping or a reverse proxy, the default answers {@code 421}.  A browser leaves the port out of {@code Host}
081 * for ports 80 and 443, so the value is then the bare host name.
082 *
083 * <p>
084 * The boundary expects the {@code Origin} {@code http://<authority>}, so this works for plain HTTP only.  An HTTPS
085 * deployment's console stays plain-HTTP-loopback only.  The API boundary reads no forwarded headers either, and the
086 * demo does not configure the framework boundary's {@code CONTAINER} origin source, so behind a TLS-terminating proxy
087 * the API refuses writes too (see {@link PetstoreApiBoundary}).
088 *
089 * <h5 class='section'>Example:</h5>
090 * <p class='bjava'>
091 *    <jk>var</jk> <jv>reg</jv> = <jk>new</jk> FilterRegistrationBean&lt;&gt;(<jk>new</jk> PetstoreConsoleBoundary(<jv>apiBoundary</jv>));
092 *    <jv>reg</jv>.addUrlPatterns(PetstoreConsoleBoundary.<jsf>URL_PATTERN</jsf>);
093 * </p>
094 *
095 * <p>
096 * In a Juneau microservice, contribute a <c>ServletFilterRegistration</c> <c>@Bean</c> instead:
097 * <p class='bjava'>
098 *    <ja>@Bean</ja>
099 *    <jk>public</jk> ServletFilterRegistration consoleBoundary() {
100 *       <jk>return</jk> ServletFilterRegistration.<jsm>create</jsm>(<jk>new</jk> PetstoreConsoleBoundary(<jf>apiBoundary</jf>))
101 *          .urlPatterns(PetstoreConsoleBoundary.<jsf>URL_PATTERN</jsf>)
102 *          .dispatcherTypes(DispatcherType.<jsf>REQUEST</jsf>, DispatcherType.<jsf>FORWARD</jsf>, DispatcherType.<jsf>ASYNC</jsf>, DispatcherType.<jsf>ERROR</jsf>)
103 *          .order(ServletFilterRegistration.<jsf>HIGHEST_PRECEDENCE</jsf>)
104 *          .build();
105 *    }
106 * </p>
107 * <p>
108 * where {@code apiBoundary} is the {@link PetstoreApiBoundary} the runner mounts on the API.
109 */
110public class PetstoreConsoleBoundary implements Filter {
111
112   /** The covered path: the console resource's mount point. */
113   public static final String PATH = "/console";
114
115   /** The servlet URL pattern the boundary is registered under. */
116   public static final String URL_PATTERN = PATH + "/*";
117
118   /** Setting (system property, environment, or any registered source) naming the one accepted console host and port. */
119   public static final String AUTHORITY_PROPERTY = "petstore.console.authority";
120
121   private final SynchronizerToken token = SynchronizerToken.generate();
122   private final AtomicReference<LoopbackBoundaryFilter> filter = new AtomicReference<>();
123   private final PetstoreApiBoundary api;
124
125   /**
126    * Constructor for a console boundary with an API boundary of its own.
127    *
128    * <p>
129    * Nothing mounts that API boundary, so the API refuses the tokens this boundary publishes.  A runner that mounts
130    * {@link PetstoreApiBoundary} uses {@link #PetstoreConsoleBoundary(PetstoreApiBoundary)} instead.
131    */
132   public PetstoreConsoleBoundary() {
133      this(new PetstoreApiBoundary());
134   }
135
136   /**
137    * Constructor.
138    *
139    * @param api The API boundary the runner mounts, whose tokens console pages carry.  Must not be <jk>null</jk>.
140    */
141   public PetstoreConsoleBoundary(PetstoreApiBoundary api) {
142      this.api = Objects.requireNonNull(api, "api");
143   }
144
145   /**
146    * Applies the boundary to console requests and passes every other request straight through.
147    *
148    * <p>
149    * An allowed console request also carries the API's token (see {@link PetstoreApiBoundary}).
150    *
151    * <p>
152    * The path check makes the boundary safe to call for every request (for example when registered under a broader
153    * pattern), not only when a container maps it to {@link #URL_PATTERN}.
154    */
155   @Override /* Filter */
156   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
157      if (! (req instanceof HttpServletRequest req2 && covers(req2.getServletPath(), req2.getPathInfo()))) {
158         chain.doFilter(req, res);
159         return;
160      }
161      // The boundary sees the effective method; the resource still gets the original request.
162      var method = MethodSafety.effectiveMethod(req2.getMethod(), req2.getQueryString(), req2::getHeader);
163      var checked = method.equals(req2.getMethod()) ? req2 : new HttpServletRequestWrapper(req2) {
164         @Override /* HttpServletRequest */
165         public String getMethod() {
166            return method;
167         }
168      };
169      filter(req2).doFilter(checked, res, (rq, rs) -> {
170         req2.setAttribute(PetstoreApiBoundary.TOKEN_ATTRIBUTE, api.tokenFor(req2));
171         chain.doFilter(req, res);
172      });
173   }
174
175   private LoopbackBoundaryFilter filter(HttpServletRequest req) {
176      var f = filter.get();
177      if (f != null)
178         return f;
179      var boundary = LoopbackBoundary.create()
180         .authority(authority(Settings.get().get(AUTHORITY_PROPERTY).orElse(null), req.getLocalPort()))
181         .token(token)
182         .build();
183      filter.compareAndSet(null, new LoopbackBoundaryFilter(boundary));
184      return filter.get();
185   }
186
187   /**
188    * The accepted console authority.
189    *
190    * @param setting The {@value #AUTHORITY_PROPERTY} setting.  Can be <jk>null</jk> or blank (not set).
191    * @param localPort The port the server is bound to.
192    * @return The setting, trimmed, when set, else {@code localhost:<localPort>}.
193    */
194   static String authority(String setting, int localPort) {
195      return setting == null || setting.isBlank() ? "localhost:" + localPort : setting.trim();
196   }
197
198   /**
199    * Whether a request path is a console path.
200    *
201    * @param servletPath The request's servlet path.  Can be <jk>null</jk>.
202    * @param pathInfo The request's path info.  Can be <jk>null</jk>.
203    * @return <jk>true</jk> for {@code /console} and every path under it.
204    */
205   static boolean covers(String servletPath, String pathInfo) {
206      var p = (servletPath == null ? "" : servletPath) + (pathInfo == null ? "" : pathInfo);
207      return p.equals(PATH) || p.startsWith(PATH + "/");
208   }
209}