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
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"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() {
 | 
			
		||||
	var err error
 | 
			
		||||
	log.Println("Reading configuration values")
 | 
			
		||||
	config := util.GetConfiguration()
 | 
			
		||||
 | 
			
		||||
	validUsername, success := os.LookupEnv("DOORIS_USERNAME")
 | 
			
		||||
	if !success || validUsername == "" {
 | 
			
		||||
		log.Fatalln("Could not retrieve DOORIS_API_KEY env variable or variable is empty")
 | 
			
		||||
	}
 | 
			
		||||
	log.Println("Reading initial SpaceAPI response from", config.TemplatePath)
 | 
			
		||||
	spaceApiResponse := util.ParseTemplate(config.TemplatePath)
 | 
			
		||||
 | 
			
		||||
	validPassword, success := os.LookupEnv("DOORIS_PASSWORD")
 | 
			
		||||
	if !success || validPassword == "" {
 | 
			
		||||
		log.Fatalln("Could not retrieve DOORIS_API_KEY env variable or variable is empty")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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")
 | 
			
		||||
	})
 | 
			
		||||
	// Register HTTP handlers
 | 
			
		||||
	http.HandleFunc("/", handlers.Root(&spaceApiResponse))
 | 
			
		||||
	http.HandleFunc("/state/open", handlers.StateOpen(config.BAUsername, config.BAPassword, &spaceApiResponse))
 | 
			
		||||
 | 
			
		||||
	log.Println("Starting HTTP server...")
 | 
			
		||||
	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