diff --git a/.gitignore b/.gitignore index b36b3008..ece5322e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) +# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) [Bb]in/ [Oo]bj/ diff --git a/SpotifyAPI.Example/WebControl.cs b/SpotifyAPI.Example/WebControl.cs index 1c0f3a39..c7af858f 100644 --- a/SpotifyAPI.Example/WebControl.cs +++ b/SpotifyAPI.Example/WebControl.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Threading.Tasks; using System.Windows.Forms; using Image = System.Drawing.Image; @@ -26,14 +27,7 @@ namespace SpotifyAPI.Example InitializeComponent(); _savedTracks = new List(); - _auth = new ImplicitGrantAuth - { - RedirectUri = "http://localhost:8000", - ClientId = "26d287105e31491889f3cd293d85bfea", - Scope = Scope.UserReadPrivate | Scope.UserReadEmail | Scope.PlaylistReadPrivate | Scope.UserLibraryRead | Scope.UserReadPrivate | Scope.UserFollowRead | Scope.UserReadBirthdate | Scope.UserTopRead, - State = "XSS" - }; - _auth.OnResponseReceivedEvent += _auth_OnResponseReceivedEvent; + } private void _auth_OnResponseReceivedEvent(Token token, string state) @@ -57,7 +51,7 @@ namespace SpotifyAPI.Example AccessToken = token.AccessToken, TokenType = token.TokenType }; - InitialSetup(); + } private async void InitialSetup() @@ -129,8 +123,32 @@ namespace SpotifyAPI.Example private void authButton_Click(object sender, EventArgs e) { - _auth.StartHttpServer(8000); - _auth.DoAuth(); + Task.Run(() => RunAuthentication()); + } + + private async void RunAuthentication() + { + WebAPIFactory webApiFactory = new WebAPIFactory( + "http://localhost", + 8000, + "26d287105e31491889f3cd293d85bfea", + Scope.UserReadPrivate | Scope.UserReadEmail | Scope.PlaylistReadPrivate | Scope.UserLibraryRead | + Scope.UserReadPrivate | Scope.UserFollowRead | Scope.UserReadBirthdate | Scope.UserTopRead, + TimeSpan.FromSeconds(20)); + + try + { + _spotify = await webApiFactory.GetWebApi(); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message); + } + + if (_spotify == null) + return; + + InitialSetup(); } } } \ No newline at end of file diff --git a/SpotifyAPI/Local/SpotifyLocalAPI.cs b/SpotifyAPI/Local/SpotifyLocalAPI.cs index 701bc8c7..b6ca9004 100644 --- a/SpotifyAPI/Local/SpotifyLocalAPI.cs +++ b/SpotifyAPI/Local/SpotifyLocalAPI.cs @@ -8,7 +8,7 @@ using System.Timers; namespace SpotifyAPI.Local { - public class SpotifyLocalAPI + public class SpotifyLocalAPI : IDisposable { [DllImport("user32.dll")] private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo); @@ -49,7 +49,7 @@ namespace SpotifyAPI.Local private const int KeyeventfKeyup = 0x2; private readonly RemoteHandler _rh; - private readonly Timer _eventTimer; + private Timer _eventTimer; private StatusResponse _eventStatusResponse; public event EventHandler OnTrackChange; @@ -60,13 +60,17 @@ namespace SpotifyAPI.Local public event EventHandler OnTrackTimeChange; - public SpotifyLocalAPI() + public SpotifyLocalAPI(int timerIntervall = 50) { _rh = new RemoteHandler(); + AttachTimer(timerIntervall); + } + private void AttachTimer(int intervall) + { _eventTimer = new Timer { - Interval = 50, + Interval = intervall, AutoReset = false, Enabled = false }; @@ -333,5 +337,13 @@ namespace SpotifyAPI.Local Process.Start(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"spotify\spotifywebhelper.exe")); } } + + public void Dispose() + { + if (_eventTimer == null) + return; + _eventTimer.Enabled = false; + _eventTimer.Elapsed -= ElapsedTick; + } } } \ No newline at end of file diff --git a/SpotifyAPI/SpotifyAPI.csproj b/SpotifyAPI/SpotifyAPI.csproj index 3f025e4b..ad08e1e3 100644 --- a/SpotifyAPI/SpotifyAPI.csproj +++ b/SpotifyAPI/SpotifyAPI.csproj @@ -58,6 +58,7 @@ + diff --git a/SpotifyAPI/Web/Auth/WebApiFactory.cs b/SpotifyAPI/Web/Auth/WebApiFactory.cs new file mode 100644 index 00000000..cf1ccbea --- /dev/null +++ b/SpotifyAPI/Web/Auth/WebApiFactory.cs @@ -0,0 +1,87 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using SpotifyAPI.Web.Enums; +using SpotifyAPI.Web.Models; + +namespace SpotifyAPI.Web.Auth +{ + public class WebAPIFactory + { + private readonly string _redirectUrl; + private readonly int _listeningPort; + private readonly string _clientId; + private readonly TimeSpan _timeout; + private readonly Scope _scope; + + public WebAPIFactory(string redirectUrl, int listeningPort, string clientId, Scope scope, TimeSpan timeout) + { + _redirectUrl = redirectUrl; + _listeningPort = listeningPort; + _clientId = clientId; + _scope = scope; + _timeout = timeout; + } + + public Task GetWebApi() + { + var authentication = new ImplicitGrantAuth + { + RedirectUri = $"{_redirectUrl}:{_listeningPort}", + ClientId = _clientId, + Scope = _scope, + State = "XSS" + }; + + AutoResetEvent authenticationWaitFlag = new AutoResetEvent(false); + SpotifyWebAPI spotifyWebApi = null; + authentication.OnResponseReceivedEvent += (token, state) => + { + spotifyWebApi = HandleSpotifyResponse(state, token); + authenticationWaitFlag.Set(); + }; + + try + { + authentication.StartHttpServer(_listeningPort); + + authentication.DoAuth(); + + authenticationWaitFlag.WaitOne(_timeout); + if (spotifyWebApi == null) + throw new TimeoutException($"No valid response received for the last {_timeout.TotalSeconds} seconds"); + } + finally + { + authentication.StopHttpServer(); + } + + return Task.FromResult(spotifyWebApi); + } + + private static SpotifyWebAPI HandleSpotifyResponse(string state, Token token) + { + if (state != "XSS") + throw new SpotifyWebApiException($"Wrong state '{state}' received."); + + if (token.Error != null) + throw new SpotifyWebApiException($"Error: {token.Error}"); + + var spotifyWebApi = new SpotifyWebAPI + { + UseAuth = true, + AccessToken = token.AccessToken, + TokenType = token.TokenType + }; + + return spotifyWebApi; + } + } + + [Serializable] + public class SpotifyWebApiException : Exception + { + public SpotifyWebApiException(string message) : base(message) + { } + } +} diff --git a/SpotifyAPI/Web/SimpleHttpServer.cs b/SpotifyAPI/Web/SimpleHttpServer.cs index ffc297b4..571fb771 100644 --- a/SpotifyAPI/Web/SimpleHttpServer.cs +++ b/SpotifyAPI/Web/SimpleHttpServer.cs @@ -1,10 +1,14 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Collections.Specialized; using System.IO; +using System.Linq; using System.Net; using System.Net.Sockets; +using System.Text; using System.Threading; +using System.Threading.Tasks; using System.Web; // offered to the public domain for any use with no restriction @@ -33,34 +37,17 @@ namespace SpotifyAPI.Web _srv = srv; } - private string StreamReadLine(Stream inputStream) + private string[] GetIncomingRequest(Stream inputStream) { - string data = ""; - while (_isActive) - { - 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; + 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(); } - public void Process(object tcpClient) + public void Process(TcpClient socket) { - TcpClient socket = tcpClient as TcpClient; - // 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()); @@ -69,8 +56,11 @@ namespace SpotifyAPI.Web OutputStream = new StreamWriter(new BufferedStream(socket.GetStream())); try { - ParseRequest(); - ReadHeaders(); + var requestLines = GetIncomingRequest(_inputStream); + + ParseRequest(requestLines.First()); + ReadHeaders(requestLines.Skip(1)); + if (HttpMethod.Equals("GET")) { HandleGetRequest(); @@ -80,19 +70,17 @@ namespace SpotifyAPI.Web HandlePostRequest(); } } - catch + catch (Exception ex) { WriteFailure(); } OutputStream.Flush(); _inputStream = null; OutputStream = null; - socket.Close(); } - public void ParseRequest() + public void ParseRequest(string request) { - string request = StreamReadLine(_inputStream); string[] tokens = request.Split(' '); if (tokens.Length < 2) { @@ -102,10 +90,9 @@ namespace SpotifyAPI.Web HttpUrl = tokens[1]; } - public void ReadHeaders() + public void ReadHeaders(IEnumerable requestLines) { - string line; - while ((line = StreamReadLine(_inputStream)) != null) + foreach(var line in requestLines) { if (string.IsNullOrEmpty(line)) { @@ -219,16 +206,8 @@ namespace SpotifyAPI.Web _listener = new TcpListener(IPAddress.Any, Port); _listener.Start(); - using (HttpProcessor processor = new HttpProcessor(this)) - { - while (IsActive) - { - TcpClient s = _listener.AcceptTcpClient(); - Thread thread = new Thread(processor.Process); - thread.Start(s); - Thread.Sleep(1); - } - } + _listener.BeginAcceptTcpClient(AcceptTcpConnection, _listener); + } catch (SocketException e) { @@ -237,6 +216,28 @@ namespace SpotifyAPI.Web } } + 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); + } + public abstract void HandleGetRequest(HttpProcessor p); public abstract void HandlePostRequest(HttpProcessor p, StreamReader inputData); @@ -315,6 +316,8 @@ namespace SpotifyAPI.Web "window.location = hashes" + "" + "

Spotify Auth successful!
Please copy the URL and paste it into the application

"); + p.OutputStream.Flush(); + p.OutputStream.Close(); return; } string url = p.HttpUrl; @@ -345,8 +348,12 @@ namespace SpotifyAPI.Web State = col.Get(3) }); }); + p.OutputStream.Flush(); + p.OutputStream.Close(); } } + + t.Start(); }