tesses.http/Tesses.Http/HttpPassthroughReverseProxy.cs

352 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Tesses.Http
{
public sealed class HTTPClientRequest
{
HttpParser p;
internal HTTPClientRequest(HttpParser parser)
{
p=parser;
}
public Dictionary<string,List<string>> Headers {get {return p.SentHeaders;}}
public void WriteRequestBody(string data)
{
WriteRequestBody(Encoding.UTF8.GetBytes(data));
}
public HTTPClientRequest WithHeader(string key,string value)
{
Headers.Add(key,value);
return this;
}
public HTTPClientRequest WithRange(long? start,long? end)
{
string _s="";
string _e="";
if(start.HasValue)
{
_s=start.Value.ToString();
}
if(end.HasValue)
{
_e =end.Value.ToString();
}
string ohdr=$"{_s}-{_e}";
WithHeader("Range",ohdr);
return this;
}
public void WriteRequestBody(object o)
{
WithContentType("application/json").WriteRequestBody(JsonConvert.SerializeObject(o));
}
public void WriteRequestBody(byte[] data)
{
WriteRequestBody(new MemoryStream(data));
}
public void WriteRequestBody(Stream strm)
{
//we need to write request body
Headers.Add("Content-Length",strm.Length.ToString());
p.SendHeaders();
p.WriteBody(strm);
p.ReceiveHeaders();
}
public HTTPClientRequest WithContentType(string content_type)
{
Headers.Add("Content-Type",content_type);
return this;
}
public void WriteUrlEncodedPost(Dictionary<string,List<string>> args)
{
string urlencoded= string.Join("&",args.Select<KeyValuePair<string,List<string>>,string>(e=>{
StringBuilder b=new StringBuilder();
foreach(var item in e.Value)
{
b.Append($"{e.Key}={WebUtility.UrlEncode(item)}");
}
return b.ToString();
}));
WithContentType("application/x-www-form-urlencoded").WriteRequestBody(urlencoded);
}
public void WriteEmptyRequest()
{
p.SendHeaders();
p.ReceiveHeaders();
}
public RequestLine RequestLine {get{return p.SentHeaders.FirstLine;} set{p.SentHeaders.FirstLine=value;}}
}
public sealed class HTTPClient : IDisposable
{
TcpClient client;
Stream strm;
HttpParser parser;
private HTTPClient(string method,string url)
{
string host="";
int port;
string hostHdr;
Uri uri=new Uri(url);
hostHdr = uri.Host;
host=hostHdr;
bool isSecure;
isSecure = uri.Scheme != "http" && uri.Scheme != "ws";
if(((uri.Scheme == "http") && uri.Port != 80 ) || ((uri.Scheme == "https") && uri.Port != 443))
{
hostHdr += $":{uri.Port}";
port = uri.Port;
}else{
if(uri.Scheme=="http")
{
port=80;
}
else{
port=443;
}
}
client=new TcpClient();
client.Connect(host,port);
if(isSecure)
{
var m=new SslStream(client.GetStream());
m.AuthenticateAsClient(host);
strm=m;
}else{
strm = client.GetStream();
}
parser=new HttpParser(strm);
parser.SentHeaders=new HeaderCollection();
parser.SentHeaders.FirstLine = new RequestLine(method,uri.PathAndQuery,"HTTP/1.1");
Request=new HTTPClientRequest(parser);
Response=new HTTPClientResponse(parser);
Request.Headers.Add("Host",hostHdr);
}
public static HTTPClient Open(string method,string url)
{
return new HTTPClient(method,url);
}
public HTTPClientRequest Request {get;private set;}
public HTTPClientResponse Response {get;private set;}
public void SendToServer(ServerContext ctx,bool replaceHost=false)
{
//Send Request to Server
foreach(var hdr in ctx.Request.Headers)
{
if(hdr.Key != "Host")
{
if(parser.SentHeaders.ContainsKey(hdr.Key))
{
parser.SentHeaders[hdr.Key].Clear();
}
foreach(var value in hdr.Value)
{
parser.SentHeaders.Add(hdr.Key,value);
}
}
}
if(replaceHost)
{
if(parser.SentHeaders.ContainsKey("Host") && ctx.Request.Headers.ContainsKey("Host"))
{
parser.SentHeaders["Host"].Clear();
parser.SentHeaders.Add("Host",ctx.Request.Headers["Host"][0]);
}
}
//we will send to the second server we are reverse proxing
//we need to set dest Method
RequestLine req=parser.SentHeaders.FirstLine;
parser.SentHeaders.FirstLine = new RequestLine(ctx.Request.RequestLine.Method,req.Path,req.HttpVersion);
parser.SendHeaders();
if(ctx.Request.Headers.ContainsKey("Content-Length") && ctx.Request.RequestLine.Method != "GET")
{
//not a get request and has content-length
//so we must copy it
parser.WriteBody(ctx.Request.GetRequestStream());
}
//we will write the response from HTTPClient to ServerContext
parser.ReceiveHeaders();
ctx.Response.Headers.Clear();
ctx.Response.StatusLine = parser.ReceivedHeaders.FirstLine;
foreach(var hdr in parser.ReceivedHeaders)
{
foreach(var value in hdr.Value)
{
ctx.Response.Headers.Add(hdr.Key,value);
}
}
ctx.p.SendHeaders();
if(ctx.Response.Headers.ContainsKey("Content-Type"))
{
ctx.p.WriteBody(parser.ReadBody());
}
}
public static HTTPClient Open(HTTPServer server,string method,string path)
{
string myUrl;
if(server.LocalEndPoint.Address.ToString()=="0.0.0.0")
{
//we can access via 127.0.0.1
myUrl = $"http://127.0.0.1:{server.LocalEndPoint.Port}/{path.TrimStart('/')}";
}else{
//we can access via X.X.X.X
myUrl = $"http://{server.LocalEndPoint.Address.ToString()}:{server.LocalEndPoint.Port}/{path.TrimStart('/')}";
}
return HTTPClient.Open(method,myUrl);
}
public void Dispose()
{
try{
if(strm != null)
strm.Dispose();
}catch(Exception ex)
{
_=ex;
}
try{
if(client != null)
client.Dispose();
}catch(Exception ex)
{
_=ex;
}
}
}
public class HTTPClientResponse
{
HttpParser p;
public HTTPClientResponse(HttpParser parser)
{
p=parser;
}
public Dictionary<string,List<string>> Headers {get{return p.ReceivedHeaders;}}
public StatusLine StatusLine {get{return p.ReceivedHeaders.FirstLine;} set{p.ReceivedHeaders.FirstLine=value;}}
public void DownloadFile(string file)
{
using(var f = File.Create(file))
{
DownloadFile(f);
}
}
public void DownloadFile(Stream strm)
{
var strm0=GetResponseStream();
strm0.CopyTo(strm);
}
public Stream GetResponseStream()
{
return p.ReadBody();
}
public string GetResponseString()
{
using(var sr=new StreamReader(GetResponseStream()))
{
return sr.ReadToEnd();
}
}
public T GetResponseJson<T>()
{
return JsonConvert.DeserializeObject<T>(GetResponseString());
}
}
public sealed class HTTPPassthroughReverseProxy
{
ServerContext ctx;
string host="";
int port;
string hostHdr;
bool useSrcHost;
bool isSecure;
string path;
public HTTPPassthroughReverseProxy(ServerContext ctx,string url,bool useSrcHost)
{
Uri uri=new Uri(url);
hostHdr = uri.Host;
host=hostHdr;
this.useSrcHost = useSrcHost;
isSecure = uri.Scheme != "http" && uri.Scheme != "ws";
if(((uri.Scheme == "http" || uri.Scheme == "ws") && uri.Port != 80 ) || ((uri.Scheme == "https" || uri.Scheme == "wss") && uri.Port != 443))
{
hostHdr += $":{uri.Port}";
port = uri.Port;
}else{
if(uri.Scheme=="http" || uri.Scheme == "ws")
{
port=80;
}
else{
port=443;
}
}
path = uri.PathAndQuery;
ctx.Request.RequestLine=new RequestLine(ctx.Request.RequestLine.Method,path,ctx.Request.RequestLine.HttpVersion);
this.ctx=ctx;
}
public void Exchange(CancellationToken token=default(CancellationToken))
{
using (TcpClient clt=new TcpClient())
{
if(!useSrcHost)
{
ctx.Request.Headers.Remove("Host");
ctx.Request.Headers.Add("Host",hostHdr);
}
clt.Connect(host,port);
Stream strm;
if(isSecure)
{
var m=new SslStream(clt.GetStream());
m.AuthenticateAsClient(host);
strm=m;
}else{
strm = clt.GetStream();
}
using(StreamPiper piper=new StreamPiper(ctx.GetRawStreamWithHeaders(),strm)){
piper.Pipe(token);
}
}
}
}
}