358 lines
12 KiB
C#
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.UrlEncode(_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();
|
||
|
}
|
||
|
}
|
||
|
}
|