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 }