001/*
002 * This file is part of the JDrupes non-blocking HTTP Codec
003 * Copyright (C) 2016, 2017  Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Lesser General Public License as published
007 * by the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
013 * License for more details.
014 *
015 * You should have received a copy of the GNU Lesser General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.httpcodec.protocols.websocket;
020
021import java.io.UnsupportedEncodingException;
022import java.security.MessageDigest;
023import java.security.NoSuchAlgorithmException;
024import java.security.SecureRandom;
025import java.util.Base64;
026import java.util.Optional;
027
028import org.jdrupes.httpcodec.Decoder;
029import org.jdrupes.httpcodec.Encoder;
030import org.jdrupes.httpcodec.ProtocolException;
031import org.jdrupes.httpcodec.plugin.UpgradeProvider;
032import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpStatus;
033import org.jdrupes.httpcodec.protocols.http.HttpField;
034import org.jdrupes.httpcodec.protocols.http.HttpRequest;
035import org.jdrupes.httpcodec.protocols.http.HttpResponse;
036import org.jdrupes.httpcodec.types.Converters;
037
038/**
039 * A protocol provider for the WebSocket protocol.
040 * 
041 * The web socket protocol is an upgrade from the HTTP protocol.
042 * 
043 * ![WsProtocolProvider](WsProtocolProvider.svg)
044 * 
045 * @startuml WsProtocolProvider.svg
046 * 
047 * class ProtocolProvider
048 * 
049 * class WsProtocolProvider {
050 *      +boolean supportsProtocol(String protocol)
051 *      +void augmentInitialResponse(HttpResponse response)
052 *      +Encoder<?> createRequestEncoder(String protocol)
053 *      +Decoder<?,?> createRequestDecoder(String protocol)
054 *      +Encoder<?> createResponseEncoder(String protocol)
055 *      +ResponseDecoder<?,?> createResponseDecoder(String protocol)
056 * }
057 * 
058 * ProtocolProvider <|-- WsProtocolProvider
059 * 
060 * @enduml
061 * 
062 */
063public class WsProtocolProvider extends UpgradeProvider {
064
065        private static SecureRandom random = new SecureRandom();
066        
067        /* (non-Javadoc)
068         * @see ProtocolProvider#supportsProtocol(java.lang.String)
069         */
070        @Override
071        public boolean supportsProtocol(String protocol) {
072                return protocol.equalsIgnoreCase("websocket");
073        }
074
075        /* (non-Javadoc)
076         * @see org.jdrupes.httpcodec.plugin.UpgradeProvider#augmentInitialRequest
077         */
078        @Override
079        public void augmentInitialRequest(HttpRequest request) {
080                Optional<HttpField<Long>> version = request.findField(
081                                "Sec-WebSocket-Version", Converters.LONG);
082                if (version.isPresent() && version.get().value() != 13) {
083                        // Sombody else's job...
084                        return;
085                }
086                request.setField(new HttpField<>(
087                                "Sec-WebSocket-Version", 13L, Converters.LONG));
088                if (!request.findField("Sec-WebSocket-Key", Converters.UNQUOTED_STRING)
089                                .isPresent()) {
090                        byte[] randomBytes = new byte[16];
091                        random.nextBytes(randomBytes);
092                        request.setField(new HttpField<String>("Sec-WebSocket-Key",
093                                        Base64.getEncoder().encodeToString(randomBytes), 
094                                        Converters.UNQUOTED_STRING));
095                }
096        }
097
098        /* (non-Javadoc)
099         * @see org.jdrupes.httpcodec.plugin.UpgradeProvider#checkSwitchingResponse
100         */
101        @Override
102        public void checkSwitchingResponse(HttpRequest request, 
103                        HttpResponse response) throws ProtocolException {
104                Optional<String> accept = response.findStringValue(
105                                "Sec-WebSocket-Accept");
106                if (!accept.isPresent()) {
107                        throw new ProtocolException(
108                                        "Header field Sec-WebSocket-Accept is missing.");
109                }
110                String wsKey = request.findStringValue("Sec-WebSocket-Key").get();
111                String magic = wsKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
112                try {
113                        MessageDigest crypt = MessageDigest.getInstance("SHA-1");
114                        byte[] sha1 = crypt.digest(magic.getBytes("ascii"));
115                        String expected = Base64.getEncoder().encodeToString(sha1);
116                        if (!accept.get().equals(expected)) {
117                                throw new ProtocolException(
118                                                "Invalid value in Sec-WebSocket-Accept header field.");
119                        }
120                } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
121                        throw new ProtocolException(e);
122                }
123        }
124
125        /* (non-Javadoc)
126         * @see ProtocolProvider#augmentInitialResponse
127         */
128        @Override
129        public void augmentInitialResponse(HttpResponse response) {
130                Optional<String> wsKey = response.request()
131                        .flatMap(r -> r.findStringValue("Sec-WebSocket-Key"));
132                if (!wsKey.isPresent()) {
133                        response.setStatus(HttpStatus.BAD_REQUEST)
134                                .setHasPayload(false).clearHeaders();
135                        return;
136                }
137                // RFC 6455 4.1
138                if(response.request().flatMap(r -> r.findField(
139                                "Sec-WebSocket-Version", Converters.LONG))
140                                .map(HttpField<Long>::value).orElse(-1L) != 13) {
141                        response.setStatus(HttpStatus.BAD_REQUEST)
142                                .setHasPayload(false).clearHeaders();
143                        // RFC 6455 4.4
144                        response.setField(new HttpField<>(
145                                        "Sec-WebSocket-Version", 13L, Converters.LONG));
146                        return;
147                        
148                }
149                String magic = wsKey.get() + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
150                try {
151                        MessageDigest crypt = MessageDigest.getInstance("SHA-1");
152                        byte[] sha1 = crypt.digest(magic.getBytes("ascii"));
153                        String accept = Base64.getEncoder().encodeToString(sha1);
154                        response.setField(new HttpField<String>(
155                                        "Sec-WebSocket-Accept", accept, Converters.UNQUOTED_STRING));
156                } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
157                        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR)
158                                .setHasPayload(false).clearHeaders();
159                        return;
160                }
161        }
162
163        /* (non-Javadoc)
164         * @see ProtocolProvider#createRequestEncoder()
165         */
166        @Override
167        public Encoder<?, ?> createRequestEncoder(String protocol) {
168                return new WsEncoder(true);
169        }
170
171        /* (non-Javadoc)
172         * @see ProtocolProvider#createRequestDecoder()
173         */
174        @Override
175        public Decoder<?, ?> createRequestDecoder(String protocol) {
176                return new WsDecoder();
177        }
178
179        /* (non-Javadoc)
180         * @see ProtocolProvider#createResponseEncoder()
181         */
182        @Override
183        public Encoder<?, ?> createResponseEncoder(String protocol) {
184                return new WsEncoder(false);
185        }
186
187        /* (non-Javadoc)
188         * @see ProtocolProvider#createResponseDecoder()
189         */
190        @Override
191        public Decoder<?, ?> createResponseDecoder(String protocol) {
192                return new WsDecoder();
193        }
194
195}