476 lines
16 KiB
Go
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: "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: information.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: information.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: information.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: information.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("/authorize", func(w http.ResponseWriter, r *http.Request) {
|
|
renderTemplate(200, w, nil, "authorize.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,
|
|
}
|
|
}
|