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}