2015-07-07 17:11:11 +01:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections;
|
2016-07-23 18:53:02 +01:00
|
|
|
|
using System.Collections.Generic;
|
2015-07-07 17:11:11 +01:00
|
|
|
|
using System.Collections.Specialized;
|
|
|
|
|
using System.IO;
|
2016-07-23 18:53:02 +01:00
|
|
|
|
using System.Linq;
|
2015-07-07 17:11:11 +01:00
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Sockets;
|
2016-07-18 23:04:27 +01:00
|
|
|
|
using System.Text;
|
2015-07-07 17:11:11 +01:00
|
|
|
|
using System.Threading;
|
2016-07-18 23:04:27 +01:00
|
|
|
|
using System.Threading.Tasks;
|
2015-07-07 17:11:11 +01:00
|
|
|
|
using System.Web;
|
|
|
|
|
|
|
|
|
|
// offered to the public domain for any use with no restriction
|
2015-10-16 23:44:35 +01:00
|
|
|
|
// and also with no warranty of any kind, please enjoy. - David Jeske.
|
2015-07-07 17:11:11 +01:00
|
|
|
|
|
|
|
|
|
// simple HTTP explanation
|
|
|
|
|
// http://www.jmarshall.com/easy/http/
|
|
|
|
|
|
|
|
|
|
namespace SpotifyAPI.Web
|
|
|
|
|
{
|
2016-07-16 23:59:53 +01:00
|
|
|
|
public class HttpProcessor : IDisposable
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
2015-10-16 23:44:35 +01:00
|
|
|
|
private const int MaxPostSize = 10 * 1024 * 1024; // 10MB
|
2015-07-07 17:11:11 +01:00
|
|
|
|
private const int BufSize = 4096;
|
|
|
|
|
private readonly HttpServer _srv;
|
|
|
|
|
private Stream _inputStream;
|
2016-07-31 15:24:27 +01:00
|
|
|
|
private readonly Hashtable _httpHeaders = new Hashtable();
|
|
|
|
|
private string _httpMethod;
|
2016-03-31 11:08:23 +01:00
|
|
|
|
public string HttpProtocolVersionstring;
|
|
|
|
|
public string HttpUrl;
|
2015-07-07 17:11:11 +01:00
|
|
|
|
public StreamWriter OutputStream;
|
|
|
|
|
|
2016-07-16 23:59:53 +01:00
|
|
|
|
public HttpProcessor(HttpServer srv)
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
|
|
|
|
_srv = srv;
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-23 18:53:02 +01:00
|
|
|
|
private string[] GetIncomingRequest(Stream inputStream)
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
2016-07-23 18:53:02 +01:00
|
|
|
|
var buffer = new byte[4096];
|
|
|
|
|
var read = inputStream.Read(buffer, 0, buffer.Length);
|
|
|
|
|
|
|
|
|
|
var inputData = Encoding.ASCII.GetString(buffer.Take(read).ToArray());
|
|
|
|
|
return inputData.Split('\n').Select(s => s.Trim()).Where(s => !string.IsNullOrEmpty(s)).ToArray();
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
2016-07-18 23:04:27 +01:00
|
|
|
|
public void Process(TcpClient socket)
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
|
|
|
|
// 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
|
2016-07-16 23:59:53 +01:00
|
|
|
|
_inputStream = new BufferedStream(socket.GetStream());
|
2015-07-07 17:11:11 +01:00
|
|
|
|
|
|
|
|
|
// we probably shouldn't be using a streamwriter for all output from handlers either
|
2016-07-16 23:59:53 +01:00
|
|
|
|
OutputStream = new StreamWriter(new BufferedStream(socket.GetStream()));
|
2015-07-07 17:11:11 +01:00
|
|
|
|
try
|
|
|
|
|
{
|
2016-07-23 18:53:02 +01:00
|
|
|
|
var requestLines = GetIncomingRequest(_inputStream);
|
|
|
|
|
|
|
|
|
|
ParseRequest(requestLines.First());
|
|
|
|
|
ReadHeaders(requestLines.Skip(1));
|
|
|
|
|
|
2016-07-31 15:24:27 +01:00
|
|
|
|
if (_httpMethod.Equals("GET"))
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
|
|
|
|
HandleGetRequest();
|
|
|
|
|
}
|
2016-07-31 15:24:27 +01:00
|
|
|
|
else if (_httpMethod.Equals("POST"))
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
|
|
|
|
HandlePostRequest();
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-07-31 15:24:27 +01:00
|
|
|
|
catch (Exception)
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
|
|
|
|
WriteFailure();
|
|
|
|
|
}
|
|
|
|
|
OutputStream.Flush();
|
|
|
|
|
_inputStream = null;
|
2015-10-16 23:44:35 +01:00
|
|
|
|
OutputStream = null;
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
2016-07-23 18:53:02 +01:00
|
|
|
|
public void ParseRequest(string request)
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
|
|
|
|
string[] tokens = request.Split(' ');
|
|
|
|
|
if (tokens.Length < 2)
|
|
|
|
|
{
|
2015-10-16 23:31:01 +01:00
|
|
|
|
throw new Exception("Invalid HTTP request line");
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
2016-07-31 15:24:27 +01:00
|
|
|
|
_httpMethod = tokens[0].ToUpper();
|
2015-07-07 17:11:11 +01:00
|
|
|
|
HttpUrl = tokens[1];
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-23 18:53:02 +01:00
|
|
|
|
public void ReadHeaders(IEnumerable<string> requestLines)
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
2016-07-23 18:53:02 +01:00
|
|
|
|
foreach(var line in requestLines)
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
2016-03-31 11:08:23 +01:00
|
|
|
|
if (string.IsNullOrEmpty(line))
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int separator = line.IndexOf(':');
|
|
|
|
|
if (separator == -1)
|
|
|
|
|
{
|
2015-10-16 23:31:01 +01:00
|
|
|
|
throw new Exception("Invalid HTTP header line: " + line);
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
2016-03-31 11:08:23 +01:00
|
|
|
|
string name = line.Substring(0, separator);
|
2015-07-07 17:11:11 +01:00
|
|
|
|
int pos = separator + 1;
|
|
|
|
|
while ((pos < line.Length) && (line[pos] == ' '))
|
|
|
|
|
{
|
|
|
|
|
pos++; // strip any spaces
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string value = line.Substring(pos, line.Length - pos);
|
2016-07-31 15:24:27 +01:00
|
|
|
|
_httpHeaders[name] = value;
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2015-10-16 23:44:35 +01:00
|
|
|
|
// 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!
|
2015-07-07 17:11:11 +01:00
|
|
|
|
|
|
|
|
|
MemoryStream ms = new MemoryStream();
|
2016-07-31 15:24:27 +01:00
|
|
|
|
if (_httpHeaders.ContainsKey("Content-Length"))
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
2016-07-31 15:24:27 +01:00
|
|
|
|
var contentLen = Convert.ToInt32(_httpHeaders["Content-Length"]);
|
2015-07-07 17:11:11 +01:00
|
|
|
|
if (contentLen > MaxPostSize)
|
|
|
|
|
{
|
2015-10-28 16:05:09 +00:00
|
|
|
|
throw new Exception($"POST Content-Length({contentLen}) too big for this simple server");
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
2015-10-16 23:31:01 +01:00
|
|
|
|
throw new Exception("Client disconnected during post");
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
|
|
|
|
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("");
|
|
|
|
|
}
|
2016-07-16 23:59:53 +01:00
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
2016-07-31 15:24:27 +01:00
|
|
|
|
|
2016-07-16 23:59:53 +01:00
|
|
|
|
}
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2015-10-16 23:31:01 +01:00
|
|
|
|
GC.SuppressFinalize(this);
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Listen()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_listener = new TcpListener(IPAddress.Any, Port);
|
|
|
|
|
_listener.Start();
|
2016-07-16 23:59:53 +01:00
|
|
|
|
|
2016-07-20 08:56:05 +01:00
|
|
|
|
_listener.BeginAcceptTcpClient(AcceptTcpConnection, _listener);
|
2016-07-19 20:15:22 +01:00
|
|
|
|
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
2015-10-01 16:31:42 +01:00
|
|
|
|
catch (SocketException e)
|
2015-07-07 17:11:11 +01:00
|
|
|
|
{
|
2015-10-01 16:31:42 +01:00
|
|
|
|
if (e.ErrorCode != 10004) //Ignore 10004, which is thrown when the thread gets terminated
|
|
|
|
|
throw;
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-20 08:56:05 +01:00
|
|
|
|
private void AcceptTcpConnection(IAsyncResult ar)
|
|
|
|
|
{
|
|
|
|
|
TcpListener listener = (TcpListener)ar.AsyncState;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var tcpCLient = listener.EndAcceptTcpClient(ar);
|
|
|
|
|
using (HttpProcessor processor = new HttpProcessor(this))
|
|
|
|
|
{
|
|
|
|
|
processor.Process(tcpCLient);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (ObjectDisposedException)
|
|
|
|
|
{
|
|
|
|
|
// Ignore
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!IsActive)
|
|
|
|
|
return;
|
|
|
|
|
//listener.Start();
|
|
|
|
|
listener.BeginAcceptTcpClient(AcceptTcpConnection, listener);
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-07 17:11:11 +01:00
|
|
|
|
public abstract void HandleGetRequest(HttpProcessor p);
|
2015-10-16 23:44:35 +01:00
|
|
|
|
|
2015-07-07 17:11:11 +01:00
|
|
|
|
public abstract void HandlePostRequest(HttpProcessor p, StreamReader inputData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class AuthEventArgs
|
|
|
|
|
{
|
|
|
|
|
//Code can be an AccessToken or an Exchange Code
|
2016-03-31 11:08:23 +01:00
|
|
|
|
public string Code { get; set; }
|
2015-10-16 23:44:35 +01:00
|
|
|
|
|
2016-03-31 11:08:23 +01:00
|
|
|
|
public string TokenType { get; set; }
|
|
|
|
|
public string State { get; set; }
|
|
|
|
|
public string Error { get; set; }
|
2015-07-07 17:11:11 +01:00
|
|
|
|
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)
|
|
|
|
|
{
|
2016-03-31 11:08:23 +01:00
|
|
|
|
string url = p.HttpUrl;
|
2015-07-07 17:11:11 +01:00
|
|
|
|
url = url.Substring(2, url.Length - 2);
|
|
|
|
|
NameValueCollection col = HttpUtility.ParseQueryString(url);
|
|
|
|
|
if (col.Keys.Get(0) != "code")
|
|
|
|
|
{
|
2017-09-03 13:44:11 +01:00
|
|
|
|
p.OutputStream.WriteLine("<html><body><h1>Spotify Auth canceled!</h1></body></html>");
|
2015-07-07 17:11:11 +01:00
|
|
|
|
t = new Thread(o =>
|
|
|
|
|
{
|
2015-10-28 16:05:09 +00:00
|
|
|
|
OnAuth?.Invoke(new AuthEventArgs()
|
|
|
|
|
{
|
|
|
|
|
State = col.Get(1),
|
|
|
|
|
Error = col.Get(0),
|
|
|
|
|
});
|
2015-07-07 17:11:11 +01:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
p.OutputStream.WriteLine("<html><body><h1>Spotify Auth successful!</h1><script>window.close();</script></body></html>");
|
|
|
|
|
t = new Thread(o =>
|
|
|
|
|
{
|
2015-10-28 16:05:09 +00:00
|
|
|
|
OnAuth?.Invoke(new AuthEventArgs()
|
|
|
|
|
{
|
|
|
|
|
Code = col.Get(0),
|
|
|
|
|
State = col.Get(1)
|
|
|
|
|
});
|
2015-07-07 17:11:11 +01:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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>");
|
2016-07-23 18:53:02 +01:00
|
|
|
|
p.OutputStream.Flush();
|
|
|
|
|
p.OutputStream.Close();
|
2015-07-07 17:11:11 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
2016-03-31 11:08:23 +01:00
|
|
|
|
string url = p.HttpUrl;
|
2015-07-07 17:11:11 +01:00
|
|
|
|
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 =>
|
|
|
|
|
{
|
2015-10-28 16:05:09 +00:00
|
|
|
|
OnAuth?.Invoke(new AuthEventArgs()
|
|
|
|
|
{
|
|
|
|
|
Error = col.Get(0),
|
|
|
|
|
State = col.Get(1)
|
|
|
|
|
});
|
2015-07-07 17:11:11 +01:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
p.OutputStream.WriteLine("<html><body><h1>Spotify Auth successful!</h1><script>window.close();</script></body></html>");
|
|
|
|
|
t = new Thread(o =>
|
|
|
|
|
{
|
2015-10-28 16:05:09 +00:00
|
|
|
|
OnAuth?.Invoke(new AuthEventArgs()
|
|
|
|
|
{
|
|
|
|
|
Code = col.Get(0),
|
|
|
|
|
TokenType = col.Get(1),
|
|
|
|
|
ExpiresIn = Convert.ToInt32(col.Get(2)),
|
|
|
|
|
State = col.Get(3)
|
|
|
|
|
});
|
2015-07-07 17:11:11 +01:00
|
|
|
|
});
|
2016-07-23 18:53:02 +01:00
|
|
|
|
p.OutputStream.Flush();
|
|
|
|
|
p.OutputStream.Close();
|
2015-07-07 17:11:11 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2016-07-23 18:53:02 +01:00
|
|
|
|
|
|
|
|
|
|
2015-07-07 17:11:11 +01:00
|
|
|
|
t.Start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void HandlePostRequest(HttpProcessor p, StreamReader inputData)
|
|
|
|
|
{
|
|
|
|
|
p.WriteSuccess();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public enum AuthType
|
|
|
|
|
{
|
|
|
|
|
Implicit,
|
|
|
|
|
Authorization
|
|
|
|
|
}
|
|
|
|
|
}
|