Move to git.themmonk.com

This commit is contained in:
the-m-monk
2025-12-29 22:05:31 +10:00
parent b63049e91c
commit f4944ab3b3
24 changed files with 701 additions and 0 deletions

161
internal/config/config.go Normal file
View File

@@ -0,0 +1,161 @@
package config
import (
"fmt"
"os"
"slices"
"strings"
)
type ConfigNode struct {
Name string
Children []ConfigNode
Properties []string
}
var configDirectoryPath string = "./config"
var RootConfigNode ConfigNode
func Init() {
//TODO: use flags module instead
for _, arg := range os.Args[1:] {
if !strings.HasPrefix(arg, "--config-dir=") {
continue
}
dirStrSplit := strings.Split(arg, "=")
configDirectoryPath = dirStrSplit[len(dirStrSplit)-1]
}
fmt.Println("Config directory:", configDirectoryPath)
RootConfigNode = LoadConfig(configDirectoryPath, "/")
PrintConfigTree(RootConfigNode)
}
func LoadConfig(filePath string, name string) ConfigNode {
var retNode ConfigNode
retNode.Name = name
fileStat, err := os.Lstat(filePath)
if err != nil {
fmt.Printf("Config parsing error: %s\n", err.Error())
os.Exit(1)
}
fileMode := fileStat.Mode()
if !fileMode.IsDir() && (filePath == configDirectoryPath) {
fmt.Printf("Config parsing error: %s is not a directory\n", configDirectoryPath)
os.Exit(1)
}
if fileMode&os.ModeSymlink != 0 {
fmt.Println("Config parsing error: symlinks currently not supported in config tree")
os.Exit(1)
}
if fileMode.IsDir() {
retNode.Properties = append(retNode.Properties, "folder=1")
subDirEntries, err := os.ReadDir(filePath)
if err != nil {
fmt.Printf("Config parsing error: %s\n", err.Error())
os.Exit(1)
}
for _, subFile := range subDirEntries {
var childName string
if filePath == configDirectoryPath {
childName = "/" + subFile.Name()
} else {
childName = name + "/" + subFile.Name()
}
retNode.Children = append(retNode.Children, LoadConfig(filePath+"/"+subFile.Name(), childName))
}
return retNode
}
if !fileMode.IsRegular() {
fmt.Printf("Config parsing error: unable to determine filetype of %s\n", filePath)
os.Exit(1)
}
fileBytes, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("Config parsing error: %s\n", err.Error())
}
fileString := string(fileBytes)
retNode.Properties = strings.Split(fileString, "\n")
retNode.Properties = slices.DeleteFunc(retNode.Properties, func(property string) bool {
return strings.HasPrefix(property, "#")
})
return retNode
}
func PrintConfigTree(node ConfigNode) {
fmt.Printf("%s\n", node.Name)
if NodeIsFolder(&node) {
for _, child := range node.Children {
PrintConfigTree(child)
}
return
}
for i, property := range node.Properties {
fmt.Printf("%v: %s\n", i, property)
}
}
func NodeIsFolder(node *ConfigNode) bool {
return slices.Contains(node.Properties, "folder=1")
}
func FindNode(path string, startNode *ConfigNode) *ConfigNode {
if startNode.Name == path {
return startNode
}
if !NodeIsFolder(startNode) {
return nil
}
for _, child := range startNode.Children {
foundNode := FindNode(path, &child)
if foundNode != nil {
return foundNode
}
}
return nil
}
func FetchValue(path string, key string, errorOnFail bool) string {
pathNode := FindNode(path, &RootConfigNode)
if pathNode != nil {
//fmt.Printf("Found node %s @ %p\n", path, pathNode)
} else {
fmt.Printf("Config fetching error: unable to find %s in config directory\n", path)
if errorOnFail {
os.Exit(1)
}
return "ERROR_COULD_NOT_FIND_NODE"
}
for _, property := range pathNode.Properties {
if strings.HasPrefix(property, key+"=") {
val := strings.SplitN(property, "=", 2)[1]
val = strings.TrimSpace(val)
val = strings.Trim(val, `"'`)
return val
}
}
if errorOnFail {
fmt.Printf("Config fetching error: unable to find %s in %s\n", key, configDirectoryPath+path)
os.Exit(1)
}
return "ERROR_NODE_DOES_NOT_CONTAIN_KEY"
}

View File

@@ -0,0 +1,46 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/google/uuid"
)
func RegisterAuthEndpoints() {
fmt.Println("TODO: change access token format from uuid to a more secure format")
http.HandleFunc("/auth/user_and_pass", EndpointAuthUserAndPass)
http.HandleFunc("/auth/check_access_token", EndpointAuthCheckAccessToken)
}
type returnEndpointAuthUserAndPass struct {
AccessToken string
}
func EndpointAuthUserAndPass(w http.ResponseWriter, r *http.Request) {
fmt.Println("STUB WARNING: EndpointAuthUserAndPass, need to check method and implement authentication")
//gopls doesnt like this line unless prefixed with var, may be a skill issue on my end
var ret returnEndpointAuthUserAndPass
ret.AccessToken = uuid.New().String()
retJson, err := json.Marshal(ret)
if err != nil {
fmt.Println("API warning: failed to marshal /auth/user_and_pass response")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
//w.WriteHeader(http.StatusUnauthorized)
w.Header().Set("Content-Type", "application/json")
w.Write(retJson)
}
func EndpointAuthCheckAccessToken(w http.ResponseWriter, r *http.Request) {
fmt.Println("STUB WARNING: EndpointAuthCheckToken, need to check method and implement authentication")
//w.WriteHeader(http.StatusUnauthorized)
}

View File

@@ -0,0 +1,139 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"opal/internal/config"
"os"
"strings"
)
func RegisterLibraryEndpoints() {
ProbeLibraries()
http.HandleFunc("/libraries", EndpointLibraries)
http.HandleFunc("/libraries/", EndpointLibraryList)
}
type Library struct {
Name string `json:"Name"`
PathName string `json:"PathName"`
Path string `json:"-"` //when marshalled, this will be excluded
}
var Libraries []Library
type endpointLibraries struct {
AvailableLibraries []Library
}
type LibraryListEntry struct {
Name string
Type string //file, movie, show, directory, may add more filetypes in future
}
type endpointLibraryList struct {
LibraryInfo Library
Listing []LibraryListEntry
}
func EndpointLibraries(w http.ResponseWriter, r *http.Request) {
var ret endpointLibraries
ret.AvailableLibraries = Libraries
retJson, err := json.Marshal(ret)
if err != nil {
fmt.Println("API warning: failed to marshal /libraries response")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(retJson)
}
// TODO/NON-CRITICAL BUG FIX: web client sends /libraries//{path}/ that then redirects to /libraries/{path}/, should put this comment in the web client code but im too lazy
func EndpointLibraryList(w http.ResponseWriter, r *http.Request) {
url_split := strings.Split(r.URL.Path, "/")
if url_split[0] != "" || url_split[1] != "libraries" { //impossible for this to fail unless there is something really wrong
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if len(url_split) < 4 {
http.Error(w, "Malformed request", http.StatusBadRequest)
return
}
library_string := url_split[2]
path := strings.Join(url_split[3:], "/")
var ret endpointLibraryList
//TODO: use a map instead
for _, lib := range Libraries {
if lib.PathName != library_string {
continue
}
ret.LibraryInfo = lib
}
if ret.LibraryInfo.Name == "" { //not found in Libraries because string un-initialised
http.Error(w, "Malformed request", http.StatusBadRequest) //TODO: library not found (should return 404?)
return
}
//TODO: this is probably a vulnerability
directoryListing, err := os.ReadDir(ret.LibraryInfo.Path + "/" + path)
if err != nil {
fmt.Printf("API warning: potential server error or malicious client: %s\n", err.Error())
http.Error(w, "Malformed request", http.StatusBadRequest)
return
}
for _, directoryEntry := range directoryListing {
var entry LibraryListEntry
fmt.Println(directoryEntry.Name())
if directoryEntry.IsDir() {
entry.Name = directoryEntry.Name()
entry.Type = "directory"
}
ret.Listing = append(ret.Listing, entry)
}
retJson, err := json.Marshal(ret)
if err != nil {
fmt.Printf("API warning: failed to marshal library listing response for %s\n", library_string)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(retJson)
}
func ProbeLibraries() {
libraries_config := config.FindNode("/libraries", &config.RootConfigNode)
if libraries_config == nil {
fmt.Println("Error: unable to find config node for libraries")
os.Exit(1)
}
for _, child := range libraries_config.Children {
var lib Library
lib.Name = config.FetchValue(child.Name, "name", false)
lib.PathName = config.FetchValue(child.Name, "path_name", false)
lib.Path = config.FetchValue(child.Name, "path", false)
if strings.HasPrefix("ERROR", lib.Name) || strings.HasPrefix("ERROR", lib.PathName) {
fmt.Printf("Error: %s: bad config\n", child.Name)
os.Exit(1)
}
Libraries = append(Libraries, lib)
}
}

View File

@@ -0,0 +1,33 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
)
func RegisterMiscEndpoints() {
http.HandleFunc("/opal", EndpointOpal)
}
type endpointOpalResponse struct {
ServerApi string
ApiVersion string
}
// GET /opal
func EndpointOpal(w http.ResponseWriter, r *http.Request) {
var ret endpointOpalResponse
ret.ServerApi = "Opal"
ret.ApiVersion = "0.0.1"
retJson, err := json.Marshal(ret)
if err != nil {
fmt.Println("API warning: failed to marshal /opal response")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(retJson)
}

View File

@@ -0,0 +1,21 @@
package httpserver
import (
"net/http"
"opal/internal/config"
"opal/internal/httpserver/api"
)
func Start() {
Addr := config.FetchValue("/server.cfg", "address", true)
Port := config.FetchValue("/server.cfg", "port", true)
api.RegisterMiscEndpoints()
api.RegisterAuthEndpoints()
api.RegisterLibraryEndpoints()
RegisterWebui()
http.ListenAndServe(Addr+":"+Port, nil)
}

View File

@@ -0,0 +1,18 @@
package httpserver
import (
"net/http"
)
func RegisterWebui() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.Redirect(w, r, "/web/", http.StatusPermanentRedirect)
return
}
http.NotFound(w, r)
})
fs := http.FileServer(http.Dir("web"))
http.Handle("/web/", http.StripPrefix("/web/", fs))
}