package main import ( "bytes" "errors" "io" "os" "time" "crypto/ed25519" "database/sql" "encoding/base64" "encoding/json" "html/template" "io/fs" "net/http" "path/filepath" "git.ailur.dev/ailur/fulgens/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" "github.com/google/uuid" ) var ServiceInformation = library.Service{ Name: "Authentication", 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 deferBody(Body io.ReadCloser, information library.ServiceInitializationInformation) { err := Body.Close() if err != nil { logFunc(err.Error(), 1, information) } } 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) w.Header().Set("Content-Type", "text/html") 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) w.Header().Set("Content-Type", "text/plain") _, 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) w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(data) if err != nil { logFunc(err.Error(), 2, information) } } func verifyJwt(token string, publicKey ed25519.PublicKey) (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 if claims.VerifyExpiresAt(time.Now().Unix(), true) == false { return claims, false } return claims, true } func Main(information library.ServiceInitializationInformation) { var conn *sql.DB gitDir := information.Configuration["gitDir"].(string) outputDir := information.Configuration["outputDir"].(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.(*sql.DB) // Create the packages table _, err := conn.Exec("CREATE TABLE IF NOT EXISTS packages (creator BLOB NOT NULL, name BLOB NOT NULL, path STRING 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, } // Wait for the response var publicKey ed25519.PublicKey response = <-information.Inbox if response.MessageType == 2 { // This is the public key publicKey = response.Message.(ed25519.PublicKey) } else { // This is an error message // Log the error message to the logger service logFunc(response.Message.(error).Error(), 3, information) } // Ask the authentication service for its host name information.Outbox <- library.InterServiceMessage{ ServiceID: ServiceInformation.ServiceID, ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service MessageType: 0, // Request host name SentAt: time.Now(), Message: nil, } // Set up the router router := information.Router // Set up the static routes staticDir, err := fs.Sub(information.ResourceDir, "static") if err != nil { logFunc(err.Error(), 3, information) } else { router.Handle("/static/*", http.StripPrefix("/static/", http.FileServerFS(staticDir))) } // Set up the API routes router.Get("/api/packages/list", func(w http.ResponseWriter, r *http.Request) { defer deferBody(r.Body, information) // Get the list of packages rows, err := conn.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) { defer deferBody(r.Body, information) type packageData struct { Name string `json:"name"` RepositoryPath string `json:"repositoryPath"` JwtToken string `json:"token"` } 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 claims, ok := verifyJwt(data.JwtToken, publicKey) 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 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.Exec("INSERT INTO packages (creator, name, path) VALUES (?, ?, ?)", 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.Get("/api/packages/remove", func(w http.ResponseWriter, r *http.Request) { defer deferBody(r.Body, information) type packageData struct { Name string `json:"name"` JwtToken string `json:"token"` } 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 claims, ok := verifyJwt(data.JwtToken, publicKey) if !ok { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } // Remove the package from the database 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.Exec("DELETE FROM packages WHERE creator = ? AND name = ?", 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.Get("/api/packages/get", func(w http.ResponseWriter, r *http.Request) { defer deferBody(r.Body, information) type packageData struct { Name string `json:"name"` JwtToken string `json:"token"` } 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 claims, ok := verifyJwt(data.JwtToken, publicKey) if !ok { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } // Fetch the package from the database userid, err := uuid.MustParse(claims["sub"].(string)).MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information) } var nameCheck string err = conn.QueryRow("SELECT name FROM packages WHERE creator = ? AND name = ?", userid, 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.Get("/api/packages/compile", func(w http.ResponseWriter, r *http.Request) { defer deferBody(r.Body, information) type packageData struct { Name string `json:"name"` JwtToken string `json:"token"` 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 claims, ok := verifyJwt(data.JwtToken, publicKey) if !ok { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } // Fetch the package from the database userid, err := uuid.MustParse(claims["sub"].(string)).MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information) } var nameCheck, path string err = conn.QueryRow("SELECT name, path FROM packages WHERE creator = ? AND name = ?", userid, 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, nil, "index.html", information) }) router.Get("/packages", func(w http.ResponseWriter, r *http.Request) { renderTemplate(200, w, nil, "packages.html", information) }) router.Get("/oauth", func(w http.ResponseWriter, r *http.Request) { renderTemplate(200, w, nil, "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: true, } }