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}