269 lines
12 KiB
C#
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}";
|
||
|
}
|
||
|
}
|
||
|
}
|