Properly validate posts added to the database from the API

dotnetflags
C-xC-c 5 years ago
parent 9c28308fb6
commit 014b889772

@ -33,23 +33,16 @@ namespace BantFlags.Controllers
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Get([FromForm]string post_nrs, [FromForm]string board, [FromForm]int? version) public async Task<IActionResult> Get([FromForm]string post_nrs, [FromForm]string board, [FromForm]int? version)
{ {
try // We only care if the post if valid. int ver = version ?? 0;
{
int ver = version ?? 0;
if (ver > 1) if (ver > 1)
{ {
// Improved data structuring, see Docs/GetPosts // Improved data structuring, see Docs/GetPosts
return Json(await Database.GetPosts_V2(post_nrs)); return Json(await Database.GetPosts_V2(post_nrs));
}
else
{
return Json(await Database.GetPosts_V1(post_nrs));
}
} }
catch (Exception e) else
{ {
return Problem(ErrorMessage(e), statusCode: StatusCodes.Status400BadRequest); return Json(await Database.GetPosts_V1(post_nrs));
} }
} }
@ -67,77 +60,23 @@ namespace BantFlags.Controllers
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Post([FromForm]string post_nr, [FromForm]string board, [FromForm]string regions, [FromForm]int? version) public async Task<IActionResult> Post([FromForm]string post_nr, [FromForm]string board, [FromForm]string regions, [FromForm]int? version)
{ {
try // We only care if the post if valid. string splitFlag = (version ?? 0) > 1 ? "," : "||"; // comma for v2+, else || for backwards compatibility.
{
string[] flags;
int ver = version ?? 0;
if (ver > 1)
{
flags = regions.Split(",");
}
else
{
flags = regions.Split("||");
}
// TODO: Currently we skip over invalid flags. Should we error instead?
// We can't easily format it like in the current bantflags - we really should continue to
// return "empty, or there were errors. Re-set your flags.", for compatibility, but we'd
// have to store that as a flag in the database and perform an expensive string comparison
// to stop people selecting it.
// Do we care if people select the broken flag?
var validFlags = flags.Where(x => Database.KnownFlags().Contains(x));
for (int i = 0; i < flags.Length; i++)
{
if (!Database.KnownFlags().Contains(flags[i]))
{
flags[i] = "empty, or there were errors. Re-set your flags.";
}
}
var numberOfFlags = validFlags.Count();
if (numberOfFlags <= 0 || numberOfFlags > 25)
{
throw new ArgumentException("Your post didn't include any flags, or your flags were invalid.");
}
FlagModel post = new FlagModel
{
PostNumber = int.TryParse(post_nr, out int temp) ? temp : throw new ArgumentException("Invalid post number."),
Board = board == "bant" ? "bant" : throw new ArgumentException("Board parameter wasn't formatted correctly."),
Flags = validFlags
};
await Database.InsertPost(post); (bool isValid, FlagModel post, string errorMessage) = FlagModel.Create(post_nr, board, regions, splitFlag, Database.KnownFlags);
return Ok(post); if (!isValid)
}
catch (Exception e)
{ {
return Problem(detail: ErrorMessage(e), statusCode: StatusCodes.Status400BadRequest); return Problem(errorMessage, statusCode: StatusCodes.Status400BadRequest);
} }
await Database.InsertPost(post);
return Ok(post);
} }
[HttpGet] [HttpGet]
[Route("flags")] [Route("flags")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Flags() => Ok(Database.FlagList()); public IActionResult Flags() => Ok(Database.FlagList);
/// <summary>
/// Creates an error mesage to send in case of 400 bad request, without giving away too much information.
/// </summary>
/// <param name="exception">Raw exception to be filtered.</param>
private string ErrorMessage(Exception exception) =>
exception switch
{
NullReferenceException _ => "Some data wasn't initialised. Are you sending everything?",
DbException _ => "Internal database error.",
ArgumentNullException _ => "No regions sent",
ArgumentException e => e.Message, // We create all arguement exceptions here, we can just pass the message on.
Exception e => e.Message, // Don't do this.
_ => "how in the hell"
}; // This needs more testing.
} }
} }

@ -12,30 +12,24 @@ namespace BantFlags.Data.Database
{ {
private MySqlConnectionPool ConnectionPool { get; } private MySqlConnectionPool ConnectionPool { get; }
private string Flags { get; set; } public string FlagList { get; private set; }
private HashSet<string> FlagsHash { get; set; } public HashSet<string> KnownFlags { get; private set; }
public DatabaseService(DatabaseServiceConfig dbConfig) public DatabaseService(DatabaseServiceConfig dbConfig)
{ {
ConnectionPool = new MySqlConnectionPool(dbConfig.ConnectionString, dbConfig.PoolSize); ConnectionPool = new MySqlConnectionPool(dbConfig.ConnectionString, dbConfig.PoolSize);
var flags = GetFlags().Result; // It's okay to error here since it's only initialised at startup. UpdateKnownFlags().Wait(); // It's okay to deadlock here since it's only initialised at startup.
Flags = string.Join("\n", flags);
FlagsHash = flags.ToHashSet();
} }
public string FlagList() => Flags;
public HashSet<string> KnownFlags() => FlagsHash;
public async Task UpdateKnownFlags() public async Task UpdateKnownFlags()
{ {
var flags = await GetFlags(); var flags = await GetFlags();
flags.Remove("empty, or there were errors. Re-set your flags.");
Flags = string.Join("\n", flags); FlagList = string.Join("\n", flags);
FlagsHash = flags.ToHashSet(); KnownFlags = flags.ToHashSet();
} }
public async Task DeleteFlagsAsync(List<FormFlag> flags) public async Task DeleteFlagsAsync(List<FormFlag> flags)
@ -46,8 +40,6 @@ namespace BantFlags.Data.Database
flags.ForEach(async f => flags.ForEach(async f =>
await query.SetParam("@flag", f.Name) await query.SetParam("@flag", f.Name)
.ExecuteNonQueryAsync(reuse: true)); .ExecuteNonQueryAsync(reuse: true));
return;
} }
public async Task RenameFlagsAsync(List<RenameFlag> flags) public async Task RenameFlagsAsync(List<RenameFlag> flags)
@ -79,8 +71,6 @@ namespace BantFlags.Data.Database
.ExecuteNonQueryAsync(reuse: true)); .ExecuteNonQueryAsync(reuse: true));
} }
} }
return;
} }
/// <summary> /// <summary>
@ -106,8 +96,6 @@ namespace BantFlags.Data.Database
flags.ForEach(async f => flags.ForEach(async f =>
await query.SetParam("@flag", f.Name) await query.SetParam("@flag", f.Name)
.ExecuteNonQueryAsync(reuse: true)); .ExecuteNonQueryAsync(reuse: true));
return;
} }
} }

@ -31,7 +31,6 @@ namespace BantFlags.Data.Database
List<Dictionary<string, string>> posts = new List<Dictionary<string, string>>(); List<Dictionary<string, string>> posts = new List<Dictionary<string, string>>();
var x = await GetPosts(input); var x = await GetPosts(input);
x.ForEach(x => posts.Add(new Dictionary<string, string> x.ForEach(x => posts.Add(new Dictionary<string, string>
{ {
{"post_nr", x.Key.ToString() }, {"post_nr", x.Key.ToString() },

@ -1,13 +1,52 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace BantFlags.Data namespace BantFlags.Data
{ {
public class FlagModel public class FlagModel
{ {
public int PostNumber { get; set; } public int PostNumber { get; private set; }
public string Board { get; set; } public string Board { get; private set; }
public IEnumerable<string> Flags { get; set; } public string[] Flags { get; private set; }
private FlagModel(int post_nr, string board, string[] flags)
{
PostNumber = post_nr;
Board = board;
Flags = flags;
}
/// <summary>
/// A wrapper around post validation so it's all in one place.
/// </summary>
public static (bool, FlagModel, string) Create(string post_nr, string board, string regions, string splitFlag, HashSet<string> knownFlags)
{
if (!int.TryParse(post_nr, out int postNumber))
return (false, default, "Invalid post number.");
if (board != "bant")
return (false, default, "Invalid board parameter.");
if (regions == null)
regions = "somebrokenflagstringsothatwegettheemptyflagwhenweshould";
var flags = regions.Split(splitFlag);
if (flags.Count() > 30)
return (false, default, "Too many flags.");
foreach (string flag in flags)
{
if (!knownFlags.Contains(flag)) // Not ideal but it's better than doing it in the controller / passing the database here.
{
flags = new string[] { "empty, or there were errors. Re-set your flags." };
break;
}
}
return (true, new FlagModel(postNumber, board, flags), default);
}
} }
} }

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace BantFlags.Data namespace BantFlags.Data
{ {
@ -12,8 +13,15 @@ namespace BantFlags.Data
public List<FormFlag> DeletedFlags { get; set; } public List<FormFlag> DeletedFlags { get; set; }
public List<FormFlag> AddedFlags { get; set; } public List<FormFlag> AddedFlags { get; set; }
/// <summary>
/// The current list of resolved flags including changes currently in Staging.
/// Exists here since it's a singleton.
/// </summary>
public List<string> Flags { get; set; } public List<string> Flags { get; set; }
/// <summary>
/// Used for commiting and unstaging staged flags.
/// </summary>
public string Password { get; } public string Password { get; }
public Staging(string password) public Staging(string password)

@ -10,4 +10,8 @@
<a href="~/bantflags.user.js">Install Bantflags</a> <a href="~/bantflags.user.js">Install Bantflags</a>
<br /> <br />
<br />
<a href="https://nineball.party/srsbsn/3521">Official Thread</a> <a href="https://nineball.party/srsbsn/3521">Official Thread</a>
<br />
<br />
<a asp-page="Upload">Upload Flags</a>

@ -49,6 +49,8 @@
<form method="post" asp-page-handler="Unstage"> <form method="post" asp-page-handler="Unstage">
<label>Password:</label> <label>Password:</label>
<input type="text" name="password" /> <input type="text" name="password" />
<br />
<br />
<button type="submit" onclick="window.confirm('Really unstage the selected flags?')">unstage</button> <button type="submit" onclick="window.confirm('Really unstage the selected flags?')">unstage</button>
@if (Model.staging.AddedFlags.Any()) @if (Model.staging.AddedFlags.Any())
{ {
@ -115,17 +117,19 @@
} }
</form> </form>
@section Head {
<link rel="stylesheet" href="~/upload.css" />
}
@section Scripts { @section Scripts {
@* Place flag image inside the <select> because ASP removes "invalid HTML" *@ @* Place flag image inside the <select> because ASP removes "invalid HTML" *@
<script> <script>
let x = document.getElementsByTagName('select')[0].children window.addEventListener('load', function (e) {
Array.prototype.slice.call(x).forEach(function (e) { let x = document.getElementsByTagName('select')[0].children
var name = e.innerHTML; Array.prototype.slice.call(x).forEach(function (y) {
e.innerHTML = "<img src=\"https://flags.plum.moe/flags/" + name + ".png\">" + name var name = y.innerHTML;
}); y.innerHTML = "<img src=\"https://flags.plum.moe/flags/" + name + ".png\">" + name
});
}, { once: true });
</script> </script>
} }
@section Head {
<link rel="stylesheet" href="~/form.css" />
}

@ -14,6 +14,7 @@ using System.Threading.Tasks;
namespace BantFlags namespace BantFlags
{ {
// I don't know if I need these anymore.
[RequestFormLimits(ValueCountLimit = 5000)] [RequestFormLimits(ValueCountLimit = 5000)]
[IgnoreAntiforgeryToken(Order = 2000)] [IgnoreAntiforgeryToken(Order = 2000)]
public class UploadModel : PageModel public class UploadModel : PageModel
@ -21,7 +22,7 @@ namespace BantFlags
private IWebHostEnvironment Env { get; } private IWebHostEnvironment Env { get; }
private DatabaseService Database { get; set; } private DatabaseService Database { get; set; }
private readonly byte[] pngHeader = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; private readonly byte[] PNGHeader = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
private string FlagsPath { get; set; } private string FlagsPath { get; set; }
@ -183,7 +184,7 @@ namespace BantFlags
return Page(); return Page();
} }
// TODO: maybe there's something no releasing memory here. // TODO: maybe there's something no releasing memory here. - can't Directory.Move().
using var memoryStream = new MemoryStream(); using var memoryStream = new MemoryStream();
await Upload.CopyToAsync(memoryStream); await Upload.CopyToAsync(memoryStream);
@ -215,7 +216,7 @@ namespace BantFlags
} }
catch (Exception e) catch (Exception e)
{ {
Message = $"Something went bang.\n{e.Message}"; Message = $"Something went bang.\n\n\n{e.Message}";
return Page(); return Page();
} }
@ -254,7 +255,7 @@ namespace BantFlags
{ {
reader.BaseStream.Position = 0; reader.BaseStream.Position = 0;
return reader.ReadBytes(pngHeader.Length).SequenceEqual(pngHeader); return reader.ReadBytes(PNGHeader.Length).SequenceEqual(PNGHeader);
} }
} }
} }
@ -268,8 +269,12 @@ namespace BantFlags
private bool FileNameIsValid(string fileName) => private bool FileNameIsValid(string fileName) =>
!(fileName == null !(fileName == null
|| fileName.Contains("||") || fileName.Contains("||")
|| Database.KnownFlags().Contains(fileName) || fileName.Contains(",")
|| Database.KnownFlags.Contains(fileName)
|| staging.AddedFlags.Select(x => x.Name).Contains(fileName) || staging.AddedFlags.Select(x => x.Name).Contains(fileName)
|| staging.DeletedFlags.Select(x => x.Name).Contains(fileName)
|| staging.RenamedFlags.Select(x => x.Name).Contains(fileName)
|| staging.RenamedFlags.Select(x => x.NewName).Contains(fileName)
|| fileName.Length > 100); || fileName.Length > 100);
} }
} }

@ -19,6 +19,6 @@ h3 {
.flag { .flag {
margin: auto; margin: auto;
width: 12%; width: 12%;
display: block; display: inline-block;
padding-bottom: 10px; padding-bottom: 10px;
} }

@ -22,7 +22,7 @@
// This script specifically targets ECMAScript 2015 (const, let, arrow functions). Update your hecking browser. // This script specifically targets ECMAScript 2015 (const, let, arrow functions). Update your hecking browser.
// Change this if you want verbose debug information in the console. // Change this if you want verbose debuging information in the console.
const debugMode = false; const debugMode = false;
// //
@ -41,7 +41,7 @@ const api_get = 'api/get';
const api_post = 'api/post'; const api_post = 'api/post';
// If you increase this the server will ignore your post. // If you increase this the server will ignore your post.
const max_flags = 24; const max_flags = 30;
var regions = []; // The flags we have selected. var regions = []; // The flags we have selected.
var postNrs = []; // all post numbers in the thread. var postNrs = []; // all post numbers in the thread.
@ -50,10 +50,10 @@ var postNrs = []; // all post numbers in the thread.
// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE CONFIGURATION BOXES // DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE CONFIGURATION BOXES
// //
let elementsInClass = x => document.getElementsByClassName(x); const elementsInClass = x => document.getElementsByClassName(x);
let sliceCall = x => Array.prototype.slice.call(x); const sliceCall = x => Array.prototype.slice.call(x);
let firstChildInClass = (parent, className) => parent.getElementsByClassName(className)[0]; const firstChildInClass = (parent, className) => parent.getElementsByClassName(className)[0];
let createAndAssign = (element, source) => Object.assign(document.createElement(element), source); const createAndAssign = (element, source) => Object.assign(document.createElement(element), source);
function addGlobalStyle(css) { function addGlobalStyle(css) {
let head = document.getElementsByTagName('head')[0]; let head = document.getElementsByTagName('head')[0];
@ -93,7 +93,7 @@ function MakeRequest(method, url, data, func) {
function retry(func, resp) { function retry(func, resp) {
console.log("[BantFlags] Could not fetch flags, status: " + resp.status); console.log("[BantFlags] Could not fetch flags, status: " + resp.status);
console.log(resp.statusText); // TODO: surely ASP.NET can return something more useful? console.log(resp.statusText);
setTimeout(func, requestRetryInterval); setTimeout(func, requestRetryInterval);
} }
@ -135,14 +135,13 @@ var nsetup = { // not anymore a clone of the original setup
flagLoad.style.display = 'none'; flagLoad.style.display = 'none';
flagSelect.style.display = 'inline-block'; flagSelect.style.display = 'inline-block';
nsetup.flagsLoaded = true; nsetup.flagsLoaded = true;
flagLoad.removeEventListener('click', nsetup.fillHtml);
}); });
}, },
save: function (v) { save: function (v) {
GM_setValue(nsetup.namespace, v); GM_setValue(nsetup.namespace, v);
regions = GM_getValue(nsetup.namespace); regions = GM_getValue(nsetup.namespace);
}, },
setFlag: function (flag) { // place a flag from the selector to the flags array variable and create an element in the flags_container div setFlag: function (flag) {
let UID = Math.random().toString(36).substring(7); let UID = Math.random().toString(36).substring(7);
let flagName = flag ? flag : document.getElementById("flagSelect").value; let flagName = flag ? flag : document.getElementById("flagSelect").value;
let flagContainer = document.getElementById("bantflags_container"); let flagContainer = document.getElementById("bantflags_container");
@ -154,16 +153,18 @@ var nsetup = { // not anymore a clone of the original setup
className: 'bantflags_flag' className: 'bantflags_flag'
})); }));
if (flagContainer.children.length > max_flags) { if (flagContainer.children.length >= max_flags) {
nsetup.ToggleFlagButton('off'); nsetup.toggleFlagButton('off');
} }
document.getElementById(UID).addEventListener("click", function () { document.getElementById(UID).addEventListener("click", function () {
console.log("removing flag");
let flagToRemove = document.getElementById(UID); let flagToRemove = document.getElementById(UID);
flagToRemove.parentNode.removeChild(flagToRemove); flagToRemove.parentNode.removeChild(flagToRemove);
nsetup.toggleFlagButton('on'); nsetup.toggleFlagButton('on');
nsetup.save(nsetup.parse()); nsetup.save(nsetup.parse());
console.log("flag removed");
}); });
if (!flag) { if (!flag) {
@ -191,7 +192,7 @@ var nsetup = { // not anymore a clone of the original setup
document.getElementById('append_flag_button').addEventListener('click', document.getElementById('append_flag_button').addEventListener('click',
() => nsetup.flagsLoaded ? nsetup.setFlag() : alert('Load flags before adding them.')); () => nsetup.flagsLoaded ? nsetup.setFlag() : alert('Load flags before adding them.'));
document.getElementById('flagLoad').addEventListener('click', nsetup.fillHtml); document.getElementById('flagLoad').addEventListener('click', nsetup.fillHtml, { once: true });
}, },
parse: function () { parse: function () {
let flagsArray = []; let flagsArray = [];
@ -203,7 +204,7 @@ var nsetup = { // not anymore a clone of the original setup
return flagsArray; return flagsArray;
}, },
ToggleFlagButton: state => document.getElementById('append_flag_button').disabled = state === 'off' ? true : false toggleFlagButton: state => document.getElementById('append_flag_button').disabled = state === 'off' ? true : false
}; };
/** Prompt to set region if regions is empty */ /** Prompt to set region if regions is empty */

Loading…
Cancel
Save