Major Refactoring
- Move components to separate packages - Fix HTTP header for 401 response - Add documentation
This commit is contained in:
		
					parent
					
						
							
								ec8e279b7a
							
						
					
				
			
			
				commit
				
					
						4b41acfa7b
					
				
			
		
					 5 changed files with 191 additions and 103 deletions
				
			
		
							
								
								
									
										11
									
								
								config/config.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								config/config.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					package config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Configuration represents the settings needed to configure spaceapid
 | 
				
			||||||
 | 
					type Configuration struct {
 | 
				
			||||||
 | 
						// The HTTP BasicAuth username for door status updates
 | 
				
			||||||
 | 
						BAUsername string
 | 
				
			||||||
 | 
						// The HTTP BasicAuth password for door status updates
 | 
				
			||||||
 | 
						BAPassword string
 | 
				
			||||||
 | 
						// The path to the JSON with initial values
 | 
				
			||||||
 | 
						TemplatePath string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										87
									
								
								handlers/handlers.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								handlers/handlers.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,87 @@
 | 
				
			||||||
 | 
					package handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gitlab.hamburg.ccc.de/ccchh/spaceapid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Root(resp *types.SpaceAPIResponseV14) func(http.ResponseWriter, *http.Request) {
 | 
				
			||||||
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							// Check if GET method
 | 
				
			||||||
 | 
							if r.Method != http.MethodGet {
 | 
				
			||||||
 | 
								log.Println("Wrong METHOD from", r.RemoteAddr)
 | 
				
			||||||
 | 
								w.Header().Set("Allow", http.MethodGet)
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusMethodNotAllowed)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Serialize response
 | 
				
			||||||
 | 
							response, err := json.Marshal(resp)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println("Failed to serialize JSON response:", err)
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Respond with OK
 | 
				
			||||||
 | 
							w.Header().Set("Access-Control-Allow-Origin", "*")
 | 
				
			||||||
 | 
							w.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusOK)
 | 
				
			||||||
 | 
							_, _ = w.Write(response)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func StateOpen(
 | 
				
			||||||
 | 
						validUsername, validPassword string, resp *types.SpaceAPIResponseV14,
 | 
				
			||||||
 | 
					) func(http.ResponseWriter, *http.Request) {
 | 
				
			||||||
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							// Check BasicAuth credentials
 | 
				
			||||||
 | 
							username, password, ok := r.BasicAuth()
 | 
				
			||||||
 | 
							if !ok || username != validUsername || password != validPassword {
 | 
				
			||||||
 | 
								log.Println("Unauthorized request from", r.RemoteAddr)
 | 
				
			||||||
 | 
								w.Header().Set("WWW-Authenticate", "Basic realm=\"space-api\"")
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusUnauthorized)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Check if PUT method
 | 
				
			||||||
 | 
							if r.Method != http.MethodPut {
 | 
				
			||||||
 | 
								log.Println("Wrong METHOD from", r.RemoteAddr)
 | 
				
			||||||
 | 
								w.Header().Set("Allow", http.MethodPut)
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusMethodNotAllowed)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Read request body
 | 
				
			||||||
 | 
							body, err := io.ReadAll(r.Body)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println("Failed to read request body from", r.RemoteAddr)
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								_, _ = io.WriteString(w, "Failed reading HTTP request body")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Parse request body
 | 
				
			||||||
 | 
							newState, err := strconv.ParseBool(string(body))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println("Failed to parse request body from", r.RemoteAddr)
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
								_, _ = io.WriteString(w, "HTTP request body should either be true or false")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Set SpaceAPI response values
 | 
				
			||||||
 | 
							resp.State.Open = newState
 | 
				
			||||||
 | 
							resp.State.LastChange = time.Now().Unix()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Respond with OK
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusOK)
 | 
				
			||||||
 | 
							_, _ = io.WriteString(w, "Update Successful")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										112
									
								
								main.go
									
										
									
									
									
								
							
							
						
						
									
										112
									
								
								main.go
									
										
									
									
									
								
							| 
						 | 
					@ -1,117 +1,23 @@
 | 
				
			||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"path/filepath"
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"gitlab.hamburg.ccc.de/ccchh/spaceapid/types"
 | 
						"gitlab.hamburg.ccc.de/ccchh/spaceapid/handlers"
 | 
				
			||||||
 | 
						"gitlab.hamburg.ccc.de/ccchh/spaceapid/util"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	var err error
 | 
						log.Println("Reading configuration values")
 | 
				
			||||||
 | 
						config := util.GetConfiguration()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	validUsername, success := os.LookupEnv("DOORIS_USERNAME")
 | 
						log.Println("Reading initial SpaceAPI response from", config.TemplatePath)
 | 
				
			||||||
	if !success || validUsername == "" {
 | 
						spaceApiResponse := util.ParseTemplate(config.TemplatePath)
 | 
				
			||||||
		log.Fatalln("Could not retrieve DOORIS_API_KEY env variable or variable is empty")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	validPassword, success := os.LookupEnv("DOORIS_PASSWORD")
 | 
						// Register HTTP handlers
 | 
				
			||||||
	if !success || validPassword == "" {
 | 
						http.HandleFunc("/", handlers.Root(&spaceApiResponse))
 | 
				
			||||||
		log.Fatalln("Could not retrieve DOORIS_API_KEY env variable or variable is empty")
 | 
						http.HandleFunc("/state/open", handlers.StateOpen(config.BAUsername, config.BAPassword, &spaceApiResponse))
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	templatePath, success := os.LookupEnv("SPACE_API_JSON_TEMPLATE_PATH")
 | 
					 | 
				
			||||||
	if !success || templatePath == "" {
 | 
					 | 
				
			||||||
		log.Fatalln("Could not retrieve SPACE_API_JSON_TEMPLATE_PATH env variable or variable is empty")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	templatePathAbs, err := filepath.Abs(templatePath)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Fatalln("Failed converting", templatePath, "to absolute path:", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	log.Println("Reading initial SpaceAPI response from", templatePathAbs)
 | 
					 | 
				
			||||||
	initialJson, err := os.ReadFile(templatePathAbs)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Fatalln("Failed reading file:", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	spaceApiResponse := new(types.SpaceAPIResponseV14)
 | 
					 | 
				
			||||||
	err = json.Unmarshal(initialJson, spaceApiResponse)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Fatalln("Could not parse SpaceAPI response template:", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !slices.Contains(spaceApiResponse.APICompatibility, "14") {
 | 
					 | 
				
			||||||
		log.Fatalln("Provided SpaceAPI response doesn't specify compatibility with API version 14")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 | 
					 | 
				
			||||||
		if r.Method != http.MethodGet {
 | 
					 | 
				
			||||||
			log.Println("Wrong METHOD from", r.RemoteAddr)
 | 
					 | 
				
			||||||
			w.Header().Set("allow", http.MethodGet)
 | 
					 | 
				
			||||||
			w.WriteHeader(http.StatusMethodNotAllowed)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		response, err := json.Marshal(spaceApiResponse)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Println("Failed to serialize JSON response:", err)
 | 
					 | 
				
			||||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		w.Header().Set("Access-Control-Allow-Origin", "*")
 | 
					 | 
				
			||||||
		w.Header().Set("Content-Type", "application/json")
 | 
					 | 
				
			||||||
		w.WriteHeader(http.StatusOK)
 | 
					 | 
				
			||||||
		_, _ = w.Write(response)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	http.HandleFunc("/state/open", func(w http.ResponseWriter, r *http.Request) {
 | 
					 | 
				
			||||||
		username, password, ok := r.BasicAuth()
 | 
					 | 
				
			||||||
		if !ok || username != validUsername || password != validPassword {
 | 
					 | 
				
			||||||
			log.Println("Unauthorized request from", r.RemoteAddr)
 | 
					 | 
				
			||||||
			w.Header().Set("www-authentication", "Basic realm=\"space-api\"")
 | 
					 | 
				
			||||||
			w.WriteHeader(http.StatusUnauthorized)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if r.Method != http.MethodPut {
 | 
					 | 
				
			||||||
			log.Println("Wrong METHOD from", r.RemoteAddr)
 | 
					 | 
				
			||||||
			w.Header().Set("allow", http.MethodPut)
 | 
					 | 
				
			||||||
			w.WriteHeader(http.StatusMethodNotAllowed)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		body, err := io.ReadAll(r.Body)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Println("Failed to read request body from", r.RemoteAddr)
 | 
					 | 
				
			||||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
					 | 
				
			||||||
			_, _ = io.WriteString(w, "Failed reading HTTP request body")
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		newState, err := strconv.ParseBool(string(body))
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Println("Failed to parse request body from", r.RemoteAddr)
 | 
					 | 
				
			||||||
			w.WriteHeader(http.StatusBadRequest)
 | 
					 | 
				
			||||||
			_, _ = io.WriteString(w, "HTTP request body should either be true or false")
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		spaceApiResponse.State.Open = newState
 | 
					 | 
				
			||||||
		spaceApiResponse.State.LastChange = time.Now().Unix()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		w.WriteHeader(http.StatusOK)
 | 
					 | 
				
			||||||
		_, _ = io.WriteString(w, "Update Successful")
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Println("Starting HTTP server...")
 | 
						log.Println("Starting HTTP server...")
 | 
				
			||||||
	log.Fatalln(http.ListenAndServe(":8080", nil))
 | 
						log.Fatalln(http.ListenAndServe(":8080", nil))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										48
									
								
								util/config.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								util/config.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,48 @@
 | 
				
			||||||
 | 
					package util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gitlab.hamburg.ccc.de/ccchh/spaceapid/config"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						envBAUsername       = "BA_USERNAME"
 | 
				
			||||||
 | 
						envBAPassword       = "BA_PASSWORD"
 | 
				
			||||||
 | 
						envJSONTemplatePath = "JSON_TEMPLATE_PATH"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetConfiguration gets the spaceapid configuration from the respective environment variables
 | 
				
			||||||
 | 
					func GetConfiguration() (c config.Configuration) {
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							success bool
 | 
				
			||||||
 | 
							err     error
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// HTTP BasicAuth username
 | 
				
			||||||
 | 
						c.BAUsername, success = os.LookupEnv(envBAUsername)
 | 
				
			||||||
 | 
						if !success || c.BAUsername == "" {
 | 
				
			||||||
 | 
							log.Fatalln("Could not retrieve env variable", envBAUsername, "or variable is empty")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// HTTP BasicAuth password
 | 
				
			||||||
 | 
						c.BAPassword, success = os.LookupEnv(envBAPassword)
 | 
				
			||||||
 | 
						if !success || c.BAPassword == "" {
 | 
				
			||||||
 | 
							log.Fatalln("Could not retrieve", envBAPassword, "env variable or variable is empty")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// JSON template path
 | 
				
			||||||
 | 
						templatePath, success := os.LookupEnv(envJSONTemplatePath)
 | 
				
			||||||
 | 
						if !success || templatePath == "" {
 | 
				
			||||||
 | 
							log.Fatalln("Could not retrieve", envJSONTemplatePath, "env variable or variable is empty")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Save as absolute path
 | 
				
			||||||
 | 
						c.TemplatePath, err = filepath.Abs(templatePath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalln("Failed converting", templatePath, "to absolute path:", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								util/util.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								util/util.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					package util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gitlab.hamburg.ccc.de/ccchh/spaceapid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ParseTemplate parses the given file and
 | 
				
			||||||
 | 
					func ParseTemplate(file string) (resp types.SpaceAPIResponseV14) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Read template file
 | 
				
			||||||
 | 
						template, err := os.ReadFile(file)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalln("Failed reading file:", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Parse JSON
 | 
				
			||||||
 | 
						dec := json.NewDecoder(bytes.NewReader(template))
 | 
				
			||||||
 | 
						dec.DisallowUnknownFields()
 | 
				
			||||||
 | 
						err = dec.Decode(&resp)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalln("Could not parse SpaceAPI response template:", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if compatible with v14
 | 
				
			||||||
 | 
						if !slices.Contains(resp.APICompatibility, "14") {
 | 
				
			||||||
 | 
							log.Fatalln("Provided template doesn't specify compatibility with API version 14")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue