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.wicket.protocol.ws;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Enumeration;
022import java.util.List;
023
024import jakarta.servlet.FilterChain;
025import jakarta.servlet.ServletException;
026import jakarta.servlet.http.HttpServletRequest;
027import jakarta.servlet.http.HttpServletResponse;
028
029import org.apache.wicket.ThreadContext;
030import org.apache.wicket.protocol.http.WebApplication;
031import org.apache.wicket.protocol.http.WicketFilter;
032import org.apache.wicket.request.cycle.RequestCycle;
033import org.apache.wicket.request.http.WebResponse;
034import org.apache.wicket.util.string.Strings;
035
036/**
037 * An extension of WicketFilter that is used to check whether
038 * the processed HttpServletRequest needs to upgrade its protocol
039 * from HTTP to something else
040 *
041 * @since 6.0
042 */
043public class AbstractUpgradeFilter extends WicketFilter
044{
045        public AbstractUpgradeFilter()
046        {
047                super();
048        }
049
050        public AbstractUpgradeFilter(WebApplication application)
051        {
052                super(application);
053        }
054
055        @Override
056        protected boolean processRequestCycle(final RequestCycle requestCycle, final WebResponse webResponse,
057                        final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse,
058                        final FilterChain chain)
059                throws IOException, ServletException
060        {
061                ThreadContext.setRequestCycle(requestCycle);
062                if (acceptWebSocket(httpServletRequest, httpServletResponse) || httpServletResponse.isCommitted())
063                {
064                        return true;
065                }
066                
067                return super.processRequestCycle(requestCycle, webResponse, httpServletRequest, httpServletResponse, chain);
068        }
069
070        protected boolean acceptWebSocket(HttpServletRequest req, HttpServletResponse resp)
071                        throws ServletException, IOException
072        {
073                // Information required to send the server handshake message
074                String key;
075                String subProtocol = null;
076
077                if (!headerContainsToken(req, "Upgrade", "websocket"))
078                {
079                        return false;
080                }
081
082                if (!headerContainsToken(req, "Connection", "upgrade"))
083                {
084                        resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
085                        return false;
086                }
087
088                if (!headerContainsToken(req, "Sec-websocket-version", "13"))
089                {
090                        resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); // http://tools.ietf.org/html/rfc6455#section-4.4
091                        resp.setHeader("Sec-WebSocket-Version", "13");
092                        return false;
093                }
094
095                key = req.getHeader("Sec-WebSocket-Key");
096                if (key == null)
097                {
098                        resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
099                        return false;
100                }
101
102                String origin = req.getHeader("Origin");
103                if (!verifyOrigin(origin))
104                {
105                        resp.sendError(HttpServletResponse.SC_FORBIDDEN);
106                        return false;
107                }
108
109                List<String> subProtocols = getTokensFromHeader(req, "Sec-WebSocket-Protocol-Client");
110                if (!subProtocols.isEmpty())
111                {
112                        subProtocol = selectSubProtocol(subProtocols);
113                }
114
115                if (subProtocol != null)
116                {
117                        resp.setHeader("Sec-WebSocket-Protocol", subProtocol);
118                }
119
120                return true;
121        }
122
123        /*
124         * This only works for tokens. Quoted strings need more sophisticated
125         * parsing.
126         */
127        private boolean headerContainsToken(HttpServletRequest req, String headerName, String target)
128        {
129                Enumeration<String> headers = req.getHeaders(headerName);
130                while (headers.hasMoreElements()) {
131                        String header = headers.nextElement();
132                        String[] tokens = Strings.split(header, ',');
133                        for (String token : tokens) {
134                                if (target.equalsIgnoreCase(token.trim())) {
135                                        return true;
136                                }
137                        }
138                }
139                return false;
140        }
141
142        /*
143         * This only works for tokens. Quoted strings need more sophisticated
144         * parsing.
145         */
146        protected List<String> getTokensFromHeader(HttpServletRequest req, String headerName)
147        {
148                List<String> result = new ArrayList<>();
149
150                Enumeration<String> headers = req.getHeaders(headerName);
151                while (headers.hasMoreElements()) {
152                        String header = headers.nextElement();
153                        String[] tokens = Strings.split(header, ',');
154                        for (String token : tokens) {
155                                result.add(token.trim());
156                        }
157                }
158                return result;
159        }
160
161        /**
162         * Intended to be overridden by sub-classes that wish to verify the origin
163         * of a WebSocket request before processing it.
164         *
165         * @param origin    The value of the origin header from the request which
166         *                  may be <code>null</code>
167         *
168         * @return  <code>true</code> to accept the request. <code>false</code> to
169         *          reject it. This default implementation always returns
170         *          <code>true</code>.
171         */
172        protected boolean verifyOrigin(String origin)
173        {
174                return true;
175        }
176
177        /**
178         * Intended to be overridden by sub-classes that wish to select a
179         * sub-protocol if the client provides a list of supported protocols.
180         *
181         * @param subProtocols  The list of sub-protocols supported by the client
182         *                      in client preference order. The server is under no
183         *                      obligation to respect the declared preference
184         * @return  <code>null</code> if no sub-protocol is selected or the name of
185         *          the protocol which <b>must</b> be one of the protocols listed by
186         *          the client. This default implementation always returns
187         *          <code>null</code>.
188         */
189        protected String selectSubProtocol(List<String> subProtocols)
190        {
191                return null;
192        }
193}