11,7 → 11,11 |
// FIXME deny write access to all public properties |
// FIXME for text/xml get encoding from body if not specified in header |
// FIXME option to not store parsed data, just raw packets and reparse on demand |
// FIXME implement chunk-encoding |
// FIXME implement all others (not 'identity' and 'chunked') transfer-encodings |
// FIXME benchmark one-by-one byte iterator vs. block read |
// FIXME recongize "100 (Continue)" response, use next (real) response as part of same |
// request-response interaction |
// FIXME split HttpMessage class to request and response |
namespace TCPproxy |
{ |
public enum BinLogTypes |
54,9 → 58,11 |
V1_1 |
} |
|
public enum HttpEncoding |
public enum HttpTransferEncoding |
{ |
Identify, |
None, // there is no body |
Identity, |
Chunked, |
Gzip, |
Compress, |
Deflate, |
63,6 → 69,16 |
Unknown |
} |
|
public enum HttpContentEncoding |
{ |
None, // there is no body |
Identity, |
Gzip, |
Compress, |
Deflate, |
Unknown |
} |
|
public class TcpLogEventArgs : EventArgs |
{ |
private readonly LogLevel level; |
407,11 → 423,6 |
} |
} |
|
public override string ToString() // FIXME delete the method |
{ |
return id + " " + startTimestamp.ToString("HH:mm:ss.ffff"); |
} |
|
protected void SetLocalPoint(IPEndPoint localPoint) |
{ |
this.localPoint = localPoint; |
1382,17 → 1393,14 |
{ |
tcp = newTcp; |
tcpLen = tcp.Length; |
tcpPos = 0; |
tcpPos = -1; |
if(nextMessageEvent != null) nextMessageEvent.Set(); |
} |
} |
|
public byte CurrentOctet() |
public void Back() |
{ |
if(tcp == null || tcpPos >= tcpLen) NextTcp(); |
if(tcpEnd) return 0; |
|
return tcp.Bytes[tcpPos]; |
tcpPos--; // FIXME double check if it's always possible to go to _one_ step back |
} |
|
public byte NextOctet() |
1399,12 → 1407,24 |
{ |
tcpPos++; |
|
if(tcp == null || tcpPos >= tcpLen) NextTcp(); |
if(tcp == null || tcpPos >= tcpLen) |
{ |
NextTcp(); |
tcpPos++; |
} |
if(tcpEnd) return 0; |
|
return tcp.Bytes[tcpPos]; |
} |
|
public byte NextOctetAndBack() |
{ |
byte b = NextOctet(); |
if(!tcpEnd) Back(); |
|
return b; |
} |
|
public void SetDirection(TcpMessageDirection direction) |
{ |
do |
1422,7 → 1442,7 |
if(!tcpEnd) |
{ |
tcpLen = tcp.Length; |
tcpPos = 0; |
tcpPos = -1; |
} |
} |
} |
1500,7 → 1520,7 |
SetRequestProperties(http); |
http.UpdateHttpMessage(); |
|
bool fullLength = ParseBody(requestPos, http, true); |
ParseBody(requestPos, http, true); |
if("text" == http.RequestContentType && "xml" == http.RequestContentSubtype) |
{ |
http.RequestXml = new XmlMessage(http.RequestText); |
1507,7 → 1527,6 |
} |
http.UpdateHttpMessage(); |
|
if(fullLength) requestPos.NextOctet(); |
http.RequestComplete = true; |
http.UpdateHttpMessage(); |
|
1566,7 → 1585,7 |
SetResponseProperties(http); |
http.UpdateHttpMessage(); |
|
bool fullLength = ParseBody(responsePos, http, false); |
ParseBody(responsePos, http, false); |
if("text" == http.ResponseContentType && "xml" == http.ResponseContentSubtype) |
{ |
http.ResponseXml = new XmlMessage(http.ResponseText); |
1573,7 → 1592,6 |
} |
http.UpdateHttpMessage(); |
|
if(fullLength) responsePos.NextOctet(); |
http.ResponseComplete = true; |
http.UpdateHttpMessage(); |
} |
1589,11 → 1607,12 |
StringBuilder res = new StringBuilder(100); |
int len = 0; |
|
for(byte b = pos.CurrentOctet(); !pos.IsEnd && tokenChars[b]; b = pos.NextOctet()) |
for(byte b = pos.NextOctet(); !pos.IsEnd && tokenChars[b]; b = pos.NextOctet()) |
{ |
res.Append(charValues[b]); |
if(limit > 0 && limit < ++len) return null; // length limit |
} |
pos.Back(); |
|
return res.ToString(); |
} |
1603,11 → 1622,12 |
StringBuilder res = new StringBuilder(1024); |
int len = 0; |
|
for(byte b = pos.CurrentOctet(); !pos.IsEnd && b != 32 && b != 13 && b != 10; b = pos.NextOctet()) |
for(byte b = pos.NextOctet(); !pos.IsEnd && b != 32 && b != 13 && b != 10; b = pos.NextOctet()) |
{ // <space> <cr> <lf> |
res.Append(charValues[b]); |
if(limit > 0 && limit < ++len) return null; // length limit |
} |
pos.Back(); |
|
return res.ToString(); |
} |
1617,11 → 1637,12 |
StringBuilder res = new StringBuilder(1024); |
int len = 0; |
|
for(byte b = pos.CurrentOctet(); !pos.IsEnd && b != 13; b = pos.NextOctet()) |
for(byte b = pos.NextOctet(); !pos.IsEnd && b != 13; b = pos.NextOctet()) |
{ // <cr> |
res.Append(charValues[b]); |
if(limit > 0 && limit < ++len) return null; // length limit |
} |
pos.Back(); |
|
return res.ToString(); |
} |
1628,27 → 1649,25 |
|
private void ExpectSpace(ParsePosition pos) |
{ |
if(pos.IsEnd || pos.CurrentOctet() != 32) |
if(pos.IsEnd || pos.NextOctet() != 32) |
throw new HttpParseException("Space expected"); |
|
pos.NextOctet(); |
} |
|
private void ExpectCRLF(ParsePosition pos) |
{ |
if(pos.IsEnd || pos.CurrentOctet() != 13) |
if(pos.IsEnd || pos.NextOctet() != 13) |
throw new HttpParseException("Carriage return expected"); |
if(pos.IsEnd || pos.NextOctet() != 10) |
throw new HttpParseException("Linefeed expected"); |
|
pos.NextOctet(); |
} |
|
private void SkipEmptyLines(ParsePosition pos) |
{ |
while(pos.CurrentOctet() == 13) |
while(pos.NextOctetAndBack() == 13) |
{ |
ExpectCRLF(pos); |
} |
} |
|
private void ParseRequestLine(ParsePosition pos, HttpMessage http) |
{ |
1667,25 → 1686,17 |
throw new HttpParseException("Unexpected end of message"); |
|
// EoL or version |
byte b = pos.CurrentOctet(); |
byte b = pos.NextOctet(); |
if(b == 13) |
{ |
if(pos.IsEnd || pos.NextOctet() != 10) |
{ |
throw new HttpParseException("Linefeed expected"); |
} |
else |
{ |
if(!pos.IsEnd) ExpectCRLF(pos); |
pos.Back(); |
ExpectCRLF(pos); |
http.RequestVersion = HttpVersion.V0_9; |
return; |
} |
} |
else if(b != 32) |
{ |
throw new HttpParseException("HTTP version expected"); |
} |
pos.NextOctet(); |
|
// check version |
string versionStr = GetUntilEoL(pos, 20); |
1717,18 → 1728,11 |
if(pos.IsEnd) |
throw new HttpParseException("Unexpected end of message"); |
|
if(pos.CurrentOctet() == 13) |
if(pos.NextOctetAndBack() == 13) |
{ |
if(pos.IsEnd || pos.NextOctet() != 10) |
{ |
throw new HttpParseException("Linefeed expected"); |
ExpectCRLF(pos); |
return; // end of header, move to body |
} |
else |
{ |
pos.NextOctet(); // end of header, move to body |
return; |
} |
} |
if(pos.IsEnd) return; // end of TCP messages |
|
string name = GetToken(pos, 0); |
1735,10 → 1739,9 |
if(name == null || name.Length == 0) |
throw new HttpParseException("Request header name expected"); |
|
if(pos.IsEnd || pos.CurrentOctet() != 58) // : |
if(pos.IsEnd || pos.NextOctet() != 58) // : |
throw new HttpParseException("Request header value expected"); |
|
pos.NextOctet(); |
string s = TrimHeaderValue(GetUntilEoL(pos, 0)); |
|
ExpectCRLF(pos); |
1838,34 → 1841,59 |
return buf.ToString(); |
} |
|
private HttpEncoding ParseEncoding(string encoding) |
private HttpTransferEncoding ParseTransferEncoding(string encoding) |
{ |
if(encoding == null || encoding == "identity") |
return HttpEncoding.Identify; |
if(encoding == null) return HttpTransferEncoding.None; |
encoding = encoding.ToLower(); |
|
if(encoding == "identity") |
return HttpTransferEncoding.Identity; |
else if(encoding == "chunked") |
return HttpTransferEncoding.Chunked; |
else if(encoding == "gzip") |
return HttpEncoding.Gzip; |
return HttpTransferEncoding.Gzip; |
else if(encoding == "compress") |
return HttpEncoding.Compress; |
return HttpTransferEncoding.Compress; |
else if(encoding == "deflate") |
return HttpEncoding.Deflate; |
return HttpTransferEncoding.Deflate; |
else |
return HttpEncoding.Unknown; |
return HttpTransferEncoding.Unknown; |
} |
|
private HttpContentEncoding ParseContentEncoding(string encoding) |
{ |
if(encoding == null) return HttpContentEncoding.None; |
encoding = encoding.ToLower(); |
|
if(encoding == "identity") |
return HttpContentEncoding.Identity; |
else if(encoding == "gzip") |
return HttpContentEncoding.Gzip; |
else if(encoding == "compress") |
return HttpContentEncoding.Compress; |
else if(encoding == "deflate") |
return HttpContentEncoding.Deflate; |
else |
return HttpContentEncoding.Unknown; |
} |
|
private void SetRequestProperties(HttpMessage http) |
{ |
// length |
string contentLength = (string)http.RequestHeadersHash["Content-Length"]; |
string contentLength = (string)http.RequestHeadersHash["content-length"]; |
if(contentLength != null) |
{ |
http.RequestLength = int.Parse(contentLength); |
} |
|
// encoding |
http.RequestEncoding = ParseEncoding((string)http.RequestHeadersHash["Content-Encoding"]); |
// transfer encoding |
http.RequestTransferEncoding = ParseTransferEncoding((string)http.RequestHeadersHash["transfer-encoding"]); |
|
// content encoding |
http.RequestContentEncoding = ParseContentEncoding((string)http.RequestHeadersHash["content-encoding"]); |
|
// type & charset |
string contentType = (string)http.RequestHeadersHash["Content-Type"]; |
string contentType = (string)http.RequestHeadersHash["content-type"]; |
if(contentType != null) |
{ |
Match match = Regex.Match(contentType, @"^\s*(\S+)/(\S+)\s*($|;\s*(charset=""?(\S+)""?)?)"); |
1885,22 → 1913,76 |
} |
} |
|
private bool ParseBody(ParsePosition pos, HttpMessage http, bool request) |
// RFC 2616: 4.3 |
private bool WaitForBody(HttpMessage http, bool isRequest) |
{ |
if(request && http.RequestMethod != "POST") return false; |
if(isRequest) |
{ |
return (http.RequestLength > 0) |
|| (http.RequestTransferEncoding != HttpTransferEncoding.None); |
} |
else |
{ |
if(http.RequestMethod == "HEAD") return false; |
if(http.ResponseStatusCode < 200) return false; |
if(http.ResponseStatusCode == 204) return false; |
if(http.ResponseStatusCode == 304) return false; |
|
return true; |
} |
} |
|
private void ParseBody(ParsePosition pos, HttpMessage http, bool isRequest) |
{ |
if(!WaitForBody(http, isRequest)) return; |
|
// FIXME parse and save on-the-fly, dont wait util end of message |
|
byte[] bin = new byte[8*1024]; |
int len = 0; |
int limit = (request ? http.RequestLength : http.ResponseLength); |
HttpEncoding encoding = (request ? http.RequestEncoding : http.ResponseEncoding); |
string contentType = (request ? http.RequestContentType : http.ResponseContentType); |
string contentSubtype = (request ? http.RequestContentSubtype : http.ResponseContentSubtype); |
string charset = (request ? http.RequestCharset : http.ResponseCharset); |
int len = 0; // current bin biffer length |
int limit = (isRequest ? http.RequestLength : http.ResponseLength); |
int chunkLen = -1; // current length of current chunk |
int chunkLimit = -1; |
HttpTransferEncoding transferEncoding = (isRequest ? http.RequestTransferEncoding : http.ResponseTransferEncoding); |
HttpContentEncoding contentEncoding = (isRequest ? http.RequestContentEncoding : http.ResponseContentEncoding); |
string contentType = (isRequest ? http.RequestContentType : http.ResponseContentType); |
string contentSubtype = (isRequest ? http.RequestContentSubtype : http.ResponseContentSubtype); |
string charset = (isRequest ? http.RequestCharset : http.ResponseCharset); |
|
for(byte b = pos.CurrentOctet(); !pos.IsEnd; b = pos.NextOctet()) |
for(byte b = pos.NextOctet(); !pos.IsEnd; b = pos.NextOctet()) |
{ |
// RFC 2616: 3.6.1 |
if(transferEncoding == HttpTransferEncoding.Chunked && chunkLimit < 0) |
{ |
// FIXME recognize chunk-extension here |
|
// get chunk length |
pos.Back(); |
string chunkLimitStr = GetUntilEoL(pos, 40); |
if(pos.IsEnd || chunkLimitStr == null || chunkLimitStr.Length == 0) |
throw new HttpParseException("Chunk length expected"); |
|
try |
{ |
chunkLimit = Convert.ToInt32(chunkLimitStr, 16); |
} |
catch(Exception) |
{ |
throw new HttpParseException("Cannot parse chunk length"); |
} |
|
ExpectCRLF(pos); |
|
if(chunkLimit == 0) { // the end marker |
ExpectCRLF(pos); |
break; |
} |
|
chunkLen = 0; |
b = pos.NextOctet(); |
} |
|
// grow array if full |
if(len >= bin.Length) |
{ |
byte[] newBin = new byte[bin.Length*2]; |
1909,14 → 1991,32 |
} |
|
bin[len++] = b; |
if(limit > 0 && limit <= len) // full length |
chunkLen++; |
if(chunkLimit > 0) |
{ |
if(chunkLen >= chunkLimit) // full chunk |
{ |
ExpectCRLF(pos); |
chunkLimit = -1; // try to find length of next chunk on next iteration |
} |
} |
else |
{ |
if(limit > 0 && len >= limit) // full length |
{ |
break; |
} |
} |
} |
if(transferEncoding == HttpTransferEncoding.Chunked && chunkLimit > 0 && chunkLen != chunkLimit) |
{ |
throw new HttpParseException("Incomplete chunk found"); |
} |
|
// FIXME parse entity-headers for chunked encoding here |
|
string text = null; |
if(encoding == HttpEncoding.Identify && contentType == "text") |
if(contentEncoding == HttpContentEncoding.Identity && contentType == "text") |
{ |
try |
{ |
1925,11 → 2025,11 |
} |
catch(NotSupportedException) |
{ |
Console.WriteLine("Unsupported encoding: " + charset); |
Console.WriteLine("Unsupported charset: " + charset); |
} |
} |
|
if(request) |
if(isRequest) |
{ |
http.RequestLength = len; |
http.RequestBody = bin; |
1941,8 → 2041,6 |
http.ResponseBody = bin; |
http.ResponseText = text; |
} |
|
return (limit > 0 && limit <= len); // full length reached, need to go to next octet |
} |
|
private void ParseResponseLine(ParsePosition pos, HttpMessage http) |
1995,18 → 2093,22 |
private void SetResponseProperties(HttpMessage http) |
{ |
// length |
HttpHeader contentLength = (HttpHeader)http.ResponseHeadersHash["Content-Length"]; |
HttpHeader contentLength = (HttpHeader)http.ResponseHeadersHash["content-length"]; |
if(contentLength != null) |
{ |
http.ResponseLength = int.Parse(contentLength.Values[0]); |
} |
|
// encoding |
HttpHeader contentEncoding = (HttpHeader)http.ResponseHeadersHash["Content-Encoding"]; |
http.ResponseEncoding = ParseEncoding((contentEncoding == null) ? null : contentEncoding.Values[0]); |
// transfer encoding |
HttpHeader transferEncoding = (HttpHeader)http.ResponseHeadersHash["transfer-encoding"]; |
http.ResponseTransferEncoding = ParseTransferEncoding((transferEncoding == null) ? null : transferEncoding.Values[0]); |
|
// content encoding |
HttpHeader contentEncoding = (HttpHeader)http.ResponseHeadersHash["content-encoding"]; |
http.ResponseContentEncoding = ParseContentEncoding((contentEncoding == null) ? null : contentEncoding.Values[0]); |
|
// type & charset |
HttpHeader contentType = (HttpHeader)http.ResponseHeadersHash["Content-Type"]; |
HttpHeader contentType = (HttpHeader)http.ResponseHeadersHash["content-type"]; |
if(contentType != null) |
{ |
Match match = Regex.Match(contentType.Values[0], @"^\s*(\S+)/(\S+)\s*($|;\s*(charset=""?(\S+)""?)?)"); |
2072,7 → 2174,8 |
private LinkedList requestHeaders = new LinkedList(); |
private Hashtable requestHeadersHash = new Hashtable(); |
private int requestLength = -1; // -1 == unknown |
private HttpEncoding requestEncoding = HttpEncoding.Identify; |
private HttpTransferEncoding requestTransferEncoding = HttpTransferEncoding.None; |
private HttpContentEncoding requestContentEncoding = HttpContentEncoding.None; |
private string requestContentType; |
private string requestContentSubtype; |
private string requestCharset; |
2089,7 → 2192,8 |
private LinkedList responseHeaders = new LinkedList(); |
private Hashtable responseHeadersHash = new Hashtable(); |
private int responseLength = -1; // -1 == unknown |
private HttpEncoding responseEncoding = HttpEncoding.Identify; |
private HttpTransferEncoding responseTransferEncoding = HttpTransferEncoding.None; |
private HttpContentEncoding responseContentEncoding = HttpContentEncoding.None; |
private string responseContentType; |
private string responseContentSubtype; |
private string responseCharset; |
2138,12 → 2242,18 |
set { requestLength = value; } |
} |
|
public HttpEncoding RequestEncoding |
public HttpTransferEncoding RequestTransferEncoding |
{ |
get { return requestEncoding; } |
set { requestEncoding = value; } |
get { return requestTransferEncoding; } |
set { requestTransferEncoding = value; } |
} |
|
public HttpContentEncoding RequestContentEncoding |
{ |
get { return requestContentEncoding; } |
set { requestContentEncoding = value; } |
} |
|
public string RequestContentType |
{ |
get { return requestContentType; } |
2232,12 → 2342,18 |
set { responseLength = value; } |
} |
|
public HttpEncoding ResponseEncoding |
public HttpTransferEncoding ResponseTransferEncoding |
{ |
get { return responseEncoding; } |
set { responseEncoding = value; } |
get { return responseTransferEncoding; } |
set { responseTransferEncoding = value; } |
} |
|
public HttpContentEncoding ResponseContentEncoding |
{ |
get { return responseContentEncoding; } |
set { responseContentEncoding = value; } |
} |
|
public string ResponseContentType |
{ |
get { return responseContentType; } |
2283,7 → 2399,7 |
public void AddRequestHeader(string name, string headerValue) |
{ |
requestHeaders.Add(new HttpHeader(name, headerValue)); |
requestHeadersHash.Add(name, headerValue); |
requestHeadersHash.Add(name.ToLower(), headerValue); |
} |
|
public void AddResponseHeader(string name, string headerValue) |
2293,7 → 2409,7 |
{ |
header = new HttpHeader(name, headerValue); |
responseHeaders.Add(header); |
responseHeadersHash.Add(name, header); |
responseHeadersHash.Add(name.ToLower(), header); |
} |
else |
{ |
2301,12 → 2417,6 |
} |
} |
|
public override string ToString() // FIXME delete the method |
{ |
return (soapAction != null ? soapAction |
: (requestMethod == null ? "" : requestMethod) + " " + (requestUri == null ? "" : requestUri)); |
} |
|
public event TcpEventHandler Update; |
|
protected virtual void OnUpdate(TcpEventArgs e) |