github.com/pbberlin/go-pwa@v0.0.0-20220328105622-7c26e0ca1ab8/pkg/cfg/config.go (about)

     1  // package cfg load app scope JSON settings
     2  // its init() func needs to be executed first
     3  package cfg
     4  
     5  import (
     6  	"encoding/json"
     7  	"fmt"
     8  	"html/template"
     9  	"io"
    10  	"log"
    11  	"math"
    12  	"net/http"
    13  	"net/http/httptest"
    14  	"os"
    15  	"strings"
    16  	"time"
    17  )
    18  
    19  // Headless executes a HTTP handle func
    20  // and writes it output into the log
    21  func Headless(fnc func(w http.ResponseWriter, r *http.Request), pth string) {
    22  	log.SetFlags(log.Lshortfile | log.Llongfile)
    23  	w := httptest.NewRecorder()
    24  	r := httptest.NewRequest("GET", pth, nil)
    25  	fnc(w, r)
    26  
    27  	res := w.Result()
    28  	defer res.Body.Close()
    29  	bts, err := io.ReadAll(res.Body)
    30  	if err != nil {
    31  		log.Printf("error reading response of %v: %v", pth, err)
    32  	}
    33  	log.Print(string(bts))
    34  }
    35  
    36  func init() {
    37  	// RunHandleFunc(Load, "/config/load")
    38  }
    39  
    40  //
    41  // cfg contain app scope data
    42  type cfg struct {
    43  	Title       string `json:"title,omitempty"`       // app title - into html title tag
    44  	TitleShort  string `json:"title_short,omitempty"` // => manifest JSON
    45  	Description string `json:"description,omitempty"` // default meta tag description
    46  
    47  	ModeHTTPS string `json:"mode_https,omitempty"`
    48  
    49  	Domains []string `json:"domains,omitempty"`
    50  
    51  	AllowHTTP        bool `json:"allow_http,omitempty"`         // or forward to HTTPS, only implemented for ModeHTTPS==letsenrypt-extended
    52  	AutoRedirectHTTP bool `json:"auto_redirect_http,omitempty"` // when HTTP is not allowed: Redirect automatically redirect to HTTPS via web browser
    53  
    54  	// serve precompressed .gzip files
    55  	//   default is dynamic compression via gziphandler
    56  	StaticFilesGZIP bool `json:"static_files_gzip,omitempty"`
    57  
    58  	//
    59  	// dynamically set on app init
    60  	TplMain *template.Template `json:"-"` // loaded from scaffold.tpl.html
    61  
    62  	CSP string `json:"-"` // content security policy
    63  	JS  string `json:"-"` // created from ./app-bucket/js
    64  	CSS string `json:"-"` // created from ./app-bucket/css
    65  
    66  	Dms string `json:"-"` // string representation of domains
    67  
    68  	AppDir        string `json:"-"`              // server side app dir; do we still need this - or is ./... always sufficient?
    69  	TS            string `json:"-"`              // prefix 'vs-'  and then timestamp of app start
    70  	SchemaVersion int    `json:"schema_version"` // client database schema version
    71  
    72  	PrefURI string `json:"pref_uri,omitempty"`
    73  }
    74  
    75  var defaultCfg = &cfg{
    76  	Title:       "your app title",
    77  	TitleShort:  "app123",
    78  	Description: "default meta tag description",
    79  
    80  	ModeHTTPS: "https-localhost-cert",
    81  	// ModeHTTPS: "letsenrypt-simple",
    82  	// ModeHTTPS: "letsenrypt-extended",
    83  
    84  	Domains: []string{"fmt.zew.de", "fmt.zew.de"},
    85  
    86  	//
    87  	AllowHTTP:        true,
    88  	AutoRedirectHTTP: true,
    89  
    90  	StaticFilesGZIP: true,
    91  
    92  	SchemaVersion: 2,
    93  }
    94  
    95  func (cfg *cfg) cSP() string {
    96  
    97  	/*
    98  		https://csp.withgoogle.com/docs/index.html
    99  		https://csp.withgoogle.com/docs/strict-csp.html#example
   100  
   101  		default-src     -  https: http:
   102  		script-src      - 'nonce-{random}'  'unsafe-inline'   - for old browsers
   103  		style-src-elem  - 'self' 'nonce-{random}'             - strangely, self is required
   104  		strict-dynamic  -  unused; 'script-xyz.js'can load additional scripts via script elements
   105  		object-src      -  sources for <object>, <embed>, <applet>
   106  		base-uri        - 'self' 'none' - for relative URLs
   107  		report-uri      -  https://your-report-collector.example.com/
   108  
   109  	*/
   110  	csp := ""
   111  	csp += fmt.Sprintf("default-src     https:; ")
   112  	csp += fmt.Sprintf("base-uri       'none'; ")
   113  	csp += fmt.Sprintf("object-src     'none'; ")
   114  	csp += fmt.Sprintf("script-src     'nonce-%s' 'unsafe-inline'; ", cfg.TS)
   115  	csp += fmt.Sprintf("style-src-elem 'nonce-%s' 'self'; ", cfg.TS)
   116  	csp += fmt.Sprintf("worker-src      https://*/service-worker.js; ")
   117  
   118  	return csp
   119  }
   120  
   121  func Load(w http.ResponseWriter, r *http.Request) {
   122  
   123  	w.Header().Set("Content-Type", "text/plain")
   124  	fmt.Fprint(w, "config load start\n")
   125  
   126  	// write example with defaults
   127  	{
   128  		pth := "./app-bucket/server-config/tmp-example-config.json"
   129  		bts, err := json.MarshalIndent(defaultCfg, "", "\t")
   130  		if err != nil {
   131  			fmt.Fprintf(w, "error marshalling defaultCfg %v \n", err)
   132  		}
   133  
   134  		err = os.WriteFile(pth, bts, 0777)
   135  		if err != nil {
   136  			fmt.Fprintf(w, "error saving %v, %v \n", pth, err)
   137  		}
   138  	}
   139  
   140  	//
   141  	tmpCfg := cfg{}
   142  	pth := "./app-bucket/server-config/config.json"
   143  	bts, err := os.ReadFile(pth)
   144  	if err != nil {
   145  		fmt.Fprintf(w, "error opening %v, %v \n", pth, err)
   146  		panic(fmt.Sprintf("need %v", pth))
   147  	} else {
   148  		err := json.Unmarshal(bts, &tmpCfg)
   149  		if err != nil {
   150  			fmt.Fprintf(w, "error unmarshalling %v, %v \n", pth, err)
   151  			panic(fmt.Sprintf("need valid %v", pth))
   152  		} else {
   153  			fmt.Fprintf(w, "config loaded from %v \n", pth)
   154  		}
   155  	}
   156  
   157  	//
   158  	//
   159  	// other dynamic stuff
   160  	tmpCfg.Dms = strings.Join(tmpCfg.Domains, ", ")
   161  
   162  	tmpCfg.AppDir, err = os.Getwd()
   163  	if err != nil {
   164  		log.Fatalf("error os.Getwd() %v \n", err)
   165  	}
   166  
   167  	// time stamp
   168  	// keeping only the last 6 digits
   169  	now := time.Now().UTC().Unix()
   170  	fst4 := float64(now) / float64(1000*1000) // first four digits
   171  	fst4 = math.Floor(fst4) * float64(1000*1000)
   172  	now -= int64(fst4)
   173  
   174  	tmpCfg.TS = fmt.Sprintf("vs-%d", now)
   175  
   176  	tmpCfg.CSP = tmpCfg.cSP()
   177  
   178  	//
   179  	{
   180  		pth := "./app-bucket/tpl/scaffold.tpl.html"
   181  		var err error
   182  		tmpCfg.TplMain, err = template.ParseFiles(pth)
   183  		if err != nil {
   184  			log.Printf("error parsing  %v, %v", pth, err)
   185  			return
   186  		}
   187  	}
   188  
   189  	defaultCfg = &tmpCfg // replace pointer at once - should be threadsafe
   190  
   191  	fmt.Fprint(w, "config load stop\n\n")
   192  
   193  }
   194  
   195  func Get() *cfg {
   196  	// this might occur during testing
   197  	// if defaultCfg == nil {
   198  	// 	log.Fatalf("config accessed before it has been loaded")
   199  	// }
   200  	return defaultCfg
   201  }
   202  
   203  func Set() *cfg {
   204  	return defaultCfg
   205  }