aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitchell Riedstra <mitch@riedstra.dev>2023-01-06 00:04:28 -0500
committerMitchell Riedstra <mitch@riedstra.dev>2023-01-06 00:04:28 -0500
commit1d01acca36b78eeba99da1adb10e72d186433b39 (patch)
treed44fd6268ee26af16acfe720abbd2b3a2e6e5574
parentad769c34b2f03bffe2c84a8872331838a80c2870 (diff)
downloadgo-website-1d01acca36b78eeba99da1adb10e72d186433b39.tar.gz
go-website-1d01acca36b78eeba99da1adb10e72d186433b39.tar.xz
Update site to server configuration via environment variables. Add a genhash command. Update docs.
-rw-r--r--cmd/server/genhash.go38
-rw-r--r--cmd/server/main.go114
-rw-r--r--envflag/main.go58
-rw-r--r--go.mod1
-rw-r--r--go.sum4
-rw-r--r--readme.md56
-rw-r--r--rediscache/main.go28
7 files changed, 240 insertions, 59 deletions
diff --git a/cmd/server/genhash.go b/cmd/server/genhash.go
new file mode 100644
index 0000000..47f7bd4
--- /dev/null
+++ b/cmd/server/genhash.go
@@ -0,0 +1,38 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+
+ "golang.org/x/crypto/bcrypt"
+ "golang.org/x/term"
+)
+
+func interactiveHashGen() {
+ fmt.Print("Enter password: ")
+
+ passwd, err := term.ReadPassword(0)
+ if err != nil {
+ logger.Fatal("\nFailed: ", err)
+ }
+
+ fmt.Printf("\nAgain: ")
+
+ passwd2, err := term.ReadPassword(0)
+ if err != nil {
+ logger.Fatal("\nFailed: ", err)
+ }
+
+ fmt.Println("")
+
+ if !bytes.Equal(passwd, passwd2) {
+ logger.Fatal("Passwords do not match")
+ }
+
+ passwd, err = bcrypt.GenerateFromPassword(passwd, bcrypt.DefaultCost)
+ if err != nil {
+ logger.Fatal("Failed: ", err)
+ }
+
+ fmt.Printf("hash: %s\n", string(passwd))
+}
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 29cffbf..f4f8dc3 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -11,83 +11,117 @@ import (
"github.com/gomodule/redigo/redis"
"gopkg.in/yaml.v3"
+ "riedstra.dev/mitch/go-website/envflag"
"riedstra.dev/mitch/go-website/page"
)
var VersionString = ""
+var logger = log.New(os.Stderr, "", 0)
+
func VersionPrint() {
fmt.Println(VersionString)
os.Exit(0)
}
+func logIfErr(err error) {
+ if err != nil {
+ logger.Println(err)
+ }
+}
+
func main() { //nolint:funlen
- fl := flag.NewFlagSet("Website", flag.ExitOnError)
- listen := fl.String("l", "0.0.0.0:8001", "Listening address")
- directory := fl.String("d", ".", "Directory to serve.")
- version := fl.Bool("v", false, "Print the version then exit")
- confFn := fl.String("c", "conf.yml", "Location for the config file")
- authConfFn := fl.String("ac", "auth.json",
- "Location for authorization configuration file")
- verbose := fl.Bool("V", false, "Be more verbose ( dump config, etc ) ")
- fl.StringVar(&page.TimeFormat, "T", page.TimeFormat,
- "Set the page time format, be careful with this")
-
- defaultIndexPath := "/reIndex"
-
- indexPath := fl.String("i", defaultIndexPath,
- "Path in which, when called will rebuild the index and clear the cache")
- redisAddr := fl.String("r", "127.0.0.1:6379", "Redis server set to \"\" to disable")
- redisKey := fl.String("rk", "go-website", "Redis key to use for storing cached pages")
+ var (
+ listen = ":8001"
+ directory = "."
+ version = false
+ confFn = "conf.yml"
+ authConfFn = "auth.json"
+ verbose = false
+ defaultIndexPath = "/reIndex"
+ indexPath = "/reIndex"
+ redisAddr = "127.0.0.1:6379"
+ redisKey = "go-website"
+ pageTimeout = 15
+ genhash = false
+ )
- pageTimeout := fl.Int("timeout", 15, "Seconds until page timeout for read and write")
+ fl := flag.NewFlagSet("Website", flag.ExitOnError)
- fl.BoolVar(&page.CacheIndex, "cache-index", true,
- "If set to false do not cache index")
+ envflag.String(fl, &directory, "d", "SITE_DIR",
+ "Website directory to serve")
+ envflag.String(fl, &listen, "l", "LISTEN_ADDR", "listening address")
+ logIfErr(envflag.Bool(fl, &version, "v", "PRINT_VERSION_AND_EXIT",
+ "print version and exit"))
+ envflag.String(fl, &confFn, "c", "CONFIG_FILE",
+ "Location for configuration file")
+ envflag.String(fl, &authConfFn, "ac", "AUTH_CONFIG",
+ "location for the authorization config")
+ logIfErr(envflag.Bool(fl, &verbose, "V", "VERBOSE",
+ "Be more verbose, dump config and such"))
+ envflag.String(fl, &page.TimeFormat, "T", "TIME_FORMAT",
+ "Override the default format used to parse page time. Be careful.")
+ envflag.String(fl, &indexPath, "i", "INDEX_PATH",
+ "Path in which, when called will rebuild the index and clear the cache")
+ envflag.String(fl, &redisAddr, "r", "REDIS_ADDR",
+ "Redis server set to \"\" to disable")
+ envflag.String(fl, &redisKey, "rk", "REDIS_KEY",
+ "Redis key to use for storing cached pages")
+ logIfErr(envflag.Int(fl, &pageTimeout, "timeout", "HTTP_TIMEOUT",
+ "Seconds until page timeout for read and write"))
+ logIfErr(envflag.Bool(fl, &page.CacheIndex, "cache-index", "CACHE_INDEX",
+ "If set to false, do not cache the page index"))
+ logIfErr(envflag.Bool(fl, &genhash, "genhash", "INTERACTIVE_HASH_GEN",
+ "If set to true, interactively generate a password hash"))
_ = fl.Parse(os.Args[1:])
- if *version {
+ if version {
VersionPrint()
}
- if err := os.Chdir(*directory); err != nil {
- log.Fatal(err)
+ if genhash {
+ interactiveHashGen()
+ os.Exit(0)
+ }
+
+ if err := os.Chdir(directory); err != nil {
+ logger.Fatal(err)
}
- app, err := loadConf(*confFn)
+ app, err := loadConf(confFn)
if err != nil {
- log.Println(err)
+ logger.Println(err)
app = &App{}
}
- err = app.ReadAuth(*authConfFn)
+ err = app.ReadAuth(authConfFn)
if err != nil {
- log.Println(err)
+ logger.Println(err)
}
- if app.ReIndexPath == "" || *indexPath != defaultIndexPath {
- app.ReIndexPath = *indexPath
+ if app.ReIndexPath == "" || indexPath != defaultIndexPath {
+ app.ReIndexPath = indexPath
}
if app.RedisKey == "" {
- app.RedisKey = *redisKey
+ app.RedisKey = redisKey
}
- if *redisAddr != "" {
+ if redisAddr != "" {
app.redisPool = &redis.Pool{
MaxIdle: 80, //nolint:gomnd
MaxActive: 12000, //nolint:gomnd
Dial: func() (redis.Conn, error) {
- c, err := redis.Dial("tcp", *redisAddr)
+ c, err := redis.Dial("tcp", redisAddr)
- if strings.HasPrefix(*redisAddr, "unix:") {
- c, err = redis.Dial("unix", strings.TrimPrefix(*redisAddr, "unix:"))
+ if strings.HasPrefix(redisAddr, "unix:") {
+ c, err = redis.Dial("unix", strings.TrimPrefix(redisAddr, "unix:"))
}
if err != nil {
- log.Println("Redis dial error: ", err)
+ logger.Println("Redis dial error: ", err)
}
return c, err //nolint
@@ -95,7 +129,7 @@ func main() { //nolint:funlen
}
}
- if *verbose {
+ if verbose {
b, _ := yaml.Marshal(app)
os.Stderr.Write(b)
}
@@ -104,9 +138,9 @@ func main() { //nolint:funlen
srv := &http.Server{
Handler: app,
- Addr: *listen,
- WriteTimeout: time.Duration(*pageTimeout) * time.Second,
- ReadTimeout: time.Duration(*pageTimeout) * time.Second,
+ Addr: listen,
+ WriteTimeout: time.Duration(pageTimeout) * time.Second,
+ ReadTimeout: time.Duration(pageTimeout) * time.Second,
}
- log.Fatal(srv.ListenAndServe())
+ logger.Fatal(srv.ListenAndServe())
}
diff --git a/envflag/main.go b/envflag/main.go
new file mode 100644
index 0000000..09c1f97
--- /dev/null
+++ b/envflag/main.go
@@ -0,0 +1,58 @@
+package envflag
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "strconv"
+)
+
+// String is a convent way to set value from environment variable,
+// and allow override when a command line flag is set. It's assumed `p` is
+// not nil.
+func String(fl *flag.FlagSet, p *string, name, envvar, usage string) {
+ if v := os.Getenv(envvar); v != "" {
+ *p = v
+ }
+
+ fl.StringVar(p, name, *p, fmt.Sprintf("%s (Environ: '%s')", usage, envvar))
+}
+
+// Bool is a convent way to set value from environment variable,
+// and allow override when a command line flag is set. It's assumed `p` is
+// not nil.
+func Bool(fl *flag.FlagSet, p *bool, name, envvar, usage string) error {
+ if v := os.Getenv(envvar); v != "" {
+ res, err := strconv.ParseBool(v)
+
+ if err != nil {
+ return fmt.Errorf("Bool: cannot parse '%s=%s', %w",
+ envvar, v, err)
+ }
+
+ *p = res
+ }
+
+ fl.BoolVar(p, name, *p, fmt.Sprintf("%s (Environ: '%s')", usage, envvar))
+
+ return nil
+}
+
+// Int is a convent way to set value from environment variable,
+// and allow override when a command line flag is set. It's assumed `p` is
+// not nil.
+func Int(fl *flag.FlagSet, p *int, name, envvar, usage string) error {
+ if v := os.Getenv(envvar); v != "" {
+ res, err := strconv.ParseInt(v, 10, 32)
+ if err != nil {
+ return fmt.Errorf("Int: cannot parse '%s=%s', %w",
+ envvar, v, err)
+ }
+
+ *p = int(res)
+ }
+
+ fl.IntVar(p, name, *p, fmt.Sprintf("%s (Environ: '%s')", usage, envvar))
+
+ return nil
+}
diff --git a/go.mod b/go.mod
index 9d5dedf..002ea6d 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible
golang.org/x/crypto v0.3.0
+ golang.org/x/term v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
diff --git a/go.sum b/go.sum
index 02968bc..4c01fa2 100644
--- a/go.sum
+++ b/go.sum
@@ -48,9 +48,13 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
diff --git a/readme.md b/readme.md
index 1adea76..5c2c44a 100644
--- a/readme.md
+++ b/readme.md
@@ -7,15 +7,24 @@ Entirely driven by the filesystem there's a special markdown format that
lets you set some variables in yaml at the top and the rest of the page
body in markdown for the rest.
-It's designed to be somewhat easy to use by an intelligent lay person, once the website is setup most pages can be edited from the dashboard once the user is logged in.
+It's designed to be somewhat easy to use by an intelligent lay person, once the
+website is setup most pages can be edited from the dashboard once the user is
+logged in.
-Multiple users can be specified in the `auth.json` file. No `auth.json` file is required, the program will create and save one automatically when it first runs. Just be aware that the username/password defaults to admin admin.
+Multiple users can be specified in the `auth.json` file. No `auth.json` file is
+required, the program will create and save one automatically when it first runs.
+Just be aware that the username/password defaults to admin admin.
-If you set a password clear text in `auth.json` it will be rewritten as the hash alone when the program is run.
+You can generate password hashes with the `-genhash` flag, it will prompt
+you to input your password and it will spit out a hash.
+
+If you set a password clear text in `auth.json` it will be rewritten as the hash
+alone when the program is run. It's not recommended, but offered as a
+convenience.
If you wish to use a file other than `auth.json` you can specify one from the command line flags.
-There's an exmaple website in `example-site`
+There's an example website in `example-site`
Additionally it's designed to handle a large amount of requests by caching the
output of the templates in redis rather than rendering them dynamically on each
@@ -62,7 +71,10 @@ The only runtime requirements for the website are the website directory and the
## Example git hook for automatically updating a production website
-Say you're like me and like editing your website from a command line editor and commiting each change. Well, it's easy enough to deploy your changes to your production website with a simple `git push`, simply clone the repository on your remote server and add this `post-receive` hook:
+Say you're like me and like editing your website from a command line editor and
+committing each change. Well, it's easy enough to deploy your changes to your
+production website with a simple `git push`, simply clone the repository on your
+remote server and add this `post-receive` hook:
```bash
#!/bin/sh
@@ -82,7 +94,9 @@ Then on your local copy:
`$ git remote add production myProductionServer.example.com:/path/to/checkout/on/disk`
-Now, when you run `git push production master` the `myProductionServer.example.com` machine will automatically update the git repository and clear the redis cache, in this case for `example.com`
+Now, when you run `git push production master` the
+`myProductionServer.example.com` machine will automatically update the git
+repository and clear the redis cache, in this case for `example.com`
## Help output from `server`
@@ -93,26 +107,30 @@ The options most people will be interested in are `-l`, `-d`, `-r`, `-c` and `-a
$ ./server -h
Usage of Website:
-T string
- Set the page time format, be careful with this (default "01.02.2006 15:04:05 MST")
- -V Be more verbose ( dump config, etc )
+ Override the default format used to parse page time. Be careful. (Environ: 'TIME_FORMAT') (default "01.02.2006 15:04:05 MST")
+ -V Be more verbose, dump config and such (Environ: 'VERBOSE')
-ac string
- Location for authorization configuration file (default "auth.json")
+ location for the authorization config (Environ: 'AUTH_CONFIG') (default "auth.json")
-c string
- Location for the config file (default "conf.yml")
+ Location for configuration file (Environ: 'CONFIG_FILE') (default "conf.yml")
-cache-index
- If set to false do not cache index (default true)
+ If set to false, do not cache the page index (Environ: 'CACHE_INDEX') (default true)
-d string
- Directory to serve. (default ".")
+ Website directory to serve (Environ: 'SITE_DIR') (default ".")
+ -genhash
+ If set to true, interactively generate a password hash (Environ: 'INTERACTIVE_HASH_GEN')
-i string
- Path in which, when called will rebuild the index and clear the cache (default "/reIndex")
+ Path in which, when called will rebuild the index and clear the cache (Environ: 'INDEX_PATH') (default "/reIndex")
-l string
- Listening address (default "0.0.0.0:8001")
+ listening address (Environ: 'LISTEN_ADDR') (default ":8001")
-r string
- Redis server set to "" to disable (default "127.0.0.1:6379")
+ Redis server set to "" to disable (Environ: 'REDIS_ADDR') (default "127.0.0.1:6379")
-rk string
- Redis key to use for storing cached pages (default "go-website")
+ Redis key to use for storing cached pages (Environ: 'REDIS_KEY') (default "go-website")
-timeout int
- Seconds until page timeout for read and write (default 15)
- -v Print the version then exit
-
+ Seconds until page timeout for read and write (Environ: 'HTTP_TIMEOUT') (default 15)
+ -v print version and exit (Environ: 'PRINT_VERSION_AND_EXIT')
+$
```
+
+Environment variables listed above can be used in lieu of the flags.
diff --git a/rediscache/main.go b/rediscache/main.go
index 04ca622..3cbfdea 100644
--- a/rediscache/main.go
+++ b/rediscache/main.go
@@ -1,3 +1,31 @@
+// Package rediscache is a piece of middleware you can use to drastically
+// improve the performance of any HTTP handler that returns mostly static
+// content.
+//
+// Usage is pretty straightforward:
+//
+// redisAddr = "localhost:6379"
+// rp := &redis.Pool{
+// MaxIdle: 80,
+// MaxActive: 12000,
+// Dial: func() (redis.Conn, error) {
+// c, err := redis.Dial("tcp", redisAddr)
+//
+// if strings.HasPrefix(redisAddr, "unix:") {
+// c, err = redis.Dial("unix", strings.TrimPrefix(redisAddr, "unix:"))
+// }
+//
+// if err != nil {
+// log.Println("Redis dial error: ", err)
+// }
+//
+// return c, err
+// },
+// }
+//
+// handler := rediscache.HandleWIthParams(rp, "some-key", someHandler)
+// // This handler will continue to serve up the cached output until then
+// // key is cleared in redis
package rediscache
import (