tesses.http/Tesses.Http/ServerContext.cs

358 lines
12 KiB
C#

using System.Net;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Net.Http;
using System;
using System.Linq;
using System.Web;
using Newtonsoft.Json;
using System.Net.Mime;
using HeyRed.Mime;
namespace Tesses.Http
{
public class ServerContext
{
public ServerRequest Request {get;private set;}
public ServerResponse Response {get;private set;}
internal HttpParser p;
public Stream GetRawStreamWithHeaders()
{
return new PrependStream(Encoding.UTF8.GetBytes(p.ReceivedHeaders.ToString()),p.GetRawStream());
}
public Stream GetRawStream()
{
return p.GetRawStream();
}
public bool IsConnected {get{
if(_isConnected!=null) return _isConnected();
return false;
}}
Func<bool> _isConnected;
internal ServerContext(Func<bool> isConnected,HttpParser parser,IPEndPoint local,IPEndPoint remote)
{
_isConnected=isConnected;
p=parser;
Request=new ServerRequest(parser,remote);
Response=new ServerResponse(isConnected,Request.RequestLine,parser,local);
}
}
public class ServerResponse
{
RequestLine ln;
Func<bool> _isConnected;
string ogurl;
internal ServerResponse(Func<bool> isConnected,RequestLine line,HttpParser parser,IPEndPoint local)
{
ln=line;
Address=local;
_isConnected=isConnected;
ogurl = ln.Path.Split('?')[0];
this.parser=parser;
StatusLine=200;
}
HttpParser parser;
public IPEndPoint Address {get;set;}
public Dictionary<string,List<string>> Headers {get {return parser.SentHeaders;}}
public StatusLine StatusLine {get {return parser.SentHeaders.FirstLine;} set { parser.SentHeaders.FirstLine=value;}}
public ServerResponse WithContentType(string contentType)
{
Headers.Add("Content-Type",contentType);
return this;
}
public void SendStatusCodeHtml()
{
StringBuilder builder=new StringBuilder();
builder.AppendLine("<html>");
builder.AppendLine($"<head><title>{StatusLine.StatusCode} {StatusLine.GetReasonPhrase()}</title></head>");
builder.AppendLine("<body>");
builder.AppendLine($"<center><h1>{StatusLine.StatusCode} {StatusLine.GetReasonPhrase()}</h1></center>");
builder.AppendLine($"<center><h4>{ogurl}</h4></center>");
builder.AppendLine("<hr><center><a href=\"https://www.nuget.org/packages/Tesses.Http\">Tesses.Http</a></center>");
builder.AppendLine("</body>");
builder.AppendLine("</html>");
WithContentType("text/html").SendText(builder);
/*
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
*/
}
public ServerResponse WithFileName(string filename,bool inline=false)
{
ContentDisposition contentDisposition=new ContentDisposition();
contentDisposition.FileName=filename;
contentDisposition.Inline=inline;
Headers.Add("Content-Disposition",contentDisposition.ToString());
return this;
}
public ServerResponse WithHeader(string key,string value)
{
Headers.Add(key,value);
return this;
}
public void ServerSentEvents(SendEvents evt)
{
bool __connected=true;
if(_isConnected == null) return;
WithContentType("text/event-stream").WithHeader("Cache-Control","no-cache").parser.SendHeaders();
try{
EventHandler<SendEventArgs> cb= (sender,e0)=>{
if(__connected)
parser.WriteBody($"data: {e0.Data}\n\n");
};
evt.EventReceived += cb;
while(_isConnected());
evt.EventReceived -= cb;
__connected=false;
}catch(Exception ex)
{
_=ex;
}
}
public void SendText(StringBuilder b)
{
SendText(b.ToString());
}
public void SendJson(object o)
{
WithContentType("application/json").SendText(JsonConvert.SerializeObject(o));
}
public void SendText(string text)
{
MemoryStream strm=new MemoryStream();
using(var sw=new StreamWriter(strm))
{
sw.Write(text);
sw.Flush();
strm.Seek(0,SeekOrigin.Begin);
SendResponseStream(strm);
}
}
private string _getFileNameWithoutQuery(string url)
{
int r = url.IndexOf('?');
if(r > -1)
{
return Path.GetFileName(url.Remove(r).TrimEnd('/')) + (url.EndsWith("/") ? "/" : "");
}
return Path.GetFileName(url.TrimEnd('/')) + (url.EndsWith("/") ? "/" : "");
}
public ServerResponse WithContextTypeFromFileName(string filename)
{
return WithContentType(MimeTypesMap.GetMimeType(filename));
}
public void SendRedirect(string redirectTo)
{
StatusLine=301;
WithHeader("Location",redirectTo).WithHeader("Cache-Control","no-cache").parser.SendHeaders();
}
public void SendFileListing(string title,string desc,IEnumerable<string> entries)
{
if(!this.ln.Path.EndsWith("/"))
{
SendRedirect(this.ln.Path + '/');
return;
}
StringBuilder b=new StringBuilder();
b.Append($"<!doctype><html><head><title>{HttpUtility.HtmlEncode(title)}</title></head><body><center><h1>{HttpUtility.HtmlEncode(title)}</h1></center><center>{HttpUtility.HtmlEncode(desc)}</center><hr><a href=\"..\">../</a><br>");
foreach(var entry in entries)
{
//<a href=
b.Append($"<a href=\"{HttpUtility.HtmlAttributeEncode(entry)}\">{HttpUtility.HtmlEncode(_getFileNameWithoutQuery(entry))}</a><br>");
}
b.Append("<hr><br><center><a href=\"https://www.nuget.org/packages/Tesses.Http\">Tesses.Http</a></center></body></html>");
WithContentType("text/html").SendText(b);
}
public void SendFileAsDownload(string file)
{
WithFileName(Path.GetFileName(file)).SendFile(file);
}
public void SendFileAsDownload(VirtualStorage storage,string file)
{
WithFileName(Path.GetFileName(file)).SendFile(storage,file);
}
public void SendFile(string file)
{
using(var f = File.OpenRead(file))
{
WithContextTypeFromFileName(file).SendRangableResponseStream(f);
}
}
public void SendFile(VirtualStorage storage,string file)
{
using(var f = storage.Open(file,FileMode.Open,FileAccess.Read,FileShare.Read))
{
WithContextTypeFromFileName(file).SendRangableResponseStream(f);
}
}
public void SendResponseStream(Stream strm)
{
Headers.Add("Content-Length",strm.Length.ToString());
parser.SendHeaders();
if(ln.Method != "HEAD")
parser.WriteBody(strm);
}
public void SendRangableResponseStream(Stream strm)
{
if(parser.ReceivedHeaders.ContainsKey("Range") && strm.CanSeek)
{
int start = 0, end = (int)strm.Length - 1;
if (parser.ReceivedHeaders["Range"].Count > 1)
{
throw new NotSupportedException("Multiple 'Range' headers are not supported.");
}
var range = parser.ReceivedHeaders["Range"][0].Replace("bytes=", String.Empty)
.Split(new string[] { "-" }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => Int32.Parse(x))
.ToArray();
start = (range.Length > 0) ? range[0] : 0;
end = (range.Length > 1) ? range[1] : (int)(strm.Length - 1);
var hdrs = parser.SentHeaders;
hdrs.Add("Accept-Ranges", "bytes");
hdrs.Add("Content-Range", "bytes " + start + "-" + end + "/" + strm.Length);
hdrs.Add("Content-Length", (end - start + 1).ToString());
StatusLine = 206;
parser.SendHeaders();
if(ln.Method != "HEAD")
parser.WriteBody(new RangeStream(strm,start,end-start+1));
}else{
SendResponseStream(strm);
}
}
}
public class ServerRequest
{
public MultipartParser GetMultipartParser()
{
return new MultipartParser(this.parser);
}
public ServerRequest(HttpParser parser,IPEndPoint remote)
{
Address=remote;
this.parser=parser;
OriginalUrl = RequestLine.Path;
}
HttpParser parser;
public IPEndPoint Address {get;set;}
public string GetQueryParameters(string pathAndQuery,Dictionary<string,List<string>> p)
{
string[] qParm = pathAndQuery.Split(new char[]{'?'},2,StringSplitOptions.RemoveEmptyEntries);
if(qParm.Length == 0) return "";
if(qParm.Length == 1)
{
return qParm[0];
}else{
foreach(var kvp in qParm[1].Split(new char[]{'&'},StringSplitOptions.RemoveEmptyEntries))
{
string[] _kvp = kvp.Split(new char[]{'='},2,StringSplitOptions.RemoveEmptyEntries);
if(_kvp.Length > 0)
{
if(_kvp.Length == 2)
{
p.Add(_kvp[0],HttpUtility.UrlDecode(_kvp[1]));
}else{
p.Add(_kvp[0],"");
}
}
}
return qParm[0];
}
}
public string OriginalUrl {get;private set;}
public string CurrentUrl {get{return RequestLine.Path;} set{RequestLine=new RequestLine(RequestLine.Method,value,RequestLine.HttpVersion);}}
public string GetQueryParameters(Dictionary<string,List<string>> p)
{
return GetQueryParameters(RequestLine.Path,p);
}
public RequestLine RequestLine {get{return parser.ReceivedHeaders.FirstLine;} set {parser.ReceivedHeaders.FirstLine=value;}}
public Dictionary<string,List<string>> Headers {get {return parser.ReceivedHeaders;}}
public string GetRequestString()
{
using(var sr=new StreamReader(GetRequestStream()))
{
return sr.ReadToEnd();
}
}
public T GetRequestJson<T>()
{
return JsonConvert.DeserializeObject<T>(GetRequestString());
}
public void GetUrlEncodedPost(Dictionary<string,List<string>> p)
{
string urlData=$"{OriginalUrl}?{GetRequestString()}";
GetQueryParameters(urlData,p);
}
public Stream GetRequestStream()
{
return parser.ReadBody();
}
}
}