Compare commits

..

19 commits

Author SHA1 Message Date
a3409c0a42 Updated chi
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-03-20 18:40:22 +00:00
6cda63c538 Fixed a potential directory traversal, fixed storage being very borked, made the auth login process less memory intensive and rely more on JWTs
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-25 14:47:51 +00:00
2ece60f84e Updated nucleus libs, yet again
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-11 15:50:51 +00:00
ffc03f2213 Updated nucleus libs, tidied modfile
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-11 15:45:57 +00:00
a65b7f6e0d Updated nucleus libs
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-11 15:42:55 +00:00
cff2e5b811 Made storage fork the ISM processor
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-08 19:10:18 +00:00
1e106bb4ca Fixed broken imports, made storage not send raw ISMs
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-08 19:07:56 +00:00
f7a1ecccdb Updated libraries
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-08 18:39:29 +00:00
d9a2999fae Updated libraries
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-08 18:37:48 +00:00
a6ef1c01fe Didn't include the main??
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-08 18:31:48 +00:00
7422201d98 ISM rewrite
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-08 18:31:34 +00:00
f26a267421 Upgrade fg library version
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-08 15:44:29 +00:00
f8fc9b7206 ISM rewrite
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2025-01-08 13:47:05 +00:00
9736939a07 Added file deletion functionality to the storage service
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-12-09 21:09:00 +00:00
6eeea11a75 Fixed the storage service not properly reading bytes out of the correct struct, and instead attempting to assert the struct as raw byte data
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-12-08 16:36:29 +00:00
48547833e4 Made the storage service make the user folders
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-15 18:54:28 +00:00
34a3580ec6 Fixed the storage service being very broken
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-15 18:51:33 +00:00
f0559ed5b5 Use int instead of float64 in configs
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-15 18:37:44 +00:00
8331219da4 Fixed autoaccept not working, made logout not run localStorage.Clear()
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-15 17:00:06 +00:00
9 changed files with 631 additions and 801 deletions

View file

@ -28,6 +28,6 @@ find -L "$searchDir" -type f -name "build.sh" | while read -r buildScript; do
done
clear
fancy "\033[1;105m" "Building Fulgens..."
go build --ldflags "-s -w" -o "$path/fulgens" || exit 1
go build -C "$path" --ldflags "-s -w" -o "$path/fulgens" || exit 1
clear
fancy "\033[1;102m" "Fulgens has been built successfully!"

24
go.mod
View file

@ -3,33 +3,33 @@ module git.ailur.dev/ailur/fulgens
go 1.23.3
require (
git.ailur.dev/ailur/fg-library/v2 v2.1.2
git.ailur.dev/ailur/fg-nucleus-library v1.0.5
git.ailur.dev/ailur/pow v1.0.2
git.ailur.dev/ailur/fg-library/v3 v3.6.2
git.ailur.dev/ailur/fg-nucleus-library v1.2.2
git.ailur.dev/ailur/pow v1.0.3
github.com/CAFxX/httpcompression v0.0.9
github.com/cespare/xxhash/v2 v2.3.0
github.com/go-chi/chi/v5 v5.1.0
github.com/go-playground/validator/v10 v10.22.1
github.com/go-chi/chi/v5 v5.2.1
github.com/go-playground/validator/v10 v10.25.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/klauspost/compress v1.17.11
github.com/klauspost/compress v1.18.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24
golang.org/x/crypto v0.29.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/stretchr/testify v1.9.0 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

48
go.sum
View file

@ -1,9 +1,9 @@
git.ailur.dev/ailur/fg-library/v2 v2.1.2 h1:Gk8ztytJfV2GYhsnfTDRWmvTzJ3Cn19V5p2suFCvu4E=
git.ailur.dev/ailur/fg-library/v2 v2.1.2/go.mod h1:gBnZQDV70YON6cnuwB+Jawm2EABbf9dGlV0Qw4obtxs=
git.ailur.dev/ailur/fg-nucleus-library v1.0.5 h1:0YVSHFOeydGR/pfq5AfiKQ5gWuxSnx8u2K8mHvEDDTI=
git.ailur.dev/ailur/fg-nucleus-library v1.0.5/go.mod h1:nKYjJ+zJD1YcrEGWlyyA5r6CrzW8DWHVAnL9hkn2tNw=
git.ailur.dev/ailur/pow v1.0.2 h1:8tb6mXZdyQYjrKRW+AUmWMi5wJoHh9Ch3oRqiJr/ivs=
git.ailur.dev/ailur/pow v1.0.2/go.mod h1:fjFb1z5KtF6V14HRhGWiDmmJKggO8KyAP20Lr5OJI/g=
git.ailur.dev/ailur/fg-library/v3 v3.6.2 h1:PNJKxpvbel2iDeB9+/rpYRyMoim6JjRHOXPYFYky7Ng=
git.ailur.dev/ailur/fg-library/v3 v3.6.2/go.mod h1:ArNsafpqES2JuxQM5aM+bNe0FwHLIsL6pbjpiWvDwGs=
git.ailur.dev/ailur/fg-nucleus-library v1.2.2 h1:JbclmxGSoL+ByGZAl0W6PqWRoyBBGTrKrizWDJ7rdI0=
git.ailur.dev/ailur/fg-nucleus-library v1.2.2/go.mod h1:stxiTyMv3Fa7GzpyLbBUh3ahlb7110p0NnCl8ZTjwBs=
git.ailur.dev/ailur/pow v1.0.3 h1:LjLSol4ax+M+SoajVjbBoDjfmjH6pKu3fDka7bl2KGY=
git.ailur.dev/ailur/pow v1.0.3/go.mod h1:ClAmIdHQ/N9wTq5S4YWhQ5d9CPUBcEjVuOkT07zBdJ4=
github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg=
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
@ -15,18 +15,18 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
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/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k=
@ -34,8 +34,8 @@ github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1
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=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -64,21 +64,21 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

548
main.go
View file

@ -1,20 +1,18 @@
package main
import (
library "git.ailur.dev/ailur/fg-library/v2"
"os/signal"
"syscall"
"errors"
library "git.ailur.dev/ailur/fg-library/v3"
"io"
"log"
"mime"
"os"
"os/signal"
"plugin"
"strconv"
"strings"
"sync"
"time"
"syscall"
"crypto/tls"
"database/sql"
@ -64,35 +62,37 @@ type Config struct {
ASPNet bool `yaml:"aspNet"`
}
} `yaml:"global" validate:"required"`
Routes []struct {
Port string `yaml:"port" validate:"required"`
Subdomain string `yaml:"subdomain" validate:"required"`
Services []string `yaml:"services"`
Paths []struct {
Paths []string `yaml:"paths" validate:"required"`
Proxy struct {
URL string `yaml:"url" validate:"required"`
StripPrefix bool `yaml:"stripPrefix"`
Headers HeaderSettings `yaml:"headers"`
} `yaml:"proxy" validate:"required_without=Static Redirect"`
Static struct {
Root string `yaml:"root" validate:"required,isDirectory"`
DirectoryListing bool `yaml:"directoryListing"`
} `yaml:"static" validate:"required_without_all=Proxy Redirect"`
Redirect struct {
URL string `yaml:"url" validate:"required"`
Permanent bool `yaml:"permanent"`
} `yaml:"redirect" validate:"required_without_all=Proxy Static"`
} `yaml:"paths"`
HTTPS struct {
CertificatePath string `yaml:"certificate" validate:"required"`
KeyPath string `yaml:"key" validate:"required"`
} `yaml:"https"`
Compression CompressionSettings `yaml:"compression"`
} `yaml:"routes"`
Routes []Route `yaml:"routes"`
Services map[string]interface{} `yaml:"services"`
}
type Route struct {
Port string `yaml:"port" validate:"required"`
Subdomain string `yaml:"subdomain" validate:"required"`
Services []string `yaml:"services"`
Paths []struct {
Paths []string `yaml:"paths" validate:"required"`
Proxy struct {
URL string `yaml:"url" validate:"required"`
StripPrefix bool `yaml:"stripPrefix"`
Headers HeaderSettings `yaml:"headers"`
} `yaml:"proxy" validate:"required_without=Static Redirect"`
Static struct {
Root string `yaml:"root" validate:"required,isDirectory"`
DirectoryListing bool `yaml:"directoryListing"`
} `yaml:"static" validate:"required_without_all=Proxy Redirect"`
Redirect struct {
URL string `yaml:"url" validate:"required"`
Permanent bool `yaml:"permanent"`
} `yaml:"redirect" validate:"required_without_all=Proxy Static"`
} `yaml:"paths"`
HTTPS struct {
CertificatePath string `yaml:"certificate" validate:"required"`
KeyPath string `yaml:"key" validate:"required"`
} `yaml:"https"`
Compression CompressionSettings `yaml:"compression"`
}
type HeaderSettings struct {
Forbid []string `yaml:"forbid"`
PreserveServer bool `yaml:"preserveServer"`
@ -105,13 +105,13 @@ type HeaderSettings struct {
type Service struct {
ServiceID uuid.UUID
ServiceMetadata library.Service
ServiceMainFunc func(library.ServiceInitializationInformation)
ServiceMainFunc func(*library.ServiceInitializationInformation)
Inbox chan library.InterServiceMessage
}
type CompressionSettings struct {
Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Level float64 `yaml:"level" validate:"omitempty,min=1,max=22"`
Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Level int `yaml:"level" validate:"omitempty,min=1,max=22"`
}
type RouterAndCompression struct {
@ -175,7 +175,7 @@ func (pr *PortRouter) HTTPSEnabled() bool {
func compressRouter(settings CompressionSettings, handler http.Handler) http.Handler {
switch settings.Algorithm {
case "gzip":
encoder, err := gzip.New(gzip.Options{Level: int(settings.Level)})
encoder, err := gzip.New(gzip.Options{Level: settings.Level})
if err != nil {
slog.Error("Error creating gzip encoder: " + err.Error())
return handler
@ -187,7 +187,7 @@ func compressRouter(settings CompressionSettings, handler http.Handler) http.Han
}
return gzipHandler(handler)
case "brotli":
encoder, err := brotli.New(brotli.Options{Quality: int(settings.Level)})
encoder, err := brotli.New(brotli.Options{Quality: settings.Level})
if err != nil {
slog.Error("Error creating brotli encoder: " + err.Error())
return handler
@ -199,7 +199,7 @@ func compressRouter(settings CompressionSettings, handler http.Handler) http.Han
}
return brotliHandler(handler)
case "zstd":
encoder, err := zstd.New(kpzstd.WithEncoderLevel(kpzstd.EncoderLevelFromZstd(int(settings.Level))))
encoder, err := zstd.New(kpzstd.WithEncoderLevel(kpzstd.EncoderLevelFromZstd(settings.Level)))
if err != nil {
slog.Error("Error creating zstd encoder: " + err.Error())
return handler
@ -392,7 +392,17 @@ func newFileServer(root string, directoryListing bool, path string) http.Handler
}
}
file, err := os.Open(filepath.Join(root, filepath.FromSlash(r.URL.Path)))
absolutePath, err := filepath.Abs(filepath.Join(root, filepath.FromSlash(r.URL.Path)))
if err != nil {
serverError(w, 500)
}
if !strings.HasPrefix(absolutePath, root) {
serverError(w, 403)
return
}
file, err := os.Open(absolutePath)
if err != nil {
serverError(w, 500)
return
@ -518,6 +528,11 @@ var (
registeredServices = make(map[string]Service)
activeServices = make(map[uuid.UUID]Service)
portRouters = make(map[string]*PortRouter)
broadcastService = uuid.MustParse("00000000-0000-0000-0000-000000000000")
databaseService = uuid.MustParse("00000000-0000-0000-0000-000000000001")
logService = uuid.MustParse("00000000-0000-0000-0000-000000000002")
blobService = uuid.MustParse("00000000-0000-0000-0000-000000000003")
authService = uuid.MustParse("00000000-0000-0000-0000-000000000004")
)
func loadTLSCertificate(certificatePath, keyPath string) (*tls.Certificate, error) {
@ -529,205 +544,113 @@ func loadTLSCertificate(certificatePath, keyPath string) (*tls.Certificate, erro
}
}
func svInit(message library.InterServiceMessage) {
// Service database initialization message
// Check if the service has the necessary permissions
if activeServices[message.ServiceID].ServiceMetadata.Permissions.Database {
// Check if we are using sqlite or postgres
if config.Global.Database.Type == "sqlite" {
// Open the database and return the connection
pluginConn, err := sql.Open("sqlite3", filepath.Join(config.Global.Database.Path, message.ServiceID.String()+".db"))
if err != nil {
// Report an error
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: err,
}
} else {
// Report a successful activation
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 2,
SentAt: time.Now(),
Message: library.Database{
DB: pluginConn,
DBType: library.Sqlite,
},
}
}
} else if config.Global.Database.Type == "postgres" {
// Connect to the database
conn, err := sql.Open("postgres", config.Global.Database.ConnectionString)
if err != nil {
// Report an error
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: err,
}
} else {
// Try to create the schema
_, err = conn.Exec("CREATE SCHEMA IF NOT EXISTS \"" + message.ServiceID.String() + "\"")
if err != nil {
// Report an error
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: err,
}
} else {
// Create a new connection to the database
var connectionString string
if strings.Contains(config.Global.Database.ConnectionString, "?") {
connectionString = config.Global.Database.ConnectionString + "&search_path=\"" + message.ServiceID.String() + "\""
} else {
connectionString = config.Global.Database.ConnectionString + "?search_path=\"" + message.ServiceID.String() + "\""
}
pluginConn, err := sql.Open("postgres", connectionString)
if err != nil {
// Report an error
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: err,
}
} else {
// Test the connection
err = pluginConn.Ping()
if err != nil {
// Report an error
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: err,
}
} else {
// Report a successful activation
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 2,
SentAt: time.Now(),
Message: library.Database{
DB: pluginConn,
DBType: library.Postgres,
},
}
}
}
}
}
}
} else {
// Report an error
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: errors.New("database access not permitted"),
}
var globalPGConn *sql.DB
func createPgSchema(id uuid.UUID) error {
_, err := globalPGConn.Exec("CREATE SCHEMA IF NOT EXISTS \"" + id.String() + "\"")
if err != nil {
return err
}
return nil
}
func svInit(message library.InterServiceMessage) {
var dummyInfo = &library.ServiceInitializationInformation{Outbox: activeServices[message.ServiceID].Inbox}
if !activeServices[message.ServiceID].ServiceMetadata.Permissions.Database {
message.Respond(library.Unauthorized, errors.New("database access not permitted"), dummyInfo)
return
}
var db library.Database
switch config.Global.Database.Type {
case "sqlite":
pluginConn, err := sql.Open("sqlite3", filepath.Join(config.Global.Database.Path, message.ServiceID.String()+".db"))
if err != nil {
message.Respond(library.InternalError, err, dummyInfo)
return
}
_, err = pluginConn.Exec("PRAGMA journal_mode=WAL")
if err != nil {
message.Respond(library.InternalError, err, dummyInfo)
return
}
db = library.Database{
DB: pluginConn,
DBType: library.Sqlite,
}
case "postgres":
err := createPgSchema(message.ServiceID)
if err != nil {
message.Respond(library.InternalError, err, dummyInfo)
return
}
connectionString := config.Global.Database.ConnectionString
if strings.Contains(config.Global.Database.ConnectionString, "?") {
connectionString += "&"
} else {
connectionString += "?"
}
connectionString += "search_path=\"" + message.ServiceID.String() + "\""
pluginConn, err := sql.Open("postgres", connectionString)
if err != nil {
message.Respond(library.InternalError, err, dummyInfo)
return
}
db = library.Database{
DB: pluginConn,
DBType: library.Postgres,
}
default:
message.Respond(library.InternalError, errors.New("database type not supported"), dummyInfo)
return
}
err := db.DB.Ping()
if err != nil {
message.Respond(library.InternalError, err, dummyInfo)
return
}
message.Respond(library.Success, db, dummyInfo)
}
func tryAuthAccess(message library.InterServiceMessage) {
// We need to check if the service is allowed to access the Authentication service
serviceMetadata, ok := activeServices[message.ServiceID]
if ok && serviceMetadata.ServiceMetadata.Permissions.Authenticate {
// Send message to Authentication service
service, ok := activeServices[uuid.MustParse("00000000-0000-0000-0000-000000000004")]
if ok {
service.Inbox <- message
} else {
// Send error message
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: errors.New("authentication service not found"),
}
} else {
// This should never happen
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
os.Exit(1)
}
}
} else {
// Send error message
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: errors.New("authentication not permitted"),
}
} else {
// This should never happen
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
os.Exit(1)
}
var dummyInfo = &library.ServiceInitializationInformation{Outbox: serviceMetadata.Inbox}
if !ok || !serviceMetadata.ServiceMetadata.Permissions.Authenticate {
message.Respond(library.Unauthorized, errors.New("authentication not permitted"), dummyInfo)
return
}
service, ok := activeServices[authService]
if !ok {
message.Respond(library.InternalError, errors.New("authentication service not found"), dummyInfo)
return
}
service.Inbox <- message
}
func tryStorageAccess(message library.InterServiceMessage) {
// We need to check if the service is allowed to access the Blob Storage service
serviceMetadata, ok := activeServices[message.ServiceID]
if ok && serviceMetadata.ServiceMetadata.Permissions.BlobStorage {
// Send message to Blob Storage service
service, ok := activeServices[uuid.MustParse("00000000-0000-0000-0000-000000000003")]
if ok {
service.Inbox <- message
} else {
// Send error message
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: errors.New("blob storage service not found"),
}
} else {
// This should never happen
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
os.Exit(1)
}
}
} else {
// Send error message
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: errors.New("blob storage is not permitted"),
}
} else {
// This should never happen
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
os.Exit(1)
}
var dummyInfo = &library.ServiceInitializationInformation{Outbox: serviceMetadata.Inbox}
if !ok || !serviceMetadata.ServiceMetadata.Permissions.BlobStorage {
message.Respond(library.Unauthorized, errors.New("storage access not permitted"), dummyInfo)
return
}
service, ok := activeServices[blobService]
if !ok {
message.Respond(library.InternalError, errors.New("storage service not found"), dummyInfo)
return
}
service.Inbox <- message
}
func tryLogger(message library.InterServiceMessage) {
@ -752,75 +675,38 @@ func tryLogger(message library.InterServiceMessage) {
}
}
func processInterServiceMessage(channel chan library.InterServiceMessage) {
func processInterServiceMessage(listener library.Listener) {
for {
message := <-channel
if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000000") {
message := listener.AcceptMessage()
switch message.ForServiceID {
case broadcastService:
// Broadcast message
for _, service := range activeServices {
service.Inbox <- message
}
} else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000001") {
// Service initialization service
case databaseService:
// Database service
switch message.MessageType {
case 0:
// This has been deprecated, ignore it
// Send "true" back
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 0,
SentAt: time.Now(),
Message: true,
}
case 1:
svInit(message)
}
} else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000002") {
case logService:
tryLogger(message)
} else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000003") {
case blobService:
tryStorageAccess(message)
} else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000004") {
case authService:
tryAuthAccess(message)
} else {
default:
serviceMetadata, ok := activeServices[message.ServiceID]
if ok && serviceMetadata.ServiceMetadata.Permissions.InterServiceCommunication {
// Send message to specific service
var dummyInfo = &library.ServiceInitializationInformation{Outbox: serviceMetadata.Inbox}
if !ok || !serviceMetadata.ServiceMetadata.Permissions.InterServiceCommunication {
message.Respond(library.Unauthorized, errors.New("inter-service communication not permitted"), dummyInfo)
} else {
service, ok := activeServices[message.ForServiceID]
if !ok {
// Send error message
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: errors.New("requested service not found"),
}
} else {
// This should never happen
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
os.Exit(1)
}
message.Respond(library.BadRequest, errors.New("service not found"), dummyInfo)
}
service.Inbox <- message
} else {
// Send error message
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: errors.New("inter-service communication not permitted"),
}
} else {
// This should never happen
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
os.Exit(1)
}
}
}
}
@ -887,6 +773,44 @@ func parseConfig(path string) Config {
return config
}
func checkHTTPS(route Route, subdomainRouter *chi.Mux, compressionSettings CompressionSettings) {
// Check if HTTPS is enabled
if route.HTTPS.KeyPath != "" && route.HTTPS.CertificatePath != "" {
certificate, err := loadTLSCertificate(route.HTTPS.CertificatePath, route.HTTPS.KeyPath)
if err != nil {
slog.Error("Error loading TLS certificate: " + err.Error())
os.Exit(1)
}
portRouters[route.Port].Register(subdomainRouter, compressionSettings, route.Subdomain, certificate)
} else {
portRouters[route.Port].Register(subdomainRouter, compressionSettings, route.Subdomain)
}
}
func checkServices(route Route, globalOutbox chan library.InterServiceMessage, subdomainRouter *chi.Mux) {
// Check the services
if route.Services != nil {
// Iterate through the services
for _, service := range route.Services {
// Check if the service is registered
registeredService, ok := registeredServices[service]
if ok {
// Check if the service is already active
_, ok := activeServices[registeredService.ServiceMetadata.ServiceID]
if ok {
slog.Error("Service with ID " + service + " is already active, will not activate again")
os.Exit(1)
} else {
// Initialize the service
initializeService(registeredService, globalOutbox, subdomainRouter, &route.Subdomain)
}
} else {
slog.Warn("Service with ID " + service + " is not registered")
}
}
}
}
func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) {
for _, route := range config.Routes {
var compressionSettings CompressionSettings
@ -902,43 +826,17 @@ func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) {
serverError(w, 404)
})
// Set the port router
_, ok := portRouters[route.Port]
if !ok {
portRouters[route.Port] = NewPortRouter()
}
// Check if HTTPS is enabled
if route.HTTPS.KeyPath != "" && route.HTTPS.CertificatePath != "" {
certificate, err := loadTLSCertificate(route.HTTPS.CertificatePath, route.HTTPS.KeyPath)
if err != nil {
slog.Error("Error loading TLS certificate: " + err.Error())
os.Exit(1)
}
portRouters[route.Port].Register(subdomainRouter, compressionSettings, route.Subdomain, certificate)
} else {
portRouters[route.Port].Register(subdomainRouter, compressionSettings, route.Subdomain)
}
checkHTTPS(route, subdomainRouter, compressionSettings)
// Check the services
if route.Services != nil {
// Iterate through the services
for _, service := range route.Services {
// Check if the service is registered
registeredService, ok := registeredServices[service]
if ok {
// Check if the service is already active
_, ok := activeServices[registeredService.ServiceMetadata.ServiceID]
if ok {
slog.Error("Service with ID " + service + " is already active, will not activate again")
os.Exit(1)
}
// Initialize the service
initializeService(registeredService, globalOutbox, subdomainRouter)
} else {
slog.Warn("Service with ID " + service + " is not registered")
}
}
}
checkServices(route, globalOutbox, subdomainRouter)
// Iterate through the paths
for _, pathBlock := range route.Paths {
@ -994,13 +892,13 @@ func registerServices() (err error) {
// Load the service information
serviceInformation, err := service.Lookup("ServiceInformation")
if err != nil {
return errors.New("service lacks necessary information")
return errors.New("service " + path + " lacks necessary service information")
}
// Load the main function
mainFunc, err := service.Lookup("Main")
if err != nil {
return errors.New("service lacks necessary main function")
return errors.New("service " + path + " lacks necessary main function")
}
// Register the service
@ -1010,7 +908,7 @@ func registerServices() (err error) {
ServiceID: serviceInformation.(*library.Service).ServiceID,
Inbox: inbox,
ServiceMetadata: *serviceInformation.(*library.Service),
ServiceMainFunc: mainFunc.(func(library.ServiceInitializationInformation)),
ServiceMainFunc: mainFunc.(func(*library.ServiceInitializationInformation)),
}
lock.Unlock()
@ -1023,23 +921,25 @@ func registerServices() (err error) {
return err
}
func initializeService(service Service, globalOutbox chan library.InterServiceMessage, subdomainRouter *chi.Mux) {
func initializeService(service Service, globalOutbox chan library.InterServiceMessage, subdomainRouter *chi.Mux, subdomain *string) {
// Get the plugin from the map
slog.Info("Activating service " + strings.ToLower(service.ServiceMetadata.Name) + " with ID " + service.ServiceMetadata.ServiceID.String())
serviceInitializationInformation := library.ServiceInitializationInformation{
Domain: strings.ToLower(service.ServiceMetadata.Name),
Configuration: config.Services[strings.ToLower(service.ServiceMetadata.Name)].(map[string]interface{}),
Outbox: globalOutbox,
Inbox: service.Inbox,
Router: subdomainRouter,
}
serviceInitializationInformation := library.NewServiceInitializationInformation(nil, globalOutbox, service.Inbox, nil, config.Services[strings.ToLower(service.ServiceMetadata.Name)].(map[string]interface{}), nil)
serviceInitializationInformation.Service = &service.ServiceMetadata
// Check if they want a resource directory
if service.ServiceMetadata.Permissions.Resources {
serviceInitializationInformation.ResourceDir = os.DirFS(filepath.Join(config.Global.ResourceDirectory, service.ServiceMetadata.ServiceID.String()))
}
// Check if they want a router
if service.ServiceMetadata.Permissions.Router {
serviceInitializationInformation.Router = subdomainRouter
serviceInitializationInformation.Domain = subdomain
}
// Add the service to the active services
lock.Lock()
activeServices[service.ServiceMetadata.ServiceID] = service
@ -1083,6 +983,14 @@ func main() {
slog.Error("Error creating database directory: " + err.Error())
os.Exit(1)
}
} else {
// Set the global database connection
var err error
globalPGConn, err = sql.Open("postgres", config.Global.Database.ConnectionString)
if err != nil {
slog.Error("Error connecting to database: " + err.Error())
os.Exit(1)
}
}
// Walk through the service directory and load the plugins
@ -1097,10 +1005,10 @@ func main() {
// Initialize the service discovery, health-check, and logging services
// Since these are core services, always allocate them the service IDs 0, 1, and 2
// These are not dynamically loaded, as they are integral to the system functioning
go processInterServiceMessage(globalOutbox)
go processInterServiceMessage(library.NewListener(globalOutbox))
// Start the storage service
initializeService(registeredServices["storage"], globalOutbox, nil)
// initializeService(registeredServices["storage"], globalOutbox, nil, nil)
// Iterate through the subdomains and create the routers as well as the compression levels and service maps
iterateThroughSubdomains(globalOutbox)

View file

@ -2,7 +2,7 @@ package main
import (
// Fulgens libraries
library "git.ailur.dev/ailur/fg-library/v2"
library "git.ailur.dev/ailur/fg-library/v3"
authLibrary "git.ailur.dev/ailur/fg-nucleus-library"
"git.ailur.dev/ailur/pow"
@ -36,6 +36,7 @@ var ServiceInformation = library.Service{
Name: "Authentication",
Permissions: library.Permissions{
Authenticate: false, // This service *is* the authentication service
Router: true, // This service does require a router
Database: true, // This service does require database access
BlobStorage: false, // This service does not require blob storage
InterServiceCommunication: true, // This service does require inter-service communication
@ -44,6 +45,10 @@ var ServiceInformation = library.Service{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"),
}
var (
loggerService = uuid.MustParse("00000000-0000-0000-0000-000000000002")
)
func checkScopes(scopes []string) (bool, string, error) {
var clientKeyShare bool
for _, scope := range scopes {
@ -65,15 +70,9 @@ func checkScopes(scopes []string) (bool, string, error) {
return clientKeyShare, string(scopeString), nil
}
func logFunc(message string, messageType uint64, information library.ServiceInitializationInformation) {
func logFunc(message string, messageType library.MessageCode, 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,
}
information.SendISMessage(loggerService, messageType, message)
}
func ensureTrailingSlash(url string) string {
@ -109,7 +108,7 @@ func sha256Base64(s string) string {
return encoded
}
func renderTemplate(statusCode int, w http.ResponseWriter, data map[string]interface{}, templatePath string, information library.ServiceInitializationInformation) {
func renderTemplate(statusCode int, w http.ResponseWriter, data map[string]interface{}, templatePath string, information *library.ServiceInitializationInformation) {
var err error
var requestedTemplate *template.Template
// Output ls of the resource directory
@ -134,7 +133,7 @@ func renderTemplate(statusCode int, w http.ResponseWriter, data map[string]inter
}
}
func renderString(statusCode int, w http.ResponseWriter, data string, information library.ServiceInitializationInformation) {
func renderString(statusCode int, w http.ResponseWriter, data string, information *library.ServiceInitializationInformation) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(statusCode)
_, err := w.Write([]byte(data))
@ -143,7 +142,7 @@ func renderString(statusCode int, w http.ResponseWriter, data string, informatio
}
}
func renderJSON(statusCode int, w http.ResponseWriter, data map[string]interface{}, information library.ServiceInitializationInformation) {
func renderJSON(statusCode int, w http.ResponseWriter, data map[string]interface{}, information *library.ServiceInitializationInformation) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
err := json.NewEncoder(w).Encode(data)
@ -189,11 +188,12 @@ func verifyJwt(token string, publicKey ed25519.PublicKey, mem *sql.DB) ([]byte,
return userId, claims, true
}
func Main(information library.ServiceInitializationInformation) {
func Main(information *library.ServiceInitializationInformation) {
var conn library.Database
var mem *sql.DB
var publicKey ed25519.PublicKey
var privateKey ed25519.PrivateKey
// Load the configuration
privacyPolicy := information.Configuration["privacyPolicy"].(string)
hostName := information.Configuration["url"].(string)
@ -205,109 +205,86 @@ func Main(information library.ServiceInitializationInformation) {
identifier := information.Configuration["identifier"].(string)
adminKey := information.Configuration["adminKey"].(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,
}
// Start the ISM processor
go information.StartISProcessor()
// 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 global table
// Uniqueness check is a hack to ensure we only have one global row
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS global (key BLOB NOT NULL, uniquenessCheck BOOLEAN NOT NULL UNIQUE CHECK (uniquenessCheck = true) DEFAULT true)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the users table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB PRIMARY KEY NOT NULL UNIQUE, created INTEGER NOT NULL, username TEXT NOT NULL UNIQUE, publicKey BLOB NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the oauth table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS oauth (appId TEXT NOT NULL UNIQUE, secret TEXT, creator BLOB NOT NULL, redirectUri TEXT NOT NULL, name TEXT NOT NULL, keyShareUri TEXT NOT NULL DEFAULT '', scopes TEXT NOT NULL DEFAULT '[\"openid\"]')")
if err != nil {
logFunc(err.Error(), 3, information)
}
} else {
// Create the global table
// Uniqueness check is a hack to ensure we only have one global row
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS global (key BYTEA NOT NULL, uniquenessCheck BOOLEAN NOT NULL UNIQUE CHECK (uniquenessCheck = true) DEFAULT true)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the users table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BYTEA PRIMARY KEY NOT NULL UNIQUE, created INTEGER NOT NULL, username TEXT NOT NULL UNIQUE, publicKey BYTEA NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the oauth table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS oauth (appId TEXT NOT NULL UNIQUE, secret TEXT, creator BYTEA NOT NULL, redirectUri TEXT NOT NULL, name TEXT NOT NULL, keyShareUri TEXT NOT NULL DEFAULT '', scopes TEXT NOT NULL DEFAULT '[\"openid\"]')")
if err != nil {
logFunc(err.Error(), 3, information)
}
}
// Set up the in-memory cache
var err error
mem, err = sql.Open("sqlite3", "file:"+ServiceInformation.ServiceID.String()+"?mode=memory&cache=shared")
// Initiate a connection to the database
conn, err := information.GetDatabase()
if err != nil {
logFunc(err.Error(), 3, information)
}
if conn.DBType == library.Sqlite {
// Create the global table
// Uniqueness check is a hack to ensure we only have one global row
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS global (key BLOB NOT NULL, uniquenessCheck BOOLEAN NOT NULL UNIQUE CHECK (uniquenessCheck = true) DEFAULT true)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Drop the tables if they exist
_, err = mem.Exec("DROP TABLE IF EXISTS sessions")
// Create the users table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB PRIMARY KEY NOT NULL UNIQUE, created INTEGER NOT NULL, username TEXT NOT NULL UNIQUE, publicKey BLOB NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
_, err = mem.Exec("DROP TABLE IF EXISTS logins")
if err != nil {
logFunc(err.Error(), 3, information)
}
_, err = mem.Exec("DROP TABLE IF EXISTS spent")
if err != nil {
logFunc(err.Error(), 3, information)
}
_, err = mem.Exec("DROP TABLE IF EXISTS challengeResponse")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the sessions table
_, err = mem.Exec("CREATE TABLE sessions (id BLOB NOT NULL, session TEXT NOT NULL, device TEXT NOT NULL DEFAULT '?')")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the logins table
_, err = mem.Exec("CREATE TABLE logins (appId TEXT NOT NULL, exchangeCode TEXT NOT NULL UNIQUE, pkce TEXT, pkceMethod TEXT, openid BOOLEAN NOT NULL, userId BLOB NOT NULL UNIQUE, nonce TEXT NOT NULL DEFAULT '', token TEXT NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the spent PoW table
_, err = mem.Exec("CREATE TABLE spent (hash BLOB NOT NULL UNIQUE, expires INTEGER NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the challenge-response table
_, err = mem.Exec("CREATE TABLE challengeResponse (challenge TEXT NOT NULL UNIQUE, userId BLOB NOT NULL, expires INTEGER NOT NULL)")
// Create the oauth table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS oauth (appId TEXT NOT NULL UNIQUE, secret TEXT, creator BLOB NOT NULL, redirectUri TEXT NOT NULL, name TEXT NOT NULL, keyShareUri TEXT NOT NULL DEFAULT '', scopes TEXT NOT NULL DEFAULT '[\"openid\"]')")
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)
// Create the global table
// Uniqueness check is a hack to ensure we only have one global row
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS global (key BYTEA NOT NULL, uniquenessCheck BOOLEAN NOT NULL UNIQUE CHECK (uniquenessCheck = true) DEFAULT true)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the users table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BYTEA PRIMARY KEY NOT NULL UNIQUE, created INTEGER NOT NULL, username TEXT NOT NULL UNIQUE, publicKey BYTEA NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the oauth table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS oauth (appId TEXT NOT NULL UNIQUE, secret TEXT, creator BYTEA NOT NULL, redirectUri TEXT NOT NULL, name TEXT NOT NULL, keyShareUri TEXT NOT NULL DEFAULT '', scopes TEXT NOT NULL DEFAULT '[\"openid\"]')")
if err != nil {
logFunc(err.Error(), 3, information)
}
}
// Set up the in-memory cache
mem, err = sql.Open("sqlite3", "file:"+ServiceInformation.ServiceID.String()+"?mode=memory&cache=shared")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Drop the tables if they exist
_, err = mem.Exec("DROP TABLE IF EXISTS sessions")
if err != nil {
logFunc(err.Error(), 3, information)
}
_, err = mem.Exec("DROP TABLE IF EXISTS logins")
if err != nil {
logFunc(err.Error(), 3, information)
}
_, err = mem.Exec("DROP TABLE IF EXISTS spent")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the sessions table
_, err = mem.Exec("CREATE TABLE sessions (id BLOB NOT NULL, session TEXT NOT NULL, device TEXT NOT NULL DEFAULT '?')")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the logins table
_, err = mem.Exec("CREATE TABLE logins (appId TEXT NOT NULL, exchangeCode TEXT NOT NULL UNIQUE, pkce TEXT, pkceMethod TEXT, openid BOOLEAN NOT NULL, userId BLOB NOT NULL UNIQUE, nonce TEXT NOT NULL DEFAULT '', token TEXT NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the spent PoW table
_, err = mem.Exec("CREATE TABLE spent (hash BLOB NOT NULL UNIQUE, expires INTEGER NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Set up the signing keys
// Check if the global table has the keys
err := conn.DB.QueryRow("SELECT key FROM global LIMIT 1").Scan(&privateKey)
err = conn.DB.QueryRow("SELECT key FROM global LIMIT 1").Scan(&privateKey)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// Generate a new key
@ -336,11 +313,10 @@ func Main(information library.ServiceInitializationInformation) {
}
if testAppIsInternalApp {
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ('TestApp-DoNotUse', 'none', $1, 'Test App', $2, '[\"openid\", \"clientKeyShare\"]', $3)", ServiceInformation.ServiceID, ensureTrailingSlash(hostName)+"testApp", ensureTrailingSlash(hostName)+"keyExchangeTester")
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ('TestApp-DoNotUse', 'none', $1, 'Test App', $2, '[\"openid\", \"clientKeyShare\"]', $3)", ServiceInformation.ServiceID[:], ensureTrailingSlash(hostName)+"testApp", ensureTrailingSlash(hostName)+"keyExchangeTester")
} else {
testAppCreator := uuid.New()
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ('TestApp-DoNotUse', 'none', $1, 'Test App', $2, '[\"openid\", \"clientKeyShare\"]', $3)", testAppCreator, ensureTrailingSlash(hostName)+"testApp", ensureTrailingSlash(hostName)+"keyExchangeTester")
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ('TestApp-DoNotUse', 'none', $1, 'Test App', $2, '[\"openid\", \"clientKeyShare\"]', $3)", testAppCreator[:], ensureTrailingSlash(hostName)+"testApp", ensureTrailingSlash(hostName)+"keyExchangeTester")
}
if err != nil {
testAppIsAvailable = false
@ -414,56 +390,30 @@ func Main(information library.ServiceInitializationInformation) {
router.Get("/authorize", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("client_id") != "" {
if conn.DBType == library.Sqlite {
var name string
var creator []byte
err := conn.DB.QueryRow("SELECT name, creator FROM oauth WHERE appId = $1", r.URL.Query().Get("client_id")).Scan(&name, &creator)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderString(404, w, "App not found", information)
} else {
logFunc(err.Error(), 2, information)
renderString(500, w, "Sorry, something went wrong on our end. Error code: 02. Please report to the administrator.", information)
}
return
}
if !bytes.Equal(creator, ServiceInformation.ServiceID[:]) {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
"name": name,
}, "authorize.html", information)
var name string
var creator []byte
err := conn.DB.QueryRow("SELECT name, creator FROM oauth WHERE appId = $1", r.URL.Query().Get("client_id")).Scan(&name, &creator)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderString(404, w, "App not found", information)
} else {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
"name": name,
}, "autoAccept.html", information)
logFunc(err.Error(), 2, information)
renderString(500, w, "Sorry, something went wrong on our end. Error code: 02. Please report to the administrator.", information)
}
return
}
// Check if the app is internal
if !bytes.Equal(creator, ServiceInformation.ServiceID[:]) {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
"name": name,
}, "authorize.html", information)
} else {
var name string
var creator uuid.UUID
err := conn.DB.QueryRow("SELECT name, creator FROM oauth WHERE appId = $1", r.URL.Query().Get("client_id")).Scan(&name, &creator)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderString(404, w, "App not found", information)
} else {
logFunc(err.Error(), 2, information)
renderString(500, w, "Sorry, something went wrong on our end. Error code: 03. Please report to the administrator.", information)
}
return
}
if creator != ServiceInformation.ServiceID {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
"name": name,
}, "authorize.html", information)
} else {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
"name": name,
}, "autoAccept.html", information)
}
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
"name": name,
}, "autoAccept.html", information)
}
} else {
http.Redirect(w, r, "/dashboard", 301)
@ -644,7 +594,7 @@ func Main(information library.ServiceInitializationInformation) {
return
}
_, err = conn.DB.Exec("INSERT INTO users (id, created, username, publicKey) VALUES ($1, $2, $3, $4)", userID, time.Now().Unix(), data.Username, publicKey)
_, err = conn.DB.Exec("INSERT INTO users (id, created, username, publicKey) VALUES ($1, $2, $3, $4)", userID[:], time.Now().Unix(), data.Username, publicKey)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
renderJSON(409, w, map[string]interface{}{"error": "Username already taken"}, information)
@ -666,37 +616,13 @@ func Main(information library.ServiceInitializationInformation) {
}
// Insert the session
_, err = mem.Exec("INSERT INTO sessions (id, session, device) VALUES (?, ?, ?)", userID, session, r.Header.Get("User-Agent"))
_, err = mem.Exec("INSERT INTO sessions (id, session, device) VALUES (?, ?, ?)", userID[:], session, r.Header.Get("User-Agent"))
// Return success, as well as the session token
renderJSON(200, w, map[string]interface{}{"key": session}, information)
})
router.Post("/api/loginChallenge", func(w http.ResponseWriter, r *http.Request) {
type login struct {
Username string `json:"username"`
}
var data login
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Get the id for the user
var userId []byte
err = conn.DB.QueryRow("SELECT id FROM users WHERE username = $1", data.Username).Scan(&userId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderJSON(401, w, map[string]interface{}{"error": "Invalid username"}, information)
} else {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "12"}, information)
logFunc(err.Error(), 2, information)
}
return
}
// Generate a new challenge
challenge, err := randomChars(512)
if err != nil {
@ -705,22 +631,27 @@ func Main(information library.ServiceInitializationInformation) {
return
}
// Insert the challenge with one minute expiration
_, err = mem.Exec("INSERT INTO challengeResponse (challenge, userId, expires) VALUES (?, ?, ?)", challenge, userId, time.Now().Unix()+60)
// Issue a new JWT token with the challenge
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
"challenge": challenge,
"exp": time.Now().Add(time.Second * 20).Unix(),
})
tokenString, err := token.SignedString(privateKey)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "53"}, information)
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "51"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return the challenge
renderJSON(200, w, map[string]interface{}{"challenge": challenge}, information)
renderJSON(200, w, map[string]interface{}{"challenge": challenge, "verifier": tokenString}, information)
})
router.Post("/api/login", func(w http.ResponseWriter, r *http.Request) {
type login struct {
Username string `json:"username"`
Signature string `json:"signature"`
Verifier string `json:"verifier"`
}
var data login
@ -732,8 +663,8 @@ func Main(information library.ServiceInitializationInformation) {
// Try to select the user
var userId []byte
var publicKey []byte
err = conn.DB.QueryRow("SELECT id, publicKey FROM users WHERE username = $1", data.Username).Scan(&userId, &publicKey)
var userPublicKey []byte
err = conn.DB.QueryRow("SELECT id, publicKey FROM users WHERE username = $1", data.Username).Scan(&userId, &userPublicKey)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderJSON(401, w, map[string]interface{}{"error": "Invalid username"}, information)
@ -752,33 +683,43 @@ func Main(information library.ServiceInitializationInformation) {
}
// Verify the challenge
// Select the current challenge from the database
var challenge string
err = mem.QueryRow("SELECT challenge FROM challengeResponse WHERE userId = ?", userId).Scan(&challenge)
token, err := jwt.Parse(data.Verifier, func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderJSON(401, w, map[string]interface{}{"error": "Invalid challenge"}, information)
} else {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "52"}, information)
logFunc(err.Error(), 2, information)
}
renderJSON(401, w, map[string]interface{}{"error": "Invalid verifier"}, information)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
renderJSON(401, w, map[string]interface{}{"error": "Invalid verifier: no claims"}, information)
return
}
expired, err := claims.GetExpirationTime()
if err != nil {
renderJSON(401, w, map[string]interface{}{"error": "Invalid verifier: no expiry"}, information)
return
}
if expired.Before(time.Now()) {
renderJSON(401, w, map[string]interface{}{"error": "Expired verifier"}, information)
return
}
challenge, ok := claims["challenge"].(string)
if !ok {
renderJSON(401, w, map[string]interface{}{"error": "Invalid verifier: no challenge"}, information)
return
}
// Check if the challenge is correct by verifying the signature
if !ed25519.Verify(publicKey, []byte(challenge), signature) {
if !ed25519.Verify(userPublicKey, []byte(challenge), signature) {
renderJSON(401, w, map[string]interface{}{"error": "Invalid signature"}, information)
return
}
// Delete the challenge
_, err = mem.Exec("DELETE FROM challengeResponse WHERE userId = ?", userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "53"}, information)
logFunc(err.Error(), 2, information)
return
}
// Create a new session
// We want the session token to be somewhat legible, so we use randomChars
// As a trade-off for this, we use a longer session token
@ -1614,17 +1555,7 @@ func Main(information library.ServiceInitializationInformation) {
if err != nil {
logFunc(err.Error(), 1, information)
} else {
affected, err := mem.Exec("DELETE FROM challengeResponse WHERE expires < ?", time.Now().Unix())
if err != nil {
logFunc(err.Error(), 1, information)
} else {
affectedCount2, err := affected.RowsAffected()
if err != nil {
logFunc(err.Error(), 1, information)
} else {
logFunc("Cleanup complete, deleted "+strconv.FormatInt(affectedCount+affectedCount2, 10)+" entries", 0, information)
}
}
logFunc("Cleanup complete, deleted "+strconv.FormatInt(affectedCount, 10)+" entries", 0, information)
}
}
}
@ -1633,128 +1564,78 @@ func Main(information library.ServiceInitializationInformation) {
go func() {
for {
// Wait for a message
message := <-information.Inbox
message := information.AcceptMessage()
if message.ServiceID != uuid.MustParse("00000000-0000-0000-0000-000000000001") {
// Check the message type
switch message.MessageType {
case 0:
// A service would like to know our hostname
// Send it to them
information.Outbox <- library.InterServiceMessage{
MessageType: 0,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: hostName,
SentAt: time.Now(),
}
case 1:
// A service would like to register a new OAuth entry
// Validate the scopes
clientKeyShare, scopes, err := checkScopes(message.Message.(authLibrary.OAuthInformation).Scopes)
if err != nil {
information.Outbox <- library.InterServiceMessage{
MessageType: 2,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: err.Error(),
SentAt: time.Now(),
}
return
}
// Check if the service already has an OAuth entry
var appId, secret string
err = conn.DB.QueryRow("SELECT appId, secret FROM oauth WHERE appId = $1", message.ServiceID.String()).Scan(&appId, &secret)
if err == nil && appId == message.ServiceID.String() {
// Update the entry to thew new scopes and redirect URI
if clientKeyShare {
_, err = conn.DB.Exec("UPDATE oauth SET name = $1, redirectUri = $2, scopes = $3, keyShareUri = $4 WHERE appId = $5", message.Message.(authLibrary.OAuthInformation).Name, message.Message.(authLibrary.OAuthInformation).RedirectUri, scopes, message.Message.(authLibrary.OAuthInformation).KeyShareUri, message.ServiceID.String())
} else {
_, err = conn.DB.Exec("UPDATE oauth SET name = $1, redirectUri = $2, scopes = $3 WHERE appId = $4", message.Message.(authLibrary.OAuthInformation).Name, message.Message.(authLibrary.OAuthInformation).RedirectUri, scopes, message.ServiceID.String())
}
if err != nil {
information.Outbox <- library.InterServiceMessage{
MessageType: 1,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "38",
SentAt: time.Now(),
}
logFunc(err.Error(), 2, information)
return
}
information.Outbox <- library.InterServiceMessage{
MessageType: 0,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: authLibrary.OAuthResponse{
AppID: appId,
SecretKey: secret,
},
SentAt: time.Now(),
}
return
}
// Generate a new secret
// It must be able to be sent via JSON, so we can't have pure-binary data
secret, err = randomChars(512)
if err != nil {
information.Outbox <- library.InterServiceMessage{
MessageType: 1,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "36",
SentAt: time.Now(),
}
logFunc(err.Error(), 2, information)
return
}
// Insert the oauth entry
if clientKeyShare {
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ($1, $2, $3, $4, $5, $6, $7)", message.ServiceID.String(), secret, ServiceInformation.ServiceID, message.Message.(authLibrary.OAuthInformation).Name, message.Message.(authLibrary.OAuthInformation).RedirectUri, scopes, message.Message.(authLibrary.OAuthInformation).KeyShareUri)
} else {
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes) VALUES ($1, $2, $3, $4, $5, $6)", message.ServiceID.String(), secret, ServiceInformation.ServiceID, message.Message.(authLibrary.OAuthInformation).Name, message.Message.(authLibrary.OAuthInformation).RedirectUri, scopes)
}
if err != nil {
information.Outbox <- library.InterServiceMessage{
MessageType: 1,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "39",
SentAt: time.Now(),
}
logFunc(err.Error(), 2, information)
return
}
// Return the appId and secret
information.Outbox <- library.InterServiceMessage{
MessageType: 0,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: authLibrary.OAuthResponse{
AppID: appId,
SecretKey: secret,
},
SentAt: time.Now(),
}
case 2:
// A service would like to have the public key
// Send it to them
information.Outbox <- library.InterServiceMessage{
MessageType: 2,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: publicKey,
SentAt: time.Now(),
}
// Check the message type
switch message.MessageType {
case 0:
// A service would like to have the hostname
// Send it to them
message.Respond(library.Success, hostName, information)
case 1:
// A service would like to register a new OAuth entry
// Validate the scopes
clientKeyShare, scopes, err := checkScopes(message.Message.(authLibrary.OAuthInformation).Scopes)
if err != nil {
message.Respond(library.BadRequest, err, information)
return
}
// Check if the service already has an OAuth entry
var appId, secret string
err = conn.DB.QueryRow("SELECT appId, secret FROM oauth WHERE appId = $1", message.ServiceID.String()).Scan(&appId, &secret)
if err == nil && appId == message.ServiceID.String() {
// Update the entry to thew new scopes and redirect URI
if clientKeyShare {
_, err = conn.DB.Exec("UPDATE oauth SET name = $1, redirectUri = $2, scopes = $3, keyShareUri = $4 WHERE appId = $5", message.Message.(authLibrary.OAuthInformation).Name, message.Message.(authLibrary.OAuthInformation).RedirectUri, scopes, message.Message.(authLibrary.OAuthInformation).KeyShareUri, message.ServiceID.String())
} else {
_, err = conn.DB.Exec("UPDATE oauth SET name = $1, redirectUri = $2, scopes = $3 WHERE appId = $4", message.Message.(authLibrary.OAuthInformation).Name, message.Message.(authLibrary.OAuthInformation).RedirectUri, scopes, message.ServiceID.String())
}
if err != nil {
message.Respond(library.InternalError, err, information)
logFunc(err.Error(), 2, information)
return
}
message.Respond(library.Success, authLibrary.OAuthResponse{
AppID: appId,
SecretKey: secret,
}, information)
return
}
// Generate a new secret
// It must be able to be sent via JSON, so we can't have pure-binary data
secret, err = randomChars(512)
if err != nil {
message.Respond(library.InternalError, err, information)
logFunc(err.Error(), 2, information)
return
}
// Insert the oauth entry
if clientKeyShare {
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ($1, $2, $3, $4, $5, $6, $7)", message.ServiceID.String(), secret, ServiceInformation.ServiceID[:], message.Message.(authLibrary.OAuthInformation).Name, message.Message.(authLibrary.OAuthInformation).RedirectUri, scopes, message.Message.(authLibrary.OAuthInformation).KeyShareUri)
} else {
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes) VALUES ($1, $2, $3, $4, $5, $6)", message.ServiceID.String(), secret, ServiceInformation.ServiceID[:], message.Message.(authLibrary.OAuthInformation).Name, message.Message.(authLibrary.OAuthInformation).RedirectUri, scopes)
}
if err != nil {
message.Respond(library.InternalError, err, information)
logFunc(err.Error(), 2, information)
return
}
// Return the appId and secret
message.Respond(library.Success, authLibrary.OAuthResponse{
AppID: appId,
SecretKey: secret,
}, information)
case 2:
// A service would like to have the public key
// Send it to them
message.Respond(library.Success, publicKey, information)
}
}
}()

View file

@ -1,7 +1,8 @@
// @license magnet:?xt=urn:btih:0ef1b8170b3b615170ff270def6427c317705f85&dn=lgpl-3.0.txt LGPL-3.0
// This sad excuse for a script is used so LibreJS doesn't scream at me
localStorage.clear()
localStorage.removeItem("DONOTSHARE-clientKey")
localStorage.removeItem("DONOTSHARE-secretKey")
window.location.replace("/login" + window.location.search)
// @license-end

View file

@ -192,6 +192,7 @@ func main() {
signupBody := map[string]interface{}{
"username": username,
"signature": base64.StdEncoding.EncodeToString(signature),
"verifier": responseMap["verifier"].(string),
}
// Marshal the body

View file

@ -236,7 +236,7 @@ func main() {
// Redirect to the client key exchange endpoint
js.Global().Get("swipe").Get("classList").Call("add", "swipe-animate")
time.Sleep(sleepTime)
js.Global().Get("window").Get("location").Call("replace", "/clientKeyShare?ecdhPublicKey="+base64.URLEncoding.EncodeToString(privateKey.PublicKey().Bytes())+"&accessToken="+responseMap["access_token"].(string))
// js.Global().Get("window").Get("location").Call("replace", "/clientKeyShare?ecdhPublicKey="+base64.URLEncoding.EncodeToString(privateKey.PublicKey().Bytes())+"&accessToken="+responseMap["access_token"].(string))
return
} else if response.StatusCode != 500 {
statusBox.Set("innerText", responseMap["error"].(string))

View file

@ -1,14 +1,15 @@
package main
import (
library "git.ailur.dev/ailur/fg-library/v2"
library "git.ailur.dev/ailur/fg-library/v3"
nucleusLibrary "git.ailur.dev/ailur/fg-nucleus-library"
"io"
"errors"
"bytes"
"os"
"time"
"database/sql"
"errors"
"path/filepath"
"github.com/google/uuid"
@ -18,47 +19,40 @@ var ServiceInformation = library.Service{
Name: "Storage",
Permissions: library.Permissions{
Authenticate: false, // This service does not require authentication
Router: false, // This service does not serve web pages
Database: true, // This service requires database access to store quotas
BlobStorage: false, // This service *is* the blob storage
InterServiceCommunication: true, // This service does require inter-service communication
Resources: false, // This service does not require access to its resource directory
},
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000003"),
}
var conn library.Database
var (
loggerService = uuid.MustParse("00000000-0000-0000-0000-000000000002")
)
func logFunc(message string, messageType uint64, information library.ServiceInitializationInformation) {
// Log the error 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 logFunc(message string, messageType library.MessageCode, information *library.ServiceInitializationInformation) {
// Log the message to the logger service
information.SendISMessage(loggerService, messageType, message)
}
func respondError(message string, information library.ServiceInitializationInformation, myFault bool, serviceID uuid.UUID) {
func respondError(message library.InterServiceMessage, err error, information *library.ServiceInitializationInformation, myFault bool) {
// Respond with an error message
var err uint64 = 1
var errCode = library.BadRequest
if myFault {
// Log the error message to the logger service
logFunc(message, 2, information)
err = 2
}
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: err,
SentAt: time.Now(),
Message: errors.New(message),
logFunc(err.Error(), 2, information)
errCode = library.InternalError
}
message.Respond(errCode, err, information)
}
func checkUserExists(userID uuid.UUID) bool {
func checkUserExists(userID uuid.UUID, conn library.Database) bool {
// Check if a user exists in the database
var userCheck []byte
err := conn.DB.QueryRow("SELECT id FROM users WHERE id = $1", userID).Scan(&userCheck)
err := conn.DB.QueryRow("SELECT id FROM users WHERE id = $1", userID[:]).Scan(&userCheck)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false
@ -66,74 +60,99 @@ func checkUserExists(userID uuid.UUID) bool {
return false
}
} else {
return uuid.Must(uuid.FromBytes(userCheck)) == userID
return bytes.Equal(userCheck, userID[:])
}
}
// addQuota can be used with a negative quota to remove quota from a user
func addQuota(information library.ServiceInitializationInformation, message library.InterServiceMessage) {
func addQuota(information *library.ServiceInitializationInformation, message library.InterServiceMessage, conn library.Database) {
// Add more quota to a user
if checkUserExists(message.Message.(nucleusLibrary.Quota).User) {
userID := message.Message.(nucleusLibrary.Quota).User
if checkUserExists(userID, conn) {
_, err := conn.DB.Exec("UPDATE users SET quota = quota + $1 WHERE id = $2", message.Message.(nucleusLibrary.Quota).Bytes, message.Message.(nucleusLibrary.Quota).User)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
} else {
_, err := conn.DB.Exec("INSERT INTO users (id, quota, reserved) VALUES ($1, $2, 0)", message.Message.(nucleusLibrary.Quota).User, int64(information.Configuration["defaultQuota"].(float64))+message.Message.(nucleusLibrary.Quota).Bytes)
_, err := conn.DB.Exec("INSERT INTO users (id, quota, reserved) VALUES ($1, $2, 0)", userID[:], int64(information.Configuration["defaultQuota"].(int))+message.Message.(nucleusLibrary.Quota).Bytes)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
}
// Success
message.Respond(library.Success, nil, information)
}
// And so does addReserved
func addReserved(information library.ServiceInitializationInformation, message library.InterServiceMessage) {
func addReserved(information *library.ServiceInitializationInformation, message library.InterServiceMessage, conn library.Database) {
// Add more reserved space to a user
if checkUserExists(message.Message.(nucleusLibrary.Quota).User) {
userID := message.Message.(nucleusLibrary.Quota).User
if checkUserExists(userID, conn) {
// Check if the user has enough space
quota, err := getQuota(message.Message.(nucleusLibrary.Quota).User)
quota, err := getQuota(information, userID, conn)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
used, err := getUsed(message.Message.(nucleusLibrary.Quota).User, information)
used, err := getUsed(userID, information, conn)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
if used+message.Message.(nucleusLibrary.Quota).Bytes > quota {
respondError("insufficient storage", information, false, message.ServiceID)
respondError(message, errors.New("insufficient storage"), information, false)
return
}
_, err = conn.DB.Exec("UPDATE users SET reserved = reserved + $1 WHERE id = $2", message.Message.(nucleusLibrary.Quota).Bytes, message.Message.(nucleusLibrary.Quota).User)
_, err = conn.DB.Exec("UPDATE users SET reserved = reserved + $1 WHERE id = $2", message.Message.(nucleusLibrary.Quota).Bytes, userID[:])
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
} else {
// Check if the user has enough space
if int64(information.Configuration["defaultQuota"].(float64)) < message.Message.(nucleusLibrary.Quota).Bytes {
respondError("insufficient storage", information, false, message.ServiceID)
if int64(information.Configuration["defaultQuota"].(int)) < message.Message.(nucleusLibrary.Quota).Bytes {
respondError(message, errors.New("insufficient storage"), information, false)
return
}
_, err := conn.DB.Exec("INSERT INTO users (id, quota, reserved) VALUES ($1, $2, $3)", message.Message.(nucleusLibrary.Quota).User, int64(information.Configuration["defaultQuota"].(float64)), message.Message.(nucleusLibrary.Quota).Bytes)
_, err := conn.DB.Exec("INSERT INTO users (id, quota, reserved) VALUES ($1, $2, $3)", userID[:], int64(information.Configuration["defaultQuota"].(int)), message.Message.(nucleusLibrary.Quota).Bytes)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
}
// Success
message.Respond(library.Success, nil, information)
}
func getQuota(userID uuid.UUID) (int64, error) {
func getQuota(information *library.ServiceInitializationInformation, userID uuid.UUID, conn library.Database) (int64, error) {
// Get the quota for a user
var quota int64
err := conn.DB.QueryRow("SELECT quota FROM users WHERE id = $1", userID).Scan(&quota)
err := conn.DB.QueryRow("SELECT quota FROM users WHERE id = $1", userID[:]).Scan(&quota)
if err != nil {
return 0, err
if errors.Is(err, sql.ErrNoRows) {
_, err := conn.DB.Exec("INSERT INTO users (id, quota, reserved) VALUES ($1, $2, 0)", userID[:], int64(information.Configuration["defaultQuota"].(int)))
if err != nil {
return 0, err
}
return int64(information.Configuration["defaultQuota"].(int)), nil
} else {
return 0, err
}
}
return quota, nil
}
func getUsed(userID uuid.UUID, information library.ServiceInitializationInformation) (int64, error) {
func getUsed(userID uuid.UUID, information *library.ServiceInitializationInformation, conn library.Database) (int64, error) {
// Get the used space for a user by first getting the reserved space from file storage
_, err := os.Stat(filepath.Join(information.Configuration["path"].(string), userID.String()))
if os.IsNotExist(err) {
// Create the directory
err = os.Mkdir(filepath.Join(information.Configuration["path"].(string), userID.String()), 0755)
if err != nil {
return 0, err
}
}
var used int64
err := filepath.Walk(filepath.Join(information.Configuration["path"].(string), userID.String()), func(path string, entry os.FileInfo, err error) error {
err = filepath.Walk(filepath.Join(information.Configuration["path"].(string), userID.String()), func(path string, entry os.FileInfo, err error) error {
if err != nil {
return err
}
@ -148,147 +167,167 @@ func getUsed(userID uuid.UUID, information library.ServiceInitializationInformat
// Then add the reserved space from the database
var reserved int64
err = conn.DB.QueryRow("SELECT reserved FROM users WHERE id = $1", userID).Scan(&reserved)
err = conn.DB.QueryRow("SELECT reserved FROM users WHERE id = $1", userID[:]).Scan(&reserved)
if err != nil {
return 0, err
if errors.Is(err, sql.ErrNoRows) {
_, err := conn.DB.Exec("INSERT INTO users (id, quota, reserved) VALUES ($1, $2, 0)", userID[:], int64(information.Configuration["defaultQuota"].(int)))
if err != nil {
return 0, err
}
return 0, nil
} else {
return 0, err
}
}
return used + reserved, nil
}
func modifyFile(information library.ServiceInitializationInformation, message library.InterServiceMessage) {
func modifyFile(information *library.ServiceInitializationInformation, message library.InterServiceMessage, conn library.Database) {
// Check if the file already exists
path := filepath.Join(information.Configuration["path"].(string), message.Message.(nucleusLibrary.File).User.String(), message.Message.(nucleusLibrary.File).Name)
logFunc(path, 0, information)
_, err := os.Stat(path)
if os.IsNotExist(err) {
if err == nil {
// Delete the file
err = os.Remove(path)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
} else if !os.IsNotExist(err) {
respondError(message, err, information, true)
}
// Check if the user has enough space
quota, err := getQuota(message.Message.(nucleusLibrary.File).User)
quota, err := getQuota(information, message.Message.(nucleusLibrary.File).User, conn)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
used, err := getUsed(message.Message.(nucleusLibrary.File).User, information)
used, err := getUsed(message.Message.(nucleusLibrary.File).User, information, conn)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
if used+int64(len(message.Message.(nucleusLibrary.File).Bytes)) > quota {
respondError("insufficient storage", information, false, message.ServiceID)
if used+message.Message.(nucleusLibrary.File).Reader.N > quota {
respondError(message, errors.New("insufficient storage"), information, false)
return
}
// Add a file to the user's storage
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
// Write the file
_, err = file.Write(message.Message.([]byte))
_, err = io.Copy(file, message.Message.(nucleusLibrary.File).Reader)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
// Close the file
err = file.Close()
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
// Success
message.Respond(library.Success, nil, information)
}
func getFile(information library.ServiceInitializationInformation, message library.InterServiceMessage) {
func getFile(information *library.ServiceInitializationInformation, message library.InterServiceMessage) {
// Check if the file exists
path := filepath.Join(information.Configuration["path"].(string), message.Message.(nucleusLibrary.File).User.String(), message.Message.(nucleusLibrary.File).Name)
_, err := os.Stat(path)
if os.IsNotExist(err) {
respondError("file not found", information, false, message.ServiceID)
println("file not found: " + path)
respondError(message, errors.New("file not found"), information, false)
return
}
// Open the file
file, err := os.Open(path)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
// Respond with the file
// It's their responsibility to close the file
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
MessageType: 1,
SentAt: time.Now(),
Message: file,
message.Respond(library.Success, file, information)
}
func deleteFile(information *library.ServiceInitializationInformation, message library.InterServiceMessage) {
// Check if the file exists
path := filepath.Join(information.Configuration["path"].(string), message.Message.(nucleusLibrary.File).User.String(), message.Message.(nucleusLibrary.File).Name)
_, err := os.Stat(path)
if os.IsNotExist(err) {
respondError(message, errors.New("file not found"), information, false)
return
}
// Delete the file
err = os.Remove(path)
if err != nil {
respondError(message, err, information, true)
}
// Success
message.Respond(library.Success, nil, information)
}
// processInterServiceMessages listens for incoming messages and processes them
func processInterServiceMessages(information library.ServiceInitializationInformation) {
func processInterServiceMessages(information *library.ServiceInitializationInformation, conn library.Database) {
// Listen for incoming messages
for {
message := <-information.Inbox
message := information.AcceptMessage()
switch message.MessageType {
case 1:
// Add quota
addQuota(information, message)
addQuota(information, message, conn)
case 2:
// Add reserved
addReserved(information, message)
addReserved(information, message, conn)
case 3:
// Modify file
modifyFile(information, message)
modifyFile(information, message, conn)
case 4:
// Get file
getFile(information, message)
case 5:
deleteFile(information, message)
default:
// Respond with an error message
respondError("invalid message type", information, false, message.ServiceID)
respondError(message, errors.New("invalid message type"), information, false)
}
}
}
func Main(information library.ServiceInitializationInformation) {
// 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,
func Main(information *library.ServiceInitializationInformation) {
// Start up the ISM processor
go information.StartISProcessor()
// Get the database connection
conn, err := information.GetDatabase()
if err != nil {
logFunc(err.Error(), 3, information)
}
// 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)
// Create the quotas table if it doesn't exist
if conn.DBType == library.Sqlite {
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS quotas (id BLOB PRIMARY KEY, quota BIGINT, reserved BIGINT)")
if err != nil {
logFunc(err.Error(), 3, information)
}
} else {
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id UUID PRIMARY KEY, quota BIGINT, reserved BIGINT)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the quotas table if it doesn't exist
if conn.DBType == library.Sqlite {
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB PRIMARY KEY, quota BIGINT, reserved BIGINT)")
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)
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BYTEA PRIMARY KEY, quota BIGINT, reserved BIGINT)")
if err != nil {
logFunc(err.Error(), 3, information)
}
}
// Listen for incoming messages
go processInterServiceMessages(information)
go processInterServiceMessages(information, conn)
}