eternity-web/main.go
2024-10-03 17:10:18 +01:00

476 lines
16 KiB
Go

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: "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 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,
}
}