diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92db4bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,679 @@ +appsettings.*.json + +# Created by https://www.toptal.com/developers/gitignore/api/aspnetcore,csharp,visualstudio,visualstudiocode,dotnetcore +# Edit at https://www.toptal.com/developers/gitignore?templates=aspnetcore,csharp,visualstudio,visualstudiocode,dotnetcore + +### ASPNETCore ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/ + +### Csharp ### +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser + +# User-specific files (MonoDevelop/Xamarin Studio) + +# Mono auto generated files +mono_crash.* + +# Build results +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +# Uncomment if you have tasks that create the project's static files in wwwroot + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results + +# NUnit +nunit-*.xml + +# Build Results of an ATL Project + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_h.h +*.iobj +*.ipdb +*_wpftmp.csproj + +# Chutzpah Test files + +# Visual C++ cache files + +# Visual Studio profiler + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace + +# Guidance Automation Toolkit + +# ReSharper is a .NET coding add-in + +# TeamCity is a build add-in + +# DotCover is a Code Coverage Tool + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*[.json, .xml, .info] + +# Visual Studio code coverage results + +# NCrunch + +# MightyMoose + +# Web workbench (sass) + +# Installshield output folder + +# DocProject is a documentation generator add-in + +# Click-Once directory + +# Publish Web Output +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted + +# NuGet Packages +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files + +# Microsoft Azure Build Output + +# Microsoft Azure Emulator + +# Windows Store app package directories and files +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) + +# RIA/Silverlight projects + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.ndf + +# Business Intelligence projects +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes + +# GhostDoc plugin setting file + +# Node.js Tools for Visual Studio + +# Visual Studio 6 build log + +# Visual Studio 6 workspace options file + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output + +# Paket dependency manager + +# FAKE - F# Make + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +### DotnetCore ### +# .NET Core build folders +bin/ +obj/ + +# Common node modules locations +/node_modules +/wwwroot/node_modules + +### VisualStudioCode ### +.vscode/* +!.vscode/tasks.json +!.vscode/launch.json +*.code-workspace + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### VisualStudio ### + +# User-specific files + +# User-specific files (MonoDevelop/Xamarin Studio) + +# Mono auto generated files + +# Build results + +# Visual Studio 2015/2017 cache/options directory +# Uncomment if you have tasks that create the project's static files in wwwroot + +# Visual Studio 2017 auto generated files + +# MSTest test Results + +# NUnit + +# Build Results of an ATL Project + +# Benchmark Results + +# .NET Core + +# ASP.NET Scaffolding + +# StyleCop + +# Files built by Visual Studio + +# Chutzpah Test files + +# Visual C++ cache files + +# Visual Studio profiler + +# Visual Studio Trace Files + +# TFS 2012 Local Workspace + +# Guidance Automation Toolkit + +# ReSharper is a .NET coding add-in + +# TeamCity is a build add-in + +# DotCover is a Code Coverage Tool + +# AxoCover is a Code Coverage Tool + +# Coverlet is a free, cross platform Code Coverage Tool + +# Visual Studio code coverage results + +# NCrunch + +# MightyMoose + +# Web workbench (sass) + +# Installshield output folder + +# DocProject is a documentation generator add-in + +# Click-Once directory + +# Publish Web Output +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted + +# NuGet Packages +# NuGet Symbol Packages +# The packages folder can be ignored because of Package Restore +# except build/, which is used as an MSBuild target. +# Uncomment if necessary however generally it will be regenerated when needed +# NuGet v3's project.json files produces more ignorable files + +# Microsoft Azure Build Output + +# Microsoft Azure Emulator + +# Windows Store app package directories and files + +# Visual Studio cache files +# files ending in .cache can be ignored +# but keep track of directories ending in .cache + +# Others + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) + +# RIA/Silverlight projects + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) + +# SQL Server files + +# Business Intelligence projects + +# Microsoft Fakes + +# GhostDoc plugin setting file + +# Node.js Tools for Visual Studio + +# Visual Studio 6 build log + +# Visual Studio 6 workspace options file + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) + +# Visual Studio LightSwitch build output + +# Paket dependency manager + +# FAKE - F# Make + +# CodeRush personal settings + +# Python Tools for Visual Studio (PTVS) + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio + +# Telerik's JustMock configuration file + +# BizTalk build output + +# OpenCover UI analysis results + +# Azure Stream Analytics local run output + +# MSBuild Binary and Structured Log + +# NVidia Nsight GPU debugger configuration file + +# MFractors (Xamarin productivity tool) working folder + +# Local History for Visual Studio + +# BeatPulse healthcheck temp database + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 + +# Ionide (cross platform F# VS Code tools) working folder + +# Fody - auto-generated XML schema + +### VisualStudio Patch ### +# Additional files built by Visual Studio +*.tlog + +# End of https://www.toptal.com/developers/gitignore/api/aspnetcore,csharp,visualstudio,visualstudiocode,dotnetcore diff --git a/Controllers/NewsController.cs b/Controllers/NewsController.cs new file mode 100644 index 0000000..0b6fee8 --- /dev/null +++ b/Controllers/NewsController.cs @@ -0,0 +1,33 @@ +using cugoj_ng_server.Models; +using cugoj_ng_server.Utilities; +using Dapper; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Distributed; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace cugoj_ng_server.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class NewsController : ControllerBase + { + [HttpGet] + [Route("List")] + public IEnumerable List() + { + using var conn = DbConn.GetConnection(); + return conn.Query(@"select user_id,title,content from news where defunct='N' order by news_id desc").Select(x => new + { + Author = x.user_id, + Title = x.title, + Content = x.content, + Log = UserModel.Authorization.CanViewAllProblemsAsync("admin") + }); + } + } +} diff --git a/Controllers/ProblemController.cs b/Controllers/ProblemController.cs new file mode 100644 index 0000000..2ac9343 --- /dev/null +++ b/Controllers/ProblemController.cs @@ -0,0 +1,87 @@ +using cugoj_ng_server.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace cugoj_ng_server.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ProblemController : ControllerBase + { + readonly int problemsPerPage; + readonly int maxSolutionSize; + public ProblemController(IConfiguration configuration) + { + problemsPerPage = configuration.GetValue("Config:ProblemsPerPage"); + maxSolutionSize = configuration.GetValue("Config:MaxSolutionSize"); + } + + [HttpGet] + [Route("List")] + public async Task GetProblemCountAsync() + { + var viewAll = await UserModel.Authorization.CanViewAllProblemsAsync(HttpContext.Session.GetString("user")); + var problemCount = await ProblemModel.GetProblemsCountAsync(viewAll); + var maxPageCount = (problemCount + problemsPerPage - 1) / problemsPerPage; + return new + { + TotalPages = maxPageCount, + ProblemCount = problemCount, + }; + } + + [HttpGet] + [Route("List/{page}")] + public async Task GetProblemListAsync(int page) + { + if (page <= 0) return BadRequest(); + bool viewAll = await UserModel.Authorization.CanViewAllProblemsAsync(HttpContext.Session.GetString("user")); + int maxPageCount = (await ProblemModel.GetProblemsCountAsync(viewAll) + problemsPerPage - 1) / problemsPerPage; + if (page > maxPageCount) return NotFound("I don't have so many problems..."); + return new + { + TotalPages = maxPageCount, + ProblemList = await ProblemModel.GetProblemListAsync((page - 1) * problemsPerPage, problemsPerPage, viewAll), + }; + } + + [HttpGet] + [Route("{pid}")] + public async Task GetProblemAsync(int pid) + { + bool viewAll = await UserModel.Authorization.CanViewAllProblemsAsync(HttpContext.Session.GetString("user")); + if (!viewAll) + if (await ProblemModel.IsProblemRestrictedAsync(pid)) + return StatusCode(StatusCodes.Status403Forbidden, "This problem is private now."); + var problem = await ProblemModel.GetProblemAsync(pid); + if (problem is null) return NotFound("No such problem."); + return Ok(problem); + } + + [RequestSizeLimit(1 << 20)] + [HttpPost] + [Route("Submit/{pid}")] + public async Task SubmitSolutionAsync(int pid, [FromForm] string lang, [FromForm] string code) + { + var user = HttpContext.Session.GetString("user"); + if (user is null) return Unauthorized("Not logged in."); + if (code.Length > maxSolutionSize) + return StatusCode(StatusCodes.Status413PayloadTooLarge, + $"Solution size should be less than {maxSolutionSize} bytes."); + if (!SolutionModel.LangMap.ContainsKey(lang)) return BadRequest("No such language."); + var viewAll = await UserModel.Authorization.CanViewAllProblemsAsync(user); + if (!viewAll) + if (await ProblemModel.IsProblemRestrictedAsync(pid)) + return StatusCode(StatusCodes.Status403Forbidden, "This problem is private now."); + if (!await ProblemModel.IsProblemExists(pid)) return NotFound("No such problem."); + var submit_id = await SolutionModel.SubmitProblemAsync(user, pid, SolutionModel.LangMap[lang], code); + return Ok(submit_id); + } + } +} diff --git a/Controllers/UserController.cs b/Controllers/UserController.cs new file mode 100644 index 0000000..add0172 --- /dev/null +++ b/Controllers/UserController.cs @@ -0,0 +1,60 @@ +using cugoj_ng_server.Models; +using cugoj_ng_server.Utilities; +using Dapper; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 + +namespace cugoj_ng_server.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class UserController : ControllerBase + { + [HttpPost] + [Route("Login")] + public async Task LoginAsync([FromForm] string username, [FromForm] string password) + { + var curTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); + var lastTry = HttpContext.Session.Get("LastTryLogin")?.Decode() ?? 0; + HttpContext.Session.Set("LastTryLogin", curTimestamp.Encode()); + if (curTimestamp - lastTry < 5) + return StatusCode(StatusCodes.Status429TooManyRequests, "Too Many Requests, wait for 5 seconds."); + var res = await UserModel.Authentication.LoginAsync(username, password); + switch (res) + { + case UserModel.Authentication.LoginResult.Success: + HttpContext.Session.SetString("user", username); + return Ok("Logged in"); + case UserModel.Authentication.LoginResult.NotExist: + return Unauthorized("User not exist"); + case UserModel.Authentication.LoginResult.WrongPassword: + return Unauthorized("Password not correct"); + case UserModel.Authentication.LoginResult.Banned: + return StatusCode(StatusCodes.Status403Forbidden, "You are banned"); + } + return BadRequest(); + } + + [Route("Logout")] + public void Logout() => HttpContext.Session.Clear(); + + [Route("WhoAmI")] + public object WhoAmI() + { + var user = HttpContext.Session.GetString("user"); + if (user is null) return new { user }; + return new + { + user, + privileges = UserModel.Authorization.GetPrivilegesAsync(user) + }; + } + } +} diff --git a/Models/DbConn.cs b/Models/DbConn.cs new file mode 100644 index 0000000..9f34ebd --- /dev/null +++ b/Models/DbConn.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace cugoj_ng_server.Models +{ + public class DbConn + { + public static Func GetConnection { get; private set; } + public static IConnectionMultiplexer ConnectionMultiplexer { get; private set; } + } +} diff --git a/Models/ProblemModel.cs b/Models/ProblemModel.cs new file mode 100644 index 0000000..5134bb5 --- /dev/null +++ b/Models/ProblemModel.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Dapper; + +namespace cugoj_ng_server.Models +{ + public class ProblemModel : DbConn + { + const string privateProblemsFilter = "where problem_id not in (SELECT problem_id from private_problems)"; + public static async Task GetProblemsCountAsync(bool viewAll = false) + { + using var conn = GetConnection(); + return await conn.QueryFirstOrDefaultAsync(@"SELECT COUNT(problem_id) from problem " + (viewAll ? "" : privateProblemsFilter)); + } + public static async Task> GetProblemListAsync(int skip, int limit, bool viewAll = false) + { + using var conn = GetConnection(); + return await conn.QueryAsync( + @"SELECT problem_id,title,source,submit,accepted from problem " + + (viewAll ? "" : privateProblemsFilter) + @"limit @skip,@limit", + new { skip, limit } + ); + } + public static async Task GetProblemAsync(int pid) + { + using var conn = GetConnection(); + return await conn.QueryFirstOrDefaultAsync(@"SELECT * FROM problem where problem_id=@pid", new { pid }); + } + public static async Task IsProblemRestrictedAsync(int pid) + { + using var conn = GetConnection(); + return await conn.QueryFirstOrDefaultAsync(@"select count(problem_id) from private_problems where problem_id=@id", new { id = pid }) > 0; + } + public static async Task IsProblemExists(int pid) + { + using var conn = GetConnection(); + return await conn.QueryFirstOrDefaultAsync(@"select count(problem_id) from problem where problem_id=@id", new { id = pid }) > 0; + } + } +} diff --git a/Models/SolutionModel.cs b/Models/SolutionModel.cs new file mode 100644 index 0000000..7db4130 --- /dev/null +++ b/Models/SolutionModel.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using cugoj_ng_server.Utilities; +using Dapper; + +namespace cugoj_ng_server.Models +{ + public class SolutionModel : DbConn + { + public enum Language + { + C, + CPP, + Java, + Python + } + public static readonly Dictionary LangMap = new(); + static SolutionModel() + { + foreach (var lang in Enum.GetValues()) + LangMap.Add(Enum.GetName(lang), lang); + } + + public static async Task SubmitProblemAsync(string user, int pid, Language lang, string code) + { + using var conn = GetConnection(); + var langid = HUSTOJ.MapLanguageToId(Enum.GetName(lang)); + var submit_id = await conn.QueryFirstAsync(@"INSERT INTO solution(problem_id,user_id,in_date,language,ip,code_length,result) + VALUES(@pid,@uid,now(),@lang,'0.0.0.0',@len,@status); + SELECT LAST_INSERT_ID();", new + { + pid, + uid = user, + lang = langid, + len = code.Length, + status = HUSTOJ.Status.MSG_Other + }); + var insert_1 = conn.ExecuteAsync(@"INSERT INTO `source_code`(`solution_id`,`source`) VALUES (@sid,@code)", new { sid = submit_id, code }); + // I don't know why there's 2 tables, just make hustoj happy... + var insert_2 = conn.ExecuteAsync(@"INSERT INTO `source_code_user`(`solution_id`,`source`) VALUES (@sid,@code)", new { sid = submit_id, code }); + await Task.WhenAll(insert_1, insert_2); + await conn.ExecuteAsync(@"UPDATE solution SET result=0 WHERE solution_id=@sid", new { sid = submit_id }); + return submit_id; + } + } +} diff --git a/Models/UserModel.cs b/Models/UserModel.cs new file mode 100644 index 0000000..76b6a03 --- /dev/null +++ b/Models/UserModel.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using cugoj_ng_server.Utilities; +using Dapper; + +namespace cugoj_ng_server.Models +{ + public class UserModel : DbConn + { + public class Authentication + { + public enum LoginResult + { + Success, + NotExist, + WrongPassword, + Banned, + } + public static async Task LoginAsync(string username, string password) + { + using var conn = GetConnection(); + var res = await conn.QueryFirstOrDefaultAsync<( + string user, + string password, + string defunct + )>(@"select user_id,password,defunct from users where user_id=@id", new { id = username }); + if (res.user == null) + return LoginResult.NotExist; + if (res.defunct == "Y") + return LoginResult.Banned; + if (!HUSTOJ.CheckPw(password, res.password)) + return LoginResult.WrongPassword; + return LoginResult.Success; + } + } + public class Authorization + { + public static async Task CanViewAllProblemsAsync(string user_id) + { + if (user_id == null) return false; + using var conn = GetConnection(); + return await conn.QueryFirstOrDefaultAsync( + @"SELECT count(DISTINCT rightstr) FROM privilege + where user_id=@id and rightstr in ('administrator','contest_creator')", + new { id = user_id }) > 0; + } + public static async Task GetPrivilegesAsync(string user_id) + { + if (string.IsNullOrEmpty(user_id)) return null; + using var conn = GetConnection(); + return (await conn.QueryAsync(@"select rightstr from privilege where user_id=@id and length(rightstr)>5", new { id = user_id })).ToArray(); + } + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..3816515 --- /dev/null +++ b/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace cugoj_ng_server +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..964bed2 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "cugoj_ng_server": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchUrl": "", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Startup.cs b/Startup.cs new file mode 100644 index 0000000..03e032d --- /dev/null +++ b/Startup.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; + +namespace cugoj_ng_server +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + var redisConnstr = Configuration.GetConnectionString("redis"); + var mysqlConnstr = Configuration.GetConnectionString("mysql"); + var sessionTimeout = Configuration.GetValue("Config:SessionTimeout"); + services.AddStackExchangeRedisCache(opt => + { + opt.Configuration = redisConnstr; + opt.InstanceName = "CUGOJ$"; + }); + services.AddSession(opt => + { + opt.Cookie.Name = "_SESSION"; + opt.Cookie.IsEssential = true; + opt.IdleTimeout = TimeSpan.FromMinutes(sessionTimeout); + }); + Utilities.Inject.SetPropertiesByType(typeof(Models.DbConn), null, new() + { + { typeof(IConnectionMultiplexer), ConnectionMultiplexer.Connect(redisConnstr) }, + { typeof(Func), new Func(() => new MySql.Data.MySqlClient.MySqlConnection(mysqlConnstr)) }, + }); + services.AddControllers().AddJsonOptions(opt => + { + opt.JsonSerializerOptions.IncludeFields = true; + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseSession(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/Utilities/Crypto.cs b/Utilities/Crypto.cs new file mode 100644 index 0000000..b4f5680 --- /dev/null +++ b/Utilities/Crypto.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace cugoj_ng_server.Utilities +{ + public static class Crypto + { + public static byte[] SHA1(byte[] data) + { + using var sha1 = System.Security.Cryptography.SHA1.Create(); + return sha1.ComputeHash(data); + } + public static byte[] MD5(byte[] data) + { + using var sha1 = System.Security.Cryptography.MD5.Create(); + return sha1.ComputeHash(data); + } + } +} diff --git a/Utilities/EncodingExtensions.cs b/Utilities/EncodingExtensions.cs new file mode 100644 index 0000000..c2d2506 --- /dev/null +++ b/Utilities/EncodingExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace cugoj_ng_server.Utilities +{ + public static class EncodingExtensions + { + public static byte[] Base64Decode(this string str) => Convert.FromBase64String(str); + public static string Base64Encode(this byte[] data) => Convert.ToBase64String(data); + public static byte[] HexDecode(this string str) => Convert.FromBase64String(str); + public static string HexEncode(this byte[] data) => BitConverter.ToString(data).Replace("-", "").ToLower(); + public static byte[] Encode(this string str, Encoding encoding = null) => (encoding ?? Encoding.Default).GetBytes(str); + public static string Decode(this byte[] data, Encoding encoding = null) => (encoding ?? Encoding.Default).GetString(data); + + public static byte[] Encode(this T value) where T : struct + { + var arr = new byte[Marshal.SizeOf(value)]; + MemoryMarshal.Write(arr, ref value); + return arr; + } + public static T Decode(this byte[] arr) where T : struct + { + return MemoryMarshal.Read(arr); + } + } +} diff --git a/Utilities/HUSTOJ.cs b/Utilities/HUSTOJ.cs new file mode 100644 index 0000000..2d2f4bd --- /dev/null +++ b/Utilities/HUSTOJ.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace cugoj_ng_server.Utilities +{ + using static Crypto; + using static Sequence; + public static class HUSTOJ + { + static readonly Dictionary langMap = new(); + public enum Langs + { + C, CPP, Pascal, Java, Ruby, Bash, Python, PHP, Perl, CSharp, ObjC, FreeBasic, Scheme, Clang, ClangPP, Lua, JavaScript, Go, Other + }; + public enum Status + { + MSG_Pending, + MSG_Pending_Rejudging, + MSG_Compiling, + MSG_Running_Judging, + MSG_Accepted, + MSG_Presentation_Error, + MSG_Wrong_Answer, + MSG_Time_Limit_Exceed, + MSG_Memory_Limit_Exceed, + MSG_Output_Limit_Exceed, + MSG_Runtime_Error, + MSG_Compile_Error, + MSG_Compile_OK, + MSG_TEST_RUN, + MSG_Other + } + static HUSTOJ() + { + foreach (var lang in Enum.GetValues()) + langMap.Add(Enum.GetName(lang), (int)lang); + } + public static bool CheckPw(string password, string saved) + { + /* + function pwCheck($password,$saved) + { + $svd=base64_decode($saved); + $salt=substr($svd,20); + if(!isOldPW($password)) $password=md5($password); + $hash = base64_encode( sha1(($password) . $salt, true) . $salt ); + if (strcmp($hash,$saved)==0) return True; + else return False; + } + */ + ReadOnlySpan pswdBytes = saved.Base64Decode(); + if (pswdBytes.Length != 24) return false; + ReadOnlySpan finalHash = SHA1(ByteArrayConcat(MD5(password.Encode()).HexEncode().Encode(), pswdBytes[20..].ToArray())); + if (!pswdBytes[..20].SequenceEqual(finalHash)) return false; + return true; + } + public static int MapLanguageToId(string lang) => langMap[lang]; + } +} diff --git a/Utilities/Inject.cs b/Utilities/Inject.cs new file mode 100644 index 0000000..bbc6a50 --- /dev/null +++ b/Utilities/Inject.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace cugoj_ng_server.Utilities +{ + public static class Inject + { + const BindingFlags bindingAttr = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + public static void SetPropertiesByType(Type type, object obj, Dictionary rules, BindingFlags bindingFlags = bindingAttr) + { + Array.ForEach(type.GetProperties(bindingFlags), propInfo => + { + if (rules.TryGetValue(propInfo.PropertyType, out object propValue)) + propInfo.SetValue(obj, propValue); + }); + } + } +} diff --git a/Utilities/Sequence.cs b/Utilities/Sequence.cs new file mode 100644 index 0000000..5175733 --- /dev/null +++ b/Utilities/Sequence.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace cugoj_ng_server.Utilities +{ + public static class Sequence + { + public static byte[] ByteArrayConcat(params byte[][] arrays) + { + var dstArr = new byte[arrays.Sum(x => x.Length)]; + var offset = 0; + Array.ForEach(arrays, arr => + { + Buffer.BlockCopy(arr, 0, dstArr, offset, arr.Length); + offset += arr.Length; + }); + return dstArr; + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..4e80911 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "Config": { + "SessionTimeout": 1440, + "ProblemsPerPage": 100 + } +} diff --git a/cugoj-ng-server.csproj b/cugoj-ng-server.csproj new file mode 100644 index 0000000..af4bd8f --- /dev/null +++ b/cugoj-ng-server.csproj @@ -0,0 +1,14 @@ + + + + net5.0 + cugoj_ng_server + + + + + + + + + diff --git a/cugoj-ng-server.sln b/cugoj-ng-server.sln new file mode 100644 index 0000000..9e8e01d --- /dev/null +++ b/cugoj-ng-server.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31112.23 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cugoj-ng-server", "cugoj-ng-server.csproj", "{ED9CA154-9750-4E44-941C-52F9C28148C8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ED9CA154-9750-4E44-941C-52F9C28148C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED9CA154-9750-4E44-941C-52F9C28148C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED9CA154-9750-4E44-941C-52F9C28148C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED9CA154-9750-4E44-941C-52F9C28148C8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7FD2509E-83B7-4FDF-A304-4EBE38755576} + EndGlobalSection +EndGlobal