using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using TwitchEventSub.Types.EventSubSubscription; using System.Security.Cryptography; using System.Linq; namespace TwitchEventSub { public class Receiver { private OAuthToken token; private HttpListener server; static readonly HttpClient client = new HttpClient(); public Dictionary> psuedoResources { get; set; } = new Dictionary>(); private string hmacSecret = ""; private bool going = false; private Uri publicUrl; private Queue twitchogrambuffer { get; set; } private string clientId; private HMACSHA256 hmacsha256 = null; private List tasksToNotActuallyAwait = new List(); public Receiver(int port, string clientId, string clientSecret, string publicUrl) { this.clientId = clientId; this.publicUrl = new Uri(publicUrl + "/twitcherize"); twitchogrambuffer = new Queue(); server = new HttpListener(); server.Prefixes.Add($"http://*:{port}/"); authorizeClient(clientId, clientSecret); clearOldSubscriptions(); prepareHmac(); } private void prepareHmac() { var r = new Random(); var stringLength = r.Next(20, 100); var charChoices = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; for (int i = 0; i < stringLength; i++) { this.hmacSecret += charChoices[r.Next(charChoices.Length)]; } this.hmacsha256 = new HMACSHA256(Encoding.ASCII.GetBytes(this.hmacSecret)); } private void authorizeClient(string clientId, string clientSecret) { this.token = OAuthTokenGetter.DoIt(client, clientId, clientSecret); Console.WriteLine($"got a token"); client.DefaultRequestHeaders.Add("Client-ID", $"{this.clientId }"); client.DefaultRequestHeaders.Add("Authorization", $"Bearer {this.token.access_token}"); } private void clearOldSubscriptions() { var r = client.GetAsync("https://api.twitch.tv/helix/eventsub/subscriptions").Result; var responseText = ""; using (var streamReader = new StreamReader(r.Content.ReadAsStream())) { responseText = streamReader.ReadToEnd(); } if(!r.IsSuccessStatusCode) { Console.Error.WriteLine(responseText); throw new Exception(responseText); } var listing = SubscribableTypesTranslation.RefineTwitchResponse(responseText); var deletes = new List>(); foreach(var subscription in listing.data) { deletes.Add(client.DeleteAsync($"https://api.twitch.tv/helix/eventsub/subscriptions?id={subscription.id}")); } Task.WaitAll(deletes.ToArray()); } public void Subscribe(SubscribableTypes type, c condition, Action handler) where c : Types.Conditions.Condition { if (!going) { Task.Run(go); } var subreq = new Request() { condition = condition, transport = new Transport() { callback = publicUrl, secret = hmacSecret }, type = SubscribableTypesTranslation.Enum2String(type), version = "1" }; var subAsString = JsonConvert.SerializeObject(subreq); var content = new StringContent(subAsString, Encoding.UTF8, "application/json"); var r = client.PostAsync("https://api.twitch.tv/helix/eventsub/subscriptions", content).Result; string responseText; using (var streamReader = new StreamReader(r.Content.ReadAsStream())) { responseText = streamReader.ReadToEnd(); } var tr = SubscribableTypesTranslation.RefineTwitchResponse(responseText); var firstSubscription = tr.data?.FirstOrDefault(); if(firstSubscription != null) { psuedoResources.Add(firstSubscription, handler); } } public async Task go() { going = true; server.Start(); await Task.Run(() => { while (true) { HttpListenerContext context = server.GetContext(); HttpListenerResponse response = context.Response; string page = context.Request.Url.LocalPath.Substring(1); var resp = "no resource"; response.StatusCode = (int)HttpStatusCode.NotFound; lock (psuedoResources) { if (page == "twitcherize") { byte[] rawIncomingBytes = new byte[context.Request.ContentLength64]; var readBytes = context.Request.InputStream.Read(rawIncomingBytes, 0, (int)context.Request.ContentLength64); if (VerifySignature(context.Request.Headers["Twitch-Eventsub-Message-Signature"], context.Request.ContentEncoding, context.Request.Headers["Twitch-Eventsub-Message-Id"], context.Request.Headers["Twitch-Eventsub-Message-Timestamp"], rawIncomingBytes)) { try { if (twitchogrambuffer.Contains(context.Request.Headers["Twitch-Eventsub-Message-Id"])) { Console.WriteLine("duplicate message received. Ignoring."); } else { var incomingText = context.Request.ContentEncoding.GetString(rawIncomingBytes); //Console.WriteLine($"{context.Request.Headers["Twitch-Eventsub-Message-Id"]} looks new to me."); lock (twitchogrambuffer) { var fromTwitch = SubscribableTypesTranslation.RefineTwitchogram(incomingText); if (fromTwitch.challenge != null) { resp = fromTwitch.challenge; Console.WriteLine("challenge responded to, should be subscribed"); } else if (fromTwitch.subscription.status == "authorization_revoked") { resp = "ok... :("; Console.WriteLine($"auth revoked for {fromTwitch.subscription.type} ({fromTwitch.subscription.id})"); } else { var foundAHandler = false; foreach (var kvp in psuedoResources) { if (kvp.Key.id == fromTwitch.subscription.id) { kvp.Value(fromTwitch); resp = ":)"; foundAHandler = true; break; } } if (!foundAHandler) { Console.WriteLine($"I don't have a handler for {fromTwitch.subscription.type} ({fromTwitch.subscription.id})? o_O"); resp = ":S"; } } } tasksToNotActuallyAwait.Add(logMessageId(context.Request.Headers["Twitch-Eventsub-Message-Id"])); response.StatusCode = (int)HttpStatusCode.OK; } } catch (Exception e) { Console.Error.WriteLine("something went wrong calling resource."); Console.Error.WriteLine(JsonConvert.SerializeObject(e)); response.StatusCode = (int)HttpStatusCode.InternalServerError; resp = "error :("; } } else { Console.WriteLine("couldn't verify signature."); response.StatusCode = (int)HttpStatusCode.Forbidden; resp = "VoteNay"; } } } if(response.StatusCode != (int)HttpStatusCode.OK) { Console.WriteLine($"returning status code {response.StatusCode}"); } byte[] buffer = Encoding.UTF8.GetBytes(resp); response.ContentLength64 = buffer.Length; Stream st = response.OutputStream; st.Write(buffer, 0, buffer.Length); response.Close(); } }); going = false; } private async Task logMessageId(string id) { lock(twitchogrambuffer) { twitchogrambuffer.Enqueue(id); } await Task.Delay(2000); string x; lock(twitchogrambuffer) { twitchogrambuffer.TryDequeue(out x); } } private bool VerifySignature(string signature, Encoding encoder, string messageId, string timestamp, byte[] rawIncomingBytes) { var hashables = new List(); hashables.Add(encoder.GetBytes(messageId + timestamp).Concat(rawIncomingBytes).ToArray()); hashables.Add(encoder.GetBytes(messageId.ToLower() + timestamp).Concat(rawIncomingBytes).ToArray()); hashables.Add(encoder.GetBytes(messageId.ToLower() + timestamp.ToLower()).Concat(rawIncomingBytes).ToArray()); hashables.Add(encoder.GetBytes(messageId + timestamp.ToLower()).Concat(rawIncomingBytes).ToArray()); var computeds = new List(); foreach(var hashable in hashables) { computeds.Add("sha256=" + Convert.ToHexString(hmacsha256.ComputeHash(hashable)).ToLower()); // var hashed = computeds.Last(); // if(signature?.ToLower() == hashed) // { // Console.Write(" * "); // } // else // { // Console.Write(" "); // } // Console.WriteLine(computeds.Last()); } return computeds.Contains(signature?.ToLower()); //Console.WriteLine($"{signature?.ToLower()} == sha256={computed}?"); //return signature?.ToLower() == $"sha256={computed}"; } } }