Skip to content

Commit

Permalink
Template better and make length/expire configurable (#44)
Browse files Browse the repository at this point in the history
* Templating all html files and adding configuration for max length, max ui length, and max expire
  • Loading branch information
aro5000 authored May 8, 2022
1 parent a6718f7 commit 8dbbd6a
Show file tree
Hide file tree
Showing 17 changed files with 262 additions and 263 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ We understand that secrets are sensitive and people may not want to use a public
The following table shows environment variables that can be used to configure your TMPNOTES installation:
| Env var | Type | Description | Default |
|---------|------|-------------|---------|
| `TMPNOTES_ENABLE_HSTS` | bool | Return `"Strict-Transport-Security", "max-age=15552000"` header to enforce TLS for web browser clients. Only use this if you are sure your instance is running behind a reverse proxy with TLS. | `false` |
| `TMPNOTES_MAX_EXPIRE` | int | The maximum number of hours allowed before a note expires. | `24` |
| `TMPNOTES_MAX_LENGTH` | int | The maximum length (in characters) that a note is allowed to be. This should always be larger than `TMPNOTES_UI_MAX_LENGTH` to give room for the optional encryption padding in the UI. | `1000` |
| `TMPNOTES_UI_MAX_LENGTH` | int | The maximum length (in characters) that a note is allowed to be in the UI. This value should always be less than `TMPNOTES_MAX_LENGTH` to give room for the optional encryption padding in the UI. | `512` |
| `TMPNOTES_PORT` | int | Port number for the application to use. The env var `PORT` can also be used. | `5000` |
| `TMPNOTES_REDIS_URL` | string | Redis URI / connection string. `REDIS_URL` can also be used. | `redis://localhost:6379` |
| `TMPNOTES_ENABLE_HSTS` | bool | Return `"Strict-Transport-Security", "max-age=15552000"` header to enforce TLS for web browser clients. Only use this if you are sure your instance is running behind a reverse proxy with TLS. | `false` |

## docker-compose
We have provided a `docker-compose` file to easily build and host a functional TMPNOTES system.
Expand Down
20 changes: 16 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
package config

import (
"fmt"
"github.com/kelseyhightower/envconfig"
)

type specification struct {
Port int `default:"5000" envconfig:"PORT"`
RedisUrl string `default:"redis://localhost:6379" envconfig:"REDIS_URL"`
EnableHsts bool `split_words:"true"`
Port int `default:"5000" envconfig:"PORT"`
RedisUrl string `default:"redis://localhost:6379" envconfig:"REDIS_URL"`
EnableHsts bool `split_words:"true"`
MaxLength int `default:"1000" split_words:"true"`
UiMaxLength int `default:"512" split_words:"true"`
MaxExpire int `default:"24" split_words:"true"`
}

var Config specification

func GetConfig() error {
return envconfig.Process("tmpnotes", &Config)
err := envconfig.Process("tmpnotes", &Config)
if err != nil {
return err
}

if Config.UiMaxLength > Config.MaxLength {
return fmt.Errorf("UiMaxLength %v should not be greater than MaxLength %v", Config.UiMaxLength, Config.MaxLength)
}
return nil
}
30 changes: 27 additions & 3 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ func Test_GetConfig(t *testing.T) {
{
name: "Test 1",
args: map[string]string{"PORT": "7000", "REDIS_URL": "redis://localhost:1234"},
want: specification{Port: 7000, RedisUrl: "redis://localhost:1234", EnableHsts: false},
want: specification{Port: 7000, RedisUrl: "redis://localhost:1234", EnableHsts: false, MaxLength: 1000, UiMaxLength: 512, MaxExpire: 24},
},
{
name: "Test 2",
args: map[string]string{"TMPNOTES_PORT": "6000", "TMPNOTES_REDIS_URL": "rediss://someserver:1234", "TMPNOTES_ENABLE_HSTS": "true"},
want: specification{Port: 6000, RedisUrl: "rediss://someserver:1234", EnableHsts: true},
args: map[string]string{"TMPNOTES_PORT": "6000", "TMPNOTES_REDIS_URL": "rediss://someserver:1234", "TMPNOTES_ENABLE_HSTS": "true", "TMPNOTES_MAX_LENGTH": "2000", "TMPNOTES_UI_MAX_LENGTH": "600", "TMPNOTES_MAX_EXPIRE": "48"},
want: specification{Port: 6000, RedisUrl: "rediss://someserver:1234", EnableHsts: true, MaxLength: 2000, UiMaxLength: 600, MaxExpire: 48},
},
}
for _, tt := range tests {
Expand All @@ -36,3 +36,27 @@ func Test_GetConfig(t *testing.T) {
})
}
}

func Test_GetConfig_Error(t *testing.T) {
tests := []struct {
name string
args map[string]string
}{
{
name: "Test 1",
args: map[string]string{"TMPNOTES_PORT": "6000", "TMPNOTES_REDIS_URL": "rediss://someserver:1234", "TMPNOTES_ENABLE_HSTS": "true", "TMPNOTES_MAX_LENGTH": "2000", "TMPNOTES_UI_MAX_LENGTH": "6000", "TMPNOTES_MAX_EXPIRE": "48"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for env, val := range tt.args {
os.Setenv(env, val)
defer os.Unsetenv(env)
}
err := GetConfig()
if err.Error() != "UiMaxLength 6000 should not be greater than MaxLength 2000" {
t.Errorf("UiMaxLength %v should cause an error since it is larger than MaxLength %v", Config.UiMaxLength, Config.MaxLength)
}
})
}
}
19 changes: 19 additions & 0 deletions internal/config/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package config

import (
"html/template"
)

var (
Tmpl *template.Template
err error
path = "templates/*"
)

func GetTemplates() error {
Tmpl, err = template.ParseGlob(path)
if err != nil {
return err
}
return nil
}
30 changes: 30 additions & 0 deletions internal/config/template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package config

import "testing"

func TestGetTemplates(t *testing.T) {
tests := []struct {
name string
wantErr bool
path string
}{
{
name: "Test 1",
wantErr: false,
path: "../../templates/*",
},
{
name: "Test 2",
wantErr: true,
path: "../../test_data/bad_templates/*",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path = tt.path
if err := GetTemplates(); (err != nil) != tt.wantErr {
t.Errorf("GetTemplates() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
21 changes: 5 additions & 16 deletions internal/notes/notes.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"strings"
Expand All @@ -19,9 +18,6 @@ import (
"tmpnotes/internal/crypto"
)

const maxLength = 1000
const maxExpire = 24

var (
ctx = context.Background()
rdb *redis.Client
Expand Down Expand Up @@ -64,7 +60,7 @@ func AddNote(w http.ResponseWriter, r *http.Request) {
n.Expire = 1
}

if n.Expire > maxExpire {
if n.Expire > cfg.Config.MaxExpire {
log.Errorf("%s Expiration set too high: %v", r.RequestURI, n.Expire)
http.Error(w, "Invalid Request - TTL is too high", 400)
return
Expand Down Expand Up @@ -120,7 +116,7 @@ func keyBytes(key string) *[32]byte {
}

func checkAcceptableLength(m string) bool {
return len(m) <= maxLength
return len(m) <= cfg.Config.MaxLength
}

// return the type of note from the first 5 characters
Expand Down Expand Up @@ -157,8 +153,7 @@ func GetNote(w http.ResponseWriter, r *http.Request) {
if textResponse(r.UserAgent()) {
fmt.Fprintf(w, "404 - Nothing to see here\n")
} else {
t, _ := template.ParseFiles("./templates/404.html")
t.Execute(w, nil)
cfg.Tmpl.ExecuteTemplate(w, "404.html", nil)
}
return
case err != nil:
Expand All @@ -170,8 +165,7 @@ func GetNote(w http.ResponseWriter, r *http.Request) {
if textResponse(r.UserAgent()) {
fmt.Fprintf(w, "404 - Nothing to see here")
} else {
t, _ := template.ParseFiles("./templates/404.html")
t.Execute(w, nil)
cfg.Tmpl.ExecuteTemplate(w, "404.html", nil)
}
return
}
Expand All @@ -192,12 +186,7 @@ func GetNote(w http.ResponseWriter, r *http.Request) {
return
}

t, err := template.ParseFiles("./templates/note.html")
if err != nil {
log.Errorf("%s Error rendering note: %s", id, err)
http.Error(w, "Error rendering note", 500)
}
t.Execute(w, nil)
cfg.Tmpl.ExecuteTemplate(w, "note.html", nil)
}

// Check headers to see if we should return the data or not.
Expand Down
7 changes: 5 additions & 2 deletions internal/notes/notes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ package notes
import (
"strings"
"testing"

cfg "tmpnotes/internal/config"
)

func Test_checkAcceptableLength(t *testing.T) {
cfg.GetConfig()
tests := []struct {
name string
args string
want bool
}{
{
name: "Test 1",
args: strings.Repeat("a", maxLength+1),
args: strings.Repeat("a", cfg.Config.MaxLength+1),
want: false,
},
{
Expand All @@ -23,7 +26,7 @@ func Test_checkAcceptableLength(t *testing.T) {
},
{
name: "Test 3",
args: strings.Repeat("a", maxLength),
args: strings.Repeat("a", cfg.Config.MaxLength),
want: true,
},
}
Expand Down
26 changes: 25 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,31 @@ import (
"tmpnotes/internal/version"
)

//used for data to template the expiration options available
type homeTemplate struct {
ExpireHours []int
UiMaxLength int
}

var ht homeTemplate

func init() {
log.SetFormatter(&log.JSONFormatter{})
err := cfg.GetConfig()
if err != nil {
log.Fatal(err)
}
err = cfg.GetTemplates()
if err != nil {
log.Fatal(err)
}
notes.RedisInit()

// create slice to template index.html
for i := 1; i <= cfg.Config.MaxExpire; i++ {
ht.ExpireHours = append(ht.ExpireHours, i)
}
ht.UiMaxLength = cfg.Config.UiMaxLength
}

func addStandardHeaders(h http.Header) {
Expand Down Expand Up @@ -59,6 +77,12 @@ func serveStatic(fs http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Info(r.RequestURI)
addStandardHeaders(w.Header())
fs.ServeHTTP(w, r)

// template index.html instead of serving it from the fileserver
if r.RequestURI == "/" || r.RequestURI == "/index.html" {
cfg.Tmpl.ExecuteTemplate(w, "index.html", ht)
} else {
fs.ServeHTTP(w, r)
}
}
}
2 changes: 1 addition & 1 deletion static/createnote.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ document.addEventListener('keydown', countInput);
document.addEventListener('keyup', countInput);
document.getElementById("send-button").addEventListener("click", postInfo)
document.getElementById("copy-button").addEventListener("click", copyLink)
const maxChars = Number(document.getElementById("ui-expire").textContent)

function countInput(e) {
const input = document.getElementById('notes-input');
const maxChars = 512
input.labels[0].innerText = input.textLength + "/" + maxChars
}

Expand Down
Loading

0 comments on commit 8dbbd6a

Please sign in to comment.