1 module hunt.http.codec.websocket.model.CloseInfo;
2 
3 import hunt.http.codec.websocket.model.CloseStatus;
4 import hunt.http.WebSocketStatusCode;
5 
6 import hunt.http.Exceptions;
7 import hunt.http.codec.websocket.frame.CloseFrame;
8 import hunt.http.WebSocketFrame;
9 
10 import hunt.io.BufferUtils;
11 import hunt.io.ByteBuffer;
12 
13 import std.algorithm;
14 import std.conv;
15 import std.format;
16 import std.utf;
17 
18 /**
19 */
20 class CloseInfo {
21     private int statusCode = 0;
22     private byte[] reasonBytes;
23 
24     this() {
25         this(StatusCode.NO_CODE, null);
26     }
27 
28     /**
29      * Parse the Close WebSocketFrame payload.
30      *
31      * @param payload  the raw close frame payload.
32      * @param validate true if payload should be validated per WebSocket spec.
33      */
34     this(ByteBuffer payload, bool validate) {
35         this.statusCode = StatusCode.NO_CODE;
36 
37         if ((payload is null) || (payload.remaining() == 0)) {
38             return; // nothing to do
39         }
40 
41         ByteBuffer data = payload.slice();
42         if ((data.remaining() == 1) && (validate)) {
43             throw new ProtocolException("Invalid 1 byte payload");
44         }
45 
46         if (data.remaining() >= 2) {
47             // Status Code
48             statusCode = 0; // start with 0
49             statusCode |= (data.get() & 0xFF) << 8;
50             statusCode |= (data.get() & 0xFF);
51 
52             if (validate) {
53                 assertValidStatusCode(statusCode);
54             }
55 
56             if (data.remaining() > 0) {
57                 // Reason (trimmed to max reason size)
58                 int len = std.algorithm.min(data.remaining(), CloseStatus.MAX_REASON_PHRASE);
59                 reasonBytes = new byte[len];
60                 data.get(reasonBytes, 0, len);
61 
62                 // Spec Requirement : throw BadPayloadException on invalid UTF8
63                 if (validate) {
64                     try {
65                         // Utf8StringBuilder utf = new Utf8StringBuilder();
66                         // // if this throws, we know we have bad UTF8
67                         // utf.append(reasonBytes, 0, reasonBytes.length);
68                         std.utf.validate(cast(string)reasonBytes);
69                     } catch (UTFException e) {
70                         throw new BadPayloadException("Invalid Close Reason", e);
71                     }
72                 }
73             }
74         }
75     }
76 
77     this(WebSocketFrame frame) {
78         this(frame.getPayload(), false);
79     }
80 
81     this(WebSocketFrame frame, bool validate) {
82         this(frame.getPayload(), validate);
83     }
84 
85     this(int statusCode) {
86         this(statusCode, null);
87     }
88 
89     /**
90      * Create a CloseInfo, trimming the reason to {@link CloseStatus#MAX_REASON_PHRASE} UTF-8 bytes if needed.
91      *
92      * @param statusCode the status code
93      * @param reason     the raw reason code
94      */
95     this(int statusCode, string reason) {
96         this.statusCode = statusCode;
97         if (reason !is null) {            
98             int len = CloseStatus.MAX_REASON_PHRASE;
99             if (reason.length > len) {
100                 this.reasonBytes = cast(byte[])reason[0..len].dup;
101             } else {
102                 this.reasonBytes = cast(byte[])reason.dup;
103             }
104         }
105     }
106 
107     private void assertValidStatusCode(int statusCode) {
108         // Status Codes outside of RFC6455 defined scope
109         if ((statusCode <= 999) || (statusCode >= 5000)) {
110             throw new ProtocolException("Out of range close status code: " ~ statusCode.to!string());
111         }
112 
113         // Status Codes not allowed to exist in a Close frame (per RFC6455)
114         if ((statusCode == StatusCode.NO_CLOSE) || 
115             (statusCode == StatusCode.NO_CODE) || 
116             (statusCode == StatusCode.FAILED_TLS_HANDSHAKE)) {
117             throw new ProtocolException("WebSocketFrame forbidden close status code: " ~ statusCode.to!string());
118         }
119 
120         // Status Code is in defined "reserved space" and is declared (all others are invalid)
121         if ((statusCode >= 1000) && (statusCode <= 2999) && !StatusCode.isTransmittable(statusCode)) {
122             throw new ProtocolException("RFC6455 and IANA Undefined close status code: " ~ statusCode.to!string());
123         }
124     }
125 
126     private ByteBuffer asByteBuffer() {
127         if ((statusCode == StatusCode.NO_CLOSE) || (statusCode == StatusCode.NO_CODE) || (statusCode == (-1))) {
128             // codes that are not allowed to be used in endpoint.
129             return null;
130         }
131 
132         int len = 2; // status code
133         bool hasReason = (this.reasonBytes !is null) && (this.reasonBytes.length > 0);
134         if (hasReason) {
135             len += this.reasonBytes.length;
136         }
137 
138         ByteBuffer buf = BufferUtils.allocate(len);
139         BufferUtils.flipToFill(buf);
140         buf.put(cast(byte) ((statusCode >>> 8) & 0xFF));
141         buf.put(cast(byte) ((statusCode >>> 0) & 0xFF));
142 
143         if (hasReason) {
144             buf.put(this.reasonBytes, 0, cast(int)this.reasonBytes.length);
145         }
146         BufferUtils.flipToFlush(buf, 0);
147 
148         return buf;
149     }
150 
151     CloseFrame asFrame() {
152         CloseFrame frame = new CloseFrame();
153         frame.setFin(true);
154         // WebSocketFrame forbidden codes result in no status code (and no reason string)
155         if ((statusCode != StatusCode.NO_CLOSE) && (statusCode != StatusCode.NO_CODE) && 
156                 (statusCode != StatusCode.FAILED_TLS_HANDSHAKE)) {
157             assertValidStatusCode(statusCode);
158             frame.setPayload(asByteBuffer());
159         }
160         return frame;
161     }
162 
163     string getReason() {
164         if (this.reasonBytes is null) {
165             return null;
166         }
167         return cast(string)(this.reasonBytes);
168     }
169 
170     int getStatusCode() {
171         return statusCode;
172     }
173 
174     bool isHarsh() {
175         return !((statusCode == StatusCode.NORMAL) || (statusCode == StatusCode.NO_CODE));
176     }
177 
178     bool isAbnormal() {
179         return (statusCode != StatusCode.NORMAL);
180     }
181 
182     override
183     string toString() {
184         return format("CloseInfo[code=%d,reason=%s]", statusCode, getReason());
185     }
186 }