Skip to content

Commit

Permalink
Merge branch 'feature/sanitize'
Browse files Browse the repository at this point in the history
- Closes #5
- Closes #70
  • Loading branch information
jhillyerd committed Feb 28, 2018
2 parents dedd0ea + 3c19e08 commit d5aea4d
Show file tree
Hide file tree
Showing 16 changed files with 892 additions and 27 deletions.
6 changes: 2 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ env:
- DEPLOY_WITH_MAJOR="1.9"

before_script:
- go vet ./...
- go get github.com/golang/lint/golint

go:
- 1.8.x
- 1.9.x

script: go test -race -v ./...
- "1.10"

deploy:
provider: script
Expand Down
30 changes: 16 additions & 14 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

[1.2.0-rc2] - 2017-12-15
------------------------
## [Unreleased]

### Added
- Button to purge mailbox contents from the UI.
- Simple HTML/CSS sanitization; `Safe HTML` and `Plain Text` UI tabs.

### Changed
- Reverse message display sort order in the UI; now newest first.

## [1.2.0-rc2] - 2017-12-15

### Added
- `rest/client` types `MessageHeader` and `Message` with convenience methods;
Expand All @@ -20,8 +28,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
types
- Fixed panic when `monitor.history` set to 0

[1.2.0-rc1] - 2017-01-29
------------------------
## [1.2.0-rc1] - 2017-01-29

### Added
- Storage of `To:` header in messages (likely breaks existing datastores)
Expand All @@ -47,17 +54,15 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Allow increased local-part length of 128 chars for Mailgun
- RedHat and Ubuntu now use systemd instead of legacy init systems

[1.1.0] - 2016-09-03
--------------------
## [1.1.0] - 2016-09-03

### Added
- Homebrew inbucket.conf and formula (see README)

### Fixed
- Log and continue when unable to delete oldest message during cap enforcement

[1.1.0-rc2] - 2016-03-06
------------------------
## [1.1.0-rc2] - 2016-03-06

### Added
- Message Cap to status page
Expand All @@ -67,8 +72,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Shutdown hang in retention scanner
- Display empty subject as `(No Subject)`

[1.1.0-rc1] - 2016-03-04
------------------------
## [1.1.0-rc1] - 2016-03-04

### Added
- Inbucket now builds with Go 1.5 or 1.6
Expand All @@ -82,8 +86,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- RESTful API moved to `/api/v1` base URI
- More graceful shutdown on Ctrl-C or when errors encountered

[1.0] - 2014-04-14
------------------
## [1.0] - 2014-04-14

### Added
- Add new configuration option `mailbox.message.cap` to prevent individual
Expand All @@ -100,8 +103,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
[1.0]: https://github.com/jhillyerd/inbucket/compare/1.0-rc1...1.0


Release Checklist
-----------------
## Release Checklist

1. Create release branch: `git flow release start 1.x.0`
2. Update CHANGELOG.md:
Expand Down
35 changes: 35 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
PKG := inbucket
SHELL := /bin/sh

SRC := $(shell find . -type f -name '*.go' -not -path "./vendor/*")
PKGS := $$(go list ./... | grep -v /vendor/)

.PHONY: all build clean fmt install lint simplify test

all: test lint build

clean:
go clean

deps:
go get -t ./...

build: clean deps
go build

install: build
go install

test: clean deps
go test -race ./...

fmt:
@gofmt -l -w $(SRC)

simplify:
@gofmt -s -l -w $(SRC)

lint:
@test -z "$(shell gofmt -l . | tee /dev/stderr)" || echo "[WARN] Fix formatting issues with 'make fmt'"
@golint -set_exit_status $${PKGS}
@go vet $${PKGS}
4 changes: 2 additions & 2 deletions inbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func main() {
}

// Setup signal handler
sigChan := make(chan os.Signal)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)

// Initialize logging
Expand Down Expand Up @@ -150,7 +150,7 @@ signalLoop:
log.Infof("Received SIGTERM, shutting down")
close(shutdownChan)
}
case _ = <-shutdownChan:
case <-shutdownChan:
rootCancel()
break signalLoop
}
Expand Down
110 changes: 110 additions & 0 deletions sanitize/css.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package sanitize

import (
"bytes"
"strings"

"github.com/gorilla/css/scanner"
)

// propertyRule may someday allow control of what values are valid for a particular property.
type propertyRule struct{}

var allowedProperties = map[string]propertyRule{
"align": {},
"background-color": {},
"border": {},
"border-bottom": {},
"border-left": {},
"border-radius": {},
"border-right": {},
"border-top": {},
"box-sizing": {},
"clear": {},
"color": {},
"content": {},
"display": {},
"font-family": {},
"font-size": {},
"font-weight": {},
"height": {},
"line-height": {},
"margin": {},
"margin-bottom": {},
"margin-left": {},
"margin-right": {},
"margin-top": {},
"max-height": {},
"max-width": {},
"overflow": {},
"padding": {},
"padding-bottom": {},
"padding-left": {},
"padding-right": {},
"padding-top": {},
"table-layout": {},
"text-align": {},
"text-decoration": {},
"text-shadow": {},
"vertical-align": {},
"width": {},
"word-break": {},
}

// Handler Token, return next state.
type stateHandler func(b *bytes.Buffer, t *scanner.Token) stateHandler

func sanitizeStyle(input string) string {
b := &bytes.Buffer{}
scan := scanner.New(input)
state := stateStart
for {
t := scan.Next()
if t.Type == scanner.TokenEOF {
return b.String()
}
if t.Type == scanner.TokenError {
return ""
}
state = state(b, t)
if state == nil {
return ""
}
}
}

func stateStart(b *bytes.Buffer, t *scanner.Token) stateHandler {
switch t.Type {
case scanner.TokenIdent:
_, ok := allowedProperties[strings.ToLower(t.Value)]
if !ok {
return stateEat
}
b.WriteString(t.Value)
return stateValid
case scanner.TokenS:
return stateStart
}
// Unexpected type.
b.WriteString("/*" + t.Type.String() + "*/")
return stateEat
}

func stateEat(b *bytes.Buffer, t *scanner.Token) stateHandler {
if t.Type == scanner.TokenChar && t.Value == ";" {
// Done eating.
return stateStart
}
// Throw away this token.
return stateEat
}

func stateValid(b *bytes.Buffer, t *scanner.Token) stateHandler {
state := stateValid
if t.Type == scanner.TokenChar && t.Value == ";" {
// End of property.
state = stateStart
}
b.WriteString(t.Value)
return state
}
34 changes: 34 additions & 0 deletions sanitize/css_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package sanitize

import (
"testing"
)

func TestSanitizeStyle(t *testing.T) {
testCases := []struct {
input, want string
}{
{"", ""},
{
"color: red;",
"color: red;",
},
{
"background-color: black; color: white",
"background-color: black;color: white",
},
{
"background-color: black; invalid: true; color: white",
"background-color: black;color: white",
},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
got := sanitizeStyle(tc.input)
if got != tc.want {
t.Errorf("got: %q, want: %q, input: %q", got, tc.want, tc.input)
}
})
}

}
88 changes: 88 additions & 0 deletions sanitize/html.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package sanitize

import (
"bufio"
"bytes"
"io"
"regexp"
"strings"

"github.com/microcosm-cc/bluemonday"
"golang.org/x/net/html"
)

var (
cssSafe = regexp.MustCompile(".*")
policy = bluemonday.UGCPolicy().
AllowElements("center").
AllowAttrs("style").Matching(cssSafe).Globally()
)

func HTML(html string) (output string, err error) {
output, err = sanitizeStyleTags(html)
if err != nil {
return "", err
}
output = policy.Sanitize(output)
return
}

func sanitizeStyleTags(input string) (string, error) {
r := strings.NewReader(input)
b := &bytes.Buffer{}
if err := styleTagFilter(b, r); err != nil {
return "", err
}
return b.String(), nil
}

func styleTagFilter(w io.Writer, r io.Reader) error {
bw := bufio.NewWriter(w)
b := make([]byte, 256)
z := html.NewTokenizer(r)
for {
b = b[:0]
tt := z.Next()
switch tt {
case html.ErrorToken:
err := z.Err()
if err == io.EOF {
return bw.Flush()
}
return err
case html.StartTagToken, html.SelfClosingTagToken:
name, hasAttr := z.TagName()
if !hasAttr {
bw.Write(z.Raw())
continue
}
b = append(b, '<')
b = append(b, name...)
for {
key, val, more := z.TagAttr()
strval := string(val)
style := false
if strings.ToLower(string(key)) == "style" {
style = true
strval = sanitizeStyle(strval)
}
if !style || strval != "" {
b = append(b, ' ')
b = append(b, key...)
b = append(b, '=', '"')
b = append(b, []byte(html.EscapeString(strval))...)
b = append(b, '"')
}
if !more {
break
}
}
if tt == html.SelfClosingTagToken {
b = append(b, '/')
}
bw.Write(append(b, '>'))
default:
bw.Write(z.Raw())
}
}
}

0 comments on commit d5aea4d

Please sign in to comment.