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 static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.util.*;
023
024import org.apache.juneau.commons.settings.*;
025import org.apache.juneau.rest.server.filter.*;
026
027import jakarta.servlet.*;
028import jakarta.servlet.http.*;
029
030/**
031 * The {@code /petstore} API's CSRF boundary: a {@link RequestBoundaryFilter} over the framework's {@link CsrfBoundary},
032 * applied to {@code /petstore} and everything under it.
033 *
034 * <p>
035 * A browser write to the API must come from the page's own origin and carry this boundary's token in the
036 * {@code X-Csrf-Token} header.  The token travels in the header only, so every HTML {@code <form>} POST to
037 * the API is refused, the application's own included.  Requests are judged on their effective method, so
038 * {@code GET /petstore/pets/1?method=DELETE} counts as a write.  Reads are allowed, and so are non-browser writes
039 * (no {@code Origin} and no {@code Sec-Fetch-Site}) that a browser could not have sent cross-site without a CORS
040 * preflight: JSON, XML or UON bodies, and {@code PUT}, {@code DELETE} or {@code PATCH}.  A non-browser urlencoded,
041 * multipart or {@code text/plain} {@code POST} is refused.  Every refusal names its reason in the
042 * {@value RequestBoundaryFilter#REJECTION_HEADER} response header.
043 *
044 * <h5 class='section'>The token</h5>
045 * <p>
046 * The token is per process ({@link CsrfTokens#perProcess()}): the petstore has no login session, and the
047 * {@code Origin} check does the real work.  A restart invalidates the token on every open page, so reload them.  A
048 * token that leaks through a page stays valid until the restart.
049 *
050 * <p>
051 * This token is not the console's.  The console boundary ({@link PetstoreConsoleBoundary}) has its own, and each
052 * boundary refuses the other's.  The console's one write to the API, its photo upload, gets this boundary's token
053 * from the console boundary, which publishes {@link #tokenFor(HttpServletRequest)} under {@link #TOKEN_ATTRIBUTE}
054 * on every console request it allows; the page's script sends it in that header.  So a runner must
055 * give the console boundary the same instance it mounts here: a token from any other instance is refused.
056 *
057 * <p>
058 * The console page shell marks a token-bearing page {@code no-store}, and when the response is already committed it
059 * strips only its own token attributes.  So on the rare page whose buffer was flushed before the shell ran, this
060 * boundary's token can still be emitted, possibly without {@code no-store}.  That is accepted: this token is only
061 * ever published on console pages, which carry the console's token too, so on every normal path the page is already
062 * marked.
063 *
064 * <h5 class='section'>The host</h5>
065 * <p>
066 * The request's own origin is its scheme plus its {@code Host} header; forwarded headers are never read.  By default
067 * any {@code Host} is answered, which suits a loopback developer setup.  On a host served over plain HTTP, set
068 * {@value #ALLOWED_HOSTS_PROPERTY} (a system property, the environment, or any registered setting source) to the
069 * comma-separated host names the browser uses, without ports.  Every request to the API under any other {@code Host},
070 * reads included, is then answered with {@code 421}: this is the DNS-rebinding defence.  The setting is read once,
071 * when the boundary is constructed, and a malformed entry fails construction rather than being ignored.
072 *
073 * <p>
074 * Behind a TLS-terminating proxy the browser's {@code https} origin does not match the {@code http} request the
075 * container sees, so writes are refused.  Such a deployment needs the container's forwarded-header support and the
076 * framework boundary's {@link org.apache.juneau.rest.server.filter.CsrfBoundary.OriginSource#CONTAINER CONTAINER} origin source, which this demo does not
077 * configure.
078 *
079 * <h5 class='section'>Example:</h5>
080 * <p>
081 * In a Juneau microservice, contribute a <c>ServletFilterRegistration</c> <c>@Bean</c> just after the console
082 * boundary's, sharing the one instance with it:
083 * <p class='bjava'>
084 *    <jk>private final</jk> PetstoreApiBoundary <jf>apiBoundary</jf> = <jk>new</jk> PetstoreApiBoundary();
085 *
086 *    <ja>@Bean</ja>
087 *    <jk>public</jk> ServletFilterRegistration apiBoundary() {
088 *       <jk>return</jk> ServletFilterRegistration.<jsm>create</jsm>(<jf>apiBoundary</jf>)
089 *          .urlPatterns(PetstoreApiBoundary.<jsf>URL_PATTERN</jsf>)
090 *          .dispatcherTypes(DispatcherType.<jsf>REQUEST</jsf>)
091 *          .order(ServletFilterRegistration.<jsf>HIGHEST_PRECEDENCE</jsf> + 10)
092 *          .build();
093 *    }
094 * </p>
095 * <p>
096 * and build the console boundary with {@code new PetstoreConsoleBoundary(apiBoundary)}.  Register it for
097 * {@code REQUEST} dispatches only: an internal forward or error dispatch is not a new request from the browser.
098 */
099public class PetstoreApiBoundary implements Filter {
100
101   /** The covered path: the API resource's mount point. */
102   public static final String PATH = "/petstore";
103
104   /** The servlet URL pattern the boundary is registered under.  It matches {@link #PATH} itself and every path under it. */
105   public static final String URL_PATTERN = PATH + "/*";
106
107   /**
108    * Setting (system property, environment, or any registered source) listing the host names the API answers to:
109    * comma-separated, no ports.  Unset or blank, any host is answered.
110    */
111   public static final String ALLOWED_HOSTS_PROPERTY = "petstore.api.allowedHosts";
112
113   /**
114    * Request attribute under which an allowed console request carries a {@link String}: a freshly masked token for
115    * this boundary, for a console page to stamp on the form that writes to the API.
116    */
117   public static final String TOKEN_ATTRIBUTE = "org.apache.juneau.petstore.apiCsrfToken";
118
119   private final CsrfBoundary boundary;
120   private final RequestBoundaryFilter filter;
121
122   /**
123    * Constructor.
124    *
125    * <p>
126    * Reads {@value #ALLOWED_HOSTS_PROPERTY} and creates the boundary's per-process token.
127    *
128    * @throws IllegalArgumentException If {@value #ALLOWED_HOSTS_PROPERTY} holds an entry that is blank or not a bare
129    *    host name.
130    */
131   public PetstoreApiBoundary() {
132      boundary = CsrfBoundary.create()
133         .tokens(CsrfTokens.perProcess())
134         .allowedHosts(allowedHosts(Settings.get().get(ALLOWED_HOSTS_PROPERTY).orElse(null)))
135         .build();
136      filter = new RequestBoundaryFilter(boundary);
137   }
138
139   /**
140    * The host names in an {@value #ALLOWED_HOSTS_PROPERTY} setting.
141    *
142    * @param setting The setting.  Can be <jk>null</jk> or blank (not set).
143    * @return The comma-separated entries, trimmed, blank ones included so the boundary refuses them; empty when the
144    *    setting is not set.
145    */
146   private static String[] allowedHosts(String setting) {
147      if (ib(setting))
148         return new String[0];
149      return Arrays.stream(setting.split(",", -1)).map(String::trim).toArray(String[]::new);
150   }
151
152   /**
153    * A freshly masked token for a page served to this request to embed.
154    *
155    * <p>
156    * Never creates a session.  The value is different on every call, and every value verifies on this instance only.
157    *
158    * @param req The request whose response will embed the token.  Must not be <jk>null</jk>.
159    * @return 128 lower-case hex characters.
160    */
161   public String tokenFor(HttpServletRequest req) {
162      return boundary.tokenFor(req);
163   }
164
165   /**
166    * Applies the boundary: a refused request is answered here and never reaches the API.
167    */
168   @Override /* Filter */
169   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
170      filter.doFilter(req, res, chain);
171   }
172}