Compare commits

..

52 Commits
1.2-1 ... main

Author SHA1 Message Date
Tracker-Friendly 3007e9f3b6 Attempt to fix the cursed subdomain architecture 2024-10-20 19:53:24 +01:00
Tracker-Friendly e8c3b11165 Fixed build script 2024-10-20 16:53:45 +01:00
Tracker-Friendly 5502758d25 Deleted useless file 2024-10-20 11:40:00 +01:00
Tracker-Friendly 3db8328cac It's time to rewrite burgernotes!! Again!!!!!!! 2024-10-20 10:45:03 +01:00
Tracker-Friendly 795fa524d0 Fixed another typo in hashcash 2024-07-30 15:40:11 +01:00
Tracker-Friendly 7d33099334 Fixed typo in hashcash 2024-07-30 15:32:32 +01:00
Tracker-Friendly 1ae5222e33 Delete burgernotes 2024-07-24 12:00:00 +01:00
Tracker-Friendly 9a831a5a6f Removed a few more "at"s and fixed ERRORS.md 2024-07-21 10:00:27 +01:00
Tracker-Friendly 6ac1c27db5 Made spent cleanups run on the correct database and make them show how many rows where deleted 2024-07-21 09:49:55 +01:00
Tracker-Friendly ef54b67803 made stamps be able to be spent and add an expiration mechanism 2024-07-21 09:45:47 +01:00
Tracker-Friendly 9725de7fb3 Added support for migration on changepassword and login 2024-07-21 09:09:30 +01:00
Tracker-Friendly 1fb0d1c85a Make the memory database use a set name so it's safe to defer 2024-07-21 09:03:32 +01:00
Tracker-Friendly d4148a4939 Make the memory database actually in memory and not defer it so that it doesn't kill itself 2024-07-21 08:59:16 +01:00
Tracker-Friendly 9bd62dbf64 Made migrations not kill the process 2024-07-21 08:49:41 +01:00
Tracker-Friendly 866f07825e Made failed migrations non-fatal 2024-07-20 20:37:00 +01:00
Tracker-Friendly c2aa6673b0 Added migration detection functionality to the server 2024-07-20 20:35:54 +01:00
Tracker-Friendly 0a22d2fd39 Made memory-based sessions work correctly, add a proof-of-work captcha to deter spamming 2024-07-20 16:45:21 +01:00
Tracker-Friendly 55bec8e5f8 Specified which encryption to use 2024-07-20 10:53:57 +01:00
Tracker-Friendly 3b70fe7b4b Removed a bunch of legacy APIs, added OAuth2 support, using Burgernotes 2.0 Draft 2 style encryption. 2024-07-20 10:52:18 +01:00
Tracker-Friendly b0a81780dc Go back to an older go version for compatibility with debian 2024-06-28 19:34:52 +01:00
Tracker-Friendly fa75483cb3 Removed temporary file 2024-06-27 18:29:33 +01:00
Tracker-Friendly a7c3a5d364 Add a JSON version option to make it easier for machine processing 2024-06-27 18:29:20 +01:00
Tracker-Friendly 8e1b3eaec9 Add /api/purgenotes 2024-06-27 17:46:50 +01:00
Tracker-Friendly 8d97cb2d0a Made all JSON safe, so that a malformed request cannot cause a panic 2024-06-26 18:53:58 +01:00
Tracker-Friendly debaf573b0 Update ERRORS.md 2024-06-25 23:56:52 +01:00
Tracker-Friendly 1522fa32e6 Add ERRORS.md 2024-06-25 23:55:36 +01:00
Tracker-Friendly e3359a02a8 Update APIDOCS.md 2024-06-25 23:43:23 +01:00
Tracker-Friendly cda77cd3b9 Moved the changepassword function to the bottom so that it's under authentication, made it actually change the password, make it && instead of || so it doesn't cause the glitch stated in the last commit 2024-06-25 16:54:26 +01:00
Tracker-Friendly bbd2ea7daa Fixed an error where version1legacychange would delete your current password on any login 2024-06-25 16:46:09 +01:00
Tracker-Friendly 6084d9ddf6 Oops, forgot the ?s 2024-06-24 20:35:58 +01:00
Tracker-Friendly 7e2a5a90c1 Forgot the substituion ?s 2024-06-24 20:36:14 +01:00
Tracker-Friendly d4fe38f245 Merge remote-tracking branch 'origin/main' 2024-06-24 16:55:56 +01:00
Tracker-Friendly 1d5b19d17f Add a version 2 API that fixes the issue with pre-PageBurger releases of Burgernotes not using the SHA-3 based hashing format to not be able to log in properly. This also fixes the older Burgernotes clients. 2024-06-24 16:55:48 +01:00
maaa d92acb48da meowing 2024-06-23 16:50:53 +02:00
Tracker-Friendly dde44016e3 Fully rewrote the server fr in go 2024-06-21 01:46:21 +01:00
Tracker-Friendly 99b8eb4240 Fixed it always removing the current session 2024-06-14 15:51:18 +01:00
Tracker-Friendly 05488938ef Backported better session removal from burgerauth 2024-06-14 07:51:24 +00:00
Tracker-Friendly c03c29bce6 Fixed loggedin 2024-05-24 19:49:04 +01:00
Tracker-Friendly 29df087261 BETA: Loggedin 2024-05-24 19:24:37 +01:00
Tracker-Friendly 5f394198f5 Finish up importing notes 2024-05-23 17:55:12 +01:00
Tracker-Friendly 658e6d8e5c WIP: Importing notes 2024-05-23 17:40:40 +01:00
Tracker-Friendly 92dc80745d Update ROADMAP.md 2024-05-06 14:14:27 +01:00
Tracker-Friendly ee4332bd81 Re-add vaccum 2024-05-06 00:50:05 +01:00
Tracker-Friendly 503b26403c Fixed live editing, corrected many small errors, added more general best practices 2024-04-29 00:15:40 +01:00
Tracker-Friendly e178bbee57 Delete down 2024-04-25 22:24:39 +01:00
Tracker-Friendly 738e5ebcfd Backported better config messages from burgerauth 2024-04-24 12:09:28 +01:00
Tracker-Friendly d2535d2647 Remove closing database unneccesarily, backported from burgerauth 2024-04-24 11:53:56 +01:00
Tracker-Friendly 53189f5196 Delete vacuum 2024-04-24 11:52:07 +01:00
Tracker-Friendly d55d22b765 Delete fixapi.sh 2024-04-24 11:51:56 +01:00
Tracker-Friendly ed6db00727 Update ROADMAP.md 2024-04-24 11:51:40 +01:00
Tracker-Friendly ab14089ce5 Not needed 2024-04-24 11:51:17 +01:00
Tracker-Friendly ab9be10c94 Delete main.save 2024-04-24 11:48:52 +01:00
20 changed files with 1251 additions and 1331 deletions

7
.gitignore vendored
View File

@ -1,3 +1,4 @@
database.db # IntelliJ IDEA
config.ini .idea
config.ini.example # Protocol Buffers
git.ailur.dev

View File

@ -1,87 +1,313 @@
# 🍔 Burgernotes API docs # API documentation
Use the Burgernotes API to automate tasks, build your own client, and more!
Headers should be: "Content-type: application/json; charset=UTF-8" for all POSTs The Burgernotes API is a RESTful API that allows you to interact with the Burgernotes service. The API is designed to be simple and easy to use.
It uses Protocol Buffers for serialization and deserialization, and POST requests for all operations.
## 🔑 Authentication ## Protobuf types
These are some basic Protobuf types that are used in the API.
All protocol buffers are using proto3 syntax.
```protobuf
syntax = "proto3";
POST - /api/signup - provide "username" and "password". // Token is a string that represents an OAuth2 JWT token.
message Token {
string token = 1;
}
POST - /api/login - provide "username", "password", "passwordchange" (must be "yes" or "no") and "newpass" // NoteID is a UUID that represents a note.
message NoteID {
bytes noteId = 1;
}
To prevent the server from knowing the encryption key, the password you provide in the request must be hashed with the SHA-3 with 128 iterations (the hash is hashed again 128 times). // NoteID and Token together represent a request involving a note.
message NoteRequest {
NoteID noteId = 1;
Token token = 2;
}
If you wish to change the user's password, set "passwordchange" to "yes" and "newpass" to the new hash. // AESData represents AES-encrypted data.
message AESData {
bytes data = 2;
bytes iv = 3;
}
// NoteMetadata represents the metadata of a note.
message NoteMetadata {
NoteID noteId = 1;
AESData title = 2;
}
Some users use the legacy argon2id mode (by which i mean about 8, so only implement if you feel like it), and to implement argon2id functionality, you hash like this: // Note represents a note.
message Note {
NoteMetadata metadata = 1;
AESData note = 2;
}
// Invitation represents an invitation to a note.
message Invitation {
string username = 1;
AESData key = 2;
NoteID noteId = 3;
}
// User represents a user editing notes.
message UserLines {
string username = 1;
bytes uuid = 2;
repeated uint64 lines = 3;
}
// Error represents an error.
message Error {
string error = 1;
}
// ServerError represents a 500 error, with a hex error code.
message ServerError {
bytes errorCode = 1;
}
``` ```
Parallelism should be 1 ## Errors
In any response, if an error occurs, it will return an `Error` or `ServerError` message.
### 400 Range
```protobuf
message Error {
string error = 1;
}
```
The error is formatted to be human-readable, you may display it to the user.
### HTTP 500
```protobuf
message ServerError {
bytes errorCode = 1;
}
```
ServerError is a hex byte which represents the error code. You should give a vague error message to the user.
Iterations should be 256 ## Authentication
### /api/signup - POST
#### Request
```protobuf
message ApiSignupRequest {
bytes publicKey = 1;
Token token = 2;
}
```
#### Response
200 OK
No response body
Memory Allocated in bytes should be 512 ### /api/delete - POST - Show a warning before this action!
#### Request
```protobuf
message Token {
string token = 1;
}
```
#### Response
HTTP 200 OK
No response body
Length of Hash should be 32 bytes ## Interacting with notes
### /api/notes/add - POST
The output should be in the encoded format, not the hashed format #### Request
```protobuf
Salt should be the SHA512 of the password message Token {
string token = 1;
}
```
#### Response
HTTP 200 OK
```protobuf
message NoteID {
bytes noteId = 1;
}
``` ```
(Yes i know this is really bad practice, guess why we are replacing it) ### /api/notes/remove - POST
#### Request
```protobuf
message NoteRequest {
NoteID noteId = 1;
Token token
}
```
#### Response
HTTP 200 OK
No response body
To test if SHA-3 or argon2 is used, just try the SHA-3 and if 422 gets returned try argon2. ### /api/notes/list - POST
#### Request
```protobuf
message Token {
string token = 1;
}
```
#### Response
HTTP 200 OK
```protobuf
message ApiNotesListResponse {
repeated NoteMetadata notes = 1;
}
```
(For the sake of all of us, change the password to the SHA-3 hash) ### /api/notes/get - POST
#### Request
```protobuf
message NoteRequest {
NoteID noteId = 1;
Token token
}
```
#### Response
HTTP 200 OK
```protobuf
message Note {
NoteMetadata metadata = 1;
AESData note = 2;
}
```
### /api/notes/edit - POST
#### Request
```protobuf
message ApiNotesEditRequest {
Note note = 1;
Token token = 2;
}
```
#### Response
HTTP 200 OK
No response body
Password should be at least 8 characters, username must be under 20 characters and alphanumeric. ### /api/notes/purge - POST - Show a warning before this action!
#### Request
```protobuf
message Token {
string token = 1;
}
```
#### Response
HTTP 200 OK
No response body
If username is taken, error code 422 will return. ## Shared notes - This is not yet implemented
### /api/invite/prepare - POST
#### Request
```protobuf
message ApiInvitePrepareRequest {
string username = 1;
Token token = 2;
}
```
#### Response
HTTP 200 OK
```protobuf
message ApiInvitePrepareResponse {
bytes ecdhExchange = 1;
}
```
Assuming everything went correctly, the server will return a secret key. ### /api/invite/check - POST
#### Request
```protobuf
message Token {
string token = 1;
}
```
#### Response
HTTP 200 OK
```protobuf
message ApiInviteCheckResponse {
repeated Invitation invitations = 1;
}
```
You'll need to store two things in local storage: ### /api/invite/link - POST
- The secret key you just got, used to fetch notes, save stuff etc. #### Request
- A SHA512 hashed password, used as encryption key ```protobuf
message ApiInviteLinkRequest {
NoteRequest noteRequest = 1;
int64 timestamp = 2;
bool singleUse = 3;
}
```
#### Response
HTTP 200 OK
```protobuf
message ApiInviteLinkResponse {
bytes inviteCode = 1;
}
```
## 🔐 Encryption ### /api/invite/accept - POST
#### Request
```protobuf
message ApiInviteAcceptRequest {
bytes inviteCode = 1;
Token token = 2;
}
```
#### Response
HTTP 200 OK
```protobuf
message NoteID {
bytes noteId = 1;
}
```
Note content and title is encrypted using AES 256-bit. ### /api/invite/leave - POST
#### Request
```protobuf
message NoteRequest {
NoteID noteId = 1;
Token token
}
```
#### Response
HTTP 200 OK
No response body
Encryption password is the SHA512 hashed password we talked about earlier. ### /api/shared - WebSocket
Every heartbeat interval (500ms):
#### Request
```protobuf
message ApiSharedRequest {
repeated uint64 lines = 1;
Token token = 2;
}
```
#### Response
```protobuf
message ApiSharedResponse {
repeated UserLines users = 1;
}
```
## 🕹️ Basic stuff ### /api/shared/edit - POST
#### Request
```protobuf
message ApiSharedEditRequest {
repeated AESData lines = 1;
Token token = 2;
}
```
#### Response
HTTP 200 OK
No response body
POST - /api/userinfo - get user info such as username, provide "secretKey" ### /api/shared/get - POST
#### Request
POST - /api/listnotes - list notes, provide "secretKey" ```protobuf
note titles will have to be decrypted. message NoteRequest {
NoteID noteId = 1;
POST - /api/newnote - create a note, provide "secretKey" and "noteName" Token token
"noteName" should be encrypted. }
```
POST - /api/readnote - read notes, provide "secretKey" and "noteId" #### Response
note content will have to be decrypted. ```protobuf
message ApiSharedGetResponse {
POST - /api/editnote - edit notes, provide "secretKey", "noteId", "title", and "content" repeated AESData lines = 1;
"content" should be encrypted. NoteMetadata metadata = 2;
"title" is the first line of the note content, and should be encrypted. }
```
**(Deprecated ⚠️)** POST - /api/editnotetitle - edit note titles, provide "secretKey", "noteId", and "content"
"content" should be encrypted.
POST - /api/removenote - remove notes, provide "secretKey" and "noteId"
## ⚙️ More stuff
POST - /api/deleteaccount - delete account, provide "secretKey"
please display a warning before this action
POST - /api/exportnotes - export notes, provide "secretKey"
note content and title will have to be decrypted
POST - /api/sessions/list - show all sessions, provide "secretKey"
POST - /api/sessions/remove - remove session, provide "secretKey" and "sessionId"

View File

@ -2,18 +2,33 @@
Burgernotes is a simple note-taking app with end-to-end encryption. Burgernotes is a simple note-taking app with end-to-end encryption.
### Setup ### Setup
To set up Burgernotes, simply run these commands: To set up Burgernotes, set it up like any other Fulgens Server module.
``` ```
cp config.ini.example config.ini cd /path/to/fulgens/directory
python3 init_db git clone https://git.ailur.dev/ailur/burgernotes.git --depth=1 services-src/burgernotes
```
If you want to rebuild all of fulgens (recommended), run `./build.sh` in the fulgens directory.
If you only want to build the Burgernotes module, run `services-src/burgernotes/build.sh`.
### Configuration
Edit the main `config.json` file to include the Burgernotes module in the `services` object.
```json
{
"burgernotes": {
"subdomain": "notes.example.org",
"hostName": "https://notes.example.org"
}
}
``` ```
Edit config.ini to your liking, then to start the server run: ### Running
Run the Fulgens server as you normally would.
``` ```
python3 main ./fulgens
``` ```
### Links ### Links
[Go to the Burgernotes website](https://notes.hectabit.org) [Go to the Burgernotes website](https://notes.ailur.dev)
[API documentation](APIDOCS.md) [API documentation](APIDOCS.md)

View File

@ -1,8 +1,5 @@
# Burgernotes Roadmap # Burgernotes Roadmap
- Switch to WebSockets for updating notes + live updating of note list and more, this involves redoing some APIs - Create the frontend
- Compress notes to reduce bandwidth and storage - Implementing shared notes
- Dedicated domain (not just a subdomain, if anyone can donate a domain let Arzumify know!) - Dedicated domain (not just a subdomain, if anyone can donate a domain let Arzumify know!)
- Native Apps (native iOS and Linux apps* are in development)
*kinda, not really active much :3

7
build.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
path=$(realpath "$(dirname "$0")") || exit 1
rm -rf "$path/../../services/burgernotes.fgs" || exit 1
cd "$path" || exit 1
protoc -I="$path" --go_out="$path" "$path/main.proto" || exit 1
go build -o "$path/../../services/burgernotes.fgs" --buildmode=plugin -ldflags "-s -w" || exit 1

View File

@ -1,20 +0,0 @@
import time
import subprocess
def run_init_db():
# Run the init_db.py script using subprocess
try:
subprocess.run(["python", "init_db"])
except Exception as e:
print("Error:", e)
def main():
while True:
# Run init_db.py
run_init_db()
# Wait for 5 minutes before running again
time.sleep(300) # 300 seconds = 5 minutes
if __name__ == "__main__":
main()

View File

@ -1,5 +0,0 @@
[config]
HOST = 0.0.0.0
PORT = 8080
SECRET_KEY = supersecretkey
MAX_STORAGE = 25000000

30
down
View File

@ -1,30 +0,0 @@
#!/usr/bin/python3
import configparser
from waitress import serve
from flask import Flask, render_template, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request
config = configparser.ConfigParser()
config.read("config.ini")
HOST = config["config"]["HOST"]
PORT = config["config"]["PORT"]
SECRET_KEY = config["config"]["SECRET_KEY"]
app = Flask(__name__, static_folder=None)
app.config["SECRET_KEY"] = SECRET_KEY
@app.errorhandler(404)
def main(e):
return render_template("down.html"), 503
@app.route("/grid.svg")
def staticgrid():
return Response("""<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="0.25" cy="0.25" r="0.25" fill="white"/><circle cx="9.25" cy="0.25" r="0.25" fill="white"/><circle cx="18.25" cy="0.25" r="0.25" fill="white"/><circle cx="27.25" cy="0.25" r="0.25" fill="white"/><circle cx="36.25" cy="0.25" r="0.25" fill="white"/><circle cx="45.25" cy="0.25" r="0.25" fill="white"/><circle cx="54.25" cy="0.25" r="0.25" fill="white"/><circle cx="63.25" cy="0.25" r="0.25" fill="white"/><circle cx="72.25" cy="0.25" r="0.25" fill="white"/><circle cx="81.25" cy="0.25" r="0.25" fill="white"/><circle cx="90.25" cy="0.25" r="0.25" fill="white"/><circle cx="0.25" cy="9.25" r="0.25" fill="white"/><circle cx="9.25" cy="9.25" r="0.25" fill="white"/><circle cx="18.25" cy="9.25" r="0.25" fill="white"/><circle cx="27.25" cy="9.25" r="0.25" fill="white"/><circle cx="36.25" cy="9.25" r="0.25" fill="white"/><circle cx="45.25" cy="9.25" r="0.25" fill="white"/><circle cx="54.25" cy="9.25" r="0.25" fill="white"/><circle cx="63.25" cy="9.25" r="0.25" fill="white"/><circle cx="72.25" cy="9.25" r="0.25" fill="white"/><circle cx="81.25" cy="9.25" r="0.25" fill="white"/><circle cx="90.25" cy="9.25" r="0.25" fill="white"/><circle cx="0.5" cy="18.5" r="0.5" fill="white"/><circle cx="9.5" cy="18.5" r="0.5" fill="white"/><circle cx="18.5" cy="18.5" r="0.5" fill="white"/><circle cx="27.5" cy="18.5" r="0.5" fill="white"/><circle cx="36.5" cy="18.5" r="0.5" fill="white"/><circle cx="45.5" cy="18.5" r="0.5" fill="white"/><circle cx="54.5" cy="18.5" r="0.5" fill="white"/><circle cx="63.5" cy="18.5" r="0.5" fill="white"/><circle cx="72.5" cy="18.5" r="0.5" fill="white"/><circle cx="81.5" cy="18.5" r="0.5" fill="white"/><circle cx="90.5" cy="18.5" r="0.5" fill="white"/><circle cx="0.5" cy="27.5" r="0.5" fill="white"/><circle cx="9.5" cy="27.5" r="0.5" fill="white"/><circle cx="18.5" cy="27.5" r="0.5" fill="white"/><circle cx="27.5" cy="27.5" r="0.5" fill="white"/><circle cx="36.5" cy="27.5" r="0.5" fill="white"/><circle cx="45.5" cy="27.5" r="0.5" fill="white"/><circle cx="54.5" cy="27.5" r="0.5" fill="white"/><circle cx="63.5" cy="27.5" r="0.5" fill="white"/><circle cx="72.5" cy="27.5" r="0.5" fill="white"/><circle cx="81.5" cy="27.5" r="0.5" fill="white"/><circle cx="90.5" cy="27.5" r="0.5" fill="white"/><circle cx="0.75" cy="36.75" r="0.75" fill="white"/><circle cx="9.75" cy="36.75" r="0.75" fill="white"/><circle cx="18.75" cy="36.75" r="0.75" fill="white"/><circle cx="27.75" cy="36.75" r="0.75" fill="white"/><circle cx="36.75" cy="36.75" r="0.75" fill="white"/><circle cx="45.75" cy="36.75" r="0.75" fill="white"/><circle cx="54.75" cy="36.75" r="0.75" fill="white"/><circle cx="63.75" cy="36.75" r="0.75" fill="white"/><circle cx="72.75" cy="36.75" r="0.75" fill="white"/><circle cx="81.75" cy="36.75" r="0.75" fill="white"/><circle cx="90.75" cy="36.75" r="0.75" fill="white"/><circle cx="0.75" cy="45.75" r="0.75" fill="white"/><circle cx="9.75" cy="45.75" r="0.75" fill="white"/><circle cx="18.75" cy="45.75" r="0.75" fill="white"/><circle cx="27.75" cy="45.75" r="0.75" fill="white"/><circle cx="36.75" cy="45.75" r="0.75" fill="white"/><circle cx="45.75" cy="45.75" r="0.75" fill="white"/><circle cx="54.75" cy="45.75" r="0.75" fill="white"/><circle cx="63.75" cy="45.75" r="0.75" fill="white"/><circle cx="72.75" cy="45.75" r="0.75" fill="white"/><circle cx="81.75" cy="45.75" r="0.75" fill="white"/><circle cx="90.75" cy="45.75" r="0.75" fill="white"/><circle cx="1" cy="55" r="1" fill="white"/><circle cx="10" cy="55" r="1" fill="white"/><circle cx="19" cy="55" r="1" fill="white"/><circle cx="28" cy="55" r="1" fill="white"/><circle cx="37" cy="55" r="1" fill="white"/><circle cx="46" cy="55" r="1" fill="white"/><circle cx="55" cy="55" r="1" fill="white"/><circle cx="64" cy="55" r="1" fill="white"/><circle cx="73" cy="55" r="1" fill="white"/><circle cx="82" cy="55" r="1" fill="white"/><circle cx="91" cy="55" r="1" fill="white"/><circle cx="1" cy="64" r="1" fill="white"/><circle cx="10" cy="64" r="1" fill="white"/><circle cx="19" cy="64" r="1" fill="white"/><circle cx="28" cy="64" r="1" fill="white"/><circle cx="37" cy="64" r="1" fill="white"/><circle cx="46" cy="64" r="1" fill="white"/><circle cx="55" cy="64" r="1" fill="white"/><circle cx="64" cy="64" r="1" fill="white"/><circle cx="73" cy="64" r="1" fill="white"/><circle cx="82" cy="64" r="1" fill="white"/>
<circle cx="91" cy="64" r="1" fill="white"/><circle cx="1.25" cy="73.25" r="1.25" fill="white"/><circle cx="10.25" cy="73.25" r="1.25" fill="white"/><circle cx="19.25" cy="73.25" r="1.25" fill="white"/><circle cx="28.25" cy="73.25" r="1.25" fill="white"/><circle cx="37.25" cy="73.25" r="1.25" fill="white"/><circle cx="46.25" cy="73.25" r="1.25" fill="white"/><circle cx="55.25" cy="73.25" r="1.25" fill="white"/><circle cx="64.25" cy="73.25" r="1.25" fill="white"/><circle cx="73.25" cy="73.25" r="1.25" fill="white"/><circle cx="82.25" cy="73.25" r="1.25" fill="white"/><circle cx="91.25" cy="73.25" r="1.25" fill="white"/><circle cx="1.25" cy="82.25" r="1.25" fill="white"/><circle cx="10.25" cy="82.25" r="1.25" fill="white"/><circle cx="19.25" cy="82.25" r="1.25" fill="white"/><circle cx="28.25" cy="82.25" r="1.25" fill="white"/><circle cx="37.25" cy="82.25" r="1.25" fill="white"/><circle cx="46.25" cy="82.25" r="1.25" fill="white"/><circle cx="55.25" cy="82.25" r="1.25" fill="white"/><circle cx="64.25" cy="82.25" r="1.25" fill="white"/><circle cx="73.25" cy="82.25" r="1.25" fill="white"/><circle cx="82.25" cy="82.25" r="1.25" fill="white"/><circle cx="91.25" cy="82.25" r="1.25" fill="white"/><circle cx="1.5" cy="91.5" r="1.5" fill="white"/>
<circle cx="10.5" cy="91.5" r="1.5" fill="white"/><circle cx="19.5" cy="91.5" r="1.5" fill="white"/><circle cx="28.5" cy="91.5" r="1.5" fill="white"/><circle cx="37.5" cy="91.5" r="1.5" fill="white"/><circle cx="46.5" cy="91.5" r="1.5" fill="white"/><circle cx="55.5" cy="91.5" r="1.5" fill="white"/><circle cx="64.5" cy="91.5" r="1.5" fill="white"/><circle cx="73.5" cy="91.5" r="1.5" fill="white"/><circle cx="82.5" cy="91.5" r="1.5" fill="white"/><circle cx="91.5" cy="91.5" r="1.5" fill="white"/>
</svg>""", mimetype="image/svg+xml")
if __name__ == "__main__":
print("[INFO] Server started")
serve(app, host=HOST, port=PORT)
print("[INFO] Server stopped")

View File

@ -1,8 +0,0 @@
#!/bin/bash
# Define the old and new URLs
OLD_URL="https://notes.canary.hectabit.org/api"
NEW_URL="https://notes.canary.hectabit.org/api"
# Recursively search and replace in files under the current directory
find . -type f -exec sed -i "s|$OLD_URL|$NEW_URL|g" {} +

15
go.mod Normal file
View File

@ -0,0 +1,15 @@
module git.ailur.dev/ailur/burgernotes
go 1.23.1
require (
git.ailur.dev/ailur/fg-library/v2 v2.1.0
git.ailur.dev/ailur/fg-nucleus-library v1.0.2
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
)
require (
github.com/go-chi/chi/v5 v5.1.0
google.golang.org/protobuf v1.35.1
)

18
go.sum Normal file
View File

@ -0,0 +1,18 @@
git.ailur.dev/ailur/fg-library/v2 v2.0.1 h1:ltPYXf/Om0hnMD8gr1K5bkYrfHqKPSbb0hxa0wtTnZ0=
git.ailur.dev/ailur/fg-library/v2 v2.0.1/go.mod h1:1jYbWhabGcIwp7CkhHqvRwC8eP+nHv5BrXPe9NX2HE8=
git.ailur.dev/ailur/fg-library/v2 v2.1.0 h1:SsLZ56poM6GZPfV/ywU/8WDTelu2dtlPp6jzbEZ4hrA=
git.ailur.dev/ailur/fg-library/v2 v2.1.0/go.mod h1:1jYbWhabGcIwp7CkhHqvRwC8eP+nHv5BrXPe9NX2HE8=
git.ailur.dev/ailur/fg-nucleus-library v1.0.2 h1:EWfeab+wJKaxx/Qg5TKpvZHicA0V/NilUv2g6W97rtg=
git.ailur.dev/ailur/fg-nucleus-library v1.0.2/go.mod h1:T2mdUiXlZqb917CkNB2vwujkD/QhJDpCHLRvKuskBpY=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=

29
init_db
View File

@ -1,29 +0,0 @@
#!/usr/bin/python3
import sqlite3
import os
def generatedb():
connection = sqlite3.connect("database.db")
with open("schema.sql") as f:
connection.executescript(f.read())
cur = connection.cursor()
connection.commit()
connection.close()
print("[INFO] Generated database")
if not os.path.exists("database.db"):
generatedb()
else:
answer = input("Proceeding will overwrite the database. Proceed? (y/N)")
if "y" in answer.lower():
generatedb()
elif "n" in answer.lower():
print("Stopped")
elif ":3" in answer:
print(":3")
else:
print("Stopped")

546
main
View File

@ -1,546 +0,0 @@
#!/usr/bin/python3
import os
import sqlite3
import time
import secrets
import configparser
import asyncio
from hypercorn.config import Config
from hypercorn.asyncio import serve
from werkzeug.security import generate_password_hash, check_password_hash
from quart import Quart, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request
# Parse configuration file, and check if anything is wrong with it
if not os.path.exists("config.ini"):
print("config.ini does not exist")
config = configparser.ConfigParser()
config.read("config.ini")
HOST = config["config"]["HOST"]
PORT = config["config"]["PORT"]
SECRET_KEY = config["config"]["SECRET_KEY"]
MAX_STORAGE = config["config"]["MAX_STORAGE"]
if SECRET_KEY == "supersecretkey" or SECRET_KEY == "placeholder":
print("[WARNING] Secret key not set")
# Define Quart
app = Quart(__name__)
app.config["SECRET_KEY"] = SECRET_KEY
# Database functions
def get_db_connection():
conn = sqlite3.connect("database.db")
conn.row_factory = sqlite3.Row
return conn
def get_user(id):
conn = get_db_connection()
post = conn.execute("SELECT * FROM users WHERE id = ?",
(id,)).fetchone()
conn.close()
if post is None:
return None
return post
def get_note(id):
conn = get_db_connection()
post = conn.execute("SELECT * FROM notes WHERE id = ?",
(id,)).fetchone()
conn.close()
if post is None:
return None
return post
def get_space(id):
conn = get_db_connection()
notes = conn.execute("SELECT content, title FROM notes WHERE creator = ? ORDER BY id DESC;", (id,)).fetchall()
conn.close()
spacetaken = 0
for x in notes:
spacetaken = spacetaken + len(x["content"].encode("utf-8"))
spacetaken = spacetaken + len(x["title"].encode("utf-8"))
return spacetaken
def get_note_count(id):
conn = get_db_connection()
notes = conn.execute("SELECT content, title FROM notes WHERE creator = ? ORDER BY id DESC;", (id,)).fetchall()
conn.close()
notecount = 0
for x in notes:
notecount = notecount + 1
return notecount
def get_session(id):
conn = get_db_connection()
post = conn.execute("SELECT * FROM sessions WHERE session = ?",
(id,)).fetchone()
conn.close()
if post is None:
return None
return post
def get_session_from_sessionid(id):
conn = get_db_connection()
post = conn.execute("SELECT * FROM sessions WHERE sessionid = ?",
(id,)).fetchone()
conn.close()
if post is None:
return None
return post
def check_username_taken(username):
conn = get_db_connection()
post = conn.execute("SELECT * FROM users WHERE lower(username) = ?",
(username.lower(),)).fetchone()
conn.close()
if post is None:
return None
return post["id"]
# Disable CORS
@app.after_request
async def add_cors_headers(response):
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "*")
response.headers.add("Access-Control-Allow-Methods", "*")
return response
# Live editing store
messages = {}
@app.route("/api/version", methods=("GET", "POST"))
async def apiversion():
return "Burgernotes Version 1.2"
@app.route("/api/signup", methods=("GET", "POST"))
async def apisignup():
if request.method == "POST":
data = await request.get_json()
username = data["username"]
password = data["password"]
if username == "":
return {}, 422
if len(username) > 20:
return {}, 422
if not username.isalnum():
return {}, 422
if password == "":
return {}, 422
if len(password) < 14:
return {}, 422
if not check_username_taken(username) == None:
return {}, 409
hashedpassword = generate_password_hash(password)
conn = get_db_connection()
conn.execute("INSERT INTO users (username, password, created) VALUES (?, ?, ?)",
(username, hashedpassword, str(time.time())))
conn.commit()
conn.close()
userID = check_username_taken(username)
user = get_user(userID)
randomCharacters = secrets.token_hex(512)
conn = get_db_connection()
conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)",
(randomCharacters, userID, request.headers.get("user-agent")))
conn.commit()
conn.close()
return {
"key": randomCharacters
}, 200
@app.route("/api/login", methods=("GET", "POST"))
async def apilogin():
if request.method == "POST":
data = await request.get_json()
username = data["username"]
password = data["password"]
passwordchange = data["passwordchange"]
newpass = data["newpass"]
check_username_thing = check_username_taken(username)
if check_username_thing == None:
return {}, 401
userID = check_username_taken(username)
user = get_user(userID)
if not check_password_hash(user["password"], (password)):
return {}, 401
randomCharacters = secrets.token_hex(512)
conn = get_db_connection()
conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)",
(randomCharacters, userID, request.headers.get("user-agent")))
conn.commit()
conn.close()
if passwordchange == "yes":
hashedpassword = generate_password_hash(newpass)
conn = get_db_connection()
conn.execute("UPDATE users SET password = ? WHERE username = ?", (hashedpassword, username))
conn.commit()
conn.close()
return {
"key": randomCharacters,
}, 200
@app.route("/api/userinfo", methods=("GET", "POST"))
async def apiuserinfo():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
datatemplate = {
"username": user["username"],
"id": user["id"],
"created": user["created"],
"storageused": get_space(user["id"]),
"storagemax": MAX_STORAGE,
"notecount": get_note_count(user["id"])
}
return datatemplate
@app.route("/api/listnotes", methods=("GET", "POST"))
async def apilistnotes():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
conn = get_db_connection()
notes = conn.execute("SELECT * FROM notes WHERE creator = ? ORDER BY edited DESC;", (user["id"],)).fetchall()
conn.close()
datatemplate = []
for note in notes:
notetemplate = {
"id": note["id"],
"title": note["title"]
}
datatemplate.append(notetemplate)
return datatemplate, 200
@app.route("/api/exportnotes", methods=("GET", "POST"))
async def apiexportnotes():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
conn = get_db_connection()
notes = conn.execute("SELECT * FROM notes WHERE creator = ? ORDER BY id DESC;", (user["id"],)).fetchall()
conn.close()
datatemplate = []
for note in notes:
notetemplate = {
"id": note["id"],
"created": note["created"],
"edited": note["edited"],
"title": note["title"],
"content": note["content"]
}
datatemplate.append(notetemplate)
return datatemplate, 200
@app.route("/api/newnote", methods=("GET", "POST"))
async def apinewnote():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
noteName = data["noteName"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
conn = get_db_connection()
conn.execute("INSERT INTO notes (title, content, creator, created, edited) VALUES (?, ?, ?, ?, ?)",
(noteName, "", user["id"], str(time.time()), str(time.time())))
conn.commit()
conn.close()
return {}, 200
@app.route("/api/readnote", methods=("GET", "POST"))
async def apireadnote():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
noteId = data["noteId"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
note = get_note(noteId)
if (note != None):
if (user["id"] == note["creator"]):
contenttemplate = {
"content": note["content"]
}
return contenttemplate, 200
else:
return {}, 422
else:
return {}, 422
@app.route("/api/waitforedit", methods=("GET", "POST"))
async def waitforedit():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
complete = true
start_time = time.time()
while user["id"] not in messages or not messages[user["id"]]:
await asyncio.sleep(0)
elapsed_time = time.time() - start_time
if elapsed_time >= 20:
break
complete = false
message = messages[user["id"]].pop(0)
del messages[user["id"]]
if complete == true:
return {
"note": message
}, 200
else:
return 400
@app.route("/api/editnote", methods=("GET", "POST"))
async def apieditnote():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
noteId = data["noteId"]
content = data["content"]
title = data["title"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
note = get_note(noteId)
if get_space(user["id"]) + len(content.encode("utf-8")) > int(MAX_STORAGE):
return {}, 418
if (note != None):
if (user["id"] == note["creator"]):
conn = get_db_connection()
conn.execute("UPDATE notes SET content = ?, title = ?, edited = ? WHERE id = ?", (content, title, str(time.time()), noteId))
conn.commit()
conn.close()
if user["id"] not in messages:
messages[user["id"]] = []
messages[user["id"]].append(noteId)
return {}, 200
else:
return {}, 403
else:
return {}, 422
@app.route("/api/editnotetitle", methods=("GET", "POST"))
async def apieditnotetitle():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
noteId = data["noteId"]
content = data["content"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
note = get_note(noteId)
if get_space(user["id"]) + len(content.encode("utf-8")) > int(MAX_STORAGE):
return {}, 418
if (note != None):
if (user["id"] == note["creator"]):
conn = get_db_connection()
conn.execute("UPDATE notes SET title = ?, edited = ? WHERE id = ?", (content, str(time.time()), noteId))
conn.commit()
conn.close()
return {}, 200
else:
return {}, 403
else:
return {}, 422
@app.route("/api/removenote", methods=("GET", "POST"))
async def apiremovenote():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
noteId = data["noteId"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
note = get_note(noteId)
if (note != None):
if (user["id"] == note["creator"]):
conn = get_db_connection()
conn.execute("DELETE FROM notes WHERE id = ?", (noteId,))
conn.commit()
conn.close()
return {}, 200
else:
return {}, 403
else:
return {}, 422
@app.route("/api/deleteaccount", methods=("GET", "POST"))
async def apideleteaccount():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
conn = get_db_connection()
conn.execute("DELETE FROM notes WHERE creator = ?", (userCookie["id"],))
conn.commit()
conn.close()
conn = get_db_connection()
conn.execute("DELETE FROM users WHERE id = ?", (userCookie["id"],))
conn.commit()
conn.close()
return {}, 200
@app.route("/api/sessions/list", methods=("GET", "POST"))
async def apisessionslist():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
conn = get_db_connection()
sessions = conn.execute("SELECT * FROM sessions WHERE id = ? ORDER BY id DESC;", (user["id"],)).fetchall()
conn.close()
datatemplate = []
for x in sessions:
device = x["device"]
thisSession = False
if (x["session"] == secretKey):
thisSession = True
sessiontemplate = {
"id": x["sessionid"],
"thisSession": thisSession,
"device": device
}
datatemplate.append(sessiontemplate)
return datatemplate, 200
@app.route("/api/sessions/remove", methods=("GET", "POST"))
async def apisessionsremove():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
sessionId = data["sessionId"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
session = get_session_from_sessionid(sessionId)
if (session != None):
if (user["id"] == session["id"]):
conn = get_db_connection()
conn.execute("DELETE FROM sessions WHERE sessionid = ?", (session["sessionid"],))
conn.commit()
conn.close()
return {}, 200
else:
return {}, 403
else:
return {}, 422
@app.route("/api/listusers", methods=("GET", "POST"))
async def listusers():
if request.method == "POST":
data = await request.get_json()
masterkey = data["masterkey"]
if masterkey == SECRET_KEY:
conn = get_db_connection()
users = conn.execute("SELECT * FROM users").fetchall()
conn.close()
thing = []
for x in users:
user_info = {
"id": str(x["id"]),
"username": str(x["username"]),
"space": str(get_space(x["id"]))
}
thing.append(user_info)
return thing, 200
else:
return {}, 401
@app.errorhandler(500)
async def burger(e):
return {}, 500
@app.errorhandler(404)
async def burger(e):
return {}, 404
# Start server
hypercornconfig = Config()
hypercornconfig.bind = (HOST + ":" + PORT)
if __name__ == "__main__":
print("[INFO] Server started")
asyncio.run(serve(app, hypercornconfig))
print("[INFO] Server stopped")

754
main.go Normal file
View File

@ -0,0 +1,754 @@
package main
import (
"bytes"
"git.ailur.dev/ailur/burgernotes/git.ailur.dev/ailur/burgernotes/protobuf"
"errors"
"io"
"os"
"strings"
"time"
"crypto/ed25519"
"encoding/json"
"io/fs"
"net/http"
"net/url"
library "git.ailur.dev/ailur/fg-library/v2"
nucleusLibrary "git.ailur.dev/ailur/fg-nucleus-library"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"google.golang.org/protobuf/proto"
)
var ServiceInformation = library.Service{
Name: "burgernotes",
Permissions: library.Permissions{
Authenticate: true, // This service does require authentication
Database: true, // This service does require database access
BlobStorage: true, // This service does require blob storage access
InterServiceCommunication: true, // This service does require inter-service communication
Resources: false, // This service is API-only, so it does not require resources
},
ServiceID: uuid.MustParse("b0bee29e-00c4-4ead-a5d6-3f792ff25174"),
}
func unmarshalProtobuf(r *http.Request, protobuf proto.Message) error {
var protobufData []byte
_, err := r.Body.Read(protobufData)
if err != nil {
return err
}
err = proto.Unmarshal(protobufData, protobuf)
if err != nil {
return err
}
return nil
}
func logFunc(message string, messageType uint64, information library.ServiceInitializationInformation) {
// Log the message to the logger service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000002"), // Logger service
MessageType: messageType,
SentAt: time.Now(),
Message: message,
}
}
func askBlobService(body any, information library.ServiceInitializationInformation, context uint64) (library.InterServiceMessage, error) {
// Ask the blob storage service for the thing
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000003"), // Blob storage service
MessageType: context,
SentAt: time.Now(),
Message: body,
}
// 3 second timeout
timeoutChan := make(chan struct{})
go func() {
time.Sleep(3 * time.Second)
logFunc("Timeout while waiting for the quota from the blob storage service", 2, information)
close(timeoutChan)
}()
// Wait for the response
select {
case response := <-information.Inbox:
return response, nil
case <-timeoutChan:
return library.InterServiceMessage{}, errors.New("timeout")
}
}
func getQuotaOrUsed(userID uuid.UUID, information library.ServiceInitializationInformation, context uint64) (int64, error) {
response, err := askBlobService(userID, information, context)
if err != nil {
return 0, err
} else if response.MessageType != 0 {
return 0, response.Message.(error)
} else {
return response.Message.(int64), nil
}
}
func getQuota(userID uuid.UUID, information library.ServiceInitializationInformation) (int64, error) {
return getQuotaOrUsed(userID, information, 3)
}
func getUsed(userID uuid.UUID, information library.ServiceInitializationInformation) (int64, error) {
return getQuotaOrUsed(userID, information, 4)
}
func deleteNote(userID uuid.UUID, noteID uuid.UUID, information library.ServiceInitializationInformation) error {
response, err := askBlobService(nucleusLibrary.File{
User: userID,
Name: noteID.String(),
}, information, 2)
if err != nil {
return err
}
if response.MessageType != 0 {
return response.Message.(error)
} else {
return nil
}
}
func modifyNote(userID uuid.UUID, noteID uuid.UUID, data []byte, information library.ServiceInitializationInformation) error {
response, err := askBlobService(nucleusLibrary.File{
User: userID,
Name: noteID.String(),
Bytes: data,
}, information, 0)
if err != nil {
return err
}
if response.MessageType != 0 {
return response.Message.(error)
} else {
return nil
}
}
func getNote(userID uuid.UUID, noteID uuid.UUID, information library.ServiceInitializationInformation) (*os.File, error) {
response, err := askBlobService(nucleusLibrary.File{
User: userID,
Name: noteID.String(),
}, information, 1)
if err != nil {
return nil, err
}
if response.MessageType != 0 {
return nil, response.Message.(error)
} else {
return response.Message.(*os.File), nil
}
}
func renderProtobuf(statusCode int, w http.ResponseWriter, protobuf proto.Message, information library.ServiceInitializationInformation) {
w.WriteHeader(statusCode)
data, err := proto.Marshal(protobuf)
if err != nil {
logFunc(err.Error(), 2, information)
}
w.Header().Add("Content-Type", "application/x-protobuf")
_, err = w.Write(data)
if err != nil {
logFunc(err.Error(), 2, information)
}
}
func verifyJWT(token string, publicKey ed25519.PublicKey) (jwt.MapClaims, error) {
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
if err != nil {
return nil, err
}
if !parsedToken.Valid {
return nil, errors.New("invalid token")
}
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.New("invalid token")
}
// Check if the token expired
date, err := claims.GetExpirationTime()
if err != nil || date.Before(time.Now()) || claims["sub"] == nil || claims["isOpenID"] == nil || claims["isOpenID"].(bool) {
return claims, errors.New("invalid token")
}
return claims, nil
}
func getUsername(token string, oauthHostName string, publicKey ed25519.PublicKey) (string, string, error) {
// Verify the JWT
_, err := verifyJWT(token, publicKey)
if err != nil {
return "", "", err
}
// Get the user's information
var responseData struct {
Username string `json:"username"`
Sub string `json:"sub"`
}
request, err := http.NewRequest("GET", oauthHostName+"/api/oauth/userinfo", nil)
request.Header.Set("Authorization", "Bearer "+token)
response, err := http.DefaultClient.Do(request)
if err != nil {
return "", "", err
}
if response.StatusCode != 200 || response.Body == nil || response.Body == http.NoBody {
return "", "", errors.New("invalid response")
}
err = json.NewDecoder(response.Body).Decode(&responseData)
if err != nil {
return "", "", err
}
return responseData.Sub, responseData.Username, nil
}
func Main(information library.ServiceInitializationInformation) {
var conn library.Database
hostName := information.Configuration["hostName"].(string)
// Initiate a connection to the database
// Call service ID 1 to get the database connection information
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), // Service initialization service
MessageType: 1, // Request connection information
SentAt: time.Now(),
Message: nil,
}
// Wait for the response
response := <-information.Inbox
if response.MessageType == 2 {
// This is the connection information
// Set up the database connection
conn = response.Message.(library.Database)
if conn.DBType == library.Sqlite {
// Create the users table
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB NOT NULL UNIQUE, publicKey BLOB NOT NULL, USERNAME TEXT NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the notes table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS notes (id BLOB NOT NULL UNIQUE, userID BLOB NOT NULL, title BLOB NOT NULL, titleIV BLOB NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
} else {
// Create the users table
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BYTEA NOT NULL UNIQUE, publicKey BYTEA NOT NULL, USERNAME TEXT NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the notes table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS notes (id BYTEA NOT NULL UNIQUE, userID BYTEA NOT NULL, title BYTEA NOT NULL, titleIV BYTEA NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
}
} else {
// This is an error message
// Log the error message to the logger service
logFunc(response.Message.(error).Error(), 3, information)
}
// Ask the authentication service for the public key
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service
MessageType: 2, // Request public key
SentAt: time.Now(),
Message: nil,
}
var publicKey ed25519.PublicKey = nil
// 3 second timeout
go func() {
time.Sleep(3 * time.Second)
if publicKey == nil {
logFunc("Timeout while waiting for the public key from the authentication service", 3, information)
}
}()
// Wait for the response
response = <-information.Inbox
if response.MessageType == 2 {
// This is the public key
publicKey = response.Message.(ed25519.PublicKey)
} else {
// This is an error message
// Log the error message to the logger service
logFunc(response.Message.(error).Error(), 3, information)
}
// Ask the authentication service for the OAuth host name
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service
MessageType: 0, // Request OAuth host name
SentAt: time.Now(),
Message: nil,
}
var oauthHostName string
// 3 second timeout
go func() {
time.Sleep(3 * time.Second)
if oauthHostName == "" {
logFunc("Timeout while waiting for the OAuth host name from the authentication service", 3, information)
}
}()
// Wait for the response
response = <-information.Inbox
if response.MessageType == 0 {
// This is the OAuth host name
oauthHostName = response.Message.(string)
} else {
// This is an error message
// Log the error message to the logger service
logFunc(response.Message.(error).Error(), 3, information)
}
// Ask the authentication service to create a new OAuth2 client
urlPath, err := url.JoinPath(hostName, "/oauth")
if err != nil {
logFunc(err.Error(), 3, information)
}
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service
MessageType: 1, // Create OAuth2 client
SentAt: time.Now(),
Message: nucleusLibrary.OAuthInformation{
Name: "Data Tracker",
RedirectUri: urlPath,
KeyShareUri: "",
Scopes: []string{"openid"},
},
}
oauthResponse := nucleusLibrary.OAuthResponse{}
// 3 second timeout
go func() {
time.Sleep(3 * time.Second)
if oauthResponse == (nucleusLibrary.OAuthResponse{}) {
logFunc("Timeout while waiting for the OAuth response from the authentication service", 3, information)
}
}()
// Wait for the response
response = <-information.Inbox
switch response.MessageType {
case 0:
// Success, set the OAuth response
oauthResponse = response.Message.(nucleusLibrary.OAuthResponse)
logFunc("Initialized with App ID: "+oauthResponse.AppID, 0, information)
case 1:
// An error which is their fault
logFunc(response.Message.(error).Error(), 3, information)
case 2:
// An error which is our fault
logFunc(response.Message.(error).Error(), 3, information)
default:
// An unknown error
logFunc("Unknown error", 3, information)
}
// Set up the router
router := information.Router
// Set up the static routes
staticDir, err := fs.Sub(information.ResourceDir, "static")
if err != nil {
logFunc(err.Error(), 3, information)
} else {
router.Handle("/bgn-static/*", http.StripPrefix("/bgn-static/", http.FileServerFS(staticDir)))
}
// Set up the routes
router.Post("/api/notes/add", func(w http.ResponseWriter, r *http.Request) {
var requestData protobuf.Token
err := unmarshalProtobuf(r, &requestData)
// Verify the JWT
claims, err := verifyJWT(requestData.Token, publicKey)
if err != nil {
renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information)
return
}
// Generate a new note UUID
noteID := uuid.New()
// Check if the user has reached their quota
quota, err := getQuota(uuid.MustParse(claims["sub"].(string)), information)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x02}}, information)
return
}
used, err := getUsed(uuid.MustParse(claims["sub"].(string)), information)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x03}}, information)
return
}
if used >= quota {
renderProtobuf(403, w, &protobuf.Error{Error: "Quota reached"}, information)
return
}
// Try to insert the note into the database
_, err = conn.DB.Exec("INSERT INTO notes (id, userID) VALUES ($1, $2)", noteID, claims["sub"])
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x04}}, information)
} else {
noteIdBytes, err := noteID.MarshalBinary()
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x05}}, information)
}
renderProtobuf(200, w, &protobuf.NoteID{NoteId: noteIdBytes}, information)
}
})
router.Post("/api/notes/remove", func(w http.ResponseWriter, r *http.Request) {
var requestData protobuf.NoteRequest
err := unmarshalProtobuf(r, &requestData)
if err != nil {
renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information)
return
}
// Verify the JWT
claims, err := verifyJWT(requestData.Token.String(), publicKey)
if err != nil {
renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information)
return
}
// Try to remove the note from the database
_, err = conn.DB.Exec("DELETE FROM notes WHERE id = $1 AND userID = $2", requestData.NoteId, claims["sub"])
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x06}}, information)
}
// If it's there, try to remove the note from the blob storage
err = deleteNote(uuid.MustParse(claims["sub"].(string)), uuid.Must(uuid.FromBytes(requestData.NoteId.GetNoteId())), information)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x07}}, information)
}
w.WriteHeader(200)
})
router.Post("/api/notes/list", func(w http.ResponseWriter, r *http.Request) {
// Verify the JWT
var requestData protobuf.Token
err := unmarshalProtobuf(r, &requestData)
if err != nil {
renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information)
return
}
claims, err := verifyJWT(requestData.Token, publicKey)
if err != nil {
renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information)
return
}
// Try to get the notes from the database
rows, err := conn.DB.Query("SELECT id, title, titleIV FROM notes WHERE userID = $1", claims["sub"])
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x08}}, information)
return
}
// Create the notes list
var notes protobuf.ApiNotesListResponse
// Iterate through the rows
for rows.Next() {
var title, titleIV, noteID []byte
err = rows.Scan(&noteID, &title, &titleIV)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x09}}, information)
return
}
// Append the note to the list
notes.Notes = append(notes.Notes, &protobuf.NoteMetadata{
NoteId: &protobuf.NoteID{
NoteId: noteID,
},
Title: &protobuf.AESData{
Data: title,
Iv: titleIV,
},
})
}
// Render the notes list
renderProtobuf(200, w, &notes, information)
})
router.Post("/api/notes/get", func(w http.ResponseWriter, r *http.Request) {
var requestData protobuf.NoteRequest
err := unmarshalProtobuf(r, &requestData)
if err != nil {
renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information)
return
}
// Verify the JWT
claims, err := verifyJWT(requestData.Token.String(), publicKey)
if err != nil {
renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information)
return
}
// Try to get the note from the database
var title, titleIV []byte
err = conn.DB.QueryRow("SELECT title, titleIV FROM notes WHERE id = $1 AND userID = $2", requestData.NoteId, claims["sub"]).Scan(&title, &titleIV)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0A}}, information)
return
}
// Get the note from the blob storage
noteFile, err := getNote(uuid.MustParse(claims["sub"].(string)), uuid.Must(uuid.FromBytes(requestData.NoteId.GetNoteId())), information)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0B}}, information)
return
}
// The IV is the first 16 bytes of the file
iv := make([]byte, 16)
_, err = noteFile.Read(iv)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0C}}, information)
return
}
// The rest of the file is the data
data, err := io.ReadAll(noteFile)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0D}}, information)
return
}
// Close the file
err = noteFile.Close()
if err != nil {
logFunc("Resource leak in /api/notes/get", 2, information)
}
// Render the note
renderProtobuf(200, w, &protobuf.Note{
Note: &protobuf.AESData{
Data: data,
Iv: iv,
},
Metadata: &protobuf.NoteMetadata{
NoteId: requestData.NoteId,
Title: &protobuf.AESData{
Data: title,
Iv: titleIV,
},
},
}, information)
})
router.Post("/api/notes/edit", func(w http.ResponseWriter, r *http.Request) {
var requestData protobuf.ApiNotesEditRequest
err := unmarshalProtobuf(r, &requestData)
if err != nil {
renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information)
return
}
// Verify the JWT
claims, err := verifyJWT(requestData.Token.String(), publicKey)
if err != nil {
renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information)
return
}
// Update the title
_, err = conn.DB.Exec("UPDATE notes SET title = $1, titleIV = $2 WHERE id = $3 AND userID = $4", requestData.Note.Metadata.Title.Data, requestData.Note.Metadata.Title.Iv, requestData.Note.Metadata.NoteId, claims["sub"])
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0E}}, information)
return
}
// Edit the note in the blob storage
err = modifyNote(uuid.MustParse(claims["sub"].(string)), uuid.Must(uuid.FromBytes(requestData.Note.Metadata.NoteId.GetNoteId())), bytes.Join([][]byte{requestData.Note.Note.Iv, requestData.Note.Note.Data}, nil), information)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0F}}, information)
return
}
w.WriteHeader(200)
})
router.Post("/api/notes/purge", func(w http.ResponseWriter, r *http.Request) {
var requestData protobuf.Token
err := unmarshalProtobuf(r, &requestData)
if err != nil {
renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information)
return
}
// Verify the JWT
claims, err := verifyJWT(requestData.Token, publicKey)
if err != nil {
renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information)
return
}
// Get the notes from the database
rows, err := conn.DB.Query("SELECT id FROM notes WHERE userID = $1", claims["sub"])
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x10}}, information)
return
}
// Iterate through the rows
for rows.Next() {
var noteID []byte
err = rows.Scan(&noteID)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x11}}, information)
return
}
// Try to remove the note from the blob storage
err = deleteNote(uuid.MustParse(claims["sub"].(string)), uuid.Must(uuid.FromBytes(noteID)), information)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x12}}, information)
return
}
}
// Remove the notes from the database
_, err = conn.DB.Exec("DELETE FROM notes WHERE userID = $1", claims["sub"])
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x13}}, information)
return
}
})
router.Post("/api/signup", func(w http.ResponseWriter, r *http.Request) {
var requestData protobuf.ApiSignupRequest
err := unmarshalProtobuf(r, &requestData)
if err != nil {
renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information)
return
}
// Verify the JWT
sub, username, err := getUsername(requestData.Token.String(), oauthHostName, publicKey)
if err != nil {
renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information)
return
}
// Try to insert the user into the database
_, err = conn.DB.Exec("INSERT INTO users (id, publicKey, username) VALUES ($1, $2, $3)", sub, requestData.PublicKey, username)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE") {
renderProtobuf(409, w, &protobuf.Error{Error: "User already exists"}, information)
} else {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x01}}, information)
}
return
}
w.WriteHeader(200)
})
router.Post("/api/delete", func(w http.ResponseWriter, r *http.Request) {
var requestData protobuf.Token
err := unmarshalProtobuf(r, &requestData)
if err != nil {
renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information)
return
}
// Verify the JWT
claims, err := verifyJWT(requestData.Token, publicKey)
if err != nil {
renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information)
return
}
// Try to remove the user from the database
_, err = conn.DB.Exec("DELETE FROM users WHERE id = $1", claims["sub"])
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x14}}, information)
return
}
// Get the notes from the database
rows, err := conn.DB.Query("SELECT id FROM notes WHERE userID = $1", claims["sub"])
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x15}}, information)
return
}
// Iterate through the rows
for rows.Next() {
var noteID []byte
err = rows.Scan(&noteID)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x16}}, information)
return
}
// Try to remove the note from the blob storage
err = deleteNote(uuid.MustParse(claims["sub"].(string)), uuid.Must(uuid.FromBytes(noteID)), information)
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x17}}, information)
return
}
}
// Remove the notes from the database
_, err = conn.DB.Exec("DELETE FROM notes WHERE userID = $1", claims["sub"])
if err != nil {
renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x18}}, information)
return
}
w.WriteHeader(200)
})
// TODO: Implement shared notes
}

142
main.proto Normal file
View File

@ -0,0 +1,142 @@
syntax = "proto3";
package main;
option go_package = "git.ailur.dev/ailur/burgernotes/protobuf";
// Token is a string that represents an OAuth2 JWT token.
message Token {
string token = 1;
}
// NoteID is a UUID that represents a note.
message NoteID {
bytes noteId = 1;
}
// NoteID and Token together represent a request involving a note.
message NoteRequest {
NoteID noteId = 1;
Token token = 2;
}
// AESData represents AES-encrypted data.
message AESData {
bytes data = 2;
bytes iv = 3;
}
// NoteMetadata represents the metadata of a note.
message NoteMetadata {
NoteID noteId = 1;
AESData title = 2;
}
// Note represents a note.
message Note {
NoteMetadata metadata = 1;
AESData note = 2;
}
// /api/notes/list returns an array of notes.
message ApiNotesListResponse {
repeated NoteMetadata notes = 1;
}
// /api/notes/edit accepts a note and a token.
message ApiNotesEditRequest {
Note note = 1;
Token token = 2;
}
// /api/signup accepts a public key and a token.
message ApiSignupRequest {
bytes publicKey = 1;
Token token = 2;
}
// /api/invite/prepare accepts an username and a token.
message ApiInvitePrepareRequest {
string username = 1;
Token token = 2;
}
// /api/invite/prepare returns a ECDH key.
message ApiInvitePrepareResponse {
bytes ecdhExchange = 1;
}
// /api/invite/send accepts a ECDH exchange, a AES-encrypted key and a NoteRequest.
message ApiInviteSendRequest {
bytes ecdhExchange = 1;
AESData key = 2;
NoteRequest noteRequest = 3;
}
// Invitation represents an invitation to a note.
message Invitation {
string username = 1;
AESData key = 2;
NoteID noteId = 3;
}
// /api/invite/check returns an array of invitations.
message ApiInviteCheckResponse {
repeated Invitation invitations = 1;
}
// /api/invite/link accepts a NoteRequest, UNIX timestamp and a singleUse boolean.
message ApiInviteLinkRequest {
NoteRequest noteRequest = 1;
int64 timestamp = 2;
bool singleUse = 3;
}
// /api/invite/link returns an invite code.
message ApiInviteLinkResponse {
bytes inviteCode = 1;
}
// /api/invite/accept accepts an invite code and a token.
message ApiInviteAcceptRequest {
bytes inviteCode = 1;
Token token = 2;
}
// /api/shared is a WebSocket which accepts an array of line numbers and a token.
message ApiSharedRequest {
repeated uint64 lines = 1;
Token token = 2;
}
// User represents a user editing notes.
message UserLines {
string username = 1;
bytes uuid = 2;
repeated uint64 lines = 3;
}
// /api/shared is a WebSocket which returns the array of line numbers for each user working on a note.
message ApiSharedResponse {
repeated UserLines users = 1;
}
// /api/shared/edit accepts multiple lines, represented as an individual AESData, and a token.
message ApiSharedEditRequest {
repeated AESData lines = 1;
Token token = 2;
}
// /api/shared/get returns the lines of a note.
message ApiSharedGetResponse {
repeated AESData lines = 1;
NoteMetadata metadata = 2;
}
// Error represents an error.
message Error {
string error = 1;
}
// ServerError represents a 500 error, with a hex error code.
message ServerError {
bytes errorCode = 1;
}

539
main.save
View File

@ -1,539 +0,0 @@
#!/usr/bin/python3
import os
import sqlite3
import time
import secrets
import configparser
import asyncio
from hypercorn.config import Config
from hypercorn.asyncio import serve
from werkzeug.security import generate_password_hash, check_password_hash
from quart import Quart, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request
# Parse configuration file, and check if anything is wrong with it
if not os.path.exists("config.ini"):
print("config.ini does not exist")
config = configparser.ConfigParser()
config.read("config.ini")
HOST = config["config"]["HOST"]
PORT = config["config"]["PORT"]
SECRET_KEY = config["config"]["SECRET_KEY"]
MAX_STORAGE = config["config"]["MAX_STORAGE"]
if SECRET_KEY == "supersecretkey" or SECRET_KEY == "placeholder":
print("[WARNING] Secret key not set")
# Define Quart
app = Quart(__name__)
app.config["SECRET_KEY"] = SECRET_KEY
# Database functions
def get_db_connection():
conn = sqlite3.connect("database.db")
conn.row_factory = sqlite3.Row
return conn
def get_user(id):
conn = get_db_connection()
post = conn.execute("SELECT * FROM users WHERE id = ?",
(id,)).fetchone()
conn.close()
if post is None:
return None
return post
def get_note(id):
conn = get_db_connection()
post = conn.execute("SELECT * FROM notes WHERE id = ?",
(id,)).fetchone()
conn.close()
if post is None:
return None
return post
def get_space(id):
conn = get_db_connection()
notes = conn.execute("SELECT content, title FROM notes WHERE creator = ? ORDER BY id DESC;", (id,)).fetchall()
conn.close()
spacetaken = 0
for x in notes:
spacetaken = spacetaken + len(x["content"].encode("utf-8"))
spacetaken = spacetaken + len(x["title"].encode("utf-8"))
return spacetaken
def get_note_count(id):
conn = get_db_connection()
notes = conn.execute("SELECT content, title FROM notes WHERE creator = ? ORDER BY id DESC;", (id,)).fetchall()
conn.close()
notecount = 0
for x in notes:
notecount = notecount + 1
return notecount
def get_session(id):
conn = get_db_connection()
post = conn.execute("SELECT * FROM sessions WHERE session = ?",
(id,)).fetchone()
conn.close()
if post is None:
return None
return post
def get_session_from_sessionid(id):
conn = get_db_connection()
post = conn.execute("SELECT * FROM sessions WHERE sessionid = ?",
(id,)).fetchone()
conn.close()
if post is None:
return None
return post
def check_username_taken(username):
conn = get_db_connection()
post = conn.execute("SELECT * FROM users WHERE lower(username) = ?",
(username.lower(),)).fetchone()
conn.close()
if post is None:
return None
return post["id"]
# Disable CORS
@app.after_request
async def add_cors_headers(response):
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "*")
response.headers.add("Access-Control-Allow-Methods", "*")
return response
# Live editing store
messages = {}
@app.route("/api/version", methods=("GET", "POST"))
async def apiversion():
return "Burgernotes Version 1.2"
@app.route("/api/signup", methods=("GET", "POST"))
async def apisignup():
if request.method == "POST":
data = await request.get_json()
username = data["username"]
password = data["password"]
if username == "":
return {}, 422
if len(username) > 20:
return {}, 422
if not username.isalnum():
return {}, 422
if password == "":
return {}, 422
if len(password) < 14:
return {}, 422
if not check_username_taken(username) == None:
return {}, 409
hashedpassword = generate_password_hash(password)
conn = get_db_connection()
conn.execute("INSERT INTO users (username, password, created) VALUES (?, ?, ?)",
(username, hashedpassword, str(time.time())))
conn.commit()
conn.close()
userID = check_username_taken(username)
user = get_user(userID)
randomCharacters = secrets.token_hex(512)
conn = get_db_connection()
conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)",
(randomCharacters, userID, request.headers.get("user-agent")))
conn.commit()
conn.close()
return {
"key": randomCharacters
}, 200
@app.route("/api/login", methods=("GET", "POST"))
async def apilogin():
if request.method == "POST":
data = await request.get_json()
username = data["username"]
password = data["password"]
passwordchange = data["passwordchange"]
newpass = data["newpass"]
check_username_thing = check_username_taken(username)
if check_username_thing == None:
return {}, 401
userID = check_username_taken(username)
user = get_user(userID)
if not check_password_hash(user["password"], (password)):
return {}, 401
randomCharacters = secrets.token_hex(512)
conn = get_db_connection()
conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)",
(randomCharacters, userID, request.headers.get("user-agent")))
conn.commit()
conn.close()
if passwordchange == "yes":
hashedpassword = generate_password_hash(newpass)
conn = get_db_connection()
conn.execute("UPDATE users SET password = ? WHERE username = ?", (hashedpassword, username))
conn.commit()
conn.close()
return {
"key": randomCharacters,
}, 200
@app.route("/api/userinfo", methods=("GET", "POST"))
async def apiuserinfo():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
datatemplate = {
"username": user["username"],
"id": user["id"],
"created": user["created"],
"storageused": get_space(user["id"]),
"storagemax": MAX_STORAGE,
"notecount": get_note_count(user["id"])
}
return datatemplate
@app.route("/api/listnotes", methods=("GET", "POST"))
async def apilistnotes():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
conn = get_db_connection()
notes = conn.execute("SELECT * FROM notes WHERE creator = ? ORDER BY edited DESC;", (user["id"],)).fetchall()
conn.close()
datatemplate = []
for note in notes:
notetemplate = {
"id": note["id"],
"title": note["title"]
}
datatemplate.append(notetemplate)
return datatemplate, 200
@app.route("/api/exportnotes", methods=("GET", "POST"))
async def apiexportnotes():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
conn = get_db_connection()
notes = conn.execute("SELECT * FROM notes WHERE creator = ? ORDER BY id DESC;", (user["id"],)).fetchall()
conn.close()
datatemplate = []
for note in notes:
notetemplate = {
"id": note["id"],
"created": note["created"],
"edited": note["edited"],
"title": note["title"],
"content": note["content"]
}
datatemplate.append(notetemplate)
return datatemplate, 200
@app.route("/api/newnote", methods=("GET", "POST"))
async def apinewnote():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
noteName = data["noteName"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
conn = get_db_connection()
conn.execute("INSERT INTO notes (title, content, creator, created, edited) VALUES (?, ?, ?, ?, ?)",
(noteName, "", user["id"], str(time.time()), str(time.time())))
conn.commit()
conn.close()
return {}, 200
@app.route("/api/readnote", methods=("GET", "POST"))
async def apireadnote():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
noteId = data["noteId"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
note = get_note(noteId)
if (note != None):
if (user["id"] == note["creator"]):
contenttemplate = {
"content": note["content"]
}
return contenttemplate, 200
else:
return {}, 422
else:
return {}, 422
@app.route("/api/waitforedit", methods=("GET", "POST"))
async def waitforedit():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
complete = true
start_time = time.time()
while user["id"] not in messages or not messages[user["id"]]:
await asyncio.sleep(0)
elapsed_time = time.time() - start_time
if elapsed_time >= 20:
break
complete = false
message = messages[user["id"]].pop(0)
del messages[user["id"]]
if complete == true:
return {
"note": message
}, 200
else:
return 400
@app.route("/api/editnote", methods=("GET", "POST"))
async def apieditnote():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
noteId = data["noteId"]
content = data["content"]
title = data["title"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
note = get_note(noteId)
if get_space(user["id"]) + len(content.encode("utf-8")) > int(MAX_STORAGE):
return {}, 418
if (note != None):
if (user["id"] == note["creator"]):
conn = get_db_connection()
conn.execute("UPDATE notes SET content = ?, title = ?, edited = ? WHERE id = ?", (content, title, str(time.time()), noteId))
conn.commit()
conn.close()
if user["id"] not in messages:
messages[user["id"]] = []
messages[user["id"]].append(noteId)
return {}, 200
else:
return {}, 403
else:
return {}, 422
@app.route("/api/editnotetitle", methods=("GET", "POST"))
async def apieditnotetitle():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
noteId = data["noteId"]
content = data["content"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
note = get_note(noteId)
if get_space(user["id"]) + len(content.encode("utf-8")) > int(MAX_STORAGE):
return {}, 418
if (note != None):
if (user["id"] == note["creator"]):
conn = get_db_connection()
conn.execute("UPDATE notes SET title = ?, edited = ? WHERE id = ?", (content, str(time.time()), noteId))
conn.commit()
conn.close()
return {}, 200
else:
return {}, 403
else:
return {}, 422
@app.route("/api/removenote", methods=("GET", "POST"))
async def apiremovenote():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
noteId = data["noteId"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
note = get_note(noteId)
if (note != None):
if (user["id"] == note["creator"]):
conn = get_db_connection()
conn.execute("DELETE FROM notes WHERE id = ?", (noteId,))
conn.commit()
conn.close()
return {}, 200
else:
return {}, 403
else:
return {}, 422
@app.route("/api/deleteaccount", methods=("GET", "POST"))
async def apideleteaccount():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
conn = get_db_connection()
conn.execute("DELETE FROM notes WHERE creator = ?", (userCookie["id"],))
conn.commit()
conn.close()
conn = get_db_connection()
conn.execute("DELETE FROM users WHERE id = ?", (userCookie["id"],))
conn.commit()
conn.close()
return {}, 200
@app.route("/api/sessions/list", methods=("GET", "POST"))
async def apisessionslist():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
conn = get_db_connection()
sessions = conn.execute("SELECT * FROM sessions WHERE id = ? ORDER BY id DESC;", (user["id"],)).fetchall()
conn.close()
datatemplate = []
for x in sessions:
device = x["device"]
thisSession = False
if (x["session"] == secretKey):
thisSession = True
sessiontemplate = {
"id": x["sessionid"],
"thisSession": thisSession,
"device": device
}
datatemplate.append(sessiontemplate)
return datatemplate, 200
@app.route("/api/sessions/remove", methods=("GET", "POST"))
async def apisessionsremove():
if request.method == "POST":
data = await request.get_json()
secretKey = data["secretKey"]
sessionId = data["sessionId"]
userCookie = get_session(secretKey)
user = get_user(userCookie["id"])
session = get_session_from_sessionid(sessionId)
if (session != None):
if (user["id"] == session["id"]):
conn = get_db_connection()
conn.execute("DELETE FROM sessions WHERE sessionid = ?", (session["sessionid"],))
conn.commit()
conn.close()
return {}, 200
else:
return {}, 403
else:
return {}, 422
@app.route("/listusers/<secretkey>", methods=("GET", "POST"))
def listusers(secretkey):
if secretkey == SECRET_KEY:
conn = get_db_connection()
users = conn.execute("SELECT * FROM users").fetchall()
conn.close()
thing = ""
for x in users:
thing = str(x["id"]) + " - " + x["username"] + " - " + str(get_space(x["id"])) + "<br>" + thing
return thing
else:
return redirect("/")
@app.errorhandler(500)
async def burger(e):
return {}, 500
@app.errorhandler(404)
async def burger(e):
return {}, 404
# Start server
hypercornconfig = Config()
hypercornconfig.bind = (HOST + ":" + PORT)
if __name__ == "__main__":
print("[INFO] Server started")
asyncio.run(serve(app, hypercornconfig))
print("[INFO] Server stopped")

View File

@ -1,31 +0,0 @@
#!/usr/bin/python3
import sqlite3
import sys
print("type n to cancel")
answer = input("delete user, what is user id?")
if (answer == "n"):
sys.exit()
def get_db_connection():
conn = sqlite3.connect("database.db")
conn.row_factory = sqlite3.Row
return conn
print("deleting notes")
conn = get_db_connection()
notes = conn.execute("DELETE FROM notes WHERE creator = ?", (int(answer),))
conn.commit()
conn.close()
print("deleting account")
conn = get_db_connection()
conn.execute("DELETE FROM users WHERE id = ?", (int(answer),))
conn.commit()
conn.close()
print("success")

View File

@ -1,3 +0,0 @@
quart
hypercorn
werkzeug

View File

@ -1,26 +0,0 @@
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS notes;
DROP TABLE IF EXISTS sessions;
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator INTEGER NOT NULL,
created TEXT NOT NULL,
edited TEXT NOT NULL,
content TEXT NOT NULL,
title TEXT NOT NULL
);
CREATE TABLE sessions (
sessionid INTEGER PRIMARY KEY AUTOINCREMENT,
session TEXT NOT NULL,
id INTEGER NOT NULL,
device TEXT NOT NULL DEFAULT "?"
);

18
vacuum
View File

@ -1,18 +0,0 @@
#!/usr/bin/python3
import sqlite3
import os
def get_db_connection():
conn = sqlite3.connect("database.db")
conn.row_factory = sqlite3.Row
return conn
if os.path.exists("database.db"):
print("vacuuming..")
conn = get_db_connection()
conn.execute("VACUUM")
conn.commit()
conn.close()
print("success")
else:
print("database not found")