feat: add CSP and other security related headers in the oCIS proxy service (#8777)
* feat: add CSP and other security related headers in the oCIS proxy service * fix: consolidate security related headers - drop middleware.Secure * fix: use github.com/DeepDiver1975/secure * fix: acceptance tests * feat: support env var replacements in csp.yaml
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Env []string
|
||||
|
||||
func (e Env) Get(name string) string {
|
||||
v, _ := e.Lookup(name)
|
||||
return v
|
||||
}
|
||||
|
||||
func (e Env) Has(name string) bool {
|
||||
_, ok := e.Lookup(name)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (e Env) Lookup(name string) (string, bool) {
|
||||
prefix := name + "="
|
||||
for _, pair := range e {
|
||||
if strings.HasPrefix(pair, prefix) {
|
||||
return pair[len(prefix):], true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// itemType identifies the type of lex items.
|
||||
type itemType int
|
||||
|
||||
// Pos represents a byte position in the original input text from which
|
||||
// this template was parsed.
|
||||
type Pos int
|
||||
|
||||
// item represents a token or text string returned from the scanner.
|
||||
type item struct {
|
||||
typ itemType // The type of this item.
|
||||
pos Pos // The starting position, in bytes, of this item in the input string.
|
||||
val string // The value of this item.
|
||||
}
|
||||
|
||||
func (i item) String() string {
|
||||
typ := "OP"
|
||||
if t, ok := tokens[i.typ]; ok {
|
||||
typ = t
|
||||
}
|
||||
return fmt.Sprintf("%s: %.40q", typ, i.val)
|
||||
}
|
||||
|
||||
const (
|
||||
eof = -1
|
||||
itemError itemType = iota // error occurred; value is text of error
|
||||
itemEOF
|
||||
itemText // plain text
|
||||
itemPlus // plus('+')
|
||||
itemDash // dash('-')
|
||||
itemEquals // equals
|
||||
itemColonEquals // colon-equals (':=')
|
||||
itemColonDash // colon-dash(':-')
|
||||
itemColonPlus // colon-plus(':+')
|
||||
itemVariable // variable starting with '$', such as '$hello' or '$1'
|
||||
itemLeftDelim // left action delimiter '${'
|
||||
itemRightDelim // right action delimiter '}'
|
||||
)
|
||||
|
||||
var tokens = map[itemType]string{
|
||||
itemEOF: "EOF",
|
||||
itemError: "ERROR",
|
||||
itemText: "TEXT",
|
||||
itemVariable: "VAR",
|
||||
itemLeftDelim: "START EXP",
|
||||
itemRightDelim: "END EXP",
|
||||
}
|
||||
|
||||
// stateFn represents the state of the lexer as a function that returns the next state.
|
||||
type stateFn func(*lexer) stateFn
|
||||
|
||||
// lexer holds the state of the scanner
|
||||
type lexer struct {
|
||||
input string // the string being lexed
|
||||
state stateFn // the next lexing function to enter
|
||||
pos Pos // current position in the input
|
||||
start Pos // start position of this item
|
||||
width Pos // width of last rune read from input
|
||||
lastPos Pos // position of most recent item returned by nextItem
|
||||
items chan item // channel of lexed items
|
||||
subsDepth int // depth of substitution
|
||||
noDigit bool // if the lexer skips variables that start with a digit
|
||||
}
|
||||
|
||||
// next returns the next rune in the input.
|
||||
func (l *lexer) next() rune {
|
||||
if int(l.pos) >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.width = Pos(w)
|
||||
l.pos += l.width
|
||||
return r
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next rune in the input.
|
||||
func (l *lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
// backup steps back one rune. Can only be called once per call of next.
|
||||
func (l *lexer) backup() {
|
||||
l.pos -= l.width
|
||||
}
|
||||
|
||||
// emit passes an item back to the client.
|
||||
func (l *lexer) emit(t itemType) {
|
||||
l.items <- item{t, l.start, l.input[l.start:l.pos]}
|
||||
l.lastPos = l.start
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// ignore skips over the pending input before this point.
|
||||
func (l *lexer) ignore() {
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// errorf returns an error token and terminates the scan by passing
|
||||
// back a nil pointer that will be the next state, terminating l.nextItem.
|
||||
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
|
||||
l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextItem returns the next item from the input.
|
||||
// Called by the parser, not in the lexing goroutine.
|
||||
func (l *lexer) nextItem() item {
|
||||
item := <-l.items
|
||||
return item
|
||||
}
|
||||
|
||||
// lex creates a new scanner for the input string.
|
||||
func lex(input string, noDigit bool) *lexer {
|
||||
l := &lexer{
|
||||
input: input,
|
||||
items: make(chan item),
|
||||
noDigit: noDigit,
|
||||
}
|
||||
go l.run()
|
||||
return l
|
||||
}
|
||||
|
||||
// run runs the state machine for the lexer.
|
||||
func (l *lexer) run() {
|
||||
for l.state = lexText; l.state != nil; {
|
||||
l.state = l.state(l)
|
||||
}
|
||||
close(l.items)
|
||||
}
|
||||
|
||||
// lexText scans until encountering with "$" or an opening action delimiter, "${".
|
||||
func lexText(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch r := l.next(); r {
|
||||
case '$':
|
||||
l.pos--
|
||||
// emit the text we've found until here, if any.
|
||||
if l.pos > l.start {
|
||||
l.emit(itemText)
|
||||
}
|
||||
l.pos++
|
||||
switch r := l.peek(); {
|
||||
case l.noDigit && unicode.IsDigit(r):
|
||||
// ignore variable starting with digit like $1.
|
||||
l.next()
|
||||
l.emit(itemText)
|
||||
case r == '$':
|
||||
// ignore the previous '$'.
|
||||
l.ignore()
|
||||
l.next()
|
||||
l.emit(itemText)
|
||||
case r == '{':
|
||||
l.next()
|
||||
r2 := l.peek()
|
||||
if l.noDigit && unicode.IsDigit(r2) {
|
||||
// ignore variable starting with digit like ${1}.
|
||||
l.next()
|
||||
l.emit(itemText)
|
||||
break
|
||||
}
|
||||
l.subsDepth++
|
||||
l.emit(itemLeftDelim)
|
||||
return lexSubstitutionOperator
|
||||
case isAlphaNumeric(r):
|
||||
return lexVariable
|
||||
}
|
||||
case eof:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
// Correctly reached EOF.
|
||||
if l.pos > l.start {
|
||||
l.emit(itemText)
|
||||
}
|
||||
l.emit(itemEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
// lexVariable scans a Variable: $Alphanumeric.
|
||||
// The $ has been scanned.
|
||||
func lexVariable(l *lexer) stateFn {
|
||||
var r rune
|
||||
for {
|
||||
r = l.next()
|
||||
if !isAlphaNumeric(r) {
|
||||
l.backup()
|
||||
break
|
||||
}
|
||||
}
|
||||
if v := l.input[l.start:l.pos]; v == "_" || v == "$_" {
|
||||
return lexText
|
||||
}
|
||||
l.emit(itemVariable)
|
||||
if l.subsDepth > 0 {
|
||||
return lexSubstitutionOperator
|
||||
}
|
||||
return lexText
|
||||
}
|
||||
|
||||
// lexSubstitutionOperator scans a starting substitution operator (if any) and continues with lexSubstitution
|
||||
func lexSubstitutionOperator(l *lexer) stateFn {
|
||||
switch r := l.next(); {
|
||||
case r == '}':
|
||||
l.subsDepth--
|
||||
l.emit(itemRightDelim)
|
||||
return lexText
|
||||
case r == eof || isEndOfLine(r):
|
||||
return l.errorf("closing brace expected")
|
||||
case isAlphaNumeric(r) && strings.HasPrefix(l.input[l.lastPos:], "${"):
|
||||
return lexVariable
|
||||
case r == '+':
|
||||
l.emit(itemPlus)
|
||||
case r == '-':
|
||||
l.emit(itemDash)
|
||||
case r == '=':
|
||||
l.emit(itemEquals)
|
||||
case r == ':':
|
||||
switch l.next() {
|
||||
case '-':
|
||||
l.emit(itemColonDash)
|
||||
case '=':
|
||||
l.emit(itemColonEquals)
|
||||
case '+':
|
||||
l.emit(itemColonPlus)
|
||||
}
|
||||
}
|
||||
return lexSubstitution
|
||||
}
|
||||
|
||||
// lexSubstitution scans the elements inside substitution delimiters.
|
||||
func lexSubstitution(l *lexer) stateFn {
|
||||
switch r := l.next(); {
|
||||
case r == '}':
|
||||
l.subsDepth--
|
||||
l.emit(itemRightDelim)
|
||||
return lexText
|
||||
case r == eof || isEndOfLine(r):
|
||||
return l.errorf("closing brace expected")
|
||||
case isAlphaNumeric(r) && strings.HasPrefix(l.input[l.lastPos:], "${"):
|
||||
fallthrough
|
||||
case r == '$':
|
||||
return lexVariable
|
||||
default:
|
||||
l.emit(itemText)
|
||||
}
|
||||
return lexSubstitution
|
||||
}
|
||||
|
||||
// isEndOfLine reports whether r is an end-of-line character.
|
||||
func isEndOfLine(r rune) bool {
|
||||
return r == '\r' || r == '\n'
|
||||
}
|
||||
|
||||
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
||||
func isAlphaNumeric(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Node interface {
|
||||
Type() NodeType
|
||||
String() (string, error)
|
||||
}
|
||||
|
||||
// NodeType identifies the type of a node.
|
||||
type NodeType int
|
||||
|
||||
// Type returns itself and provides an easy default implementation
|
||||
// for embedding in a Node. Embedded in all non-trivial Nodes.
|
||||
func (t NodeType) Type() NodeType {
|
||||
return t
|
||||
}
|
||||
|
||||
const (
|
||||
NodeText NodeType = iota
|
||||
NodeSubstitution
|
||||
NodeVariable
|
||||
)
|
||||
|
||||
type TextNode struct {
|
||||
NodeType
|
||||
Text string
|
||||
}
|
||||
|
||||
func NewText(text string) *TextNode {
|
||||
return &TextNode{NodeText, text}
|
||||
}
|
||||
|
||||
func (t *TextNode) String() (string, error) {
|
||||
return t.Text, nil
|
||||
}
|
||||
|
||||
type VariableNode struct {
|
||||
NodeType
|
||||
Ident string
|
||||
Env Env
|
||||
Restrict *Restrictions
|
||||
}
|
||||
|
||||
func NewVariable(ident string, env Env, restrict *Restrictions) *VariableNode {
|
||||
return &VariableNode{NodeVariable, ident, env, restrict}
|
||||
}
|
||||
|
||||
func (t *VariableNode) String() (string, error) {
|
||||
if err := t.validateNoUnset(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
value := t.Env.Get(t.Ident)
|
||||
if err := t.validateNoEmpty(value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (t *VariableNode) isSet() bool {
|
||||
return t.Env.Has(t.Ident)
|
||||
}
|
||||
|
||||
func (t *VariableNode) validateNoUnset() error {
|
||||
if t.Restrict.NoUnset && !t.isSet() {
|
||||
return fmt.Errorf("variable ${%s} not set", t.Ident)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *VariableNode) validateNoEmpty(value string) error {
|
||||
if t.Restrict.NoEmpty && value == "" && t.isSet() {
|
||||
return fmt.Errorf("variable ${%s} set but empty", t.Ident)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SubstitutionNode struct {
|
||||
NodeType
|
||||
ExpType itemType
|
||||
Variable *VariableNode
|
||||
Default Node // Default could be variable or text
|
||||
}
|
||||
|
||||
func (t *SubstitutionNode) String() (string, error) {
|
||||
if t.ExpType >= itemPlus && t.Default != nil {
|
||||
switch t.ExpType {
|
||||
case itemColonDash, itemColonEquals:
|
||||
if s, _ := t.Variable.String(); s != "" {
|
||||
return s, nil
|
||||
}
|
||||
return t.Default.String()
|
||||
case itemPlus, itemColonPlus:
|
||||
if t.Variable.isSet() {
|
||||
return t.Default.String()
|
||||
}
|
||||
return "", nil
|
||||
default:
|
||||
if !t.Variable.isSet() {
|
||||
return t.Default.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return t.Variable.String()
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
// Most of the code in this package taken from golang/text/template/parse
|
||||
package parse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A mode value is a set of flags (or 0). They control parser behavior.
|
||||
type Mode int
|
||||
|
||||
// Mode for parser behaviour
|
||||
const (
|
||||
Quick Mode = iota // stop parsing after first error encoutered and return
|
||||
AllErrors // report all errors
|
||||
)
|
||||
|
||||
// The restrictions option controls the parsring restriction.
|
||||
type Restrictions struct {
|
||||
NoUnset bool
|
||||
NoEmpty bool
|
||||
NoDigit bool
|
||||
}
|
||||
|
||||
// Restrictions specifier
|
||||
var (
|
||||
Relaxed = &Restrictions{false, false, false}
|
||||
NoEmpty = &Restrictions{false, true, false}
|
||||
NoUnset = &Restrictions{true, false, false}
|
||||
Strict = &Restrictions{true, true, false}
|
||||
)
|
||||
|
||||
// Parser type initializer
|
||||
type Parser struct {
|
||||
Name string // name of the processing template
|
||||
Env Env
|
||||
Restrict *Restrictions
|
||||
Mode Mode
|
||||
// parsing state;
|
||||
lex *lexer
|
||||
token [3]item // three-token lookahead
|
||||
peekCount int
|
||||
nodes []Node
|
||||
}
|
||||
|
||||
// New allocates a new Parser with the given name.
|
||||
func New(name string, env []string, r *Restrictions) *Parser {
|
||||
return &Parser{
|
||||
Name: name,
|
||||
Env: Env(env),
|
||||
Restrict: r,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the given string.
|
||||
func (p *Parser) Parse(text string) (string, error) {
|
||||
p.lex = lex(text, p.Restrict.NoDigit)
|
||||
// Build internal array of all unset or empty vars here
|
||||
var errs []error
|
||||
// clean parse state
|
||||
p.nodes = make([]Node, 0)
|
||||
p.peekCount = 0
|
||||
if err := p.parse(); err != nil {
|
||||
switch p.Mode {
|
||||
case Quick:
|
||||
return "", err
|
||||
case AllErrors:
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
var out string
|
||||
for _, node := range p.nodes {
|
||||
s, err := node.String()
|
||||
if err != nil {
|
||||
switch p.Mode {
|
||||
case Quick:
|
||||
return "", err
|
||||
case AllErrors:
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
out += s
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
var b strings.Builder
|
||||
for i, err := range errs {
|
||||
if i > 0 {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteString(err.Error())
|
||||
}
|
||||
return "", errors.New(b.String())
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// parse is the top-level parser for the template.
|
||||
// It runs to EOF and return an error if something isn't right.
|
||||
func (p *Parser) parse() error {
|
||||
Loop:
|
||||
for {
|
||||
switch t := p.next(); t.typ {
|
||||
case itemEOF:
|
||||
break Loop
|
||||
case itemError:
|
||||
return p.errorf(t.val)
|
||||
case itemVariable:
|
||||
varNode := NewVariable(strings.TrimPrefix(t.val, "$"), p.Env, p.Restrict)
|
||||
p.nodes = append(p.nodes, varNode)
|
||||
case itemLeftDelim:
|
||||
if p.peek().typ == itemVariable {
|
||||
n, err := p.action()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.nodes = append(p.nodes, n)
|
||||
continue
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
textNode := NewText(t.val)
|
||||
p.nodes = append(p.nodes, textNode)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse substitution. first item is a variable.
|
||||
func (p *Parser) action() (Node, error) {
|
||||
var expType itemType
|
||||
var defaultNode Node
|
||||
varNode := NewVariable(p.next().val, p.Env, p.Restrict)
|
||||
Loop:
|
||||
for {
|
||||
switch t := p.next(); t.typ {
|
||||
case itemRightDelim:
|
||||
break Loop
|
||||
case itemError:
|
||||
return nil, p.errorf(t.val)
|
||||
case itemVariable:
|
||||
defaultNode = NewVariable(strings.TrimPrefix(t.val, "$"), p.Env, p.Restrict)
|
||||
case itemText:
|
||||
n := NewText(t.val)
|
||||
Text:
|
||||
for {
|
||||
switch p.peek().typ {
|
||||
case itemRightDelim, itemError, itemEOF:
|
||||
break Text
|
||||
default:
|
||||
// patch to accept all kind of chars
|
||||
n.Text += p.next().val
|
||||
}
|
||||
}
|
||||
defaultNode = n
|
||||
default:
|
||||
expType = t.typ
|
||||
}
|
||||
}
|
||||
return &SubstitutionNode{NodeSubstitution, expType, varNode, defaultNode}, nil
|
||||
}
|
||||
|
||||
func (p *Parser) errorf(s string) error {
|
||||
return errors.New(s)
|
||||
}
|
||||
|
||||
// next returns the next token.
|
||||
func (p *Parser) next() item {
|
||||
if p.peekCount > 0 {
|
||||
p.peekCount--
|
||||
} else {
|
||||
p.token[0] = p.lex.nextItem()
|
||||
}
|
||||
return p.token[p.peekCount]
|
||||
}
|
||||
|
||||
// backup backs the input stream up one token.
|
||||
func (p *Parser) backup() {
|
||||
p.peekCount++
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next token.
|
||||
func (p *Parser) peek() item {
|
||||
if p.peekCount > 0 {
|
||||
return p.token[p.peekCount-1]
|
||||
}
|
||||
p.peekCount = 1
|
||||
p.token[0] = p.lex.nextItem()
|
||||
return p.token[0]
|
||||
}
|
||||
Reference in New Issue
Block a user