410 lines
17 KiB
C#
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;
|
|
}
|
|
|
|
|
|
}
|
|
} |