Compare commits

..

43 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
d40bc7dc8e Fixed a bug where setting compression would crash the server on local routes, fixed some useless never-will-happen eventualities
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-04 17:09:45 +00:00
eebe3763f5 Added support for more advanced proxy configuration
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 19:10:28 +00:00
13d00e8222 Rewrote the entire blob storage service, made auth not use MarshalBinary, which is useless, and made the Start button in captchaDiv the correct colour and same size as it's surrounding elements
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 17:03:35 +00:00
d302745c9a Switch to httpcompression in order to save me some coding time and also fix all of our assorted compression problems
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 15:55:11 +00:00
3114dd556e Ok forget http/3
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 15:33:12 +00:00
dac060cee5 HTTP/3: Attempt 1
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 14:59:05 +00:00
8d27ad6b98 Add a "stealth mode" in case for whatever reason you don't want to show the entire world you're using a super obscure http server
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 14:26:02 +00:00
694f62f238 Added redirects, make one pathBlock able to contain multiple paths
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 13:20:09 +00:00
ac7230c6b5 Fixed not parsing yaml https certificates correctly
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 13:05:44 +00:00
322e901d16 Add globally defined HTTPS certs, fixed an ineffective assignment, isolated registering services into its own function
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 13:03:49 +00:00
975b24a13f This is hell
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 12:21:45 +00:00
144452f798 Fix the directory listing, once and for all
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 12:13:34 +00:00
65de87137d Make it always redirect to dir/ for directories otherwise it breaks HTML relatives
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 12:11:40 +00:00
b0fb00d7fd This directory listing isn't very good, is it...
Signed-off-by: arzumify <jliwin98@danwin1210.de>
2024-11-03 12:09:05 +00:00
13 changed files with 1216 additions and 1492 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,15 +4,11 @@
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.
resourceDirectory: "./resources",
# compression defines the compression settings on a global level - per-route settings override these.
# compression defines the compression settings on a global level - per-route settings override these. It is optional.
compression: {
# algorithm defines the compression algorithm to use, possible values are "gzip", "brotli" and "zstd".
algorithm: "gzip",
@ -34,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
}
}
@ -42,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"]
@ -49,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",
@ -59,10 +78,10 @@ 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/*",
# static defines the static file serving settings for this path. This conflicts with proxy.
# If both proxy and static are defined, static will take precedence.
# 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: {
# root defines the root directory to serve static files from.
root: "./static",
@ -72,19 +91,41 @@ routes: [
}
},
{
# path defines the path to match. They can contain wildcards.
path: "/proxy/*",
# proxy defines the proxy settings for this path. This conflicts with static.
# If both proxy and static are defined, static will take precedence.
# 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
# TODO: In a future update, passing X-Forwarded-For and X-Forwarded-Proto headers will be supported.
# TODO: In a future update, forbidding certain headers from being passed will be supported.
# TODO: In a future update, passing X-Powered-By and Server headers will be supported.
# TODO: In a future update, passing Host header will be supported.
stripPrefix: true,
headers: {
# forbid defines the headers to forbid from being sent to the proxied server.
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: false
}
},
},
{
# 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
}
}
]

34
go.mod
View file

@ -1,33 +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.3
git.ailur.dev/ailur/pow v1.0.2
github.com/andybalholm/brotli v1.1.1
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/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/andybalholm/brotli v1.1.1 // 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
)

85
go.sum
View file

@ -1,60 +1,87 @@
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.3 h1:C0xgfZg7bkULhh9Ci7ZoAcx4QIqxLh+QW9/ng1kKTFU=
git.ailur.dev/ailur/fg-nucleus-library v1.0.3/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=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
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=
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
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.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
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/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
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/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/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.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=

1214
main.go

File diff suppressed because it is too large Load diff

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,109 +205,81 @@ func Main(information library.ServiceInitializationInformation) {
identifier := information.Configuration["identifier"].(string)
adminKey := information.Configuration["adminKey"].(string)
var err error
serviceIDBytes, err = ServiceInformation.ServiceID.MarshalBinary()
// Start the ISM processor
go information.StartISProcessor()
// Initiate a connection to the database
conn, err := information.GetDatabase()
if err != nil {
logFunc(err.Error(), 3, information)
}
// Initiate a connection to the database
// Call service ID 1 to get the database connection information
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), // Service initialization service
MessageType: 1, // Request connection information
SentAt: time.Now(),
Message: nil,
}
// Wait for the response
response := <-information.Inbox
if response.MessageType == 2 {
// This is the connection information
// Set up the database connection
conn = response.Message.(library.Database)
if conn.DBType == library.Sqlite {
// Create the 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")
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
@ -325,15 +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, err := uuid.New().MarshalBinary()
if err != nil {
testAppIsAvailable = false
logFunc(err.Error(), 2, information)
}
_, 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")
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")
}
if err != nil {
testAppIsAvailable = false
@ -407,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)
@ -532,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)
@ -550,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 {
@ -650,14 +587,14 @@ func Main(information library.ServiceInitializationInformation) {
}
// Try to insert the user
userID, err := uuid.New().MarshalBinary()
userID := uuid.New()
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "08"}, information)
logFunc(err.Error(), 2, information)
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)
@ -679,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 {
@ -718,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
@ -745,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)
@ -765,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
@ -1247,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)
@ -1283,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
}
@ -1646,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)
}
}
}
@ -1665,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

@ -114,16 +114,13 @@ input {
border-radius: 0;
padding: 0 10px 0 0;
margin: 0 0 0 10px;
background-color: var(--background);
}
.inputBox .captchaDiv .vAlign {
margin-left: 5px;
}
.inputBox .captchaDiv .vAlign span {
font-size: 14px;
}
.inputBox input {
margin-left: 5px;
margin-right: 0;

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,16 +1,17 @@
package main
import (
library "git.ailur.dev/ailur/fg-library/v3"
nucleusLibrary "git.ailur.dev/ailur/fg-nucleus-library"
"io"
"bytes"
"os"
"database/sql"
"errors"
library "git.ailur.dev/ailur/fg-library/v2"
nucleusLibrary "git.ailur.dev/ailur/fg-nucleus-library"
"path/filepath"
"os"
"time"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
@ -18,442 +19,315 @@ 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 getQuota(user uuid.UUID, information library.ServiceInitializationInformation) (int64, error) {
// Get the user's quota from the database
var quota int64
userBytes, err := user.MarshalBinary()
if err != nil {
return 0, err
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 library.InterServiceMessage, err error, information *library.ServiceInitializationInformation, myFault bool) {
// Respond with an error message
var errCode = library.BadRequest
if myFault {
// Log the error message to the logger service
logFunc(err.Error(), 2, information)
errCode = library.InternalError
}
err = conn.DB.QueryRow("SELECT quota FROM quotas WHERE id = $1", userBytes).Scan(&quota)
message.Respond(errCode, err, information)
}
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)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// The user has no quota set, so we'll set it to the default quota
_, err = conn.DB.Exec("INSERT INTO quotas (id, quota) VALUES ($1, $2)", userBytes, int64(information.Configuration["defaultQuota"].(float64)))
return false
} else {
return false
}
} else {
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, conn library.Database) {
// Add more quota to a 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(message, err, information, true)
}
} else {
_, 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(message, err, information, true)
}
}
// Success
message.Respond(library.Success, nil, information)
}
// And so does addReserved
func addReserved(information *library.ServiceInitializationInformation, message library.InterServiceMessage, conn library.Database) {
// Add more reserved space to a 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(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 {
// 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(message, err, information, true)
}
}
// Success
message.Respond(library.Success, nil, information)
}
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)
if err != nil {
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"].(float64)), nil
return int64(information.Configuration["defaultQuota"].(int)), nil
} else {
return 0, err
}
return 0, err
}
return quota, nil
}
func getUsed(user uuid.UUID, information library.ServiceInitializationInformation) (int64, error) {
// Check the user's used space via the filesystem
var used int64
_, err := os.Stat(filepath.Join(information.Configuration["path"].(string), user.String()))
if err != nil {
if os.IsNotExist(err) {
// The user has no files stored, so we'll set it to 0
return 0, nil
}
return 0, err
} else {
err := filepath.Walk(filepath.Join(information.Configuration["path"].(string), user.String()), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
used += info.Size()
return nil
})
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
}
return used, nil
}
}
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,
}
}
var used int64
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
}
func storeFile(file nucleusLibrary.File, serviceID uuid.UUID, information library.ServiceInitializationInformation) {
// Create a folder for the user if it doesn't exist
err := os.MkdirAll(filepath.Join(information.Configuration["path"].(string), file.User.String()), 0755)
used += entry.Size()
return nil
})
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
return 0, err
}
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
// 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)
if err != nil {
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
}
}
// Check if the user has enough space to store the file
// Get the user's used space
used, err := getUsed(file.User, information)
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
}
}
return used + reserved, nil
}
func modifyFile(information *library.ServiceInitializationInformation, message library.InterServiceMessage, conn library.Database) {
// Check if the file already exists
stats, err := os.Stat(filepath.Join(information.Configuration["path"].(string), file.User.String(), serviceID.String(), file.Name))
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 err == nil {
// The file already exists, subtract the old file size from the user's used space
used -= stats.Size()
// Delete the file
err = os.Remove(path)
if err != nil {
respondError(message, err, information, true)
}
} else if !os.IsNotExist(err) {
respondError(message, err, information, true)
}
// Get the user's quota
quota, err := getQuota(file.User, information)
// Check if the user has enough space
quota, err := getQuota(information, message.Message.(nucleusLibrary.File).User, conn)
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
}
respondError(message, err, information, true)
}
// Check if the user has enough space to store the file
if used+int64(len(file.Bytes)) > quota {
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 3, // It's the user's fault (never say that to the customer ;P)
SentAt: time.Now(),
Message: "User has exceeded their quota",
}
}
// Create a folder within that for the service if it doesn't exist
err = os.MkdirAll(filepath.Join(information.Configuration["path"].(string), file.User.String(), serviceID.String()), 0755)
used, err := getUsed(message.Message.(nucleusLibrary.File).User, information, conn)
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
}
respondError(message, err, information, true)
}
if used+message.Message.(nucleusLibrary.File).Reader.N > quota {
respondError(message, errors.New("insufficient storage"), information, false)
return
}
// Store the file
fileStream, err := os.OpenFile(filepath.Join(information.Configuration["path"].(string), file.User.String(), serviceID.String(), file.Name), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
// Add a file to the user's storage
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
}
respondError(message, err, information, true)
}
// Write the file
_, err = fileStream.Write(file.Bytes)
_, err = io.Copy(file, message.Message.(nucleusLibrary.File).Reader)
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
}
respondError(message, err, information, true)
}
// Close the file
err = fileStream.Close()
err = file.Close()
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
}
respondError(message, err, information, true)
}
// Report success
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 0, // Success
SentAt: time.Now(),
Message: nil,
}
// Success
message.Respond(library.Success, nil, information)
}
func readFile(file nucleusLibrary.File, serviceID uuid.UUID, information library.ServiceInitializationInformation) {
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) {
println("file not found: " + path)
respondError(message, errors.New("file not found"), information, false)
return
}
// Open the file
fileStream, err := os.Open(filepath.Join(information.Configuration["path"].(string), file.User.String(), serviceID.String(), file.Name))
file, err := os.Open(path)
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
}
respondError(message, err, information, true)
}
// Return the reader
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 0, // Success
SentAt: time.Now(),
Message: fileStream,
// Respond with the file
// It's their responsibility to close the 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, conn library.Database) {
// Listen for incoming messages
for {
message := information.AcceptMessage()
switch message.MessageType {
case 1:
// Add quota
addQuota(information, message, conn)
case 2:
// Add reserved
addReserved(information, message, conn)
case 3:
// Modify file
modifyFile(information, message, conn)
case 4:
// Get file
getFile(information, message)
case 5:
deleteFile(information, message)
default:
// Respond with an error message
respondError(message, errors.New("invalid message type"), information, false)
}
}
}
func removeFile(file nucleusLibrary.File, serviceID uuid.UUID, information library.ServiceInitializationInformation) {
// Remove the file
err := os.Remove(filepath.Join(information.Configuration["path"].(string), file.User.String(), serviceID.String(), file.Name))
func Main(information *library.ServiceInitializationInformation) {
// Start up the ISM processor
go information.StartISProcessor()
// Get the database connection
conn, err := information.GetDatabase()
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
}
logFunc(err.Error(), 3, information)
}
// Report success
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: serviceID,
MessageType: 0, // Success
SentAt: time.Now(),
Message: nil,
}
}
func Main(information library.ServiceInitializationInformation) {
go func() {
for {
message := <-information.Inbox
if message.ServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000001") {
if message.MessageType == 1 {
// We've received an error message. This should never happen.
logFunc("Bit flip error: Error given to non-errored service. Move away from radiation or use ECC memory.", 3, information)
}
} else {
switch message.MessageType {
case 0:
// Insert file
validate := validator.New()
err := validate.Struct(message.Message.(nucleusLibrary.File))
if err != nil {
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
MessageType: 2, // An error that's your fault
SentAt: time.Now(),
Message: err.Error(),
}
} else {
// Store file
storeFile(message.Message.(nucleusLibrary.File), message.ServiceID, information)
}
case 1:
// Read file
validate := validator.New()
err := validate.Struct(message.Message.(nucleusLibrary.File))
if err != nil {
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
MessageType: 2, // An error that's your fault
SentAt: time.Now(),
Message: err.Error(),
}
} else {
// Read file
readFile(message.Message.(nucleusLibrary.File), message.ServiceID, information)
}
case 2:
// Remove file
validate := validator.New()
err := validate.Struct(message.Message.(nucleusLibrary.File))
if err != nil {
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
MessageType: 2, // An error that's your fault
SentAt: time.Now(),
Message: err.Error(),
}
} else {
// Remove file
removeFile(message.Message.(nucleusLibrary.File), message.ServiceID, information)
}
case 3:
// Get quota
validate := validator.New()
err := validate.Struct(message.Message.(uuid.UUID))
if err != nil {
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
MessageType: 2, // An error that's your fault
SentAt: time.Now(),
Message: err.Error(),
}
} else {
// Get quota
quota, err := getQuota(message.Message.(uuid.UUID), information)
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
}
}
// Report success
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
MessageType: 0, // Success
SentAt: time.Now(),
Message: quota,
}
}
case 4:
// Get used
validate := validator.New()
err := validate.Struct(message.Message.(uuid.UUID))
if err != nil {
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
MessageType: 2, // An error that's your fault
SentAt: time.Now(),
Message: err.Error(),
}
} else {
// Get used
used, err := getUsed(message.Message.(uuid.UUID), information)
if err != nil {
// First contact the logger service
logFunc(err.Error(), 2, information)
// Then send the error message to the requesting service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
MessageType: 1, // An error that's not your fault
SentAt: time.Now(),
Message: err.Error(),
}
}
// Report success
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
MessageType: 0, // Success
SentAt: time.Now(),
Message: used,
}
}
}
}
}
}()
// Initiate a connection to the database
// Call service ID 1 to get the database connection information
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), // Service initialization service
MessageType: 1, // Request connection information
SentAt: time.Now(),
Message: nil,
}
// Wait for the response
response := <-information.Inbox
if response.MessageType == 2 {
// This is the connection information
// Set up the database connection
conn = response.Message.(library.Database)
// 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)")
if err != nil {
logFunc(err.Error(), 3, information)
}
} else {
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS quotas (id BYTEA PRIMARY KEY, quota 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, conn)
}