all repos — todo.txt-web @ a795cba0c655a10601ab9ab5a44bb76e0b82108a

Minimalist Web interface for todo.txt file management

✨ Use TLS client auth
Tim Izzo tim@5ika.ch
Sun, 20 Jul 2025 15:05:36 +0200
commit

a795cba0c655a10601ab9ab5a44bb76e0b82108a

parent

6eab03481330f6359fc318041b6c3a9468e3d8ca

4 files changed, 82 insertions(+), 8 deletions(-)

jump to
A .gitignore

@@ -0,0 +1,2 @@

+config.yaml +*.txt
A config.example.yaml

@@ -0,0 +1,11 @@

+ +# For server HTTPS configuration +server: + certFilePath: certs/cert.pem + keyFilePath: certs/key.pem + +# For client HTTPS authorization +client: + cACertFilePath: ca/root-cert.pem + +todoPath: todo.txt
A config.go

@@ -0,0 +1,55 @@

+package main + +import ( + "crypto/tls" + "crypto/x509" + "os" + + "github.com/charmbracelet/log" + "gopkg.in/yaml.v3" +) + +type Config struct { + Server struct { + CertFilePath string `yaml:"certFilePath"` + KeyFilePath string `yaml:"keyFilePath"` + } `yaml:"server"` + Client struct { + CACertFilePath string `yaml:"cACertFilePath"` + } `yaml:"client"` + + TodoPath string `yaml:"todoPath"` +} + +func getConfig() Config { + configFile, err := os.ReadFile("./config.yaml") + if err != nil { + log.Fatalf("Can't read config file: %s", err) + } + + var cfg Config + yaml.Unmarshal(configFile, &cfg) + return cfg +} + +func getTLSConfig(config Config) *tls.Config { + + serverTLSCert, err := tls.LoadX509KeyPair(config.Server.CertFilePath, config.Server.KeyFilePath) + if err != nil { + log.Fatalf("error opening certificate and key file for control connection. Error %v", err) + return nil + } + + certPool := x509.NewCertPool() + if caCertPEM, err := os.ReadFile(config.Client.CACertFilePath); err != nil { + panic(err) + } else if ok := certPool.AppendCertsFromPEM(caCertPEM); !ok { + panic("invalid cert in CA PEM") + } + + return &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: certPool, + Certificates: []tls.Certificate{serverTLSCert}, + } +}
M main.gomain.go

@@ -10,22 +10,28 @@

todo "github.com/1set/todotxt" ) -var todoFilePath string = "todo.txt" - func main() { - gin.SetMode(gin.DebugMode) router := gin.Default() + config := getConfig() + router.LoadHTMLGlob("templates/*.html") router.Static("/static", "static/") router.GET("/", loadTasksList) router.POST("/", loadTasksList) - router.Run() + server := http.Server{ + Addr: ":4443", + Handler: router, + TLSConfig: getTLSConfig(config), + } + defer server.Close() + log.Fatal(server.ListenAndServeTLS("", "")) } func loadTasksList(c *gin.Context) { - taskList := getTaskList() + config := getConfig() // TODO Fix: YAML config file is read for each request + taskList := getTaskList(config.TodoPath) id := c.PostForm("taskId") content := c.PostForm("taskContent")

@@ -47,11 +53,11 @@ } else {

updatedTask.Reopen() } } - taskList.WriteToPath(todoFilePath) + taskList.WriteToPath(config.TodoPath) } else if content != "" { newTask, _ := todo.ParseTask(content) taskList.AddTask(newTask) - taskList.WriteToPath(todoFilePath) + taskList.WriteToPath(config.TodoPath) } c.HTML(http.StatusOK, "tasks-list.html", gin.H{

@@ -59,7 +65,7 @@ "tasks": taskList,

}) } -func getTaskList() todo.TaskList { +func getTaskList(todoFilePath string) todo.TaskList { if tasklist, err := todo.LoadFromPath(todoFilePath); err != nil { log.Fatalf("Can't load file %s", todoFilePath) return nil