Add migrations, add private streams

This commit is contained in:
Laura Hausmann 2022-02-04 04:48:04 +01:00
parent cade2df6a5
commit 59b3dc6f68
Signed by: zotan
GPG Key ID: D044E84C5BE01605
12 changed files with 333 additions and 184 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ obj/
/packages/
/data/
app.db
app.db.bak

View File

@ -2,7 +2,8 @@
using Microsoft.AspNetCore.Mvc;
using RTMPDash.DataModels;
namespace RTMPDash.Controllers {
namespace RTMPDash.Controllers;
[ApiController, Route("/api/authenticate")]
public class RtmpAuthController : ControllerBase {
[HttpGet]
@ -15,7 +16,7 @@ namespace RTMPDash.Controllers {
var user = db.Users.FirstOrDefault(p => p.StreamKey == Request.Query["name"]);
Response.Headers.Add("x-rtmp-user", user!.Username);
Response.Headers.Add("x-rtmp-user", user!.IsPrivate ? user!.PrivateAccessKey : user!.Username);
if (user.AllowRestream && !string.IsNullOrWhiteSpace(user.RestreamTargets))
Response.Headers.Add("x-rtmp-target", user.RestreamTargets);
@ -24,4 +25,3 @@ namespace RTMPDash.Controllers {
return "authorized as " + user!.Username;
}
}
}

View File

@ -5,7 +5,8 @@ using LinqToDB.Configuration;
using LinqToDB.Data;
using RTMPDash.DataModels.Tables;
namespace RTMPDash.DataModels {
namespace RTMPDash.DataModels;
public class AppDb {
public class ConnectionStringSettings : IConnectionStringSettings {
public string ConnectionString { get; set; }
@ -21,11 +22,7 @@ namespace RTMPDash.DataModels {
public string DefaultDataProvider => "SQLite";
public IEnumerable<IConnectionStringSettings> ConnectionStrings {
get {
yield return new ConnectionStringSettings {
Name = "db", ProviderName = "SQLite", ConnectionString = @"Data Source=app.db;"
};
}
get { yield return new ConnectionStringSettings { Name = "db", ProviderName = "SQLite", ConnectionString = @"Data Source=app.db;" }; }
}
}
@ -34,6 +31,6 @@ namespace RTMPDash.DataModels {
public ITable<User> Users => GetTable<User>();
public ITable<Invite> Invites => GetTable<Invite>();
}
public ITable<DbInfo> DbInfo => GetTable<DbInfo>();
}
}

View File

@ -0,0 +1,9 @@
using LinqToDB.Mapping;
namespace RTMPDash.DataModels.Tables;
[Table(Name = "DbInfo")]
public class DbInfo {
[Column(Name = "ID"), PrimaryKey, Identity, NotNull] public int Id { get; set; }
[Column(Name = "DbVer"), NotNull] public int DbVer { get; set; }
}

View File

@ -1,6 +1,7 @@
using LinqToDB.Mapping;
namespace RTMPDash.DataModels.Tables {
namespace RTMPDash.DataModels.Tables;
[Table(Name = "Users")]
public class User {
[Column(Name = "Username"), PrimaryKey] public string Username { get; set; }
@ -14,5 +15,6 @@ namespace RTMPDash.DataModels.Tables {
[Column(Name = "AnnouncementUrl")] public string AnnouncementUrl { get; set; }
[Column(Name = "RestreamTargets")] public string RestreamTargets { get; set; }
[Column(Name = "RestreamUrls")] public string RestreamUrls { get; set; }
}
[Column(Name = "IsPrivate")] public bool IsPrivate { get; set; }
[Column(Name = "PrivateAccessKey")] public string PrivateAccessKey { get; set; }
}

75
Migrations.cs Normal file
View File

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LinqToDB;
using LinqToDB.Data;
using RTMPDash.DataModels;
using RTMPDash.DataModels.Tables;
namespace RTMPDash;
/**
* TODO: Initial migrations should create all tables & an admin user and print a randomly generated password for the admin user
* *
*/
public static class Migrations {
public const int DbVer = 1;
private static readonly List<Migration> _migrations = new() {
new Migration(1, "ALTER TABLE Users ADD IsPrivate INTEGER DEFAULT 0 NOT NULL"), new Migration(1, "ALTER TABLE Users ADD PrivateAccessKey TEXT")
};
public static void RunMigrations() {
using var db = new AppDb.DbConn();
if (db.DataProvider.GetSchemaProvider().GetSchema(db).Tables.All(t => t.TableName != "DbInfo")) {
db.CreateTable<DbInfo>();
db.InsertWithIdentity(new DbInfo { DbVer = 0 });
}
var dbinfo = db.DbInfo.First();
var ccolor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Database version: {db.DbInfo.ToList().First().DbVer}");
var migrationsToRun = _migrations.FindAll(p => p.IntroducedWithDbVer > db.DbInfo.First().DbVer);
if (migrationsToRun.Count == 0) {
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("No migrations to run.");
}
else {
migrationsToRun.ForEach(RunMigration);
dbinfo.DbVer = DbVer;
db.Update(dbinfo);
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine($"Database version is now: {DbVer}");
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Finished running migrations.");
}
Console.ForegroundColor = ccolor;
}
private static void RunMigration(Migration mig) => mig.Run();
private class Migration {
private readonly string _sql;
public readonly int IntroducedWithDbVer;
public Migration(int introducedWithDbVer, string sql) {
IntroducedWithDbVer = introducedWithDbVer;
_sql = sql;
}
public void Run() {
using var db = new AppDb.DbConn();
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.Write("Running migration: ");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(_sql);
db.Execute(_sql);
}
}
}

View File

@ -24,11 +24,29 @@ else {
<p>Thanks for using @Program.SiteName. If you have any issues, please contact me on @Html.Raw(Program.ContactInfo)</p>
<hr/>
<p>Please subscribe to the <a href="@Program.ServiceAnnouncementUrl" target="_blank">Service Announcements Channel</a> to get informed about maintenance and other important things.</p>
<hr>
<p class="mb-0">When using OBS, the stream key and leading slash before it is to be left out in the RTMP url.</p>
<hr/>
<p class="mb-0">For low-latancy streams, please set your keyframe interval to 1-2 seconds. Otherwise, automatic or something in the range of 4-8 seconds is fine.</p>
</div>
if (StreamUtils.ListLiveUsers().Contains(user.Username) && user.IsPrivate) {
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Warning!</h4>
<p class="mb-0">
You set your stream to private, but have not restarted your stream since.
<br/>
While this setting is applied immediately, the old player URL will remain accessible until you stop and restart your stream.
</p>
</div>
}
else if (StreamUtils.ListLiveUsers().Contains(user.PrivateAccessKey) && !user.IsPrivate) {
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Warning!</h4>
<p class="mb-0">
You set your stream to public, but have not restarted your stream since.
<br/>
While this setting is applied immediately, the public player will not work until you restart your stream.
</p>
</div>
}
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" style="width:23ch">Profile URL</span>
@ -42,10 +60,21 @@ else {
<div class="input-group-prepend">
<span class="input-group-text" style="width:23ch">Player URL</span>
</div>
<input type="text" class="form-control" id="input-playerurl" value="@Program.PlayerDomain/@user.Username" disabled>
@if (user.IsPrivate) {
<input type="text" class="form-control" id="input-playerurl" value="@Program.PlayerDomain/@user.PrivateAccessKey" disabled>
<div class="input-group-append">
<button onclick="ajax_and_reload('private_toggle')" class="btn btn-outline-info" role="button" id="button-toggle-private">Private</button>
<button class="btn btn-outline-secondary" role="button" id="button-copy-playerurl" onclick="copyToClipboard(document.getElementById('input-playerurl').value);">Copy</button>
</div>
}
else {
<input type="text" class="form-control" id="input-playerurl" value="@Program.PlayerDomain/@user.Username" disabled>
<div class="input-group-append">
<button onclick="ajax_and_reload('private_toggle')" class="btn btn-outline-success" role="button" id="button-toggle-private">Public</button>
<button class="btn btn-outline-secondary" role="button" id="button-copy-playerurl" onclick="copyToClipboard(document.getElementById('input-playerurl').value);">Copy</button>
</div>
}
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
@ -61,9 +90,10 @@ else {
<div class="input-group-prepend">
<span class="input-group-text" style="width:23ch">Stream URL</span>
</div>
<input type="text" class="form-control" id="input-streamurl" value="@Program.IngressDomain/ingress/@user.StreamKey" disabled>
<input type="text" class="form-control" id="input-streamurl" value="@Program.IngressDomain/ingress" disabled>
<div class="input-group-append">
@if (StreamUtils.IsLive(user.Username, stats)) {
@* ReSharper disable once ConvertIfStatementToSwitchStatement *@
@if (!user.IsPrivate && StreamUtils.IsLive(user.Username, stats)) {
var uptime = TimeSpan.FromMilliseconds(StreamUtils.GetClientTime(user.Username, stats)).StripMilliseconds();
if (user.AllowRestream && !string.IsNullOrWhiteSpace(user.RestreamTargets)) {
if (StreamUtils.GetClientTime(user.Username, stats) > 5000) {
@ -84,6 +114,27 @@ else {
}
<button class="btn btn-dark" role="button" disabled>@uptime.ToString("c")</button>
}
else if (user.IsPrivate && StreamUtils.IsLive(user.PrivateAccessKey, stats)) {
var uptime = TimeSpan.FromMilliseconds(StreamUtils.GetClientTime(user.PrivateAccessKey, stats)).StripMilliseconds();
if (user.AllowRestream && !string.IsNullOrWhiteSpace(user.RestreamTargets)) {
if (StreamUtils.GetClientTime(user.Username, stats) > 5000) {
var restreams = StreamUtils.CountLiveRestreams(user.PrivateAccessKey, stats);
if (restreams > 0) {
<button class="btn btn-success" role="button" style="width:20ch" disabled>Live & restreaming</button>
}
else {
<button class="btn btn-warning" role="button" style="width:22ch" disabled>Live & restream down</button>
}
}
else {
<button class="btn btn-dark" role="button" style="width:13ch" disabled>Starting...</button>
}
}
else {
<button class="btn btn-success" role="button" style="width:13ch" disabled>Live</button>
}
<button class="btn btn-dark" role="button" disabled>@uptime.ToString("c")</button>
}
else {
<button class="btn btn-danger" role="button" style="width:13ch" disabled>No data</button>
}

View File

@ -5,14 +5,13 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RTMPDash.DataModels;
namespace RTMPDash.Pages {
namespace RTMPDash.Pages;
public class DashboardModel : PageModel {
public void OnGet() { }
public void OnPost() {
if (!Request.HasFormContentType
|| string.IsNullOrWhiteSpace(Request.Form["action"])
|| string.IsNullOrWhiteSpace(HttpContext.Session.GetString("authenticatedUser")))
if (!Request.HasFormContentType || string.IsNullOrWhiteSpace(Request.Form["action"]) || string.IsNullOrWhiteSpace(HttpContext.Session.GetString("authenticatedUser")))
return;
using var db = new AppDb.DbConn();
@ -46,9 +45,7 @@ namespace RTMPDash.Pages {
if (Request.Form["action"] == "restream_targets_set") {
var newtgts = Request.Form["value"].ToString();
if (newtgts.Contains("localhost")
|| newtgts.Contains("127.0.0.1")
|| newtgts.Contains(user.StreamKey))
if (newtgts.Contains("localhost") || newtgts.Contains("127.0.0.1") || newtgts.Contains(user.StreamKey))
return;
user!.RestreamTargets = newtgts;
@ -58,18 +55,14 @@ namespace RTMPDash.Pages {
}
if (Request.Form["action"] == "pronoun_subj_set") {
var target = string.IsNullOrWhiteSpace(Request.Form["value"])
? "they"
: Request.Form["value"].ToString();
var target = string.IsNullOrWhiteSpace(Request.Form["value"]) ? "they" : Request.Form["value"].ToString();
user!.PronounSubject = target.ToLowerInvariant();
db.Update(user);
Response.Redirect("/Dashboard");
}
if (Request.Form["action"] == "pronoun_poss_set") {
var target = string.IsNullOrWhiteSpace(Request.Form["value"])
? "their"
: Request.Form["value"].ToString();
var target = string.IsNullOrWhiteSpace(Request.Form["value"]) ? "their" : Request.Form["value"].ToString();
user!.PronounPossessive = target.ToLowerInvariant();
db.Update(user);
Response.Redirect("/Dashboard");
@ -79,6 +72,17 @@ namespace RTMPDash.Pages {
user!.StreamKey = Guid.NewGuid().ToString();
db.Update(user);
}
if (Request.Form["action"] == "private_toggle") {
if (user.IsPrivate) {
user!.IsPrivate = false;
}
else {
user!.PrivateAccessKey = Guid.NewGuid().ToString();
user!.IsPrivate = true;
}
db.Update(user);
}
}
}

View File

@ -1,13 +1,17 @@
@page
@using RTMPDash.DataModels
@model IndexModel
@{
ViewData["Title"] = "Home";
var liveUsers = StreamUtils.ListLiveUsers();
var db = new AppDb.DbConn();
var allStreams = StreamUtils.ListLiveUsers();
var allUsers = db.Users.Where(p => !p.IsPrivate).Select(p => p.Username);
var liveUsers = allStreams.Intersect(allUsers);
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
@if (liveUsers.Count > 0) {
@if (liveUsers.Any()) {
<p>The following users are currently live:</p>
<div class="btn-group btn-group" role="group">
@foreach (var user in liveUsers) {

View File

@ -6,7 +6,8 @@ using Microsoft.AspNetCore.Mvc.RazorPages;
using RTMPDash.DataModels;
using RTMPDash.DataModels.Tables;
namespace RTMPDash.Pages {
namespace RTMPDash.Pages;
public class RegisterModel : PageModel {
public void OnPost() {
if (!Request.HasFormContentType
@ -26,6 +27,12 @@ namespace RTMPDash.Pages {
return;
}
if (db.Users.Any(p => p.StreamKey == Request.Form["user"] || p.PrivateAccessKey == Request.Form["user"])) {
//user invalid
Response.Redirect("/Register?e=user_invalid");
return;
}
user = new User {
Username = Request.Form["user"].ToString(),
Password = Request.Form["pass"].ToString().Sha256(),
@ -42,4 +49,3 @@ namespace RTMPDash.Pages {
HttpContext.Session.SetString("authenticatedUser", user.Username);
}
}
}

View File

@ -10,7 +10,7 @@
}
var user = db.Users.First(p => p.Username == Model.User);
var stats = StreamUtils.GetStatsObject();
var live = StreamUtils.IsLive(user.Username, stats);
var live = StreamUtils.IsLive(user.Username, stats) && !user.IsPrivate;
Stream stream = null;
if (live) {
stream = stats.Server.Applications.First(p => p.Name == "ingress").MethodLive.Streams.FirstOrDefault(p => p.Name == user.Username);

View File

@ -5,7 +5,8 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using RTMPDash.DataModels;
namespace RTMPDash {
namespace RTMPDash;
public class Program {
public const string SiteName = "chaos.stream";
public const string IngressDomain = "rtmp://chaos.stream";
@ -25,14 +26,13 @@ namespace RTMPDash {
public static void Main(string[] args) {
DataConnection.DefaultSettings = new AppDb.Settings();
ThreadPool.SetMinThreads(100, 100);
Migrations.RunMigrations();
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
public static class TimeExtensions {
public static TimeSpan StripMilliseconds(this TimeSpan time) => new(time.Days, time.Hours, time.Minutes, time.Seconds);
}
}