Compare commits

..

No commits in common. "main" and "1.2-1" have entirely different histories.
main ... 1.2-1

20 changed files with 1331 additions and 1251 deletions

7
.gitignore vendored
View File

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

View File

@ -1,313 +1,87 @@
# API documentation
# 🍔 Burgernotes API docs
Use the Burgernotes API to automate tasks, build your own client, and more!
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.
Headers should be: "Content-type: application/json; charset=UTF-8" for all POSTs
## Protobuf types
These are some basic Protobuf types that are used in the API.
All protocol buffers are using proto3 syntax.
```protobuf
syntax = "proto3";
## 🔑 Authentication
// Token is a string that represents an OAuth2 JWT token.
message Token {
string token = 1;
}
POST - /api/signup - provide "username" and "password".
// NoteID is a UUID that represents a note.
message NoteID {
bytes noteId = 1;
}
POST - /api/login - provide "username", "password", "passwordchange" (must be "yes" or "no") and "newpass"
// NoteID and Token together represent a request involving a note.
message NoteRequest {
NoteID noteId = 1;
Token token = 2;
}
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).
// AESData represents AES-encrypted data.
message AESData {
bytes data = 2;
bytes iv = 3;
}
If you wish to change the user's password, set "passwordchange" to "yes" and "newpass" to the new hash.
// 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;
}
// 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;
}
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:
```
## 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.
Parallelism should be 1
## Authentication
### /api/signup - POST
#### Request
```protobuf
message ApiSignupRequest {
bytes publicKey = 1;
Token token = 2;
}
```
#### Response
200 OK
No response body
Iterations should be 256
### /api/delete - POST - Show a warning before this action!
#### Request
```protobuf
message Token {
string token = 1;
}
```
#### Response
HTTP 200 OK
No response body
Memory Allocated in bytes should be 512
## Interacting with notes
### /api/notes/add - POST
#### Request
```protobuf
message Token {
string token = 1;
}
```
#### Response
HTTP 200 OK
```protobuf
message NoteID {
bytes noteId = 1;
}
Length of Hash should be 32 bytes
The output should be in the encoded format, not the hashed format
Salt should be the SHA512 of the password
```
### /api/notes/remove - POST
#### Request
```protobuf
message NoteRequest {
NoteID noteId = 1;
Token token
}
```
#### Response
HTTP 200 OK
No response body
(Yes i know this is really bad practice, guess why we are replacing it)
### /api/notes/list - POST
#### Request
```protobuf
message Token {
string token = 1;
}
```
#### Response
HTTP 200 OK
```protobuf
message ApiNotesListResponse {
repeated NoteMetadata notes = 1;
}
```
To test if SHA-3 or argon2 is used, just try the SHA-3 and if 422 gets returned try argon2.
### /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;
}
```
(For the sake of all of us, change the password to the SHA-3 hash)
### /api/notes/edit - POST
#### Request
```protobuf
message ApiNotesEditRequest {
Note note = 1;
Token token = 2;
}
```
#### Response
HTTP 200 OK
No response body
### /api/notes/purge - POST - Show a warning before this action!
#### Request
```protobuf
message Token {
string token = 1;
}
```
#### Response
HTTP 200 OK
No response body
Password should be at least 8 characters, username must be under 20 characters and alphanumeric.
## 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;
}
```
If username is taken, error code 422 will return.
### /api/invite/check - POST
#### Request
```protobuf
message Token {
string token = 1;
}
```
#### Response
HTTP 200 OK
```protobuf
message ApiInviteCheckResponse {
repeated Invitation invitations = 1;
}
```
Assuming everything went correctly, the server will return a secret key.
### /api/invite/link - POST
#### Request
```protobuf
message ApiInviteLinkRequest {
NoteRequest noteRequest = 1;
int64 timestamp = 2;
bool singleUse = 3;
}
```
#### Response
HTTP 200 OK
```protobuf
message ApiInviteLinkResponse {
bytes inviteCode = 1;
}
```
You'll need to store two things in local storage:
- The secret key you just got, used to fetch notes, save stuff etc.
- A SHA512 hashed password, used as encryption key
### /api/invite/accept - POST
#### Request
```protobuf
message ApiInviteAcceptRequest {
bytes inviteCode = 1;
Token token = 2;
}
```
#### Response
HTTP 200 OK
```protobuf
message NoteID {
bytes noteId = 1;
}
```
## 🔐 Encryption
### /api/invite/leave - POST
#### Request
```protobuf
message NoteRequest {
NoteID noteId = 1;
Token token
}
```
#### Response
HTTP 200 OK
No response body
Note content and title is encrypted using AES 256-bit.
### /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;
}
```
Encryption password is the SHA512 hashed password we talked about earlier.
### /api/shared/edit - POST
#### Request
```protobuf
message ApiSharedEditRequest {
repeated AESData lines = 1;
Token token = 2;
}
```
#### Response
HTTP 200 OK
No response body
## 🕹️ Basic stuff
### /api/shared/get - POST
#### Request
```protobuf
message NoteRequest {
NoteID noteId = 1;
Token token
}
```
#### Response
```protobuf
message ApiSharedGetResponse {
repeated AESData lines = 1;
NoteMetadata metadata = 2;
}
```
POST - /api/userinfo - get user info such as username, provide "secretKey"
POST - /api/listnotes - list notes, provide "secretKey"
note titles will have to be decrypted.
POST - /api/newnote - create a note, provide "secretKey" and "noteName"
"noteName" should be encrypted.
POST - /api/readnote - read notes, provide "secretKey" and "noteId"
note content will have to be decrypted.
POST - /api/editnote - edit notes, provide "secretKey", "noteId", "title", and "content"
"content" should be encrypted.
"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,33 +2,18 @@
Burgernotes is a simple note-taking app with end-to-end encryption.
### Setup
To set up Burgernotes, set it up like any other Fulgens Server module.
To set up Burgernotes, simply run these commands:
```
cd /path/to/fulgens/directory
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"
}
}
cp config.ini.example config.ini
python3 init_db
```
### Running
Run the Fulgens server as you normally would.
Edit config.ini to your liking, then to start the server run:
```
./fulgens
python3 main
```
### Links
[Go to the Burgernotes website](https://notes.ailur.dev)
[Go to the Burgernotes website](https://notes.hectabit.org)
[API documentation](APIDOCS.md)

View File

@ -1,5 +1,8 @@
# Burgernotes Roadmap
- Create the frontend
- Implementing shared notes
- Dedicated domain (not just a subdomain, if anyone can donate a domain let Arzumify know!)
- Switch to WebSockets for updating notes + live updating of note list and more, this involves redoing some APIs
- Compress notes to reduce bandwidth and storage
- 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

View File

@ -1,7 +0,0 @@
#!/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

20
cleardb.py Normal file
View File

@ -0,0 +1,20 @@
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()

5
config.ini.example Normal file
View File

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

30
down Normal file
View File

@ -0,0 +1,30 @@
#!/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")

8
fixapi.sh Executable file
View File

@ -0,0 +1,8 @@
#!/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
View File

@ -1,15 +0,0 @@
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
View File

@ -1,18 +0,0 @@
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 Executable file
View File

@ -0,0 +1,29 @@
#!/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 Normal file
View File

@ -0,0 +1,546 @@
#!/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
View File

@ -1,754 +0,0 @@
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
}

View File

@ -1,142 +0,0 @@
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 Normal file
View File

@ -0,0 +1,539 @@
#!/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")

31
removeuser Normal file
View File

@ -0,0 +1,31 @@
#!/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")

3
requirements.txt Normal file
View File

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

26
schema.sql Normal file
View File

@ -0,0 +1,26 @@
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 Normal file
View File

@ -0,0 +1,18 @@
#!/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")