mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-01-07 19:57:06 +00:00
Reorganize packages, closes #79
- All packages go into either cmd or pkg directories - Most packages renamed - Server packages moved into pkg/server - sanitize moved into webui, as that's the only place it's used - filestore moved into pkg/storage/file - Makefile updated, and PKG variable use fixed
This commit is contained in:
110
pkg/webui/sanitize/css.go
Normal file
110
pkg/webui/sanitize/css.go
Normal file
@@ -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
pkg/webui/sanitize/css_test.go
Normal file
34
pkg/webui/sanitize/css_test.go
Normal file
@@ -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
pkg/webui/sanitize/html.go
Normal file
88
pkg/webui/sanitize/html.go
Normal file
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
171
pkg/webui/sanitize/html_test.go
Normal file
171
pkg/webui/sanitize/html_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package sanitize_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jhillyerd/inbucket/pkg/webui/sanitize"
|
||||
)
|
||||
|
||||
// TestHTMLPlainStrings test plain text passthrough
|
||||
func TestHTMLPlainStrings(t *testing.T) {
|
||||
testStrings := []string{
|
||||
"",
|
||||
"plain string",
|
||||
"one < two",
|
||||
}
|
||||
for _, ts := range testStrings {
|
||||
t.Run(ts, func(t *testing.T) {
|
||||
got, err := sanitize.HTML(ts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != ts {
|
||||
t.Errorf("Got: %q, want: %q", got, ts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTMLSimpleFormatting tests basic tags we should allow
|
||||
func TestHTMLSimpleFormatting(t *testing.T) {
|
||||
testStrings := []string{
|
||||
"<p>paragraph</p>",
|
||||
"<b>bold</b>",
|
||||
"<i>italic</b>",
|
||||
"<em>emphasis</em>",
|
||||
"<strong>strong</strong>",
|
||||
"<div><span>text</span></div>",
|
||||
"<center>text</center>",
|
||||
}
|
||||
for _, ts := range testStrings {
|
||||
t.Run(ts, func(t *testing.T) {
|
||||
got, err := sanitize.HTML(ts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != ts {
|
||||
t.Errorf("Got: %q, want: %q", got, ts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTMLScriptTags tests some strings with JavaScript
|
||||
func TestHTMLScriptTags(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input, want string
|
||||
}{
|
||||
{
|
||||
`safe<script>nope</script>`,
|
||||
`safe`,
|
||||
},
|
||||
{
|
||||
`<a onblur="alert(something)" href="http://mysite.com">mysite</a>`,
|
||||
`<a href="http://mysite.com" rel="nofollow">mysite</a>`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
got, err := sanitize.HTML(tc.input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("Got: %q, want: %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeStyleTags(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name, input, want string
|
||||
}{
|
||||
{
|
||||
"empty",
|
||||
``,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"open",
|
||||
`<div>`,
|
||||
`<div>`,
|
||||
},
|
||||
{
|
||||
"open close",
|
||||
`<div></div>`,
|
||||
`<div></div>`,
|
||||
},
|
||||
{
|
||||
"inner text",
|
||||
`<div>foo bar</div>`,
|
||||
`<div>foo bar</div>`,
|
||||
},
|
||||
{
|
||||
"self close",
|
||||
`<br/>`,
|
||||
`<br/>`,
|
||||
},
|
||||
{
|
||||
"open params",
|
||||
`<div id="me">`,
|
||||
`<div id="me">`,
|
||||
},
|
||||
{
|
||||
"open params squote",
|
||||
`<div id="me" title='best'>`,
|
||||
`<div id="me" title="best">`,
|
||||
},
|
||||
{
|
||||
"open style",
|
||||
`<div id="me" style="color: red;">`,
|
||||
`<div id="me" style="color: red;">`,
|
||||
},
|
||||
{
|
||||
"open style squote",
|
||||
`<div id="me" style='color: red;'>`,
|
||||
`<div id="me" style="color: red;">`,
|
||||
},
|
||||
{
|
||||
"open style mixed case",
|
||||
`<div id="me" StYlE="color: red;">`,
|
||||
`<div id="me" style="color: red;">`,
|
||||
},
|
||||
{
|
||||
"closed style",
|
||||
`<br style="border: 1px solid red;"/>`,
|
||||
`<br style="border: 1px solid red;"/>`,
|
||||
},
|
||||
{
|
||||
"mixed case style",
|
||||
`<br StYlE="border: 1px solid red;"/>`,
|
||||
`<br style="border: 1px solid red;"/>`,
|
||||
},
|
||||
{
|
||||
"mixed case invalid style",
|
||||
`<br StYlE="position: fixed;"/>`,
|
||||
`<br/>`,
|
||||
},
|
||||
{
|
||||
"mixed",
|
||||
`<p id='i' title="cla'zz" style="font-size: 25px;"><b>some text</b></p>`,
|
||||
`<p id="i" title="cla'zz" style="font-size: 25px;"><b>some text</b></p>`,
|
||||
},
|
||||
{
|
||||
"invalid styles",
|
||||
`<div id="me" style='position: absolute;'>`,
|
||||
`<div id="me">`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := sanitize.HTML(tc.input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("input: %s\ngot : %s\nwant: %s", tc.input, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user