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.api;
018
019import java.util.Collections;
020import java.util.Enumeration;
021import java.util.List;
022
023import javax.servlet.http.HttpServletRequest;
024
025import org.apache.wicket.util.lang.Args;
026import org.apache.wicket.util.string.Strings;
027
028/**
029 * This filter will reject those requests which contain 'Origin' header that does not match the origin of the
030 * application host. This kind of extended security might be necessary if the application needs to enforce the
031 * Same Origin Policy which is not provided by the HTML5 WebSocket protocol.
032 *
033 * @see <a href="http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html">http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html</a>
034 *
035 * @author Gergely Nagy
036 */
037public class WebSocketConnectionOriginFilter implements IWebSocketConnectionFilter
038{
039
040        /**
041         * Error code 1008 indicates that an endpoint is terminating the connection because it has received a message that
042     * violates its policy. This is a generic status code that can be returned when there is no other more suitable
043     * status code (e.g., 1003 or 1009) or if there is a need to hide specific details about the policy.
044         * <p>
045         * See <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1">RFC 6455, Section 7.4.1 Defined Status Codes</a>.
046         */
047        public static final int POLICY_VIOLATION_ERROR_CODE = 1008;
048
049        /**
050         * Explanatory text for the client to explain why the connection is getting aborted
051         */
052        public static final String ORIGIN_MISMATCH = "Origin mismatch";
053
054        private final List<String> allowedDomains;
055
056        public WebSocketConnectionOriginFilter(final List<String> allowedDomains)
057        {
058                this.allowedDomains = Args.notNull(allowedDomains, "allowedDomains");
059        }
060
061        @Override
062        public ConnectionRejected doFilter(HttpServletRequest servletRequest)
063        {
064                if (allowedDomains != null && !allowedDomains.isEmpty())
065                {
066                        String oUrl = getOriginUrl(servletRequest);
067                        if (invalid(oUrl, allowedDomains))
068                        {
069                                return new ConnectionRejected(POLICY_VIOLATION_ERROR_CODE, ORIGIN_MISMATCH);
070                        }
071                }
072
073                return null;
074        }
075
076        /**
077         * The list of whitelisted domains which are allowed to initiate a websocket connection. This
078         * list will be eventually used by the
079         * {@link org.apache.wicket.protocol.ws.api.IWebSocketConnectionFilter} to abort potentially
080         * unsafe connections. Example domain names might be:
081         *
082         * <pre>
083         *      http://www.example.com
084         *      http://ww2.example.com
085         * </pre>
086         *
087         * @param domains
088         *            The collection of domains
089         */
090        public void setAllowedDomains(Iterable<String> domains) {
091                this.allowedDomains.clear();
092                if (domains != null)
093                {
094                        for (String domain : domains)
095                        {
096                                this.allowedDomains.add(domain);
097                        }
098                }
099        }
100
101        /**
102         * The list of whitelisted domains which are allowed to initiate a websocket connection. This
103         * list will be eventually used by the
104         * {@link org.apache.wicket.protocol.ws.api.IWebSocketConnectionFilter} to abort potentially
105         * unsafe connections
106         */
107        public List<String> getAllowedDomains()
108        {
109                return allowedDomains;
110        }
111
112        private boolean invalid(String oUrl, List<String> allowedDomains)
113        {
114                return Strings.isEmpty(oUrl) || !allowedDomains.contains(oUrl);
115        }
116
117        private String getOriginUrl(final HttpServletRequest servletRequest)
118        {
119                Enumeration<String> originHeaderValues = servletRequest.getHeaders("Origin");
120                List<String> origins;
121                if (originHeaderValues != null)
122                {
123                        origins = Collections.list(originHeaderValues);
124                }
125                else
126                {
127                        origins = Collections.emptyList();
128                }
129
130                if (origins.size() != 1)
131                {
132                        return null;
133                }
134                return origins.get(0);
135        }
136}