using Hamakaze.Headers; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; namespace Hamakaze { public class HttpResponseMessage : HttpMessage { public override string ProtocolVersion { get; } public int StatusCode { get; } public string StatusMessage { get; } public override IEnumerable Headers { get; } public override Stream Body { get; } public string Connection => Headers.FirstOrDefault(x => x.Name == HttpConnectionHeader.NAME)?.Value.ToString() ?? string.Empty; public string Server => Headers.FirstOrDefault(x => x.Name == HttpServerHeader.NAME)?.Value.ToString() ?? string.Empty; public DateTimeOffset Date => Headers.Where(x => x.Name == HttpDateHeader.NAME).Cast().FirstOrDefault()?.DateTime ?? DateTimeOffset.MinValue; public HttpMediaType ContentType => Headers.Where(x => x.Name == HttpContentTypeHeader.NAME).Cast().FirstOrDefault()?.MediaType ?? HttpMediaType.OctetStream; public Encoding ResponseEncoding => Encoding.GetEncoding(ContentType.GetParamValue(@"charset") ?? @"iso8859-1"); public IEnumerable ContentEncodings => Headers.Where(x => x.Name == HttpContentEncodingHeader.NAME).Cast().FirstOrDefault()?.Encodings ?? Enumerable.Empty(); public IEnumerable TransferEncodings => Headers.Where(x => x.Name == HttpTransferEncodingHeader.NAME).Cast().FirstOrDefault()?.Encodings ?? Enumerable.Empty(); public HttpResponseMessage( int statusCode, string statusMessage, string protocolVersion, IEnumerable headers, Stream body ) { ProtocolVersion = protocolVersion ?? throw new ArgumentNullException(nameof(protocolVersion)); StatusCode = statusCode; StatusMessage = statusMessage ?? string.Empty; Headers = (headers ?? throw new ArgumentNullException(nameof(headers))).ToArray(); OwnsBodyStream = true; Body = body; } public byte[] GetBodyBytes() { if(Body == null) return null; if(Body is MemoryStream msBody) return msBody.ToArray(); using MemoryStream ms = new(); if(Body.CanSeek) Body.Seek(0, SeekOrigin.Begin); Body.CopyTo(ms); return ms.ToArray(); } public string GetBodyString() { byte[] bytes = GetBodyBytes(); return bytes == null || bytes.Length < 1 ? string.Empty : ResponseEncoding.GetString(bytes); } // there's probably a less stupid way to do this, be my guest and call me an idiot private static void ProcessEncoding(Stack encodings, Stream stream, bool transfer) { using MemoryStream temp = new(); bool inTemp = false; while(encodings.TryPop(out string encoding)) { Stream target = (inTemp = !inTemp) ? temp : stream, source = inTemp ? stream : temp; target.SetLength(0); source.Seek(0, SeekOrigin.Begin); switch(encoding) { case HttpEncoding.GZIP: case HttpEncoding.XGZIP: using(GZipStream gzs = new(source, CompressionMode.Decompress, true)) gzs.CopyTo(target); break; case HttpEncoding.DEFLATE: using(DeflateStream def = new(source, CompressionMode.Decompress, true)) def.CopyTo(target); break; case HttpEncoding.BROTLI: if(transfer) goto default; using(BrotliStream br = new(source, CompressionMode.Decompress, true)) br.CopyTo(target); break; case HttpEncoding.IDENTITY: break; case HttpEncoding.CHUNKED: if(!transfer) goto default; throw new IOException(@"Invalid use of chunked encoding type in Transfer-Encoding header."); default: throw new IOException(@"Unsupported encoding supplied."); } } if(inTemp) { stream.SetLength(0); temp.Seek(0, SeekOrigin.Begin); temp.CopyTo(stream); } } public static HttpResponseMessage ReadFrom(Stream stream, Action onProgress = null) { // ignore this function, it doesn't exist string readLine() { const ushort crlf = 0x0D0A; using MemoryStream ms = new(); int byt; ushort lastTwo = 0; for(;;) { byt = stream.ReadByte(); if(byt == -1 && ms.Length == 0) throw new IOException(@"readLine: There is no data."); ms.WriteByte((byte)byt); lastTwo <<= 8; lastTwo |= (byte)byt; if(lastTwo == crlf) { ms.SetLength(ms.Length - 2); break; } } return Encoding.ASCII.GetString(ms.ToArray()); } long contentLength = -1; Stack transferEncodings = null; Stack contentEncodings = null; // Read initial header string line = readLine(); if(line == null) throw new IOException(@"Failed to read initial HTTP header."); if(!line.StartsWith(@"HTTP/")) throw new IOException($@"Response is not a valid HTTP message: {line}."); string[] parts = line[5..].Split(' ', 3); if(!int.TryParse(parts.ElementAtOrDefault(1), out int statusCode)) throw new IOException(@"Invalid HTTP status code format."); string protocolVersion = parts.ElementAtOrDefault(0); string statusMessage = parts.ElementAtOrDefault(2); // Read header key-value pairs List headers = new(); while((line = readLine()) != null) { if(string.IsNullOrWhiteSpace(line)) break; parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if(parts.Length < 2) throw new IOException(@"Invalid HTTP header in response."); string hName = HttpHeader.NormaliseName(parts.ElementAtOrDefault(0) ?? string.Empty), hValue = parts.ElementAtOrDefault(1); if(string.IsNullOrEmpty(hName)) throw new IOException(@"Invalid HTTP header name."); HttpHeader header = HttpHeader.Create(hName, hValue); if(header is HttpContentLengthHeader hclh) contentLength = (long)hclh.Value; else if(header is HttpTransferEncodingHeader hteh) transferEncodings = new Stack(hteh.Encodings); else if(header is HttpContentEncodingHeader hceh) contentEncodings = new Stack(hceh.Encodings); headers.Add(header); } if(statusCode is < 200 or 201 or 204 or 205) contentLength = 0; Stream body = null; long totalRead = 0; const int buffer_size = 8192; byte[] buffer = new byte[buffer_size]; int read; void readBuffer(long length = -1) { if(length == 0) return; long remaining = length; int bufferRead = buffer_size; if(bufferRead > length) bufferRead = (int)length; if(totalRead < 1) onProgress?.Invoke(0, contentLength); while((read = stream.Read(buffer, 0, bufferRead)) > 0) { body.Write(buffer, 0, read); totalRead += read; onProgress?.Invoke(totalRead, contentLength); if(length >= 0) { remaining -= read; if(remaining < 1) break; if(bufferRead > remaining) bufferRead = (int)remaining; } } } // Read body if(transferEncodings != null && transferEncodings.Any() && transferEncodings.Peek() == HttpEncoding.CHUNKED) { // oh no the poop is chunky transferEncodings.Pop(); body = new MemoryStream(); while((line = readLine()) != null) { if(string.IsNullOrWhiteSpace(line)) break; if(!int.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int chunkLength)) throw new IOException(@"Failed to decode chunk length."); if(chunkLength == 0) // final chunk break; readBuffer(chunkLength); readLine(); } readLine(); } else if(contentLength != 0) { body = new MemoryStream(); readBuffer(contentLength); } if(body != null) // Check if body is empty and null it again if so if(body.Length == 0) { body.Dispose(); body = null; } else { if(transferEncodings != null) ProcessEncoding(transferEncodings, body, true); if(contentEncodings != null) ProcessEncoding(contentEncodings, body, false); body.Seek(0, SeekOrigin.Begin); } return new HttpResponseMessage(statusCode, statusMessage, protocolVersion, headers, body); } } }