Compare commits

..

29 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
9cbe1e8ecc Fixed registration not changing the redirect URI
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-15 16:45:13 +00:00
a2dab0869d Made mem work correctly
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-15 16:34:40 +00:00
684c9d6d48 Cleaned up the logging
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-14 19:52:57 +00:00
d8a6a48c43 Got rid of debug code
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-14 19:51:47 +00:00
5145c65d04 Added support for custom ports
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-14 19:48:39 +00:00
d04a40f655 Made it not constantly regenerate the oauth entries for services
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-13 16:36:58 +00:00
9ca7caf2c3 Fixed the password changing API using the old argon2 hashing algorithm
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-11 17:25:01 +00:00
a447fde86a Updated fulgens library versions
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-08 18:09:26 +00:00
1302147be2 Updated go version, updated dependencies
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-08 18:02:41 +00:00
e2a4c13a60 Made addReserved check for space
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-04 17:17:49 +00:00
12 changed files with 845 additions and 997 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" "Fulgensfas has been built successfully!"
fancy "\033[1;102m" "Fulgens has been built successfully!"

View file

@ -4,10 +4,6 @@
global: {
# IP defines the IP address to bind to.
ip: "0.0.0.0",
# httpPort defines the port to bind to for HTTP.
httpPort: "8080",
# httpsPort defines the port to bind to for HTTPS (TLS).
httpsPort: "8443",
# serviceDirectory defines the directory to look for services in.
serviceDirectory: "./services",
# resourceDirectory defines the directory to look for resources in.
@ -19,13 +15,6 @@ global: {
# level defines the compression level to use, possible values are 1-9 for gzip, 0-11 for brotli and 1-22 for zstd.
level: 5
},
# https defines the HTTPS settings on a global level - per-route settings override these. It is optional.
https: {
# certificate defines the path to the certificate file (must be a wildcard in order to support multiple subdomains).
certificate: "./certs/localhost.crt",
# key defines the path to the key file (must be a wildcard in order to support multiple subdomains).
key: "./certs/localhost.key"
},
# logging defines the logging settings.
logging: {
# enabled defines whether logging is enabled.
@ -41,6 +30,23 @@ global: {
path: "./databases",
# connectionString defines the connection string to use for the database (postgres only).
connectionString: "postgres://user:password@localhost:5432/database"
},
# stealth enables stealth mode, which makes the server look like some preset http servers.
# stealth mode overrides all proxy preservations and headers.
stealth: {
# enabled defines whether stealth mode is enabled.
enabled: true,
# server defines the server to pretend to be, possible values are "nginx" or "net/http".
server: "nginx",
# php defines if the server should pretend to be running PHP. This should only be used on nginx.
php: {
# enabled defines whether PHP spoofing is enabled.
enabled: true,
# version defines the version of PHP to pretend to be.
version: "8.2.25"
},
# aspnet defines if the server should pretend to be running ASP.NET. This should only be used on nginx.
aspNet: true
}
}
@ -49,6 +55,8 @@ routes: [
{
# none is a special subdomain that matches all requests without a subdomain (Host header).
subdomain: "none",
# port defines the port to use for this route. They do not have to be unique.
port: "8080",
# services defines the services to use for this route. Services must be defined on a per-subdomain basis.
# Each service may not be used more than once globally. The server will fail to start if this is violated.
services: ["authentication"]
@ -56,7 +64,11 @@ routes: [
{
# any subdomain value that isn't "none" will match that specific subdomain.
subdomain: "www.localhost",
# https defines the HTTPS settings for this route.
# port defines the port to use for this route. They do not have to be unique.
port: "8443",
# https defines the HTTPS settings for this route. If this block is missing, HTTPS will not be enabled for this port.
# If https is set once for any subdomain with this port, it will be enabled for all subdomains with this port.
# The connection will fail if the above condition is true, but there is not an HTTPS block for that subdomain.
https: {
# certificate defines the path to the certificate file.
certificate: "./certs/localhost.crt",
@ -66,8 +78,8 @@ routes: [
# paths defines per-path settings (NOT for services, which MUST be defined on a per-subdomain basis).
paths: [
{
# path defines the path to match. They can contain wildcards.
path: "/static/*",
# paths defines the paths to match. They can contain wildcards.
paths: ["/static", "/static/*"],
# static defines the static file serving settings for this path. This conflicts with proxy and redirect.
# static > proxy > redirect in terms of precedence.
static: {
@ -79,37 +91,41 @@ routes: [
}
},
{
# path defines the path to match. They can contain wildcards.
path: "/proxy/*",
# paths defines the paths to match. They can contain wildcards.
paths: ["/proxy", "/proxy/*"],
# proxy defines the proxy settings for this path. This conflicts with static and redirect.
# static > proxy > redirect in terms of precedence.
proxy: {
# url defines the URL to proxy requests to.
url: "http://localhost:8000",
# stripPrefix defines whether to strip the prefix from the path before proxying.
stripPrefix: true
stripPrefix: true,
headers: {
# forbid defines the headers to forbid from being sent to the proxied server.
forbid: ["Authorization"],
# preserve defines the headers to preserve when sending to the client.
preserve: [X-Powered-By", "Server"]
# host defines whether the host / :authority header should be sent to the proxied server.
host: true,
forbid: [ "User-Agent" ],
# preserveServer defines whether to preserve the server header from the proxied server.
preserveServer: true,
# preserveAltSvc defines whether to preserve the Alt-Svc header from the proxied server.
preserveAltSvc: true,
# preserveXPoweredBy defines whether to preserve the X-Powered-By header from the proxied server.
preserveXPoweredBy: true,
# passHost defines whether the host / :authority header should be sent to the proxied server.
passHost: true,
# xForward defines whether to send the X-Forwarded-For and X-Forwarded-Proto headers.
xForward: true
xForward: false
}
},
{
# path defines the path to match. They can contain wildcards.
path: "/redirect/*",
# redirect defines the redirect settings for this path. This conflicts with proxy and static.
# static > proxy > redirect in terms of precedence.
redirect: {
# url defines the URL to redirect to.
url: "https://www.google.com",
# permanent defines whether the redirect is permanent (301) or temporary (302).
permanent: true
}
},
{
# paths defines the paths to match. They can contain wildcards.
paths: ["/redirect", "/redirect/*"],
# redirect defines the redirect settings for this path. This conflicts with proxy and static.
# static > proxy > redirect in terms of precedence.
redirect: {
# url defines the URL to redirect to.
url: "https://www.ailur.dev",
# permanent defines whether the redirect is permanent (301) or temporary (302).
permanent: true
}
}
]

31
go.mod
View file

@ -1,34 +1,35 @@
module git.ailur.dev/ailur/fulgens
go 1.23.1
go 1.23.3
require (
git.ailur.dev/ailur/fg-library/v2 v2.1.1
git.ailur.dev/ailur/fg-nucleus-library v1.0.4
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.28.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.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
github.com/rogpeppe/go-internal v1.13.1 // 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
)

65
go.sum
View file

@ -1,9 +1,9 @@
git.ailur.dev/ailur/fg-library/v2 v2.1.1 h1:49NgKud/gJjlTPZQfrZkmx4b/IEa3ZkO/NNM8QGA3qk=
git.ailur.dev/ailur/fg-library/v2 v2.1.1/go.mod h1:Il3+GZ7tClNx4/QSt3eY4tR7NdvO+Qf00J0PKkxPrl4=
git.ailur.dev/ailur/fg-nucleus-library v1.0.4 h1:eDMkZm1OgHJtO7xyrAwZx2rsv77P6pes5FXvDNhMQ5g=
git.ailur.dev/ailur/fg-nucleus-library v1.0.4/go.mod h1:RbBVFRwtQgYvCWoru1mC3vUJ1dMftkNbvd7hVFtREFw=
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=
@ -11,21 +11,22 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
@ -33,14 +34,16 @@ 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.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@ -49,32 +52,36 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

746
main.go
View file

@ -1,25 +1,25 @@
package main
import (
library "git.ailur.dev/ailur/fg-library/v2"
"net/http/httputil"
"net/url"
"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"
"log/slog"
"net/http"
"net/http/httputil"
"net/url"
"path/filepath"
"github.com/go-chi/chi/v5"
@ -39,20 +39,11 @@ import (
type Config struct {
Global struct {
IP string `yaml:"ip" validate:"required,ip_addr"`
HTTPPort string `yaml:"httpPort" validate:"required"`
HTTPSPort string `yaml:"httpsPort" validate:"required"`
ServiceDirectory string `yaml:"serviceDirectory" validate:"required"`
ResourceDirectory string `yaml:"resourceDirectory" validate:"required"`
Compression struct {
Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Level float64 `yaml:"level" validate:"omitempty,min=1,max=22"`
} `yaml:"compression"`
HTTPS struct {
CertificatePath string `yaml:"certificate" validate:"required"`
KeyPath string `yaml:"key" validate:"required"`
} `yaml:"https"`
Logging struct {
IP string `yaml:"ip" validate:"required,ip_addr"`
ServiceDirectory string `yaml:"serviceDirectory" validate:"required"`
ResourceDirectory string `yaml:"resourceDirectory" validate:"required"`
Compression CompressionSettings `yaml:"compression"`
Logging struct {
Enabled bool `yaml:"enabled"`
File string `yaml:"file" validate:"required_if=Enabled true"`
} `yaml:"logging"`
@ -71,37 +62,37 @@ type Config struct {
ASPNet bool `yaml:"aspNet"`
}
} `yaml:"global" validate:"required"`
Routes []struct {
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 struct {
Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Level float64 `yaml:"level" validate:"omitempty,min=1,max=22"`
} `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"`
@ -114,27 +105,77 @@ 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 {
Level int
Algorithm string
Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Level int `yaml:"level" validate:"omitempty,min=1,max=22"`
}
func checkCompressionAlgorithm(algorithm string, handler http.Handler, request *http.Request) http.Handler {
var compressionLevel int
compressionSettings, ok := compression[request.Host]
type RouterAndCompression struct {
Router *chi.Mux
Compression CompressionSettings
}
type PortRouter struct {
https struct {
enabled bool
httpSettings map[string]*tls.Certificate
}
routers map[string]RouterAndCompression
}
func NewPortRouter() *PortRouter {
return &PortRouter{
routers: make(map[string]RouterAndCompression),
https: struct {
enabled bool
httpSettings map[string]*tls.Certificate
}{enabled: false, httpSettings: make(map[string]*tls.Certificate)},
}
}
func (pr *PortRouter) Register(router *chi.Mux, compression CompressionSettings, subdomain string, certificate ...*tls.Certificate) {
pr.routers[subdomain] = RouterAndCompression{Router: router, Compression: compression}
if len(certificate) > 0 {
pr.https.enabled = true
pr.https.httpSettings[subdomain] = certificate[0]
}
}
func (pr *PortRouter) Router(w http.ResponseWriter, r *http.Request) {
host := strings.Split(r.Host, ":")[0]
router, ok := pr.routers[host]
if !ok {
compressionLevel = int(config.Global.Compression.Level)
} else {
compressionLevel = compressionSettings.Level
router, ok = pr.routers["none"]
}
switch algorithm {
if router.Compression.Algorithm != "none" {
compressRouter(router.Compression, router.Router).ServeHTTP(w, r)
} else {
router.Router.ServeHTTP(w, r)
}
}
func (pr *PortRouter) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, ok := pr.https.httpSettings[hello.ServerName]
if !ok {
return nil, errors.New("certificate not found")
} else {
return cert, nil
}
}
func (pr *PortRouter) HTTPSEnabled() bool {
return pr.https.enabled
}
func compressRouter(settings CompressionSettings, handler http.Handler) http.Handler {
switch settings.Algorithm {
case "gzip":
encoder, err := gzip.New(gzip.Options{Level: compressionLevel})
encoder, err := gzip.New(gzip.Options{Level: settings.Level})
if err != nil {
slog.Error("Error creating gzip encoder: " + err.Error())
return handler
@ -146,7 +187,7 @@ func checkCompressionAlgorithm(algorithm string, handler http.Handler, request *
}
return gzipHandler(handler)
case "brotli":
encoder, err := brotli.New(brotli.Options{Quality: compressionLevel})
encoder, err := brotli.New(brotli.Options{Quality: settings.Level})
if err != nil {
slog.Error("Error creating brotli encoder: " + err.Error())
return handler
@ -158,7 +199,7 @@ func checkCompressionAlgorithm(algorithm string, handler http.Handler, request *
}
return brotliHandler(handler)
case "zstd":
encoder, err := zstd.New(kpzstd.WithEncoderLevel(kpzstd.EncoderLevelFromZstd(compressionLevel)))
encoder, err := zstd.New(kpzstd.WithEncoderLevel(kpzstd.EncoderLevelFromZstd(settings.Level)))
if err != nil {
slog.Error("Error creating zstd encoder: " + err.Error())
return handler
@ -351,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
@ -470,35 +521,18 @@ func serverError(w http.ResponseWriter, status int) {
}
}
func hostRouter(w http.ResponseWriter, r *http.Request) {
host := strings.Split(r.Host, ":")[0]
router, ok := subdomains[host]
if !ok {
router, ok = subdomains["none"]
if !ok {
serverError(w, 404)
slog.Error("No subdomain found for " + host)
}
}
compressionSettings, ok := compression[host]
if !ok {
checkCompressionAlgorithm(config.Global.Compression.Algorithm, router, r).ServeHTTP(w, r)
} else {
checkCompressionAlgorithm(compressionSettings.Algorithm, router, r).ServeHTTP(w, r)
}
}
var (
validate *validator.Validate
lock sync.RWMutex
config Config
registeredServices = make(map[string]Service)
activeServices = make(map[uuid.UUID]Service)
certificates = make(map[string]*tls.Certificate)
compression = make(map[string]CompressionSettings)
subdomains = make(map[string]*chi.Mux)
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) {
@ -510,218 +544,113 @@ func loadTLSCertificate(certificatePath, keyPath string) (*tls.Certificate, erro
}
}
func getTLSCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, ok := certificates[hello.ServerName]
if !ok {
if config.Global.HTTPS.CertificatePath == "" || config.Global.HTTPS.KeyPath == "" {
return nil, errors.New("no certificate found")
} else {
return certificates["none"], nil
}
} else {
return cert, nil
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) {
// 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 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) {
@ -746,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)
}
}
}
}
@ -881,13 +773,51 @@ 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 {
if route.Compression.Level != 0 {
compression[route.Subdomain] = CompressionSettings{
Level: int(route.Compression.Level),
Algorithm: route.Compression.Algorithm,
}
var compressionSettings CompressionSettings
if route.Compression != (CompressionSettings{}) {
compressionSettings = route.Compression
} else {
compressionSettings = config.Global.Compression
}
// Create the subdomain router
@ -896,30 +826,17 @@ func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) {
serverError(w, 404)
})
subdomains[route.Subdomain] = subdomainRouter
subdomains[route.Subdomain].Use(logger)
subdomains[route.Subdomain].Use(serverChanger)
// Set the port router
_, ok := portRouters[route.Port]
if !ok {
portRouters[route.Port] = NewPortRouter()
}
// Check if HTTPS is enabled
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 {
@ -953,15 +870,6 @@ func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) {
}
}
}
// Add the TLS certificate
if route.HTTPS.CertificatePath != "" && route.HTTPS.KeyPath != "" {
certificate, err := loadTLSCertificate(route.HTTPS.CertificatePath, route.HTTPS.KeyPath)
if err != nil {
slog.Error("Error loading TLS certificate: " + err.Error() + ", TLS will not be available for subdomain " + route.Subdomain)
}
certificates[route.Subdomain] = certificate
}
}
}
@ -984,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
@ -1000,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()
@ -1013,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
@ -1073,16 +983,14 @@ func main() {
slog.Error("Error creating database directory: " + err.Error())
os.Exit(1)
}
}
// Check if the root TLS certificate exists
if config.Global.HTTPS.CertificatePath != "" && config.Global.HTTPS.KeyPath != "" {
certificate, err := loadTLSCertificate(config.Global.HTTPS.CertificatePath, config.Global.HTTPS.KeyPath)
} else {
// Set the global database connection
var err error
globalPGConn, err = sql.Open("postgres", config.Global.Database.ConnectionString)
if err != nil {
slog.Error("Error loading TLS certificate: " + err.Error() + ", TLS will not be available unless specified in the subdomains")
slog.Error("Error connecting to database: " + err.Error())
os.Exit(1)
}
certificates["none"] = certificate
}
// Walk through the service directory and load the plugins
@ -1097,39 +1005,47 @@ 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)
// Start the server
slog.Info("Starting server on " + config.Global.IP + " with ports " + config.Global.HTTPPort + " and " + config.Global.HTTPSPort)
go func() {
// Create the TLS server
server := &http.Server{
Addr: config.Global.IP + ":" + config.Global.HTTPSPort,
Handler: http.HandlerFunc(hostRouter),
TLSConfig: &tls.Config{
GetCertificate: getTLSCertificate,
},
// Start the servers
slog.Info("Starting servers")
for port, router := range portRouters {
if !router.HTTPSEnabled() {
go func() {
// Start the HTTP server
err = http.ListenAndServe(config.Global.IP+":"+port, logger(serverChanger(http.HandlerFunc(router.Router))))
slog.Error("Error starting server: " + err.Error())
os.Exit(1)
}()
} else {
// Create the TLS server
server := &http.Server{
Addr: config.Global.IP + ":" + port,
Handler: logger(serverChanger(http.HandlerFunc(router.Router))),
TLSConfig: &tls.Config{
GetCertificate: router.GetCertificate,
},
}
go func() {
// Start the TLS server
err = server.ListenAndServeTLS("", "")
slog.Error("Error starting HTTPS server: " + err.Error())
os.Exit(1)
}()
}
// Start the TLS server
err = server.ListenAndServeTLS("", "")
slog.Error("Error starting HTTPS server: " + err.Error())
os.Exit(1)
}()
// Start the HTTP server
err = http.ListenAndServe(config.Global.IP+":"+config.Global.HTTPPort, http.HandlerFunc(hostRouter))
if err != nil {
slog.Error("Error starting server: " + err.Error())
} else {
// This should never happen
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
}
os.Exit(1)
slog.Info("Servers started. Fulgens is now running. Press Ctrl+C to stop the server.")
// Wait for a signal to stop the server
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM)
<-signalChannel
}

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"
@ -25,9 +25,6 @@ import (
"io/fs"
"net/http"
// Extra libraries
"golang.org/x/crypto/argon2"
// External libraries
"github.com/cespare/xxhash/v2"
"github.com/golang-jwt/jwt/v5"
@ -39,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
@ -47,17 +45,34 @@ var ServiceInformation = library.Service{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"),
}
var serviceIDBytes []byte
var (
loggerService = uuid.MustParse("00000000-0000-0000-0000-000000000002")
)
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 checkScopes(scopes []string) (bool, string, error) {
var clientKeyShare bool
for _, scope := range scopes {
if scope != "openid" && scope != "clientKeyShare" {
return false, "", errors.New("invalid scope")
} else {
if scope == "clientKeyShare" {
clientKeyShare = true
}
}
}
// Marshal the scopes
scopeString, err := json.Marshal(scopes)
if err != nil {
return clientKeyShare, "", err
}
return clientKeyShare, string(scopeString), nil
}
func logFunc(message string, messageType library.MessageCode, information *library.ServiceInitializationInformation) {
// Log the message to the logger service
information.SendISMessage(loggerService, messageType, message)
}
func ensureTrailingSlash(url string) string {
@ -93,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
@ -118,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))
@ -127,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)
@ -173,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)
@ -189,108 +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
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
@ -319,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)", serviceIDBytes, 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
@ -397,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, serviceIDBytes) {
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)
@ -522,8 +489,8 @@ func Main(information library.ServiceInitializationInformation) {
router.Post("/api/changePassword", func(w http.ResponseWriter, r *http.Request) {
type changePassword struct {
Session string `json:"session"`
NewPassword string `json:"newPassword"`
Session string `json:"session"`
NewPublicKey string `json:"newPublicKey"`
}
var data changePassword
err = json.NewDecoder(r.Body).Decode(&data)
@ -540,34 +507,14 @@ func Main(information library.ServiceInitializationInformation) {
return
}
// Generate a new salt
// We want it to be binary data, not alphanumerical, so we don't use randomChars
salt := make([]byte, 16)
_, err = rand.Read(salt)
// Update the public key
_, err = conn.DB.Exec("UPDATE users SET publicKey = $1 WHERE id = $2", data.NewPublicKey, userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information)
logFunc(err.Error(), 2, information)
return
}
// Decode the new password
newPassword, err := base64.StdEncoding.DecodeString(data.NewPassword)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Hash the password
hashedPassword := argon2.IDKey(newPassword, salt, 64, 4096, 1, 32)
// Update the password
_, err = conn.DB.Exec("UPDATE users SET password = $1, salt = $2 WHERE id = $3", hashedPassword, salt, userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "05"}, information)
logFunc(err.Error(), 2, information)
return
}
// Invalidate all sessions
_, err = mem.Exec("DELETE FROM sessions WHERE id = ?", userId)
if err != nil {
@ -647,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)
@ -669,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 {
@ -708,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
@ -735,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)
@ -755,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
@ -1237,7 +1175,6 @@ func Main(information library.ServiceInitializationInformation) {
})
router.Post("/api/oauth/add", func(w http.ResponseWriter, r *http.Request) {
// Conveniently, we use this one for ISB as well, so we can re-use the struct
var data authLibrary.OAuthInformation
err = json.NewDecoder(r.Body).Decode(&data)
@ -1273,27 +1210,9 @@ func Main(information library.ServiceInitializationInformation) {
}
// Validate the scopes
var clientKeyShare bool
for _, scope := range data.Scopes {
if scope != "openid" && scope != "clientKeyShare" {
renderJSON(400, w, map[string]interface{}{"error": "Invalid scope"}, information)
return
} else {
if scope == "clientKeyShare" {
clientKeyShare = true
} else if scope != "openid" {
logFunc("An impossible logic error has occurred, please move away from radiation or use ECC RAM", 1, information)
renderJSON(400, w, map[string]interface{}{"error": "Invalid scope"}, information)
return
}
}
}
// Marshal the scopes
scopes, err := json.Marshal(data.Scopes)
clientKeyShare, scopes, err := checkScopes(data.Scopes)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "36"}, information)
logFunc(err.Error(), 2, information)
renderJSON(400, w, map[string]interface{}{"error": err.Error()}, information)
return
}
@ -1636,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)
}
}
}
@ -1655,136 +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
// 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
}
// Generate a new appId
// It must be able to be sent via JSON, so we can't have pure-binary data
appId, err := randomChars(32)
if err != nil {
information.Outbox <- library.InterServiceMessage{
MessageType: 1,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "37",
SentAt: time.Now(),
}
logFunc(err.Error(), 2, information)
return
}
// Validate the scopes
var clientKeyShare bool
for _, scope := range message.Message.(authLibrary.OAuthInformation).Scopes {
if scope != "openid" && scope != "clientKeyShare" {
information.Outbox <- library.InterServiceMessage{
MessageType: 2,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "Invalid scope",
SentAt: time.Now(),
}
return
} else {
if scope == "clientKeyShare" {
clientKeyShare = true
} else if scope != "openid" {
logFunc("An impossible logic error has occurred, please move away from radiation or use ECC RAM", 1, information)
information.Outbox <- library.InterServiceMessage{
MessageType: 2,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "Invalid scope",
SentAt: time.Now(),
}
return
}
}
}
// Marshal the scopes
scopes, err := json.Marshal(message.Message.(authLibrary.OAuthInformation).Scopes)
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
}
// 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)", appId, secret, serviceIDBytes, 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)", appId, secret, serviceIDBytes, 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,14 +1,14 @@
module git.ailur.dev/fulgens/services-src/auth/resources
go 1.23.0
go 1.23.3
require (
git.ailur.dev/ailur/jsFetch v1.1.1
github.com/cespare/xxhash/v2 v2.3.0
golang.org/x/crypto v0.28.0
golang.org/x/crypto v0.29.0
)
require (
git.ailur.dev/ailur/jsStreams v1.2.1 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/sys v0.27.0 // indirect
)

View file

@ -1,12 +1,10 @@
git.ailur.dev/ailur/jsFetch v1.1.1 h1:kdCkrNr2mRvTG6hlK3YwnqlwfvzIQaw4z4AXLXewQ38=
git.ailur.dev/ailur/jsFetch v1.1.1/go.mod h1:eaQVFOlHwcPHCqh3oyQkQrpltmILOaiA9DKq3oTHBbM=
git.ailur.dev/ailur/jsStreams v1.2.0 h1:BRtLEyjkUoPKPu0Y6odUbSMlKCYNyR792TYRtujKfPw=
git.ailur.dev/ailur/jsStreams v1.2.0/go.mod h1:/ZCvbUcWkZRuKIkO7jH6b5vIjzdxIOP8ET8X0src5Go=
git.ailur.dev/ailur/jsStreams v1.2.1 h1:nXZYZrxHJCVwR0Kx/X+TenMBmS6Gh8Uc2DMinbyiGoo=
git.ailur.dev/ailur/jsStreams v1.2.1/go.mod h1:/ZCvbUcWkZRuKIkO7jH6b5vIjzdxIOP8ET8X0src5Go=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
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=

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,56 +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) {
_, err := conn.DB.Exec("UPDATE users SET reserved = reserved + $1 WHERE id = $2", message.Message.(nucleusLibrary.Quota).Bytes, 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(information, userID, conn)
if err != nil {
respondError(err.Error(), information, true, message.ServiceID)
respondError(message, err, information, true)
}
used, err := getUsed(userID, information, conn)
if err != nil {
respondError(message, err, information, true)
}
if used+message.Message.(nucleusLibrary.Quota).Bytes > quota {
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, userID[:])
if err != nil {
respondError(message, err, information, true)
}
} else {
_, 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)
// Check if the user has enough space
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)", 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
}
@ -130,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)
}