package main import ( "bytes" "errors" "github.com/go-chi/chi/v5" "net/url" "os" "time" "crypto/ed25519" "database/sql" "encoding/base64" "encoding/json" "html/template" "io/fs" "net/http" "path/filepath" library "git.ailur.dev/ailur/fg-library/v2" authLibrary "git.ailur.dev/ailur/fg-nucleus-library" "git.oreonproject.org/oreonproject/eternity/common" "git.oreonproject.org/oreonproject/eternity/lib" "github.com/go-git/go-git/v5" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) var ServiceInformation = library.Service{ Name: "eternity", Permissions: library.Permissions{ Authenticate: true, // This service does require authentication 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 Resources: true, // This service does require its HTTP templates and static files }, ServiceID: uuid.MustParse("0f31fa2d-43ca-410f-8148-239b298112b3"), } 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 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 requestedTemplate, err = template.ParseFS(information.ResourceDir, "templates/"+templatePath) if err != nil { logFunc(err.Error(), 2, information) renderString(500, w, "Sorry, something went wrong on our end. Error code: 01. Please report to the administrator.", information) } else { w.WriteHeader(statusCode) err = requestedTemplate.Execute(w, data) if err != nil { logFunc(err.Error(), 2, information) renderString(500, w, "Sorry, something went wrong on our end. Error code: 02. Please report to the administrator.", information) } } } func renderString(statusCode int, w http.ResponseWriter, data string, information library.ServiceInitializationInformation) { w.WriteHeader(statusCode) _, err := w.Write([]byte(data)) if err != nil { logFunc(err.Error(), 2, information) } } func renderJSON(statusCode int, w http.ResponseWriter, data map[string]interface{}, information library.ServiceInitializationInformation) { w.WriteHeader(statusCode) err := json.NewEncoder(w).Encode(data) if err != nil { logFunc(err.Error(), 2, information) } } func verifySecret(secret string, conn library.Database) bool { // Check if the secret is in the secrets table var secretCheck string err := conn.DB.QueryRow("SELECT secret FROM secrets WHERE secret = $1", secret).Scan(&secretCheck) if err != nil || secretCheck != secret { return false } return true } func verifyJwt(token string, publicKey ed25519.PublicKey, conn library.Database) (jwt.MapClaims, bool) { parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { return publicKey, nil }) if err != nil { return nil, false } if !parsedToken.Valid { return nil, false } claims, ok := parsedToken.Claims.(jwt.MapClaims) if !ok { return nil, false } // Check if the token expired date, err := claims.GetExpirationTime() if err != nil || date.Before(time.Now()) || claims["sub"] != nil || claims["isOpenID"] != nil || claims["isOAuth"].(bool) { return claims, false } // Check if the token is in users var idCheck []byte err = conn.DB.QueryRow("SELECT id FROM users WHERE id = $1", claims["sub"]).Scan(&idCheck) if err != nil || claims["sub"] != uuid.Must(uuid.FromBytes(idCheck)).String() { return claims, false } return claims, true } func Main(information library.ServiceInitializationInformation) { var conn library.Database gitDir := information.Configuration["gitDir"].(string) outputDir := information.Configuration["outputDir"].(string) hostName := information.Configuration["hostName"].(string) // Initiate a connection to the database // Call service ID 1 to get the database connection information information.Outbox <- library.InterServiceMessage{ ServiceID: ServiceInformation.ServiceID, ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), // Service initialization service MessageType: 1, // Request connection information SentAt: time.Now(), Message: nil, } // Wait for the response response := <-information.Inbox if response.MessageType == 2 { // This is the connection information // Set up the database connection conn = response.Message.(library.Database) if conn.DBType == library.Sqlite { // Create the packages table _, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS packages (creator BLOB NOT NULL, name TEXT NOT NULL, path TEXT NOT NULL)") if err != nil { logFunc(err.Error(), 3, information) } // Create the secrets table _, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS secrets (secret BLOB NOT NULL)") if err != nil { logFunc(err.Error(), 3, information) } // Create the users table _, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB NOT NULL)") if err != nil { logFunc(err.Error(), 3, information) } } else { // Create the packages table _, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS packages (creator BYTEA NOT NULL, name TEXT NOT NULL, path TEXT NOT NULL)") if err != nil { logFunc(err.Error(), 3, information) } // Create the secrets table _, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS secrets (secret BYTEA NOT NULL)") if err != nil { logFunc(err.Error(), 3, information) } // Create the users table _, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BYTEA NOT NULL)") if err != nil { logFunc(err.Error(), 3, information) } } } else { // This is an error message // Log the error message to the logger service logFunc(response.Message.(error).Error(), 3, information) } // Ask the authentication service for the public key information.Outbox <- library.InterServiceMessage{ ServiceID: ServiceInformation.ServiceID, ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service MessageType: 2, // Request public key SentAt: time.Now(), Message: nil, } var publicKey ed25519.PublicKey = nil // 3 second timeout go func() { time.Sleep(3 * time.Second) if publicKey == nil { logFunc("Timeout while waiting for the public key from the authentication service", 3, information) } }() // Wait for the response response = <-information.Inbox if response.MessageType == 2 { // This is the public key publicKey = response.Message.(ed25519.PublicKey) } else { // This is an error message // Log the error message to the logger service logFunc(response.Message.(error).Error(), 3, information) } // Ask the authentication service to create a new OAuth2 client urlPath, err := url.JoinPath(hostName, "/oauth") if err != nil { logFunc(err.Error(), 3, information) } information.Outbox <- library.InterServiceMessage{ ServiceID: ServiceInformation.ServiceID, ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service MessageType: 1, // Create OAuth2 client SentAt: time.Now(), Message: authLibrary.OAuthInformation{ Name: "Eternity Web", RedirectUri: urlPath, KeyShareUri: "", Scopes: []string{"openid"}, }, } oauthResponse := authLibrary.OAuthResponse{} // 3 second timeout go func() { time.Sleep(3 * time.Second) if oauthResponse == (authLibrary.OAuthResponse{}) { logFunc("Timeout while waiting for the OAuth response from the authentication service", 3, information) } }() // Wait for the response response = <-information.Inbox switch response.MessageType { case 0: // Success, set the OAuth response oauthResponse = response.Message.(authLibrary.OAuthResponse) logFunc("Initialized with App ID: "+oauthResponse.AppID, 0, information) case 1: // An error which is their fault logFunc(response.Message.(error).Error(), 3, information) case 2: // An error which is our fault logFunc(response.Message.(error).Error(), 3, information) default: // An unknown error logFunc("Unknown error", 3, information) } // Set up the router router := chi.NewRouter() // Set up the static routes staticDir, err := fs.Sub(information.ResourceDir, "static") if err != nil { logFunc(err.Error(), 3, information) } else { router.Handle("/static-eternity/*", http.StripPrefix("/static-eternity/", http.FileServerFS(staticDir))) } // Set up the API routes router.Post("/api/packages/list", func(w http.ResponseWriter, r *http.Request) { // Get the list of packages rows, err := conn.DB.Query("SELECT name FROM packages") if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "01"}, information) return } var packages []string for rows.Next() { var name string err = rows.Scan(&name) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "02"}, information) return } packages = append(packages, name) } renderJSON(200, w, map[string]interface{}{ "packages": packages, }, information) }) router.Post("/api/packages/add", func(w http.ResponseWriter, r *http.Request) { type packageData struct { Name string `json:"name"` RepositoryPath string `json:"repositoryPath"` JwtToken string `json:"token"` Secret string `json:"secret"` } var data packageData err := json.NewDecoder(r.Body).Decode(&data) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information) return } var claims jwt.MapClaims if data.Secret != "" { // Verify the secret if !verifySecret(data.Secret, conn) { renderJSON(400, w, map[string]interface{}{"error": "Invalid secret"}, information) return } } else { // Verify the JWT token var ok bool claims, ok = verifyJwt(data.JwtToken, publicKey, conn) if !ok { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } } // Fetch the Git repository _, err = git.PlainClone(filepath.Join(gitDir, data.Name), false, &git.CloneOptions{ Depth: 1, URL: data.RepositoryPath, }) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid repository path"}, information) return } // Add the package to the database var userid []byte if data.Secret != "" { userid, err = uuid.MustParse("00000000-0000-0000-0000-000000000000").MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information) } } else { userid, err = uuid.MustParse(claims["sub"].(string)).MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information) } } _, err = conn.DB.Exec("INSERT INTO packages (creator, name, path) VALUES ($1, $2, $3)", userid, data.Name, data.RepositoryPath) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information) return } renderJSON(200, w, map[string]interface{}{"success": true}, information) }) router.Post("/api/packages/remove", func(w http.ResponseWriter, r *http.Request) { type packageData struct { Name string `json:"name"` JwtToken string `json:"token"` Secret string `json:"secret"` } var data packageData err := json.NewDecoder(r.Body).Decode(&data) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information) return } // Verify the JWT token var claims jwt.MapClaims if data.Secret != "" { // Verify the secret if !verifySecret(data.Secret, conn) { renderJSON(400, w, map[string]interface{}{"error": "Invalid secret"}, information) return } } else { // Verify the JWT token var ok bool claims, ok = verifyJwt(data.JwtToken, publicKey, conn) if !ok { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } } // Remove the package from the database var userid []byte if data.Secret != "" { userid, err = uuid.MustParse("00000000-0000-0000-0000-000000000000").MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information) } } else { userid, err = uuid.MustParse(claims["sub"].(string)).MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information) } } _, err = conn.DB.Exec("DELETE FROM packages WHERE creator = $1 AND name = $2", userid, data.Name) if err != nil { if errors.Is(err, sql.ErrNoRows) { renderJSON(404, w, map[string]interface{}{"error": "Package not found"}, information) return } else { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information) return } } // Remove the package from the filesystem err = os.RemoveAll(filepath.Join(gitDir, data.Name)) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "05"}, information) return } renderJSON(200, w, map[string]interface{}{"success": true}, information) }) router.Post("/api/packages/get", func(w http.ResponseWriter, r *http.Request) { type packageData struct { Name string `json:"name"` JwtToken string `json:"token"` Secret string `json:"secret"` } var data packageData err := json.NewDecoder(r.Body).Decode(&data) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information) return } // Verify the JWT token if data.Secret != "" { // Verify the secret if !verifySecret(data.Secret, conn) { renderJSON(400, w, map[string]interface{}{"error": "Invalid secret"}, information) return } } else { // Verify the JWT token var ok bool _, ok = verifyJwt(data.JwtToken, publicKey, conn) if !ok { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } } var nameCheck string err = conn.DB.QueryRow("SELECT name FROM packages WHERE name = $1", data.Name).Scan(&nameCheck) if err != nil || nameCheck != data.Name { if err != nil && errors.Is(err, sql.ErrNoRows) { renderJSON(404, w, map[string]interface{}{"error": "Package not found"}, information) return } else { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information) return } } // Get the package from the resource directory packageEpk, err := os.ReadFile(filepath.Join(outputDir, "packages", data.Name+".epk")) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "06"}, information) return } w.Header().Set("Content-Type", "application/octet-stream") w.WriteHeader(200) _, err = w.Write(packageEpk) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "07"}, information) logFunc(err.Error(), 2, information) } }) router.Post("/api/packages/compile", func(w http.ResponseWriter, r *http.Request) { type packageData struct { Name string `json:"name"` JwtToken string `json:"token"` Secret string `json:"secret"` PrivateKey string `json:"privateKey"` } var data packageData err := json.NewDecoder(r.Body).Decode(&data) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information) return } // Verify the JWT token if data.Secret != "" { // Verify the secret if !verifySecret(data.Secret, conn) { renderJSON(400, w, map[string]interface{}{"error": "Invalid secret"}, information) return } } else { // Verify the JWT token var ok bool _, ok = verifyJwt(data.JwtToken, publicKey, conn) if !ok { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } } // Fetch the package from the database var nameCheck, path string err = conn.DB.QueryRow("SELECT name, path FROM packages WHERE name = $1", data.Name).Scan(&nameCheck, &path) if err != nil || nameCheck != data.Name { if err != nil && errors.Is(err, sql.ErrNoRows) { renderJSON(404, w, map[string]interface{}{"error": "Package not found"}, information) return } else { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information) return } } // Decode the base64 private key privateKeyBytes, err := base64.StdEncoding.DecodeString(data.PrivateKey) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid private key"}, information) return } // Compile the package // I'm sure you all know how eternity works, so I'm not going to explain this // If you don't, https://git.oreonproject.org/oreonproject/eternity, it's really cool! err = os.Chdir(filepath.Join(gitDir, data.Name)) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "08"}, information) return } var stdout bytes.Buffer logger := &lib.Logger{ LogFunc: func(log lib.Log) string { stdout.Write([]byte(log.Content)) if log.Level == "fatal" { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "09", "stdout": stdout.String()}, information) } return "" }, PromptSupported: false, StdoutSupported: true, Stdout: &stdout, } config, err, vagueErr := lib.ParseConfig("eternity.json", logger) if err != nil || vagueErr != nil { common.EternityJsonHandleError(err, vagueErr, logger) } size, tempDir, err, vagueErr := lib.BuildEPK("./", false, config.Build, logger) if err != nil || vagueErr != nil { common.BuildEPKHandleError(tempDir, err, vagueErr, logger) } config.Metadata.DecompressedSize = size err, vagueErr = lib.PackageEPK(config.Metadata, config.Build, tempDir, filepath.Join(outputDir, "packages", data.Name+".epk"), privateKeyBytes, logger) if err != nil || vagueErr != nil { common.PackageEPKHandleError(err, vagueErr, logger) } common.BuildEPKRemoveTemporaryDirectory(tempDir, logger) renderJSON(200, w, map[string]interface{}{"success": true, "stdout": stdout.String()}, information) }) // Set up the template routes router.Get("/", func(w http.ResponseWriter, r *http.Request) { renderTemplate(200, w, map[string]interface{}{}, "index.html", information) }) router.Get("/packages", func(w http.ResponseWriter, r *http.Request) { renderTemplate(200, w, map[string]interface{}{}, "packages.html", information) }) router.Get("/oauth", func(w http.ResponseWriter, r *http.Request) { renderTemplate(200, w, map[string]interface{}{ "ClientId": oauthResponse.AppID, }, "oauth.html", information) }) // Report a successful activation information.Outbox <- library.InterServiceMessage{ ServiceID: ServiceInformation.ServiceID, ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), // Activation service MessageType: 0, SentAt: time.Now(), Message: router, } }