Skip to content

Commit

Permalink
Merge eecb394 into bbf6ef1
Browse files Browse the repository at this point in the history
  • Loading branch information
csstaub committed Oct 11, 2018
2 parents bbf6ef1 + eecb394 commit bcf6897
Show file tree
Hide file tree
Showing 8 changed files with 434 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ build_script:
- set CGO_ENABLED=1
- go build -o ghostunnel-%GIT_VERSION%-windows-%GOARCH%-with-pkcs11.exe -ldflags "-w -extldflags \"-static\" -extld=%EXTLD%" -i .
# Execute tests
- go test -v . ./auth
- go test -v ./...
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ unit:
go test -v -covermode=count -coverprofile=coverage-unit-test-auth.out ./auth
go test -v -covermode=count -coverprofile=coverage-unit-test-certloader.out ./certloader
go test -v -covermode=count -coverprofile=coverage-unit-test-proxy.out ./proxy
go test -v -covermode=count -coverprofile=coverage-unit-test-wildcard.out ./wildcard
.PHONY: unit

# Run integration tests
Expand Down
8 changes: 5 additions & 3 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"errors"
"net"
"net/url"

"github.com/square/ghostunnel/wildcard"
)

// Logger is used by this package to log messages
Expand Down Expand Up @@ -53,7 +55,7 @@ type ACL struct {
// AllowURIs lists URI SANs that should be allowed access. If a principal
// has a valid certificate with at least one of these URI SANs, we grant
// access.
AllowedURIs []string
AllowedURIs []wildcard.Matcher
// Logger is used to log authorization decisions.
Logger Logger
}
Expand Down Expand Up @@ -181,10 +183,10 @@ func intersectsIP(left, right []net.IP) bool {
}

// Returns true if at least one item from left is also contained in right.
func intersectsURI(left []string, right []*url.URL) bool {
func intersectsURI(left []wildcard.Matcher, right []*url.URL) bool {
for _, l := range left {
for _, r := range right {
if r.String() == l {
if l.Matches(r.String()) {
return true
}
}
Expand Down
11 changes: 6 additions & 5 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net/url"
"testing"

"github.com/square/ghostunnel/wildcard"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -55,7 +56,7 @@ func TestAuthorizeReject(t *testing.T) {
AllowedCNs: []string{"test"},
AllowedOUs: []string{"test"},
AllowedDNSs: []string{"test"},
AllowedURIs: []string{"test"},
AllowedURIs: []wildcard.Matcher{wildcard.MustCompile("test")},
}

assert.NotNil(t, testACL.VerifyPeerCertificateServer(nil, fakeChains), "should reject cert w/o matching CN/OU")
Expand Down Expand Up @@ -103,15 +104,15 @@ func TestAuthorizeAllowIP(t *testing.T) {

func TestAuthorizeAllowURI(t *testing.T) {
testACL := ACL{
AllowedURIs: []string{"scheme://valid/path"},
AllowedURIs: []wildcard.Matcher{wildcard.MustCompile("scheme://valid/path")},
}

assert.Nil(t, testACL.VerifyPeerCertificateServer(nil, fakeChains), "allow-uri-san should allow clients with matching URI SAN")
}

func TestAuthorizeRejectURI(t *testing.T) {
testACL := ACL{
AllowedURIs: []string{"schema://invalid/path"},
AllowedURIs: []wildcard.Matcher{wildcard.MustCompile("scheme://invalid/path")},
}

assert.NotNil(t, testACL.VerifyPeerCertificateServer(nil, fakeChains), "should reject cert w/o matching URI")
Expand Down Expand Up @@ -192,15 +193,15 @@ func TestVerifyRejectIP(t *testing.T) {

func TestVerifyAllowURI(t *testing.T) {
testACL := ACL{
AllowedURIs: []string{"scheme://valid/path"},
AllowedURIs: []wildcard.Matcher{wildcard.MustCompile("scheme://valid/path")},
}

assert.Nil(t, testACL.VerifyPeerCertificateClient(nil, fakeChains), "verify-uri-san should allow clients with matching URI SAN")
}

func TestVerifyRejectURI(t *testing.T) {
testACL := ACL{
AllowedURIs: []string{"scheme://invalid/path"},
AllowedURIs: []wildcard.Matcher{wildcard.MustCompile("scheme://invalid/path")},
}

assert.NotNil(t, testACL.VerifyPeerCertificateClient(nil, fakeChains), "should reject cert w/o matching URI")
Expand Down
6 changes: 6 additions & 0 deletions certloader/keystore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"crypto/tls"
"io/ioutil"
"os"
"runtime"
"testing"
"unsafe"

Expand Down Expand Up @@ -95,6 +96,11 @@ func TestCertificateFromPEMFilesValid(t *testing.T) {
assert.Nil(t, cert.Reload(), "should be able to reload")

// Remove file & test reload failure
if runtime.GOOS == "windows" {
// Reloading not supported on Windows
return
}

os.Remove(file.Name())
assert.NotNil(t, cert.Reload(), "should not be able to reload")
}
Expand Down
17 changes: 15 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/square/ghostunnel/auth"
"github.com/square/ghostunnel/certloader"
"github.com/square/ghostunnel/proxy"
"github.com/square/ghostunnel/wildcard"
"github.com/square/go-sq-metrics"
"gopkg.in/alecthomas/kingpin.v2"

Expand Down Expand Up @@ -400,13 +401,19 @@ func serverListen(context *Context) error {
return err
}

allowedURIs, err := wildcard.CompileList(*serverAllowedURIs)
if err != nil {
logger.Printf("invalid URI pattern in --allow-uri flag (%s)", err)
return err
}

serverACL := auth.ACL{
AllowAll: *serverAllowAll,
AllowedCNs: *serverAllowedCNs,
AllowedOUs: *serverAllowedOUs,
AllowedDNSs: *serverAllowedDNSs,
AllowedIPs: *serverAllowedIPs,
AllowedURIs: *serverAllowedURIs,
AllowedURIs: allowedURIs,
Logger: logger,
}

Expand Down Expand Up @@ -589,12 +596,18 @@ func clientBackendDialer(cert certloader.Certificate, network, address, host str
config.ServerName = *clientServerName
}

allowedURIs, err := wildcard.CompileList(*clientAllowedURIs)
if err != nil {
logger.Printf("invalid URI pattern in --verify-uri flag (%s)", err)
return nil, err
}

clientACL := auth.ACL{
AllowedCNs: *clientAllowedCNs,
AllowedOUs: *clientAllowedOUs,
AllowedDNSs: *clientAllowedDNSs,
AllowedIPs: *clientAllowedIPs,
AllowedURIs: *clientAllowedURIs,
AllowedURIs: allowedURIs,
Logger: logger,
}

Expand Down
159 changes: 159 additions & 0 deletions wildcard/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*-
* Copyright 2018 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// Package wildcard implements simple wildcard matching meant to be used to
// match URIs and paths against simple patterns. It's less powerful but also
// less error-prone than regular expressions.
//
// We expose functions to build matchers from simple wildcard patterns. Each
// pattern is a sequence of segments separated by a separator, usually a
// forward slash. Each segment may be a literal string, or a wildcard. We
// support two types of wildcards, a single '*' wildcard and a double '**'
// wildcard.
//
// A single '*' wildcard will match any literal string that does not contain
// the separator. It may occur anywhere between two separators in the pattern.
//
// A double '**' wildcard will match anything, including the separator rune.
// It may only occur at the end of a pattern.
//
// Furthermore, the matcher will consider the separator optional if it occurs
// at the end of a string. This means that the paths "foo/bar" and "foo/bar/"
// are treated as equivalent.
package wildcard

import (
"bytes"
"errors"
"regexp"
"strings"
)

const (
defaultSeparator = '/'
)

var (
errEmptyPattern = errors.New("input pattern was empty string")
errInvalidWildcard = errors.New("wildcard '*' can only appear between two separators")
errInvalidDoubleWildcard = errors.New("wildcard '**' can only appear at end of pattern")
errRegexpCompile = errors.New("unable to compile generated regex (internal bug)")
)

// Matcher represents a compiled pattern that can be matched against a string.
type Matcher interface {
// Matches checks if the given input matches the compiled pattern.
Matches(string) bool
}

type regexpMatcher struct {
// Compiled regular expression for this matcher
pattern *regexp.Regexp
}

// Compile creates a new Matcher given a pattern, using '/' as the separator.
func Compile(pattern string) (Matcher, error) {
return CompileWithSeparator(pattern, defaultSeparator)
}

// CompileList creates new Matchers given a list patterns, using '/' as the separator.
func CompileList(patterns []string) ([]Matcher, error) {
ms := []Matcher{}
for _, pattern := range patterns {
m, err := Compile(pattern)
if err != nil {
return nil, err
}
ms = append(ms, m)
}
return ms, nil
}

// MustCompile creates a new Matcher given a pattern, using '/' as the separator,
// and panics if the given pattern was invalid.
func MustCompile(pattern string) Matcher {
m, err := CompileWithSeparator(pattern, defaultSeparator)
if err != nil {
panic(err)
}
return m
}

// CompileWithSeparator creates a new Matcher given a pattern and separator rune.
func CompileWithSeparator(pattern string, separator rune) (Matcher, error) {
// Build regular expression from wildcard pattern
// - Wildcard '*' should match all chars except forward slash
// - Wildcard '**' should match all chars, including forward slash
// All other regex meta chars will need to be quoted

if pattern == "" {
return nil, errEmptyPattern
}

segments := strings.Split(pattern, string(separator))

var regex bytes.Buffer
regex.WriteString("^")

loop:
for i, segment := range segments {
switch segment {
case "*":
// Segment with wildcard
regex.WriteString("[^")
regex.WriteRune(separator)
regex.WriteString("]+")
case "**":
// Segment with double wildcard
// May only appear at the end of a pattern
if i != len(segments)-1 {
return nil, errInvalidDoubleWildcard
}
regex.WriteRune('?')
regex.WriteString(".*$")
break loop
default:
// Segment to match literal string
if strings.Contains(segment, "*") {
return nil, errInvalidWildcard
}
regex.WriteString(regexp.QuoteMeta(segment))
}

// Separate this segment from next one
regex.WriteRune(separator)

if i == len(segments)-1 {
// Final slash should be optional
// We want "path" and "path/" to match
regex.WriteString("?$")
}
}

compiled, err := regexp.Compile(regex.String())
if err != nil {
return nil, errRegexpCompile
}

return regexpMatcher{
pattern: compiled,
}, nil
}

// Matches checks if the given input matches the compiled pattern.
func (rm regexpMatcher) Matches(input string) bool {
return rm.pattern.Match([]byte(input))
}

0 comments on commit bcf6897

Please sign in to comment.