001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.socket; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.LinkedHashMap; 022import java.util.List; 023import java.util.Locale; 024import java.util.Map; 025 026import org.springframework.lang.Nullable; 027import org.springframework.util.Assert; 028import org.springframework.util.CollectionUtils; 029import org.springframework.util.LinkedCaseInsensitiveMap; 030import org.springframework.util.StringUtils; 031 032/** 033 * Represents a WebSocket extension as defined in the RFC 6455. 034 * WebSocket extensions add protocol features to the WebSocket protocol. The extensions 035 * used within a session are negotiated during the handshake phase as follows: 036 * <ul> 037 * <li>the client may ask for specific extensions in the HTTP handshake request</li> 038 * <li>the server responds with the final list of extensions to use in the current session</li> 039 * </ul> 040 * 041 * <p>WebSocket Extension HTTP headers may include parameters and follow 042 * <a href="https://tools.ietf.org/html/rfc7230#section-3.2">RFC 7230 section 3.2</a></p> 043 * 044 * <p>Note that the order of extensions in HTTP headers defines their order of execution, 045 * e.g. extensions "foo, bar" will be executed as "bar(foo(message))".</p> 046 * 047 * @author Brian Clozel 048 * @author Juergen Hoeller 049 * @since 4.0 050 * @see <a href="https://tools.ietf.org/html/rfc6455#section-9">WebSocket Protocol Extensions, RFC 6455 - Section 9</a> 051 */ 052public class WebSocketExtension { 053 054 private final String name; 055 056 private final Map<String, String> parameters; 057 058 059 /** 060 * Create a WebSocketExtension with the given name. 061 * @param name the name of the extension 062 */ 063 public WebSocketExtension(String name) { 064 this(name, null); 065 } 066 067 /** 068 * Create a WebSocketExtension with the given name and parameters. 069 * @param name the name of the extension 070 * @param parameters the parameters 071 */ 072 public WebSocketExtension(String name, @Nullable Map<String, String> parameters) { 073 Assert.hasLength(name, "Extension name must not be empty"); 074 this.name = name; 075 if (!CollectionUtils.isEmpty(parameters)) { 076 Map<String, String> map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ENGLISH); 077 map.putAll(parameters); 078 this.parameters = Collections.unmodifiableMap(map); 079 } 080 else { 081 this.parameters = Collections.emptyMap(); 082 } 083 } 084 085 086 /** 087 * Return the name of the extension (never {@code null) or empty}. 088 */ 089 public String getName() { 090 return this.name; 091 } 092 093 /** 094 * Return the parameters of the extension (never {@code null}). 095 */ 096 public Map<String, String> getParameters() { 097 return this.parameters; 098 } 099 100 101 @Override 102 public boolean equals(@Nullable Object other) { 103 if (this == other) { 104 return true; 105 } 106 if (other == null || getClass() != other.getClass()) { 107 return false; 108 } 109 WebSocketExtension otherExt = (WebSocketExtension) other; 110 return (this.name.equals(otherExt.name) && this.parameters.equals(otherExt.parameters)); 111 } 112 113 @Override 114 public int hashCode() { 115 return this.name.hashCode() * 31 + this.parameters.hashCode(); 116 } 117 118 @Override 119 public String toString() { 120 StringBuilder str = new StringBuilder(); 121 str.append(this.name); 122 this.parameters.forEach((key, value) -> str.append(';').append(key).append('=').append(value)); 123 return str.toString(); 124 } 125 126 127 /** 128 * Parse the given, comma-separated string into a list of {@code WebSocketExtension} objects. 129 * <p>This method can be used to parse a "Sec-WebSocket-Extension" header. 130 * @param extensions the string to parse 131 * @return the list of extensions 132 * @throws IllegalArgumentException if the string cannot be parsed 133 */ 134 public static List<WebSocketExtension> parseExtensions(String extensions) { 135 if (StringUtils.hasText(extensions)) { 136 String[] tokens = StringUtils.tokenizeToStringArray(extensions, ","); 137 List<WebSocketExtension> result = new ArrayList<>(tokens.length); 138 for (String token : tokens) { 139 result.add(parseExtension(token)); 140 } 141 return result; 142 } 143 else { 144 return Collections.emptyList(); 145 } 146 } 147 148 private static WebSocketExtension parseExtension(String extension) { 149 if (extension.contains(",")) { 150 throw new IllegalArgumentException("Expected single extension value: [" + extension + "]"); 151 } 152 String[] parts = StringUtils.tokenizeToStringArray(extension, ";"); 153 String name = parts[0].trim(); 154 155 Map<String, String> parameters = null; 156 if (parts.length > 1) { 157 parameters = new LinkedHashMap<>(parts.length - 1); 158 for (int i = 1; i < parts.length; i++) { 159 String parameter = parts[i]; 160 int eqIndex = parameter.indexOf('='); 161 if (eqIndex != -1) { 162 String attribute = parameter.substring(0, eqIndex); 163 String value = parameter.substring(eqIndex + 1); 164 parameters.put(attribute, value); 165 } 166 } 167 } 168 169 return new WebSocketExtension(name, parameters); 170 } 171 172}