using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace pyxis.gemini.Protocol { public class Response { private const int MAX_HEADER_LENGTH = 1029; public int Status { private set; get; } public string Meta { private set; get; } public byte[] Body { private set; get; } public void setStatus(int status) { this.Status = status; } public void setMeta(string meta) { this.Meta = meta; } public void setBody(byte[] body) { this.Body = body; } public static int Parse(byte[] data, int length, out Response response) { int i; bool found_cr = false, found_lf = false; response = null; // Minimum response length would be 6 bytes (status=2, space=1, meta=1, crlf=2) // Anything less just bail early. if (length < 6) { return 0; } for (i = 0; i < length; i++) { // If we haven't found the header terminating crlf by this point, then it can't be a valid response if (i >= MAX_HEADER_LENGTH) { throw new Exception.ProtocolViolationException(String.Format("Request header length exceeded maximum: %d", MAX_HEADER_LENGTH)); } if (data[i] == '\r') { found_cr = true; } if (found_cr && data[i] == '\n') { found_lf = true; break; } else { found_cr = false; } } if (!(found_cr && found_lf)) { return 0; } int code; string meta; (code, meta) = Response.ParseHeader(data, 0, i); response = new Response(); response.setStatus(code); response.setMeta(meta); return i + 1; } private static (int, string) ParseHeader(byte[] buffer, int start, int count) { int state = 0; // 0 - reading code // 1 - reading separator // 2 - reading meta // 3 - reading cr // 4 - reading lf int field_start = start; string code = ""; string meta = ""; for (int i = start; i <= start + count; i++) { switch (state) { case 0: // reading code if (buffer[i] == ' ') { if (field_start == i) { throw new Exception.ProtocolViolationException("Found separator in header before reading status code."); } code = Encoding.UTF8.GetString(buffer, field_start, i - field_start); field_start = i; state = 1; i--; } else { if (i - field_start > 2) { throw new Exception.ProtocolViolationException("Status code too long"); } } break; case 1: // reading separator if (buffer[i] != ' ') { field_start = i; state = 2; i--; } else { if (i - field_start > 0) { throw new Exception.ProtocolViolationException("Found multiple spaces in header separator."); } } break; case 2: // reading meta if (buffer[i] == '\r') { meta = Encoding.UTF8.GetString(buffer, field_start, i - field_start); field_start = i; state = 3; i--; } else { if (i - field_start > 1024) { throw new Exception.ProtocolViolationException("Meta field too long."); } } break; case 3: // reading cr if (buffer[i] == '\r') { state = 4; } else { throw new Exception.ProtocolViolationException("Could not find carriage return to terminate header."); } break; case 4: // reading lf if (buffer[i] == '\n') { state = 5; } else { throw new Exception.ProtocolViolationException("Could not find line feed to terminate header."); } break; case 5: // shouldn't get here, read past \r\n throw new Exception.ProtocolViolationException("Header contained data past terminated crlf."); break; } } return (int.Parse(code), meta); } public byte[] Serialize() { // TODO: Validate Meta is <=1024 bytes // TODO: Validate Meta is valid for specific response codes? // TODO: Validate no response body unless Status >=20 && <=29 List data = new List(); data.AddRange(Encoding.UTF8.GetBytes(this.Status.ToString() + ' ' + this.Meta + "\r\n")); if (this.Body != null) { data.AddRange(this.Body); } return data.ToArray(); } } }