simplenotesserver/Program.cs

410 lines
17 KiB
C#

using System.Text;
using System.Text.Encodings.Web;
using System.Web;
using LiteDB;
using Newtonsoft.Json;
using Tesses.WebServer;
namespace SimpleNotes
{
public class Note
{
public Note()
{
}
public Note(string title,string body)
{
Title = title;
Body = body;
}
[JsonProperty("id")]
public long Id {get;set;}
[JsonProperty("title")]
public string Title {get;set;}="";
[JsonProperty("body")]
public string Body {get;set;}="";
}
public static class Program
{
const string Index = "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>{0}</title>{2}</head><body><h1>{0}</h1>{1}</body></html>";
const string CreateAccount = "<form action=\"./adduser\" method=\"post\"><input type=\"text\" name=\"username\" placeholder=\"Username\"><br><input type=\"password\" name=\"password\" placeholder=\"Password\"><br><input type=\"password\" name=\"password_confirm\" placeholder=\"Confirm Password\"><br><input type=\"submit\" value=\"Create User\"></form>";
const string ChangePassword = "<form action=\"./changepassword\" method=\"post\"><input type=\"password\" name=\"old_password\" placeholder=\"Old Password\"><br><input type=\"password\" name=\"password\" placeholder=\"New Password\"><br><input type=\"password\" name=\"password_confirm\" placeholder=\"Confirm New Password\"><br><input type=\"submit\" value=\"Change Password\"></form>";
const string Css = "";
static string CreateIndex(string title,string page,string extraHead="")
{
return string.Format(Index,HttpUtility.HtmlEncode(title),page,$"<style>{Css}</style>{extraHead}");
}
static string A(string href,string content,bool escapeContent=true)
{
return $"<a href=\"{HttpUtility.HtmlAttributeEncode(href)}\">{(escapeContent ? HttpUtility.HtmlEncode(content) : content)}</a>";
}
static string NewNoteForm()
{
return "<form action=\"./\" method=\"post\"><input type=\"text\" name=\"title\" placeholder=\"Title\"><br><textarea name=\"body\" placeholder=\"Body\" cols=\"30\" rows=\"10\"></textarea><br><input type=\"submit\" value=\"Post\"></form>";
}
static string ExistingNoteForm(Note note)
{
return $"<form action=\"./?id={note.Id}\" method=\"post\"><input type=\"text\" name=\"title\" placeholder=\"Title\" value=\"{HttpUtility.HtmlAttributeEncode(note.Title)}\"><br><textarea name=\"body\" placeholder=\"Body\" cols=\"30\" rows=\"10\">{HttpUtility.HtmlEncode(note.Body)}</textarea><br><input type=\"submit\" value=\"Save Changes\"></form>";
}
static string MainIndex(IEnumerable<Note> notes)
{
StringBuilder page = new StringBuilder();
page.Append($"{A("/adduser","Add User")} {A("/changepassword","Change Password")}<br>");
page.Append(NewNoteForm());
page.Append("<ul>");
foreach(var item in notes)
{
page.Append($"<li>{A($"./?id={item.Id}",item.Title)} {A($"./delete?id={item.Id}","Delete")}</li>");
}
page.Append("</ul>");
return CreateIndex("SimpleNotes",page.ToString());
}
public static string GetUserName(this ServerContext ctx)
{
string username = "";
if (ctx.RequestHeaders.TryGetFirst("Authorization", out var auth))
{
string[] authorization = auth.Split(' ');
//authorization_basic
if (authorization[0] == "Basic")
{
string[] userPass = Encoding.UTF8.GetString(Convert.FromBase64String(authorization[1])).Split(new char[] { ':' },2);
//return userPass.Equals($"{config.UserName}:{config.Password}", StringComparison.Ordinal);
username = userPass[0];
}
}
return username;
}
private static string Clean(string text)
{
StringBuilder builder=new StringBuilder();
foreach(var c in text)
{
if(c > sbyte.MaxValue)
{
byte[] data = Encoding.UTF8.GetBytes($"{c}");
foreach(var d in data)
{
builder.Append($"${d.ToString("X2")}");
}
}else
if(!char.IsLetterOrDigit(c) && c != '$' && c != '_')
{
byte data = (byte)c;
builder.Append($"${data.ToString("X2")}");
}
else if(c == '$')
{
builder.Append("$$");
}
else
{
builder.Append(c);
}
}
return builder.ToString();
}
public static ILiteCollection<Note> GetCollection2(this ILiteDatabase db,ServerContext ctx)
{
return db.GetCollection<Note>($"notes_{Clean(ctx.GetUserName())}",BsonAutoId.ObjectId);
}
public static void Main(string[] args)
{
Directory.CreateDirectory("data");
using ILiteDatabase db = new LiteDatabase("data/mydb.db");
RouteServer routeServer = new RouteServer();
routeServer.Add("/adduser",async(ctx)=>{
await ctx.SendTextAsync(CreateIndex("Add User",CreateAccount));
});
routeServer.Add("/changepassword",async(ctx)=>{
await ctx.SendTextAsync(CreateIndex("Change Password",ChangePassword));
});
routeServer.Add("/changepassword",async(ctx)=>{
ctx.ParseBody();
if(ctx.QueryParams.TryGetFirst("old_password",out var old_password) && ctx.QueryParams.TryGetFirst("password",out var password) && ctx.QueryParams.TryGetFirst("password_confirm",out var password_confirm) && password==password_confirm)
{
if(User.ChangePassword(db.GetCollection<User>("users"),ctx.GetUserName(),old_password,password))
{
await ctx.SendRedirectAsync("/");
}
else
{
await ctx.SendTextAsync(CreateIndex("Failed",A("/adduser","<- Back")));
}
}
else
{
await ctx.SendTextAsync(CreateIndex("Failed",A("/adduser","<- Back")));
}
},"POST");
routeServer.Add("/adduser",async(ctx)=>{
ctx.ParseBody();
if(ctx.QueryParams.TryGetFirst("username",out var username) && ctx.QueryParams.TryGetFirst("password",out var password) && ctx.QueryParams.TryGetFirst("password_confirm",out var password_confirm) && password==password_confirm)
{
if(User.AddUser(db.GetCollection<User>("users"),username,password))
{
await ctx.SendRedirectAsync("/");
}
else
{
await ctx.SendTextAsync(CreateIndex("Failed",A("/adduser","<- Back")));
}
}
else
{
await ctx.SendTextAsync(CreateIndex("Failed",A("/adduser","<- Back")));
}
},"POST");
foreach(var meth in new[]{"GET","POST"})
routeServer.Add("/",RootPage,meth);
async Task RootPage(ServerContext ctx)
{
if(ctx.Method == "POST")
{
ctx.ParseBody();
}
if(ctx.QueryParams.TryGetFirst("id",out var id) && long.TryParse(id,out var idV))
{
var item=db.GetCollection2(ctx).FindById(idV);
if(item == null)
{
await ctx.SendRedirectAsync("/");
}
else
{
if(ctx.Method == "POST")
{
if(ctx.QueryParams.TryGetFirst("title",out var title) && ctx.QueryParams.TryGetFirst("body",out var body))
{
item.Title = title;
item.Body = body;
db.GetCollection2(ctx).Update(item);
}
}
await ctx.SendTextAsync(CreateIndex(item.Title,ExistingNoteForm(item)));
}
}
else
{
if(ctx.Method == "POST")
{
if(ctx.QueryParams.TryGetFirst("title",out var title) && ctx.QueryParams.TryGetFirst("body",out var body))
{
var item = new Note();
item.Title = title;
item.Body = body;
db.GetCollection2(ctx).Insert(item);
}
}
await ctx.SendTextAsync(MainIndex(db.GetCollection2(ctx).FindAll()));
}
}
async Task ApiNote(ServerContext ctx)
{
try{
if(ctx.Method != "DELETE")
{
if(ctx.Method == "POST")
{
Note n = await ctx.ReadJsonAsync<Note>();
var f = n.Id == 0 ? null : db.GetCollection2(ctx).FindById(n.Id);
if(f == null)
n.Id = (long)db.GetCollection2(ctx).Insert(n);
else
db.GetCollection2(ctx).Update(n);
await ctx.SendJsonAsync(new{success=true,note=n});
}
else if (ctx.Method == "GET")
{
bool justKeysBool=false;
if(ctx.QueryParams.TryGetFirst("justkeys",out var justKeys) && justKeys == "true")
justKeysBool=true;
if(ctx.QueryParams.TryGetFirst("id",out var id) && long.TryParse(id,out var idV))
{
Note note = db.GetCollection2(ctx).FindById(idV);
if(note == null)
await ctx.SendJsonAsync(new{success=false});
else
await ctx.SendJsonAsync(new{success=true,note = note});
}
else
{
if(justKeysBool)
{
}
else
{
List<Note> notes = new List<Note>();
foreach(var item in db.GetCollection2(ctx).FindAll())
{
notes.Add(item);
}
await ctx.SendJsonAsync(new{success=true,notes=notes});
}
}
}
}
else
{
if(ctx.QueryParams.TryGetFirst("id",out var id) && long.TryParse(id,out var idV))
{
bool res=db.GetCollection2(ctx).Delete(idV);
await ctx.SendJsonAsync(new {
success=res,
});
}
else
{
int v=db.GetCollection2(ctx).DeleteAll();
await ctx.SendJsonAsync(new {
success=true,
deleted=v
});
}
}
}catch(Exception ex)
{
Console.WriteLine(ex);
await ctx.SendJsonAsync(new{success=false,exception=ex});
}
}
foreach(var meth in new[]{"GET","POST","DELETE"})
routeServer.Add("/api/note",ApiNote,meth);
routeServer.Add("/delete",async(ctx)=>{
// /delete?confirm=true
if(ctx.QueryParams.TryGetFirst("id",out var id) && long.TryParse(id,out var idV))
{
if(ctx.QueryParams.TryGetFirst("confirm",out var confirm) && confirm == "true")
{
db.GetCollection2(ctx).Delete(idV);
await ctx.SendRedirectAsync("/");
}
else
{
await ctx.SendTextAsync(CreateIndex("Delete note?",$"{A($"/delete?id={idV}&confirm=true","Yes")} {A("/","No")}"));
}
}
else
{
if(ctx.QueryParams.TryGetFirst("confirm",out var confirm) && confirm == "true")
{
db.GetCollection2(ctx).DeleteAll();
await ctx.SendRedirectAsync("/");
}
else
{
await ctx.SendTextAsync(CreateIndex("Delete note?",$"{A($"/delete?confirm=true","Yes")} {A("/","No")}"));
}
}
},"GET");
BasicAuthServer authServer=new BasicAuthServer((user,pass)=>{
var userColl = db.GetCollection<User>("users");
return User.ValidUser(userColl,user,pass);
},routeServer,"NotesRealm");
authServer.StartServer(19428);
}
}
public class User
{
public long Id {get;set;}
public string Username {get;set;}="";
public string Salt {get;set;}="";
public string HashedPassword {get;set;}="";
public static bool ValidUser(ILiteCollection<User> users,string username,string password)
{
if(users.Count() == 0)
{
return AddUser(users,username,password);
}
else
{
var user = FindUser(users,username);
if(user == null) return false;
return user.CorrectPassword(password);
}
}
public bool CorrectPassword(string password)
{
string passwordWithSalt = $"{Salt}{password}";
return HashedPassword==Convert.ToBase64String(System.Security.Cryptography.SHA512.HashData(System.Text.Encoding.UTF8.GetBytes(passwordWithSalt)));
}
public static bool ValidPassword(string password)
{
return password.Length >= 12;
}
public void SetPassword(string password)
{
byte[] salt=System.Security.Cryptography.RandomNumberGenerator.GetBytes(32);
Salt = Convert.ToBase64String(salt);
string passwordWithSalt = $"{Salt}{password}";
HashedPassword=Convert.ToBase64String(System.Security.Cryptography.SHA512.HashData(System.Text.Encoding.UTF8.GetBytes(passwordWithSalt)));
}
public static User FindUser(ILiteCollection<User> users,string username)
{
return users.FindOne(e=>e.Username==username);
}
public static bool AddUser(ILiteCollection<User> users,string username,string password)
{
if(username.Contains(':')) return false;
if(FindUser(users,username) != null)
return false;
User user = new User();
user.Username = username;
if(!ValidPassword(password)) return false;
user.SetPassword(password);
users.Insert(user);
return true;
}
public static bool ChangePassword(ILiteCollection<User> users,string username,string oldPassword,string newPassword)
{
var user=FindUser(users,username);
if(user == null) return false;
if(!user.CorrectPassword(oldPassword)) return false;
if(!ValidPassword(newPassword)) return false;
user.SetPassword(newPassword);
users.Update(user);
return true;
}
}
}