twitch-agent/TwitchEventSub/Receiver.cs
Adam R. Grey 3f717df1d4 psuedoinitial
actually I had a previous repo but my dumb ass let the real appsettings sneak in
2021-10-24 01:13:07 -04:00

269 lines
12 KiB
C#

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<Subscription, Action<Twitchogram>> psuedoResources { get; set; }
= new Dictionary<Subscription, Action<Twitchogram>>();
private string hmacSecret = "";
private bool going = false;
private Uri publicUrl;
private Queue<string> twitchogrambuffer { get; set; }
private string clientId;
private HMACSHA256 hmacsha256 = null;
private List<Task> tasksToNotActuallyAwait = new List<Task>();
public Receiver(int port, string clientId, string clientSecret, string publicUrl)
{
this.clientId = clientId;
this.publicUrl = new Uri(publicUrl + "/twitcherize");
twitchogrambuffer = new Queue<string>();
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<Task<HttpResponseMessage>>();
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<c>(SubscribableTypes type, c condition, Action<Twitchogram> 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);
Console.WriteLine($"{DateTime.Now.ToShortTimeString()} page requested: {page}");
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";
}
}
}
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<byte[]>();
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<string>();
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}";
}
}
}