Gemini protocol server in C#
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

200 lines
4.5 KiB

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<byte> data = new List<byte>();
data.AddRange(Encoding.UTF8.GetBytes(this.Status.ToString() + ' ' + this.Meta + "\r\n"));
if (this.Body != null)
{
data.AddRange(this.Body);
}
return data.ToArray();
}
}
}