This repository has been archived on 2023-10-16. You can view files and clone it, but cannot push or open issues or pull requests.
hamakaze/Hamakaze/HttpResponseMessage.cs

266 lines
10 KiB
C#

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<HttpHeader> 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<HttpDateHeader>().FirstOrDefault()?.DateTime ?? DateTimeOffset.MinValue;
public HttpMediaType ContentType
=> Headers.Where(x => x.Name == HttpContentTypeHeader.NAME).Cast<HttpContentTypeHeader>().FirstOrDefault()?.MediaType
?? HttpMediaType.OctetStream;
public Encoding ResponseEncoding
=> Encoding.GetEncoding(ContentType.GetParamValue(@"charset") ?? @"iso8859-1");
public IEnumerable<string> ContentEncodings
=> Headers.Where(x => x.Name == HttpContentEncodingHeader.NAME).Cast<HttpContentEncodingHeader>().FirstOrDefault()?.Encodings
?? Enumerable.Empty<string>();
public IEnumerable<string> TransferEncodings
=> Headers.Where(x => x.Name == HttpTransferEncodingHeader.NAME).Cast<HttpTransferEncodingHeader>().FirstOrDefault()?.Encodings
?? Enumerable.Empty<string>();
public HttpResponseMessage(
int statusCode, string statusMessage, string protocolVersion,
IEnumerable<HttpHeader> 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<string> 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<long, long> 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<string> transferEncodings = null;
Stack<string> 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<HttpHeader> 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<string>(hteh.Encodings);
else if(header is HttpContentEncodingHeader hceh)
contentEncodings = new Stack<string>(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);
}
}
}