Spotify.NET/SpotifyAPI/Web/SimpleHttpServer.cs
mrnikbobjeff 10c8944e05 Replaced all occurances of checking whether a string is empty via str == "" with String.IsNullOrEmpty(str)
sealed StringAttribute because it is only contains text as attribute
sealed SpotifyWebAPI class as it implements IDisposable
Changed Exception catching where the Exception is not used to ignore the Exception object
Removed Console.WriteLine("Exception: " + e.Message) int HttpProcessor.Process as it is a library function which might not be used in a console application
2015-10-17 00:31:01 +02:00

359 lines
12 KiB
C#

using System;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Web;
// offered to the public domain for any use with no restriction
// and also with no warranty of any kind, please enjoy. - David Jeske.
// simple HTTP explanation
// http://www.jmarshall.com/easy/http/
namespace SpotifyAPI.Web
{
public class HttpProcessor
{
private const int MaxPostSize = 10*1024*1024; // 10MB
private const int BufSize = 4096;
private readonly TcpClient _socket;
private readonly HttpServer _srv;
private Stream _inputStream;
public Hashtable HttpHeaders = new Hashtable();
public String HttpMethod;
public String HttpProtocolVersionstring;
public String HttpUrl;
public StreamWriter OutputStream;
public HttpProcessor(TcpClient s, HttpServer srv)
{
_socket = s;
_srv = srv;
}
private string StreamReadLine(Stream inputStream)
{
string data = "";
while (true)
{
var nextChar = inputStream.ReadByte();
if (nextChar == '\n')
{
break;
}
if (nextChar == '\r')
{
continue;
}
if (nextChar == -1)
{
Thread.Sleep(1);
continue;
}
data += Convert.ToChar(nextChar);
}
return data;
}
public void Process()
{
// we can't use a StreamReader for input, because it buffers up extra data on us inside it's
// "processed" view of the world, and we want the data raw after the headers
_inputStream = new BufferedStream(_socket.GetStream());
// we probably shouldn't be using a streamwriter for all output from handlers either
OutputStream = new StreamWriter(new BufferedStream(_socket.GetStream()));
try
{
ParseRequest();
ReadHeaders();
if (HttpMethod.Equals("GET"))
{
HandleGetRequest();
}
else if (HttpMethod.Equals("POST"))
{
HandlePostRequest();
}
}
catch
{
WriteFailure();
}
OutputStream.Flush();
_inputStream = null;
OutputStream = null;
_socket.Close();
}
public void ParseRequest()
{
String request = StreamReadLine(_inputStream);
string[] tokens = request.Split(' ');
if (tokens.Length < 2)
{
throw new Exception("Invalid HTTP request line");
}
HttpMethod = tokens[0].ToUpper();
HttpUrl = tokens[1];
}
public void ReadHeaders()
{
String line;
while ((line = StreamReadLine(_inputStream)) != null)
{
if (String.IsNullOrEmpty(line))
{
return;
}
int separator = line.IndexOf(':');
if (separator == -1)
{
throw new Exception("Invalid HTTP header line: " + line);
}
String name = line.Substring(0, separator);
int pos = separator + 1;
while ((pos < line.Length) && (line[pos] == ' '))
{
pos++; // strip any spaces
}
string value = line.Substring(pos, line.Length - pos);
HttpHeaders[name] = value;
}
}
public void HandleGetRequest()
{
_srv.HandleGetRequest(this);
}
public void HandlePostRequest()
{
// this post data processing just reads everything into a memory stream.
// this is fine for smallish things, but for large stuff we should really
// hand an input stream to the request processor. However, the input stream
// we hand him needs to let him see the "end of the stream" at this content
// length, because otherwise he won't know when he's seen it all!
MemoryStream ms = new MemoryStream();
if (HttpHeaders.ContainsKey("Content-Length"))
{
var contentLen = Convert.ToInt32(HttpHeaders["Content-Length"]);
if (contentLen > MaxPostSize)
{
throw new Exception(String.Format("POST Content-Length({0}) too big for this simple server", contentLen));
}
byte[] buf = new byte[BufSize];
int toRead = contentLen;
while (toRead > 0)
{
int numread = _inputStream.Read(buf, 0, Math.Min(BufSize, toRead));
if (numread == 0)
{
if (toRead == 0)
{
break;
}
throw new Exception("Client disconnected during post");
}
toRead -= numread;
ms.Write(buf, 0, numread);
}
ms.Seek(0, SeekOrigin.Begin);
}
_srv.HandlePostRequest(this, new StreamReader(ms));
}
public void WriteSuccess(string contentType = "text/html")
{
OutputStream.WriteLine("HTTP/1.0 200 OK");
OutputStream.WriteLine("Content-Type: " + contentType);
OutputStream.WriteLine("Connection: close");
OutputStream.WriteLine("");
}
public void WriteFailure()
{
OutputStream.WriteLine("HTTP/1.0 404 File not found");
OutputStream.WriteLine("Connection: close");
OutputStream.WriteLine("");
}
}
public abstract class HttpServer : IDisposable
{
private TcpListener _listener;
protected int Port;
protected HttpServer(int port)
{
IsActive = true;
Port = port;
}
public bool IsActive { get; set; }
public void Dispose()
{
IsActive = false;
_listener.Stop();
Dispose();
GC.SuppressFinalize(this);
}
public void Listen()
{
try
{
_listener = new TcpListener(IPAddress.Any, Port);
_listener.Start();
while (IsActive)
{
TcpClient s = _listener.AcceptTcpClient();
HttpProcessor processor = new HttpProcessor(s, this);
Thread thread = new Thread(processor.Process);
thread.Start();
Thread.Sleep(1);
}
}
catch (SocketException e)
{
if (e.ErrorCode != 10004) //Ignore 10004, which is thrown when the thread gets terminated
throw;
}
}
public abstract void HandleGetRequest(HttpProcessor p);
public abstract void HandlePostRequest(HttpProcessor p, StreamReader inputData);
}
public class AuthEventArgs
{
//Code can be an AccessToken or an Exchange Code
public String Code { get; set; }
public String TokenType { get; set; }
public String State { get; set; }
public String Error { get; set; }
public int ExpiresIn { get; set; }
}
public class SimpleHttpServer : HttpServer
{
private readonly AuthType _type;
public delegate void AuthEventHandler(AuthEventArgs e);
public event AuthEventHandler OnAuth;
public SimpleHttpServer(int port, AuthType type) : base(port)
{
_type = type;
}
public override void HandleGetRequest(HttpProcessor p)
{
p.WriteSuccess();
if (p.HttpUrl == "/favicon.ico")
return;
Thread t;
if (_type == AuthType.Authorization)
{
String url = p.HttpUrl;
url = url.Substring(2, url.Length - 2);
NameValueCollection col = HttpUtility.ParseQueryString(url);
if (col.Keys.Get(0) != "code")
{
p.OutputStream.WriteLine("<html><body><h1>Spotify Auth canceled!</h1></body></html>");
t = new Thread(o =>
{
if(OnAuth != null)
OnAuth(new AuthEventArgs()
{
State = col.Get(1),
Error = col.Get(0),
});
});
}
else
{
p.OutputStream.WriteLine("<html><body><h1>Spotify Auth successful!</h1><script>window.close();</script></body></html>");
t = new Thread(o =>
{
if(OnAuth != null)
OnAuth(new AuthEventArgs()
{
Code = col.Get(0),
State = col.Get(1)
});
});
}
}
else
{
if (p.HttpUrl == "/")
{
p.OutputStream.WriteLine("<html><body>" +
"<script>" +
"" +
"var hashes = window.location.hash;" +
"hashes = hashes.replace('#','&');" +
"window.location = hashes" +
"</script>" +
"<h1>Spotify Auth successful!<br>Please copy the URL and paste it into the application</h1></body></html>");
return;
}
String url = p.HttpUrl;
url = url.Substring(2, url.Length - 2);
NameValueCollection col = HttpUtility.ParseQueryString(url);
if (col.Keys.Get(0) != "access_token")
{
p.OutputStream.WriteLine("<html><body><h1>Spotify Auth canceled!</h1></body></html>");
t = new Thread(o =>
{
if (OnAuth != null)
OnAuth(new AuthEventArgs()
{
Error = col.Get(0),
State = col.Get(1)
});
});
}
else
{
p.OutputStream.WriteLine("<html><body><h1>Spotify Auth successful!</h1><script>window.close();</script></body></html>");
t = new Thread(o =>
{
if (OnAuth != null)
OnAuth(new AuthEventArgs()
{
Code = col.Get(0),
TokenType = col.Get(1),
ExpiresIn = Convert.ToInt32(col.Get(2)),
State = col.Get(3)
});
});
}
}
t.Start();
}
public override void HandlePostRequest(HttpProcessor p, StreamReader inputData)
{
p.WriteSuccess();
}
}
public enum AuthType
{
Implicit,
Authorization
}
}