├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── filter.go ├── filter_test.go ├── go.mod ├── go.sum ├── init.go ├── init_test.go ├── integration_test.go ├── package_test.go ├── resources └── test │ ├── integrationTest.Test_fastcgi.conf │ ├── integrationTest.Test_fastcgiWithGzip.conf │ ├── integrationTest.Test_fastcgiWithRedirect.conf │ ├── integrationTest.Test_markdown.conf │ ├── integrationTest.Test_markdown │ └── index.md │ ├── integrationTest.Test_markdownWithGzip.conf │ ├── integrationTest.Test_proxy.conf │ ├── integrationTest.Test_proxyWithGzip.conf │ ├── integrationTest.Test_proxyWithGzipUpstream.conf │ ├── integrationTest.Test_redir.conf │ ├── integrationTest.Test_static.conf │ ├── integrationTest.Test_static │ ├── text.txt │ └── utf8.txt │ ├── integrationTest.Test_staticWithBasicAuth.conf │ ├── integrationTest.Test_staticWithGzip.conf │ ├── integrationTest.Test_staticWithUtf8.conf │ └── testReplacement ├── responseWriterWrapper.go ├── responseWriterWrapper_test.go ├── rule.go ├── ruleReplaceAction.go ├── ruleReplaceAction_test.go ├── rule_test.go └── utils ├── fcgi ├── child.go └── fcgi.go └── test ├── testingCaddy.go ├── testingFcgiServer.go ├── testingHttpServer.go └── testingResources.go /.gitattributes: -------------------------------------------------------------------------------- 1 | LICENSE text eol=lf 2 | *.conf text eol=lf 3 | *.md text eol=lf 4 | *.txt text eol=lf 5 | *.go text eol=lf 6 | *.sh text eol=lf 7 | *.bat text eol=crlf 8 | testReplacement text eol=lf 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.idea 3 | /vendor 4 | /learning 5 | coverage.out 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.12" 4 | install: skip 5 | os: 6 | - linux 7 | script: skip 8 | env: 9 | global: 10 | - GO111MODULE=on 11 | - CGO_ENABLED=0 12 | 13 | jobs: 14 | include: 15 | - stage: test 16 | name: Run Tests 17 | install: 18 | - go mod download 19 | - go get -u github.com/mattn/goveralls 20 | script: 21 | - go test -v -covermode=count -coverprofile=coverage.out . 22 | - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN || true' 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | caddy-filter 2 | The MIT License (MIT) 3 | 4 | Copyright (c) echocat 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/echocat/caddy-filter)](https://goreportcard.com/report/github.com/echocat/caddy-filter) 2 | [![Build Status](https://travis-ci.org/echocat/caddy-filter.svg?branch=master)](https://travis-ci.org/echocat/caddy-filter) 3 | [![Coverage Status](https://img.shields.io/coveralls/echocat/caddy-filter/master.svg?style=flat-square)](https://coveralls.io/github/echocat/caddy-filter?branch=master) 4 | [![License](https://img.shields.io/github/license/echocat/caddy-filter.svg?style=flat-square)](LICENSE) 5 | 6 | # caddy-filter 7 | 8 | filter allows you to modify the responses. 9 | 10 | This could be useful to modify static HTML files to add (for example) Google Analytics source code to it. 11 | 12 | * [Syntax](#syntax) 13 | * [Examples](#examples) 14 | * [Run tests](#run-tests) 15 | * [Contributing](#contributing) 16 | * [License](#license) 17 | 18 | ## Syntax 19 | 20 | ``` 21 | filter rule { 22 | path 23 | content_type 24 | path_content_type_combination 25 | search_pattern 26 | replacement 27 | } 28 | filter rule ... 29 | filter max_buffer_size 30 | ``` 31 | 32 | * **rule**: Defines a new filter rule for a file to respond. 33 | > **Important:** Define ``path`` and/or ``content_type`` not to open. Slack rules could dramatically impact the system performance because every response is recorded to memory before returning it. 34 | 35 | * **path**: Regular expression that matches the requested path. 36 | * **content_type**: Regular expression that matches the requested content type that results after the evaluation of the whole request. 37 | * **path_content_type_combination**: _(Since 0.8)_ Could be `and` or `or`. (Default: `and` - before this parameter existed it was `or`) 38 | * **search_pattern**: Regular expression to find in the response body to replace it. 39 | * **replacement**: Pattern to replace the ``search_pattern`` with. 40 |
You can use parameters. Each parameter must be formatted like: ``{name}``. 41 | * Regex group: Every group of the ``search_pattern`` could be addressed with ``{index}``. 42 |
Example: ``"My name is (.*?) (.*?)." => "Name: {2}, {1}."`` 43 | 44 | * Request context: Parameters like URL ... could be accessed. 45 |
Example: ``Host: {request_host}`` 46 | * ``request_header_
``: Contains a header value of the request, if provided or empty. 47 | * ``request_url``: Full requested url 48 | * ``request_path``: Requested path 49 | * ``request_method``: Used method 50 | * ``request_host``: Target host 51 | * ``request_proto``: Used proto 52 | * ``request_remoteAddress``: Remote address of the calling client 53 | * ``response_header_
``: Contains a header value of the response, if provided or empty. 54 | * ``env_``: Contains an environment variable value, if provided or empty. 55 | * ``now[:]``: Current timestamp. If pattern not provided, `RFC` or `RFC3339` [RFC3339](https://tools.ietf.org/html/rfc3339) is used. Other values: [`unix`](https://en.wikipedia.org/wiki/Unix_time), [`timestamp`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Date/now) or free format following [Golang time formatting rules](https://golang.org/pkg/time/#pkg-constants). 56 | * ``response_header_last_modified[:]``: Same like `now` for last modification time of current resource - see above. If not send by server current time will be used. 57 | * Replacements in files: If the replacement is prefixed with a ``@`` character it will be tried 58 | to find a file with this name and load the replacement from there. This will help you to also 59 | add replacements with larger payloads which will be ugly direct within the Caddyfile. 60 |
Example: ``@myfile.html`` 61 | * **max_buffer_size**: Limit the buffer size to the specified maximum number of bytes. If a rules matches the whole body will be recorded at first to memory before delivery to HTTP client. If this limit is reached no filtering will executed and the content is directly forwarded to the client to prevent memory overload. Default is: ``10485760`` (=10 MB) 62 | 63 | ## Examples 64 | 65 | Replace in every text file ``Foo`` with ``Bar``. 66 | 67 | ``` 68 | filter rule { 69 | path .*\.txt 70 | search_pattern Foo 71 | replacement Bar 72 | } 73 | ``` 74 | 75 | Add Google Analytics to every HTML page from a file. 76 | 77 | **``Caddyfile``**: 78 | ``` 79 | filter rule { 80 | path .*\.html 81 | search_pattern 82 | replacement @header.html 83 | } 84 | ``` 85 | 86 | **``header.html``**: 87 | ```html 88 | 89 | 90 | ``` 91 | 92 | Insert server name in every HTML page 93 | 94 | ``` 95 | filter rule { 96 | content_type text/html.* 97 | search_pattern Server 98 | replacement "This site was provided by {response_header_Server}" 99 | } 100 | ``` 101 | 102 | ## Run tests 103 | 104 | ### Full 105 | 106 | This includes download of all dependencies and also creation and upload of coverage reports. 107 | 108 | > No working golang installation is required but Java 8+ (in ``PATH`` or ``JAVA_HOME`` set.). 109 | 110 | ```bash 111 | # On Linux/macOS 112 | $ ./gradlew test 113 | 114 | # On Windows 115 | $ gradlew test 116 | ``` 117 | ### Native 118 | 119 | > Requires a working golang installation in ``PATH`` and ``GOPATH`` set. 120 | 121 | ```bash 122 | $ go test 123 | ``` 124 | 125 | ## Contributing 126 | 127 | caddy-filter is an open source project by [echocat](https://echocat.org). 128 | So if you want to make this project even better, you can contribute to this project on [Github](https://github.com/echocat/caddy-filter) 129 | by [fork us](https://github.com/echocat/caddy-filter/fork). 130 | 131 | If you commit code to this project, you have to accept that this code will be released under the [license](#license) of this project. 132 | 133 | ## License 134 | 135 | See the [LICENSE](LICENSE) file. 136 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/caddyserver/caddy/caddyhttp/fastcgi" 5 | "github.com/caddyserver/caddy/caddyhttp/httpserver" 6 | "io" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | const defaultMaxBufferSize = 10 * 1024 * 1024 12 | 13 | type filterHandler struct { 14 | next httpserver.Handler 15 | rules []*rule 16 | maximumBufferSize int 17 | } 18 | 19 | func (instance filterHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) (int, error) { 20 | // Do not intercept if this is a websocket upgrade request. 21 | if request.Method == "GET" && request.Header.Get("Upgrade") == "websocket" { 22 | return instance.next.ServeHTTP(writer, request) 23 | } 24 | 25 | wrapper := newResponseWriterWrapperFor(writer, func(wrapper *responseWriterWrapper) bool { 26 | header := wrapper.Header() 27 | for _, rule := range instance.rules { 28 | if rule.matches(request, &header) { 29 | return true 30 | } 31 | } 32 | return false 33 | }) 34 | wrapper.maximumBufferSize = instance.maximumBufferSize 35 | result, err := instance.next.ServeHTTP(wrapper, request) 36 | if wrapper.skipped { 37 | return result, err 38 | } 39 | var logError error 40 | if err != nil { 41 | var ok bool 42 | // This handles https://github.com/echocat/caddy-filter/issues/4 43 | // If the fastcgi module is used and the FastCGI server produces log output 44 | // this is send (by the FastCGI module) as an error. We have to check this and 45 | // handle this case of error in a special way. 46 | if logError, ok = err.(fastcgi.LogError); !ok { 47 | return result, err 48 | } 49 | } 50 | if !wrapper.isInterceptingRequired() || !wrapper.isBodyAllowed() { 51 | wrapper.writeHeadersToDelegate(result) 52 | return result, logError 53 | } 54 | if !wrapper.isBodyAllowed() { 55 | return result, logError 56 | } 57 | header := wrapper.Header() 58 | var body []byte 59 | bodyRetrieved := false 60 | for _, rule := range instance.rules { 61 | if rule.matches(request, &header) { 62 | if !bodyRetrieved { 63 | body = wrapper.recordedAndDecodeIfRequired() 64 | bodyRetrieved = true 65 | } 66 | body = rule.execute(request, &header, body) 67 | } 68 | } 69 | var n int 70 | if bodyRetrieved { 71 | oldContentLength := wrapper.Header().Get("Content-Length") 72 | if len(oldContentLength) > 0 { 73 | newContentLength := strconv.Itoa(len(body)) 74 | wrapper.Header().Set("Content-Length", newContentLength) 75 | } 76 | n, err = wrapper.writeToDelegateAndEncodeIfRequired(body, result) 77 | } else { 78 | n, err = wrapper.writeRecordedToDelegate(result) 79 | } 80 | if err != nil { 81 | return result, err 82 | } 83 | if n < len(body) { 84 | return result, io.ErrShortWrite 85 | } 86 | return result, logError 87 | } 88 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/caddyserver/caddy/caddyhttp/fastcgi" 7 | . "gopkg.in/check.v1" 8 | "net/http" 9 | "regexp" 10 | ) 11 | 12 | type filterTest struct { 13 | request *http.Request 14 | writer *mockResponseWriter 15 | nextHandler *mockHandler 16 | handler *filterHandler 17 | } 18 | 19 | func init() { 20 | Suite(&filterTest{}) 21 | } 22 | 23 | func (s *filterTest) SetUpTest(c *C) { 24 | s.request = &http.Request{ 25 | URL: testUrl1, 26 | } 27 | s.writer = newMockResponseWriter() 28 | s.nextHandler = newMockHandler("Hello world!", 200) 29 | s.handler = &filterHandler{ 30 | next: s.nextHandler, 31 | rules: []*rule{ 32 | { 33 | path: regexp.MustCompile(".*\\.html"), 34 | searchPattern: regexp.MustCompile("w(.)rld"), 35 | replacement: []byte("2nd is '{1}'"), 36 | }, 37 | }, 38 | maximumBufferSize: defaultMaxBufferSize, 39 | } 40 | } 41 | func (s *filterTest) Test_withFiltering(c *C) { 42 | status, err := s.handler.ServeHTTP(s.writer, s.request) 43 | c.Assert(err, IsNil) 44 | c.Assert(status, Equals, 200) 45 | c.Assert(s.writer.buffer.String(), Equals, "Hello 2nd is 'o'!") 46 | } 47 | 48 | func (s *filterTest) Test_withBufferOverflow(c *C) { 49 | s.handler.maximumBufferSize = 5 50 | status, err := s.handler.ServeHTTP(s.writer, s.request) 51 | c.Assert(err, IsNil) 52 | c.Assert(status, Equals, 200) 53 | c.Assert(s.writer.buffer.String(), Equals, "Hello world!") 54 | } 55 | 56 | func (s *filterTest) Test_withoutFiltering(c *C) { 57 | s.request.URL = testUrl2 58 | status, err := s.handler.ServeHTTP(s.writer, s.request) 59 | c.Assert(err, IsNil) 60 | c.Assert(status, Equals, 200) 61 | c.Assert(s.writer.buffer.String(), Equals, "Hello world!") 62 | } 63 | 64 | func (s *filterTest) Test_withErrorInNext(c *C) { 65 | s.nextHandler.error = errors.New("Oops") 66 | status, err := s.handler.ServeHTTP(s.writer, s.request) 67 | c.Assert(err, DeepEquals, s.nextHandler.error) 68 | c.Assert(status, Equals, 200) 69 | c.Assert(s.writer.buffer.String(), Equals, "") 70 | } 71 | 72 | // This handles the bug https://github.com/echocat/caddy-filter/issues/4 73 | // See filter.go for more details. 74 | func (s *filterTest) Test_withLogErrorInNext(c *C) { 75 | s.nextHandler.error = fastcgi.LogError("Oops") 76 | status, err := s.handler.ServeHTTP(s.writer, s.request) 77 | c.Assert(err, DeepEquals, s.nextHandler.error) 78 | c.Assert(status, Equals, 200) 79 | c.Assert(s.writer.buffer.String(), Equals, "Hello 2nd is 'o'!") 80 | } 81 | 82 | func (s *filterTest) Test_withErrorInWriter(c *C) { 83 | s.writer.error = errors.New("Oops") 84 | status, err := s.handler.ServeHTTP(s.writer, s.request) 85 | c.Assert(err, DeepEquals, s.writer.error) 86 | c.Assert(status, Equals, 200) 87 | c.Assert(s.writer.buffer.String(), Equals, "") 88 | } 89 | 90 | /////////////////////////////////////////////////////////////////////////////////////////// 91 | // MOCKS 92 | /////////////////////////////////////////////////////////////////////////////////////////// 93 | 94 | func newMockHandler(response string, status int) *mockHandler { 95 | result := new(mockHandler) 96 | result.response = response 97 | result.status = status 98 | return result 99 | } 100 | 101 | type mockHandler struct { 102 | response string 103 | status int 104 | error error 105 | } 106 | 107 | func (instance mockHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) (int, error) { 108 | writer.WriteHeader(instance.status) 109 | toReturn := []byte(instance.response) 110 | if len(toReturn) < 2 { 111 | return 0, fmt.Errorf("Response is too short: %v", toReturn) 112 | } 113 | middle := len(toReturn) / 2 114 | part1 := toReturn[:middle] 115 | part2 := toReturn[middle:] 116 | written, err := writer.Write(part1) 117 | if err != nil { 118 | return 0, err 119 | } 120 | if len(part1) != written { 121 | return 0, fmt.Errorf("Part 1 (%v) of response (%v) length was not written. Expected bytes written %v but got %v.", part1, toReturn, len(part1), written) 122 | } 123 | written, err = writer.Write(part2) 124 | if err != nil { 125 | return 0, err 126 | } 127 | if len(part2) != written { 128 | return 0, fmt.Errorf("Part 2 (%v) of response (%v) length was not written. Expected bytes written %v but got %v.", part2, toReturn, len(part2), written) 129 | } 130 | return instance.status, instance.error 131 | } 132 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/echocat/caddy-filter 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/NYTimes/gziphandler v1.1.1 7 | github.com/caddyserver/caddy v1.0.1 8 | github.com/echocat/gocheck-addons v0.0.0-20170127185256-3597b4964e95 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= 4 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 5 | github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115 h1:fUjoj2bT6dG8LoEe+uNsKk8J+sLkDbQkJnB6Z1F02Bc= 6 | github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU= 7 | github.com/caddyserver/caddy v1.0.1 h1:oor6ep+8NoJOabpFXhvjqjfeldtw1XSzfISVrbfqTKo= 8 | github.com/caddyserver/caddy v1.0.1/go.mod h1:G+ouvOY32gENkJC+jhgl62TyhvqEsFaDiZ4uw0RzP1E= 9 | github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= 10 | github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 11 | github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9 h1:a1zrFsLFac2xoM6zG1u72DWJwZG3ayttYLfmLbxVETk= 12 | github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= 13 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 16 | github.com/echocat/gocheck-addons v0.0.0-20170127185256-3597b4964e95 h1:5ESCLoHOP65wmP5xHZcLtLDQfYgjqEMgphLuX5Bnv4k= 17 | github.com/echocat/gocheck-addons v0.0.0-20170127185256-3597b4964e95/go.mod h1:JTou1m4P0UzXC5tXVmE6IrqO/sdgZI8+BARMFd+SZzQ= 18 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 19 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 20 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 21 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 22 | github.com/go-acme/lego v2.5.0+incompatible h1:5fNN9yRQfv8ymH3DSsxla+4aYeQt2IgfZqHKVnK8f0s= 23 | github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= 24 | github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= 25 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 26 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 27 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 29 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 30 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 31 | github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= 32 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 33 | github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 h1:UnszMmmmm5vLwWzDjTFVIkfhvWF1NdrmChl8L2NUDCw= 34 | github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 35 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 36 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 37 | github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a h1:BcF8coBl0QFVhe8vAMMlD+CV8EISiu9MGKLoj6ZEyJA= 38 | github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= 39 | github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= 40 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 41 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= 42 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 43 | github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f h1:sSeNEkJrs+0F9TUau0CgWTTNEwF23HST3Eq0A+QIx+A= 44 | github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04= 45 | github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk= 46 | github.com/lucas-clemente/quic-go v0.10.2 h1:iQtTSZVbd44k94Lu0U16lLBIG3lrnjDvQongjPd4B/s= 47 | github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao= 48 | github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced h1:zqEC1GJZFbGZA0tRyNZqRjep92K5fujFtFsu5ZW7Aug= 49 | github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58= 50 | github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= 51 | github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2 h1:xKE9kZ5C8gelJC3+BNM6LJs1x21rivK7yxfTZMAuY2s= 52 | github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= 53 | github.com/miekg/dns v1.1.3 h1:1g0r1IvskvgL8rR+AcHzUA+oFmGcQlaIm4IqakufeMM= 54 | github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 55 | github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= 56 | github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= 57 | github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8= 58 | github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= 59 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 60 | github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= 61 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 62 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 63 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 64 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 65 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4 h1:S9YlS71UNJIyS61OqGAmLXv3w5zclSidN+qwr80XxKs= 67 | github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 68 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 69 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 70 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 71 | golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 72 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 73 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 74 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 75 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 76 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 77 | golang.org/x/net v0.0.0-20190328230028-74de082e2cca h1:hyA6yiAgbUwuWqtscNvWAI7U1CtlaD1KilQ6iudt1aI= 78 | golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 79 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 80 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 81 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 82 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 83 | golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 84 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 85 | golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2Vd4msMcrDECFxS+tL9c= 86 | golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 87 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 88 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 89 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 90 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 91 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 92 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 93 | gopkg.in/mcuadros/go-syslog.v2 v2.2.1 h1:60g8zx1BijSVSgLTzLCW9UC4/+i1Ih9jJ1DR5Tgp9vE= 94 | gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= 95 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 96 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 97 | gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA= 98 | gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 99 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 100 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 101 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 102 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 103 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 104 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/caddyserver/caddy" 5 | "github.com/caddyserver/caddy/caddyhttp/httpserver" 6 | "io/ioutil" 7 | "os" 8 | "regexp" 9 | "strconv" 10 | ) 11 | 12 | func init() { 13 | caddy.RegisterPlugin("filter", caddy.Plugin{ 14 | ServerType: "http", 15 | Action: setup, 16 | }) 17 | } 18 | 19 | func setup(controller *caddy.Controller) error { 20 | handler, err := parseConfiguration(controller) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | config := httpserver.GetConfig(controller) 26 | config.AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 27 | handler.next = next 28 | return handler 29 | }) 30 | 31 | return nil 32 | } 33 | 34 | func parseConfiguration(controller *caddy.Controller) (*filterHandler, error) { 35 | handler := new(filterHandler) 36 | handler.rules = []*rule{} 37 | handler.maximumBufferSize = defaultMaxBufferSize 38 | 39 | for controller.Next() { 40 | err := evalFilterBlock(controller, handler) 41 | if err != nil { 42 | return nil, err 43 | } 44 | } 45 | 46 | if len(handler.rules) <= 0 { 47 | return nil, controller.Err("No rule block provided.") 48 | } 49 | return handler, nil 50 | } 51 | 52 | func evalFilterBlock(controller *caddy.Controller, target *filterHandler) error { 53 | args := controller.RemainingArgs() 54 | if len(args) == 0 { 55 | return evalDefaultFilterBlock(controller, target) 56 | } 57 | return evalNamedBlock(controller, args, target) 58 | } 59 | 60 | func evalDefaultFilterBlock(controller *caddy.Controller, target *filterHandler) error { 61 | for controller.NextBlock() { 62 | args := []string{controller.Val()} 63 | args = append(args, controller.RemainingArgs()...) 64 | 65 | err := evalNamedBlock(controller, args, target) 66 | if err != nil { 67 | return err 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | func evalNamedBlock(controller *caddy.Controller, args []string, target *filterHandler) error { 74 | switch args[0] { 75 | case "rule": 76 | return evalRule(controller, args[1:], target) 77 | case "max_buffer_size": 78 | return evalMaximumBufferSize(controller, args[1:], target) 79 | } 80 | return controller.Errf("Unknown directive: %v", args[0]) 81 | } 82 | 83 | func evalRule(controller *caddy.Controller, args []string, target *filterHandler) (err error) { 84 | if len(args) > 0 { 85 | return controller.Errf("No more arguments for filter block 'rule' supported.") 86 | } 87 | targetRule := new(rule) 88 | targetRule.pathAndContentTypeCombination = pathAndContentTypeAndCombination 89 | for controller.NextBlock() { 90 | optionName := controller.Val() 91 | switch optionName { 92 | case "path": 93 | err = evalPath(controller, targetRule) 94 | case "content_type": 95 | err = evalContentType(controller, targetRule) 96 | case "path_content_type_combination": 97 | err = evalPathAndContentTypeCombination(controller, targetRule) 98 | case "search_pattern": 99 | err = evalSearchPattern(controller, targetRule) 100 | case "replacement": 101 | err = evalReplacement(controller, targetRule) 102 | default: 103 | err = controller.Errf("Unknown option: %v", optionName) 104 | } 105 | if err != nil { 106 | return err 107 | } 108 | } 109 | if targetRule.path == nil && targetRule.contentType == nil { 110 | return controller.Errf("Neither 'path' nor 'content_type' definition was provided for filter rule block.") 111 | } 112 | if targetRule.searchPattern == nil { 113 | return controller.Errf("No 'search_pattern' definition was provided for filter rule block.") 114 | } 115 | target.rules = append(target.rules, targetRule) 116 | return nil 117 | } 118 | 119 | func evalPath(controller *caddy.Controller, target *rule) error { 120 | return evalRegexpOption(controller, func(value *regexp.Regexp) error { 121 | target.path = value 122 | return nil 123 | }) 124 | } 125 | 126 | func evalContentType(controller *caddy.Controller, target *rule) error { 127 | return evalRegexpOption(controller, func(value *regexp.Regexp) error { 128 | target.contentType = value 129 | return nil 130 | }) 131 | } 132 | 133 | func evalPathAndContentTypeCombination(controller *caddy.Controller, target *rule) error { 134 | return evalSimpleOption(controller, func(plainValue string) error { 135 | for _, candidate := range possiblePathAndContentTypeCombination { 136 | if string(candidate) == plainValue { 137 | target.pathAndContentTypeCombination = candidate 138 | return nil 139 | } 140 | } 141 | return controller.Errf("Illegal value for 'path_content_type_combination': %v", plainValue) 142 | }) 143 | } 144 | 145 | func evalSearchPattern(controller *caddy.Controller, target *rule) error { 146 | return evalRegexpOption(controller, func(value *regexp.Regexp) error { 147 | target.searchPattern = value 148 | return nil 149 | }) 150 | } 151 | 152 | func evalReplacement(controller *caddy.Controller, target *rule) error { 153 | return evalSimpleOption(controller, func(value string) error { 154 | target.replacement = []byte(value) 155 | if len(target.replacement) > 1 && target.replacement[0] == '@' { 156 | targetFilename := string(target.replacement[1:]) 157 | content, err := ioutil.ReadFile(targetFilename) 158 | if err != nil { 159 | if !os.IsNotExist(err) { 160 | return controller.Errf("Could not read file provided in 'replacement' definition. Got: %v", err) 161 | } 162 | } else { 163 | target.replacement = content 164 | } 165 | } 166 | return nil 167 | }) 168 | } 169 | 170 | func evalSimpleOption(controller *caddy.Controller, setter func(string) error) error { 171 | args := controller.RemainingArgs() 172 | if len(args) != 1 { 173 | return controller.ArgErr() 174 | } 175 | return setter(args[0]) 176 | } 177 | 178 | func evalRegexpOption(controller *caddy.Controller, setter func(*regexp.Regexp) error) error { 179 | return evalSimpleOption(controller, func(plainValue string) error { 180 | value, err := regexp.Compile(plainValue) 181 | if err != nil { 182 | return err 183 | } 184 | return setter(value) 185 | }) 186 | } 187 | 188 | func evalMaximumBufferSize(controller *caddy.Controller, args []string, target *filterHandler) (err error) { 189 | if len(args) != 1 { 190 | return controller.Errf("There are exact one argument for filter directive 'max_buffer_size' expected.") 191 | } 192 | value, err := strconv.Atoi(args[0]) 193 | if err != nil { 194 | return controller.Errf("There is no valid value for filter directive 'max_buffer_size' provided. Got: %v", err) 195 | } 196 | target.maximumBufferSize = value 197 | return nil 198 | } 199 | -------------------------------------------------------------------------------- /init_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "errors" 5 | "github.com/caddyserver/caddy" 6 | "github.com/caddyserver/caddy/caddyhttp/httpserver" 7 | . "gopkg.in/check.v1" 8 | "regexp" 9 | "regexp/syntax" 10 | ) 11 | 12 | type initTest struct{} 13 | 14 | func init() { 15 | Suite(&initTest{}) 16 | } 17 | 18 | func (s *initTest) Test_setup(c *C) { 19 | controller := s.newControllerFor("filter rule {\npath myPath\ncontent_type myContentType\nsearch_pattern mySearchPattern\nreplacement myReplacement\n}\n") 20 | err := setup(controller) 21 | c.Assert(err, IsNil) 22 | config := httpserver.GetConfig(controller) 23 | middlewares := config.Middleware() 24 | c.Assert(len(middlewares), Equals, 1) 25 | handler, ok := middlewares[0](newMockHandler("moo", 200)).(*filterHandler) 26 | c.Assert(ok, Equals, true) 27 | c.Assert(len(handler.rules), Equals, 1) 28 | r := handler.rules[0] 29 | c.Assert(r.path.String(), Equals, "myPath") 30 | c.Assert(r.contentType.String(), Equals, "myContentType") 31 | c.Assert(r.pathAndContentTypeCombination, Equals, pathAndContentTypeAndCombination) 32 | c.Assert(r.searchPattern.String(), Equals, "mySearchPattern") 33 | c.Assert(string(r.replacement), Equals, "myReplacement") 34 | } 35 | 36 | func (s *initTest) Test_parseConfiguration_withPathAndContentTypeCombination(c *C) { 37 | controller := s.newControllerFor("filter rule {\npath myPath\ncontent_type myContentType\nsearch_pattern mySearchPattern\nreplacement myReplacement\n}\n") 38 | err := setup(controller) 39 | c.Assert(err, IsNil) 40 | handler, ok := httpserver.GetConfig(controller).Middleware()[0](newMockHandler("moo", 200)).(*filterHandler) 41 | c.Assert(ok, Equals, true) 42 | c.Assert(len(handler.rules), Equals, 1) 43 | c.Assert(handler.rules[0].pathAndContentTypeCombination, Equals, pathAndContentTypeAndCombination) 44 | 45 | controller = s.newControllerFor("filter rule {\npath_content_type_combination or\npath myPath\ncontent_type myContentType\nsearch_pattern mySearchPattern\n}\n") 46 | err = setup(controller) 47 | c.Assert(err, IsNil) 48 | handler, ok = httpserver.GetConfig(controller).Middleware()[0](newMockHandler("moo", 200)).(*filterHandler) 49 | c.Assert(ok, Equals, true) 50 | c.Assert(len(handler.rules), Equals, 1) 51 | c.Assert(handler.rules[0].pathAndContentTypeCombination, Equals, pathAndContentTypeOrCombination) 52 | 53 | controller = s.newControllerFor("filter rule {\npath_content_type_combination and\npath myPath\ncontent_type myContentType\nsearch_pattern mySearchPattern\n}\n") 54 | err = setup(controller) 55 | c.Assert(err, IsNil) 56 | handler, ok = httpserver.GetConfig(controller).Middleware()[0](newMockHandler("moo", 200)).(*filterHandler) 57 | c.Assert(ok, Equals, true) 58 | c.Assert(len(handler.rules), Equals, 1) 59 | c.Assert(handler.rules[0].pathAndContentTypeCombination, Equals, pathAndContentTypeAndCombination) 60 | 61 | controller = s.newControllerFor("filter rule {\npath_content_type_combination foo\npath myPath\ncontent_type myContentType\nsearch_pattern mySearchPattern\n}\n") 62 | err = setup(controller) 63 | c.Assert(err, NotNil) 64 | c.Assert(err.Error(), Equals, "Testfile:2 - Error during parsing: Illegal value for 'path_content_type_combination': foo") 65 | } 66 | 67 | func (s *initTest) Test_parseConfiguration_directNamed(c *C) { 68 | handler, err := parseConfiguration(s.newControllerFor("filter rule {\npath myPath\ncontent_type myContentType\nsearch_pattern mySearchPattern\nreplacement myReplacement\n}\n")) 69 | c.Assert(err, IsNil) 70 | c.Assert(len(handler.rules), Equals, 1) 71 | r := handler.rules[0] 72 | c.Assert(r.path.String(), Equals, "myPath") 73 | c.Assert(r.contentType.String(), Equals, "myContentType") 74 | c.Assert(r.searchPattern.String(), Equals, "mySearchPattern") 75 | c.Assert(string(r.replacement), Equals, "myReplacement") 76 | 77 | handler, err = parseConfiguration(s.newControllerFor( 78 | "filter rule {\npath myPath\nsearch_pattern mySearchPattern\n}\n" + 79 | "filter rule {\npath myPath2\nsearch_pattern mySearchPattern2\n}\n" + 80 | "filter max_buffer_size 666\n"), 81 | ) 82 | c.Assert(err, IsNil) 83 | c.Assert(len(handler.rules), Equals, 2) 84 | r = handler.rules[0] 85 | c.Assert(r.path.String(), Equals, "myPath") 86 | c.Assert(r.searchPattern.String(), Equals, "mySearchPattern") 87 | r = handler.rules[1] 88 | c.Assert(r.path.String(), Equals, "myPath2") 89 | c.Assert(r.searchPattern.String(), Equals, "mySearchPattern2") 90 | c.Assert(handler.maximumBufferSize, Equals, 666) 91 | } 92 | 93 | func (s *initTest) Test_parseConfiguration_withReplacementFromFile(c *C) { 94 | handler, err := parseConfiguration(s.newControllerFor("filter rule {\npath myPath\ncontent_type myContentType\nsearch_pattern mySearchPattern\nreplacement @resources/test/testReplacement\n}\n")) 95 | c.Assert(err, IsNil) 96 | c.Assert(len(handler.rules), Equals, 1) 97 | r := handler.rules[0] 98 | c.Assert(r.path.String(), Equals, "myPath") 99 | c.Assert(r.contentType.String(), Equals, "myContentType") 100 | c.Assert(r.searchPattern.String(), Equals, "mySearchPattern") 101 | c.Assert(string(r.replacement), Equals, "Replacement from file.\n") 102 | } 103 | 104 | func (s *initTest) Test_evalSimpleOption(c *C) { 105 | err := evalSimpleOption(s.newControllerFor("\"my value\""), func(value string) error { 106 | c.Assert(value, Equals, "my value") 107 | return nil 108 | }) 109 | c.Assert(err, IsNil) 110 | 111 | err = evalSimpleOption(s.newControllerFor(""), func(value string) error { 112 | c.Error("This method should not be called.") 113 | return nil 114 | }) 115 | c.Assert(err, DeepEquals, errors.New("Testfile:1 - Error during parsing: Wrong argument count or unexpected line ending after 'start'")) 116 | } 117 | 118 | func (s *initTest) Test_evalRegexpOption(c *C) { 119 | err := evalRegexpOption(s.newControllerFor("f.*bar"), func(value *regexp.Regexp) error { 120 | c.Assert(value.MatchString("foobar"), Equals, true) 121 | return nil 122 | }) 123 | c.Assert(err, IsNil) 124 | 125 | err = evalRegexpOption(s.newControllerFor("Hello replaced world!") 154 | } 155 | 156 | func (s *integrationTest) Test_fastcgiWithGzip(c *C) { 157 | s.fcgiServer = test.NewTestingFcgiServer(22791) 158 | 159 | resp, err := http.Get("http://localhost:22781/index.cgi") 160 | c.Assert(err, IsNil) 161 | 162 | defer resp.Body.Close() 163 | content, err := ioutil.ReadAll(resp.Body) 164 | c.Assert(err, IsNil) 165 | c.Assert(string(content), Contains, "Hello replaced world!") 166 | } 167 | 168 | func (s *integrationTest) Test_fastcgiWithRedirect(c *C) { 169 | s.fcgiServer = test.NewTestingFcgiServer(22792) 170 | 171 | client := http.Client{ 172 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 173 | return http.ErrUseLastResponse 174 | }, 175 | } 176 | req, err := http.NewRequest("GET", "http://localhost:22782/redirect.cgi", nil) 177 | 178 | c.Assert(err, IsNil) 179 | resp, err := client.Do(req) 180 | c.Assert(err, IsNil) 181 | 182 | defer resp.Body.Close() 183 | content, err := ioutil.ReadAll(resp.Body) 184 | c.Assert(err, IsNil) 185 | c.Assert(resp.StatusCode, Equals, 301) 186 | c.Assert(resp.Status, Equals, "301 Moved Permanently") 187 | c.Assert(resp.Header.Get("Location"), Equals, "/another.cgi") 188 | c.Assert(string(content), Equals, "Moved Permanently.") 189 | 190 | resp2, err := http.Get("http://caddyserver.com") 191 | c.Assert(err, IsNil) 192 | defer resp2.Body.Close() 193 | content, err = ioutil.ReadAll(resp2.Body) 194 | c.Assert(err, IsNil) 195 | c.Assert(resp.StatusCode, Equals, 301) 196 | c.Assert(string(content), Contains, "Caddy - ") 197 | 198 | resp3, err := http.Get("http://localhost:22782/redirect.cgi") 199 | c.Assert(err, IsNil) 200 | defer resp3.Body.Close() 201 | content, err = ioutil.ReadAll(resp3.Body) 202 | c.Assert(err, IsNil) 203 | c.Assert(resp.StatusCode, Equals, 301) 204 | c.Assert(string(content), Contains, "<title>Replaced another!") 205 | } 206 | 207 | func (s *integrationTest) Test_markdown(c *C) { 208 | resp, err := http.Get("http://localhost:22783/index.md") 209 | c.Assert(err, IsNil) 210 | 211 | defer resp.Body.Close() 212 | content, err := ioutil.ReadAll(resp.Body) 213 | c.Assert(err, IsNil) 214 | c.Assert(string(content), Contains, "Hello replaced world!") 215 | } 216 | 217 | func (s *integrationTest) Test_markdownWithGzip(c *C) { 218 | resp, err := http.Get("http://localhost:22784/index.md") 219 | c.Assert(err, IsNil) 220 | 221 | defer resp.Body.Close() 222 | content, err := ioutil.ReadAll(resp.Body) 223 | c.Assert(err, IsNil) 224 | c.Assert(string(content), Contains, "Hello replaced world!") 225 | } 226 | 227 | func (s *integrationTest) SetUpTest(c *C) { 228 | c.Check(s.httpServer, IsNil) 229 | c.Check(s.fcgiServer, IsNil) 230 | c.Check(s.caddy, IsNil) 231 | s.caddy = test.NewTestingCaddy(fmt.Sprintf("%s.conf", c.TestName())) 232 | } 233 | 234 | func (s *integrationTest) TearDownTest(c *C) { 235 | if s.httpServer != nil { 236 | s.httpServer.Close() 237 | } 238 | if s.fcgiServer != nil { 239 | s.fcgiServer.Close() 240 | } 241 | if s.caddy != nil { 242 | s.caddy.Close() 243 | } 244 | s.httpServer = nil 245 | s.fcgiServer = nil 246 | s.caddy = nil 247 | } 248 | 249 | func (s *integrationTest) getWithEtag(url string, etag string) (*http.Response, error) { 250 | req, err := http.NewRequest("GET", url, nil) 251 | if err != nil { 252 | return nil, err 253 | } 254 | req.Header.Set("If-None-Match", etag) 255 | resp, err := http.DefaultClient.Do(req) 256 | return resp, err 257 | } 258 | -------------------------------------------------------------------------------- /package_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | "testing" 6 | ) 7 | 8 | func Test(t *testing.T) { 9 | TestingT(t) 10 | } 11 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_fastcgi.conf: -------------------------------------------------------------------------------- 1 | :22780 { 2 | tls off 3 | errors stdout 4 | filter rule { 5 | content_type "text/html.*" 6 | search_pattern "Hello world!" 7 | replacement "Hello replaced world!" 8 | } 9 | fastcgi / 127.0.0.1:22790 { 10 | ext .cgi 11 | split .cgi 12 | index index.cgi 13 | root resources/test/integrationTest.Test_fastcgi 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_fastcgiWithGzip.conf: -------------------------------------------------------------------------------- 1 | :22781 { 2 | tls off 3 | errors stdout 4 | gzip 5 | filter rule { 6 | content_type "text/html.*" 7 | search_pattern "Hello world!" 8 | replacement "Hello replaced world!" 9 | } 10 | fastcgi / 127.0.0.1:22791 { 11 | ext .cgi 12 | split .cgi 13 | index index.cgi 14 | root resources/test/integrationTest.Test_fastcgi 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_fastcgiWithRedirect.conf: -------------------------------------------------------------------------------- 1 | :22782 { 2 | tls off 3 | errors stdout 4 | filter rule { 5 | content_type "text/html.*" 6 | search_pattern "I'am another!" 7 | replacement "Replaced another!" 8 | } 9 | fastcgi / 127.0.0.1:22792 { 10 | ext .cgi 11 | split .cgi 12 | index index.cgi 13 | root resources/test/integrationTest.Test_fastcgi 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_markdown.conf: -------------------------------------------------------------------------------- 1 | :22783 { 2 | tls off 3 | root resources/test/integrationTest.Test_markdown 4 | markdown 5 | filter rule { 6 | content_type "text/html.*" 7 | search_pattern "Hello world!" 8 | replacement "Hello replaced world!" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_markdown/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world! 4 | 5 | 6 |

Hello world!

7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_markdownWithGzip.conf: -------------------------------------------------------------------------------- 1 | :22784 { 2 | tls off 3 | gzip 4 | root resources/test/integrationTest.Test_markdown 5 | markdown 6 | filter rule { 7 | content_type "text/html.*" 8 | search_pattern "Hello world!" 9 | replacement "Hello replaced world!" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_proxy.conf: -------------------------------------------------------------------------------- 1 | :22785 { 2 | tls off 3 | filter rule { 4 | content_type "text/plain.*" 5 | search_pattern "Hello world!" 6 | replacement "Hello replaced world!" 7 | } 8 | proxy / localhost:22775 9 | } 10 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_proxyWithGzip.conf: -------------------------------------------------------------------------------- 1 | :22786 { 2 | tls off 3 | gzip 4 | filter rule { 5 | content_type "text/plain.*" 6 | search_pattern "Hello world!" 7 | replacement "Hello replaced world!" 8 | } 9 | proxy / localhost:22776 10 | } 11 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_proxyWithGzipUpstream.conf: -------------------------------------------------------------------------------- 1 | :22789 { 2 | tls off 3 | filter rule { 4 | content_type "text/plain.*" 5 | search_pattern "Hello world!" 6 | replacement "Hello replaced world!" 7 | } 8 | proxy / localhost:22777 9 | } 10 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_redir.conf: -------------------------------------------------------------------------------- 1 | :22791 { 2 | tls off 3 | redir /text.txt /new 302 4 | filter rule { 5 | content_type "text/plain.*" 6 | search_pattern "Found" 7 | replacement "Hello world!" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_static.conf: -------------------------------------------------------------------------------- 1 | :22787 { 2 | tls off 3 | root resources/test/integrationTest.Test_static 4 | filter rule { 5 | content_type "text/plain.*" 6 | search_pattern "Hello world!" 7 | replacement "Hello replaced world!" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_static/text.txt: -------------------------------------------------------------------------------- 1 | Hello world! 2 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_static/utf8.txt: -------------------------------------------------------------------------------- 1 | 电影 2 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_staticWithBasicAuth.conf: -------------------------------------------------------------------------------- 1 | :22790 { 2 | tls off 3 | root resources/test/integrationTest.Test_static 4 | basicauth / user password 5 | filter rule { 6 | content_type "text/plain.*" 7 | search_pattern "Hello world!" 8 | replacement "Hello replaced world!" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_staticWithGzip.conf: -------------------------------------------------------------------------------- 1 | :22788 { 2 | tls off 3 | root resources/test/integrationTest.Test_static 4 | filter rule { 5 | content_type "text/plain.*" 6 | search_pattern "Hello world!" 7 | replacement "Hello replaced world!" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /resources/test/integrationTest.Test_staticWithUtf8.conf: -------------------------------------------------------------------------------- 1 | :22792 { 2 | tls off 3 | root resources/test/integrationTest.Test_static 4 | filter rule { 5 | content_type "text/plain.*" 6 | search_pattern "电影" 7 | replacement "电视" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /resources/test/testReplacement: -------------------------------------------------------------------------------- 1 | Replacement from file. 2 | -------------------------------------------------------------------------------- /responseWriterWrapper.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/gzip" 7 | "errors" 8 | "io/ioutil" 9 | "net" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/caddyserver/caddy/caddyhttp/httpserver" 14 | ) 15 | 16 | func newResponseWriterWrapperFor(delegate http.ResponseWriter, beforeFirstWrite func(*responseWriterWrapper) bool) *responseWriterWrapper { 17 | wrapper := &responseWriterWrapper{ 18 | skipped: false, 19 | delegate: delegate, 20 | beforeFirstWrite: beforeFirstWrite, 21 | statusSetAtDelegate: 0, 22 | bodyAllowed: true, 23 | maximumBufferSize: -1, 24 | header: http.Header{}, 25 | } 26 | for key, values := range delegate.Header() { 27 | for i, value := range values { 28 | if i == 0 { 29 | wrapper.header.Set(key, value) 30 | } else { 31 | wrapper.header.Add(key, value) 32 | } 33 | } 34 | } 35 | return wrapper 36 | } 37 | 38 | type responseWriterWrapper struct { 39 | skipped bool 40 | delegate http.ResponseWriter 41 | buffer *bytes.Buffer 42 | beforeFirstWrite func(*responseWriterWrapper) bool 43 | bodyAllowed bool 44 | firstContentWritten bool 45 | headerSetAtDelegate bool 46 | statusSetAtDelegate int 47 | maximumBufferSize int 48 | header http.Header 49 | } 50 | 51 | func (instance *responseWriterWrapper) Header() http.Header { 52 | if instance.skipped { 53 | return instance.delegate.Header() 54 | } 55 | return instance.header 56 | } 57 | 58 | func (instance *responseWriterWrapper) WriteHeader(status int) { 59 | if instance.skipped { 60 | instance.delegate.WriteHeader(status) 61 | } 62 | instance.bodyAllowed = bodyAllowedForStatus(status) 63 | instance.statusSetAtDelegate = status 64 | } 65 | 66 | func (instance *responseWriterWrapper) Write(content []byte) (int, error) { 67 | if instance.skipped { 68 | return instance.delegate.Write(content) 69 | } 70 | 71 | if len(content) <= 0 { 72 | return 0, nil 73 | } 74 | 75 | if !instance.firstContentWritten { 76 | if instance.beforeFirstWrite(instance) { 77 | instance.buffer = new(bytes.Buffer) 78 | } else { 79 | instance.skipped = true 80 | instance.buffer = nil 81 | } 82 | instance.firstContentWritten = true 83 | } 84 | 85 | if instance.buffer == nil { 86 | if err := instance.writeHeadersToDelegate(200); err != nil { 87 | return 0, err 88 | } 89 | return instance.delegate.Write(content) 90 | } 91 | 92 | if (instance.maximumBufferSize >= 0) && 93 | ((instance.buffer.Len() + len(content)) > instance.maximumBufferSize) { 94 | _, err := instance.delegate.Write(instance.buffer.Bytes()) 95 | if err != nil { 96 | return 0, err 97 | } 98 | instance.buffer = nil 99 | return instance.delegate.Write(content) 100 | } 101 | 102 | return instance.buffer.Write(content) 103 | } 104 | 105 | func (instance *responseWriterWrapper) selectStatus(def int) int { 106 | if instance.statusSetAtDelegate > 0 { 107 | return instance.statusSetAtDelegate 108 | } 109 | if def > 0 { 110 | return def 111 | } 112 | return 200 113 | } 114 | 115 | func (instance *responseWriterWrapper) writeToDelegate(content []byte, defStatus int) (int, error) { 116 | if !instance.headerSetAtDelegate { 117 | err := instance.writeHeadersToDelegate(defStatus) 118 | if err != nil { 119 | return 0, err 120 | } 121 | } 122 | return instance.delegate.Write(content) 123 | } 124 | 125 | func (instance *responseWriterWrapper) writeRecordedToDelegate(defStatus int) (int, error) { 126 | recorded := instance.recorded() 127 | return instance.writeToDelegate(recorded, defStatus) 128 | } 129 | 130 | func (instance *responseWriterWrapper) writeToDelegateAndEncodeIfRequired(content []byte, defStatus int) (int, error) { 131 | if !instance.isGzipEncoded() { 132 | return instance.writeToDelegate(content, defStatus) 133 | } 134 | if !instance.headerSetAtDelegate { 135 | err := instance.writeHeadersToDelegate(defStatus) 136 | if err != nil { 137 | return 0, err 138 | } 139 | } 140 | writer, err := gzip.NewWriterLevel(instance.delegate, gzip.BestCompression) 141 | if err != nil { 142 | return instance.writeToDelegate(content, defStatus) 143 | } 144 | return writer.Write(content) 145 | } 146 | 147 | func (instance *responseWriterWrapper) writeHeadersToDelegate(defStatus int) error { 148 | if instance.headerSetAtDelegate { 149 | return errors.New("headers already set at response") 150 | } 151 | instance.headerSetAtDelegate = true 152 | w := instance.delegate 153 | for key, values := range instance.header { 154 | for i, value := range values { 155 | if i == 0 { 156 | w.Header().Set(key, value) 157 | } else { 158 | w.Header().Add(key, value) 159 | } 160 | } 161 | } 162 | w.WriteHeader(instance.selectStatus(defStatus)) 163 | return nil 164 | } 165 | 166 | func (instance *responseWriterWrapper) isBodyAllowed() bool { 167 | return instance.bodyAllowed 168 | } 169 | 170 | func (instance *responseWriterWrapper) isGzipEncoded() bool { 171 | contentEncoding := instance.Header().Get("Content-Encoding") 172 | return strings.ToLower(contentEncoding) == "gzip" 173 | } 174 | 175 | func (instance *responseWriterWrapper) wasSomethingRecorded() bool { 176 | return instance.buffer != nil && instance.buffer.Len() > 0 177 | } 178 | 179 | func (instance *responseWriterWrapper) isInterceptingRequired() bool { 180 | return !instance.skipped && instance.wasSomethingRecorded() 181 | } 182 | 183 | func (instance *responseWriterWrapper) recorded() []byte { 184 | buffer := instance.buffer 185 | if buffer == nil { 186 | return []byte{} 187 | } 188 | return buffer.Bytes() 189 | } 190 | 191 | // Hijack implements http.Hijacker. It simply wraps the underlying 192 | // ResponseWriter's Hijack method if there is one, or returns an error. 193 | func (instance *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { 194 | if hj, ok := instance.delegate.(http.Hijacker); ok { 195 | return hj.Hijack() 196 | } 197 | return nil, nil, httpserver.NonHijackerError{Underlying: instance.delegate} 198 | } 199 | 200 | // CloseNotify implements http.CloseNotifier. 201 | // It just inherits the underlying ResponseWriter's CloseNotify method. 202 | // It panics if the underlying ResponseWriter is not a CloseNotifier. 203 | func (instance *responseWriterWrapper) CloseNotify() <-chan bool { 204 | if cn, ok := instance.delegate.(http.CloseNotifier); ok { 205 | return cn.CloseNotify() 206 | } 207 | panic(httpserver.NonCloseNotifierError{Underlying: instance.delegate}) 208 | } 209 | 210 | func (instance *responseWriterWrapper) recordedAndDecodeIfRequired() []byte { 211 | result := instance.recorded() 212 | if !instance.isGzipEncoded() { 213 | return result 214 | } 215 | src := bytes.NewBuffer(result) 216 | gzipSrc, err := gzip.NewReader(src) 217 | if err != nil { 218 | return result 219 | } 220 | result, err = ioutil.ReadAll(gzipSrc) 221 | if err != nil { 222 | return result 223 | } 224 | instance.Header().Del("Content-Encoding") 225 | return result 226 | } 227 | 228 | func bodyAllowedForStatus(status int) bool { 229 | switch { 230 | case status >= 100 && status <= 199: 231 | return false 232 | case status == 204: 233 | return false 234 | case status == 304: 235 | return false 236 | } 237 | return true 238 | } 239 | -------------------------------------------------------------------------------- /responseWriterWrapper_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "bytes" 5 | . "gopkg.in/check.v1" 6 | "net/http" 7 | "reflect" 8 | ) 9 | 10 | type responseWriterWrapperTest struct{} 11 | 12 | func init() { 13 | Suite(&responseWriterWrapperTest{}) 14 | } 15 | 16 | func (s *responseWriterWrapperTest) Test_newResponseWriterWrapperFor(c *C) { 17 | original := newMockResponseWriter() 18 | beforeFirstWrite := func(*responseWriterWrapper) bool { 19 | return false 20 | } 21 | wrapper := newResponseWriterWrapperFor(original, beforeFirstWrite) 22 | c.Assert(wrapper.delegate, DeepEquals, original) 23 | c.Assert(wrapper.buffer, IsNil) 24 | c.Assert(reflect.ValueOf(wrapper.beforeFirstWrite), Equals, reflect.ValueOf(beforeFirstWrite)) 25 | c.Assert(wrapper.bodyAllowed, Equals, true) 26 | c.Assert(wrapper.firstContentWritten, Equals, false) 27 | } 28 | 29 | func (s *responseWriterWrapperTest) Test_Header(c *C) { 30 | original := newMockResponseWriter() 31 | original.header.Add("a", "1") 32 | original.header.Add("b", "2") 33 | wrapper := newResponseWriterWrapperFor(original, nil) 34 | 35 | c.Assert(wrapper.Header().Get("a"), Equals, "1") 36 | c.Assert(wrapper.Header().Get("b"), Equals, "2") 37 | c.Assert(wrapper.Header().Get("c"), Equals, "") 38 | 39 | wrapper.Header().Del("a") 40 | wrapper.Header().Add("c", "3") 41 | c.Assert(wrapper.Header().Get("a"), Equals, "") 42 | c.Assert(wrapper.Header().Get("c"), Equals, "3") 43 | } 44 | 45 | func (s *responseWriterWrapperTest) Test_WriteHeader(c *C) { 46 | original := newMockResponseWriter() 47 | wrapper := newResponseWriterWrapperFor(original, nil) 48 | 49 | c.Assert(original.status, Equals, 0) 50 | 51 | wrapper.WriteHeader(200) 52 | c.Assert(wrapper.statusSetAtDelegate, Equals, 200) 53 | c.Assert(original.status, Equals, 0) 54 | c.Assert(wrapper.bodyAllowed, Equals, true) 55 | 56 | wrapper.WriteHeader(204) 57 | c.Assert(wrapper.statusSetAtDelegate, Equals, 204) 58 | c.Assert(original.status, Equals, 0) 59 | c.Assert(wrapper.bodyAllowed, Equals, false) 60 | } 61 | 62 | func (s *responseWriterWrapperTest) Test_bodyAllowedForStatus(c *C) { 63 | c.Assert(bodyAllowedForStatus(200), Equals, true) 64 | c.Assert(bodyAllowedForStatus(208), Equals, true) 65 | c.Assert(bodyAllowedForStatus(404), Equals, true) 66 | c.Assert(bodyAllowedForStatus(500), Equals, true) 67 | c.Assert(bodyAllowedForStatus(503), Equals, true) 68 | for i := 100; i < 200; i++ { 69 | c.Assert(bodyAllowedForStatus(i), Equals, false) 70 | } 71 | c.Assert(bodyAllowedForStatus(204), Equals, false) 72 | c.Assert(bodyAllowedForStatus(304), Equals, false) 73 | } 74 | 75 | func (s *responseWriterWrapperTest) Test_WriteWithoutRecording(c *C) { 76 | beforeFirstWriteCalled := false 77 | original := newMockResponseWriter() 78 | beforeFirstWrite := func(*responseWriterWrapper) bool { 79 | beforeFirstWriteCalled = true 80 | return false 81 | } 82 | wrapper := newResponseWriterWrapperFor(original, beforeFirstWrite) 83 | len, err := wrapper.Write([]byte("")) 84 | c.Assert(len, Equals, 0) 85 | c.Assert(err, IsNil) 86 | c.Assert(wrapper.firstContentWritten, Equals, false) 87 | c.Assert(beforeFirstWriteCalled, Equals, false) 88 | 89 | len, err = wrapper.Write([]byte("foo")) 90 | c.Assert(len, Equals, 3) 91 | c.Assert(err, IsNil) 92 | c.Assert(wrapper.firstContentWritten, Equals, true) 93 | c.Assert(beforeFirstWriteCalled, Equals, true) 94 | 95 | len, err = wrapper.Write([]byte("bar")) 96 | c.Assert(len, Equals, 3) 97 | c.Assert(err, IsNil) 98 | 99 | c.Assert(original.buffer.Bytes(), DeepEquals, []byte("foobar")) 100 | c.Assert(wrapper.buffer, IsNil) 101 | c.Assert(wrapper.firstContentWritten, Equals, true) 102 | c.Assert(wrapper.isBodyAllowed(), Equals, true) 103 | c.Assert(wrapper.recorded(), DeepEquals, []byte{}) 104 | c.Assert(wrapper.wasSomethingRecorded(), Equals, false) 105 | } 106 | 107 | func (s *responseWriterWrapperTest) Test_WriteWithRecording(c *C) { 108 | beforeFirstWriteCalled := false 109 | original := newMockResponseWriter() 110 | beforeFirstWrite := func(*responseWriterWrapper) bool { 111 | beforeFirstWriteCalled = true 112 | return true 113 | } 114 | wrapper := newResponseWriterWrapperFor(original, beforeFirstWrite) 115 | len, err := wrapper.Write([]byte("")) 116 | c.Assert(len, Equals, 0) 117 | c.Assert(err, IsNil) 118 | c.Assert(wrapper.firstContentWritten, Equals, false) 119 | c.Assert(beforeFirstWriteCalled, Equals, false) 120 | 121 | len, err = wrapper.Write([]byte("foo")) 122 | c.Assert(len, Equals, 3) 123 | c.Assert(err, IsNil) 124 | c.Assert(wrapper.firstContentWritten, Equals, true) 125 | c.Assert(beforeFirstWriteCalled, Equals, true) 126 | 127 | len, err = wrapper.Write([]byte("bar")) 128 | c.Assert(len, Equals, 3) 129 | c.Assert(err, IsNil) 130 | 131 | c.Assert(original.buffer.Bytes(), DeepEquals, []byte(nil)) 132 | c.Assert(wrapper.buffer.Bytes(), DeepEquals, []byte("foobar")) 133 | c.Assert(wrapper.firstContentWritten, Equals, true) 134 | c.Assert(wrapper.isBodyAllowed(), Equals, true) 135 | c.Assert(wrapper.recorded(), DeepEquals, []byte("foobar")) 136 | c.Assert(wrapper.wasSomethingRecorded(), Equals, true) 137 | } 138 | 139 | func (s *responseWriterWrapperTest) Test_WriteWithBufferOverflow(c *C) { 140 | original := newMockResponseWriter() 141 | beforeFirstWrite := func(*responseWriterWrapper) bool { 142 | return true 143 | } 144 | wrapper := newResponseWriterWrapperFor(original, beforeFirstWrite) 145 | wrapper.maximumBufferSize = 5 146 | wrapper.Write([]byte("foo")) 147 | c.Assert(wrapper.wasSomethingRecorded(), Equals, true) 148 | c.Assert(wrapper.recorded(), DeepEquals, []byte("foo")) 149 | c.Assert(original.buffer.Bytes(), DeepEquals, []byte(nil)) 150 | 151 | wrapper.Write([]byte("bar")) 152 | c.Assert(wrapper.wasSomethingRecorded(), Equals, false) 153 | c.Assert(wrapper.recorded(), DeepEquals, []byte{}) 154 | c.Assert(original.buffer.Bytes(), DeepEquals, []byte("foobar")) 155 | } 156 | 157 | /////////////////////////////////////////////////////////////////////////////////////////// 158 | // MOCKS 159 | /////////////////////////////////////////////////////////////////////////////////////////// 160 | 161 | func newMockResponseWriter() *mockResponseWriter { 162 | result := new(mockResponseWriter) 163 | result.header = http.Header{} 164 | result.buffer = new(bytes.Buffer) 165 | return result 166 | } 167 | 168 | type mockResponseWriter struct { 169 | header http.Header 170 | status int 171 | buffer *bytes.Buffer 172 | error error 173 | } 174 | 175 | func (instance *mockResponseWriter) Header() http.Header { 176 | return instance.header 177 | } 178 | 179 | func (instance *mockResponseWriter) WriteHeader(status int) { 180 | instance.status = status 181 | } 182 | 183 | func (instance *mockResponseWriter) Write(content []byte) (int, error) { 184 | if instance.error != nil { 185 | return 0, instance.error 186 | } 187 | return instance.buffer.Write(content) 188 | } 189 | -------------------------------------------------------------------------------- /rule.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "net/http" 5 | "regexp" 6 | ) 7 | 8 | type rule struct { 9 | path *regexp.Regexp 10 | contentType *regexp.Regexp 11 | pathAndContentTypeCombination pathAndContentTypeCombination 12 | searchPattern *regexp.Regexp 13 | replacement []byte 14 | } 15 | 16 | type pathAndContentTypeCombination string 17 | 18 | const ( 19 | pathAndContentTypeAndCombination = pathAndContentTypeCombination("and") 20 | pathAndContentTypeOrCombination = pathAndContentTypeCombination("or") 21 | ) 22 | 23 | var possiblePathAndContentTypeCombination = []pathAndContentTypeCombination{ 24 | pathAndContentTypeAndCombination, 25 | pathAndContentTypeOrCombination, 26 | } 27 | 28 | func (instance rule) evaluatePathAndContentTypeResult(pathMatch bool, contentTypeMatch bool) bool { 29 | combination := instance.pathAndContentTypeCombination 30 | if combination == pathAndContentTypeCombination("") { 31 | combination = pathAndContentTypeAndCombination 32 | } 33 | if combination == pathAndContentTypeAndCombination { 34 | return pathMatch && contentTypeMatch 35 | } 36 | if combination == pathAndContentTypeOrCombination { 37 | return pathMatch || contentTypeMatch 38 | } 39 | return false 40 | } 41 | 42 | func (instance *rule) matches(request *http.Request, responseHeader *http.Header) bool { 43 | var pathMatch, contentTypeMatch bool 44 | 45 | if instance.path != nil { 46 | pathMatch = request != nil && instance.path.MatchString(request.URL.Path) 47 | } else { 48 | pathMatch = true 49 | } 50 | 51 | if instance.contentType != nil { 52 | contentTypeMatch = responseHeader != nil && instance.contentType.MatchString(responseHeader.Get("Content-Type")) 53 | } else { 54 | contentTypeMatch = true 55 | } 56 | 57 | return instance.evaluatePathAndContentTypeResult(pathMatch, contentTypeMatch) 58 | } 59 | 60 | func (instance *rule) execute(request *http.Request, responseHeader *http.Header, input []byte) []byte { 61 | pattern := instance.searchPattern 62 | if pattern == nil { 63 | return input 64 | } 65 | action := &ruleReplaceAction{ 66 | request: request, 67 | responseHeader: responseHeader, 68 | searchPattern: instance.searchPattern, 69 | replacement: instance.replacement, 70 | } 71 | output := pattern.ReplaceAllFunc(input, action.replacer) 72 | return output 73 | } 74 | -------------------------------------------------------------------------------- /ruleReplaceAction.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | var paramReplacementPattern = regexp.MustCompile("\\{[a-zA-Z0-9_\\-.]+}") 15 | 16 | type ruleReplaceAction struct { 17 | request *http.Request 18 | responseHeader *http.Header 19 | searchPattern *regexp.Regexp 20 | replacement []byte 21 | } 22 | 23 | func (instance *ruleReplaceAction) replacer(input []byte) []byte { 24 | pattern := instance.searchPattern 25 | if pattern == nil { 26 | return input 27 | } 28 | rawReplacement := instance.replacement 29 | if len(rawReplacement) <= 0 { 30 | return []byte{} 31 | } 32 | groups := pattern.FindSubmatch(input) 33 | replacement := paramReplacementPattern.ReplaceAllFunc(rawReplacement, func(input2 []byte) []byte { 34 | return instance.paramReplacer(input2, groups) 35 | }) 36 | return replacement 37 | } 38 | 39 | func (instance *ruleReplaceAction) paramReplacer(input []byte, groups [][]byte) []byte { 40 | if len(input) < 3 { 41 | return input 42 | } 43 | name := string(input[1 : len(input)-1]) 44 | if index, err := strconv.Atoi(name); err == nil { 45 | if index >= 0 && index < len(groups) { 46 | return groups[index] 47 | } 48 | return input 49 | } 50 | 51 | if value, ok := instance.contextValueBy(name); ok { 52 | return []byte(value) 53 | } 54 | return input 55 | } 56 | 57 | func (instance *ruleReplaceAction) contextValueBy(name string) (string, bool) { 58 | if strings.HasPrefix(name, "request_") { 59 | return instance.contextRequestValueBy(name[8:]) 60 | } 61 | if strings.HasPrefix(name, "response_") { 62 | return instance.contextResponseValueBy(name[9:]) 63 | } 64 | if strings.HasPrefix(name, "env_") { 65 | return instance.contextEnvironmentValueBy(name[4:]) 66 | } 67 | if name == "now" { 68 | return instance.contextNowValueBy("") 69 | } 70 | if strings.HasPrefix(name, "now:") { 71 | return instance.contextNowValueBy(name[4:]) 72 | } 73 | return "", false 74 | } 75 | 76 | func (instance *ruleReplaceAction) contextRequestValueBy(name string) (string, bool) { 77 | request := instance.request 78 | if strings.HasPrefix(name, "header_") { 79 | return request.Header.Get(name[7:]), true 80 | } 81 | switch name { 82 | case "url": 83 | return request.URL.String(), true 84 | case "path": 85 | return request.URL.Path, true 86 | case "method": 87 | return request.Method, true 88 | case "host": 89 | return request.Host, true 90 | case "proto": 91 | return request.Proto, true 92 | case "remoteAddress": 93 | return request.RemoteAddr, true 94 | } 95 | return "", false 96 | } 97 | 98 | func (instance *ruleReplaceAction) contextResponseValueBy(name string) (string, bool) { 99 | if name == "header_last_modified" || name == "header_last-modified" { 100 | return instance.contextLastModifiedValueBy("") 101 | } 102 | if strings.HasPrefix(name, "header_last_modified:") || strings.HasPrefix(name, "header_last-modified:") { 103 | return instance.contextLastModifiedValueBy(name[21:]) 104 | } 105 | if strings.HasPrefix(name, "header_") { 106 | return (*instance.responseHeader).Get(name[7:]), true 107 | } 108 | return "", false 109 | } 110 | 111 | func (instance *ruleReplaceAction) contextEnvironmentValueBy(name string) (string, bool) { 112 | return os.Getenv(name), true 113 | } 114 | 115 | func (instance *ruleReplaceAction) contextNowValueBy(pattern string) (string, bool) { 116 | return instance.formatTimeBy(time.Now(), pattern), true 117 | } 118 | 119 | func (instance *ruleReplaceAction) contextLastModifiedValueBy(pattern string) (string, bool) { 120 | plain := instance.responseHeader.Get("last-Modified") 121 | if plain == "" { 122 | // Fallback to now 123 | return instance.contextNowValueBy(pattern) 124 | } 125 | t, err := time.Parse(time.RFC1123, plain) 126 | if err != nil { 127 | log.Printf("[WARN] Serving illegal 'Last-Modified' header value '%v' for '%v': Got: %v", plain, instance.request.URL, err) 128 | // Fallback to now 129 | return instance.contextNowValueBy(pattern) 130 | } 131 | return instance.formatTimeBy(t, pattern), true 132 | } 133 | 134 | func (instance *ruleReplaceAction) formatTimeBy(t time.Time, pattern string) string { 135 | if pattern == "" || pattern == "RFC" || pattern == "RFC3339" { 136 | return t.Format(time.RFC3339) 137 | } 138 | if pattern == "unix" { 139 | return fmt.Sprintf("%d", t.Unix()) 140 | } 141 | if pattern == "timestamp" { 142 | stamp := t.Unix() * 1000 143 | stamp += int64(t.Nanosecond()) / int64(1000000) 144 | return fmt.Sprintf("%d", stamp) 145 | } 146 | return t.Format(pattern) 147 | } 148 | -------------------------------------------------------------------------------- /ruleReplaceAction_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | . "gopkg.in/check.v1" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "regexp" 10 | "time" 11 | ) 12 | 13 | const ( 14 | testEnvironmentVariableName = "X_CADDY_FILTER_TESTING" 15 | ) 16 | 17 | var ( 18 | testUrl, _ = url.ParseRequestURI("http://foo.bar/my/path") 19 | ) 20 | 21 | type ruleReplaceActionTest struct{} 22 | 23 | func init() { 24 | Suite(&ruleReplaceActionTest{}) 25 | } 26 | 27 | func (s *ruleReplaceActionTest) SetUpTest(c *C) { 28 | os.Setenv(testEnvironmentVariableName, c.TestName()) 29 | } 30 | 31 | func (s *ruleReplaceActionTest) TearDownTest(c *C) { 32 | os.Unsetenv(testEnvironmentVariableName) 33 | } 34 | 35 | func (s *ruleReplaceActionTest) Test_replacer(c *C) { 36 | rra := &ruleReplaceAction{ 37 | replacement: []byte(""), 38 | searchPattern: nil, 39 | responseHeader: &http.Header{ 40 | "A": []string{"foobar"}, 41 | }, 42 | } 43 | c.Assert(rra.replacer([]byte("My name is Caddy.")), DeepEquals, []byte("My name is Caddy.")) 44 | rra.searchPattern = regexp.MustCompile("My name is (.*?)\\.") 45 | c.Assert(rra.replacer([]byte("My name is Caddy.")), DeepEquals, []byte("")) 46 | 47 | rra.replacement = []byte("Your name is {1}.") 48 | c.Assert(rra.replacer([]byte("My name is Caddy.")), DeepEquals, []byte("Your name is Caddy.")) 49 | 50 | rra.replacement = []byte("Hi {1}! The header A is {response_header_A}.") 51 | c.Assert(rra.replacer([]byte("My name is Caddy.")), DeepEquals, []byte("Hi Caddy! The header A is foobar.")) 52 | } 53 | 54 | func (s *ruleReplaceActionTest) Test_paramReplacer(c *C) { 55 | groups := [][]byte{ 56 | []byte("a"), 57 | []byte("b"), 58 | } 59 | rra := &ruleReplaceAction{ 60 | responseHeader: &http.Header{ 61 | "A": []string{"c"}, 62 | "Last-Modified": []string{"Tue, 01 Aug 2017 15:13:59 GMT"}, 63 | }, 64 | } 65 | yearString := time.Now().Format("2006-") 66 | 67 | c.Assert(rra.paramReplacer([]byte("{0}"), groups), DeepEquals, []byte("a")) 68 | c.Assert(rra.paramReplacer([]byte("{1}"), groups), DeepEquals, []byte("b")) 69 | c.Assert(rra.paramReplacer([]byte("{response_header_A}"), groups), DeepEquals, []byte("c")) 70 | 71 | c.Assert(rra.paramReplacer([]byte(""), groups), DeepEquals, []byte("")) 72 | c.Assert(rra.paramReplacer([]byte("{}"), groups), DeepEquals, []byte("{}")) 73 | c.Assert(rra.paramReplacer([]byte("{2}"), groups), DeepEquals, []byte("{2}")) 74 | c.Assert(rra.paramReplacer([]byte("{response_headers_A}"), groups), DeepEquals, []byte("{response_headers_A}")) 75 | c.Assert(rra.paramReplacer([]byte("{foo}"), groups), DeepEquals, []byte("{foo}")) 76 | c.Assert(rra.paramReplacer([]byte("{now:2006-}"), groups), DeepEquals, []byte(yearString)) 77 | c.Assert(string(rra.paramReplacer([]byte("{response_header_last_modified}"), groups)), DeepEquals, "2017-08-01T15:13:59Z") 78 | c.Assert(string(rra.paramReplacer([]byte("{response_header_last_modified:RFC}"), groups)), DeepEquals, "2017-08-01T15:13:59Z") 79 | c.Assert(string(rra.paramReplacer([]byte("{response_header_last_modified:timestamp}"), groups)), DeepEquals, "1501600439000") 80 | c.Assert(string(rra.paramReplacer([]byte("{env_X_CADDY_FILTER_TESTING}"), groups)), DeepEquals, c.TestName()) 81 | } 82 | 83 | func (s *ruleReplaceActionTest) Test_contextValueBy(c *C) { 84 | rra := &ruleReplaceAction{ 85 | responseHeader: &http.Header{ 86 | "A": []string{"fromResponse"}, 87 | "Last-Modified": []string{"Tue, 01 Aug 2017 15:13:59 GMT"}, 88 | }, 89 | request: &http.Request{ 90 | Header: http.Header{ 91 | "A": []string{"fromRequest"}, 92 | }, 93 | }, 94 | } 95 | 96 | yearString := time.Now().Format("2006-") 97 | 98 | r, ok := rra.contextValueBy("request_header_A") 99 | c.Assert(ok, Equals, true) 100 | c.Assert(r, Equals, "fromRequest") 101 | 102 | r, ok = rra.contextValueBy("response_header_A") 103 | c.Assert(ok, Equals, true) 104 | c.Assert(r, Equals, "fromResponse") 105 | 106 | r, ok = rra.contextValueBy("now") 107 | c.Assert(ok, Equals, true) 108 | c.Assert(r, Matches, yearString+".*") 109 | 110 | r, ok = rra.contextValueBy("now:") 111 | c.Assert(ok, Equals, true) 112 | c.Assert(r, Matches, yearString+".*") 113 | 114 | r, ok = rra.contextValueBy("now:xxx2006-xxx") 115 | c.Assert(ok, Equals, true) 116 | c.Assert(r, Equals, fmt.Sprintf("xxx%sxxx", yearString)) 117 | 118 | r, ok = rra.contextValueBy("response_header_last_modified") 119 | c.Assert(ok, Equals, true) 120 | c.Assert(r, Equals, "2017-08-01T15:13:59Z") 121 | 122 | r, ok = rra.contextValueBy("response_header_last_modified:RFC") 123 | c.Assert(ok, Equals, true) 124 | c.Assert(r, Equals, "2017-08-01T15:13:59Z") 125 | 126 | r, ok = rra.contextValueBy("response_header_last_modified:timestamp") 127 | c.Assert(ok, Equals, true) 128 | c.Assert(r, Equals, "1501600439000") 129 | 130 | r, ok = rra.contextValueBy("env_X_CADDY_FILTER_TESTING") 131 | c.Assert(ok, Equals, true) 132 | c.Assert(r, Equals, c.TestName()) 133 | 134 | r, ok = rra.contextValueBy("env_X_CADDY_FILTER_TESTING-XYZ") 135 | c.Assert(ok, Equals, true) 136 | c.Assert(r, Equals, "") 137 | 138 | r, ok = rra.contextValueBy("foo") 139 | c.Assert(ok, Equals, false) 140 | c.Assert(r, Equals, "") 141 | } 142 | 143 | func (s *ruleReplaceActionTest) Test_contextRequestValueBy(c *C) { 144 | rra := &ruleReplaceAction{ 145 | request: &http.Request{ 146 | URL: testUrl, 147 | Method: "GET", 148 | Host: "foo.bar", 149 | Proto: "http", 150 | RemoteAddr: "1.2.3.4:6677", 151 | Header: http.Header{ 152 | "A": []string{"1"}, 153 | "B": []string{"2"}, 154 | }, 155 | }, 156 | } 157 | r, ok := rra.contextRequestValueBy("header_A") 158 | c.Assert(ok, Equals, true) 159 | c.Assert(r, Equals, "1") 160 | 161 | r, ok = rra.contextRequestValueBy("header_B") 162 | c.Assert(ok, Equals, true) 163 | c.Assert(r, Equals, "2") 164 | 165 | r, ok = rra.contextRequestValueBy("header_C") 166 | c.Assert(ok, Equals, true) 167 | c.Assert(r, Equals, "") 168 | 169 | r, ok = rra.contextRequestValueBy("url") 170 | c.Assert(ok, Equals, true) 171 | c.Assert(r, Equals, testUrl.String()) 172 | 173 | r, ok = rra.contextRequestValueBy("path") 174 | c.Assert(ok, Equals, true) 175 | c.Assert(r, Equals, testUrl.Path) 176 | 177 | r, ok = rra.contextRequestValueBy("method") 178 | c.Assert(ok, Equals, true) 179 | c.Assert(r, Equals, rra.request.Method) 180 | 181 | r, ok = rra.contextRequestValueBy("host") 182 | c.Assert(ok, Equals, true) 183 | c.Assert(r, Equals, rra.request.Host) 184 | 185 | r, ok = rra.contextRequestValueBy("proto") 186 | c.Assert(ok, Equals, true) 187 | c.Assert(r, Equals, rra.request.Proto) 188 | 189 | r, ok = rra.contextRequestValueBy("remoteAddress") 190 | c.Assert(ok, Equals, true) 191 | c.Assert(r, Equals, rra.request.RemoteAddr) 192 | 193 | r, ok = rra.contextRequestValueBy("headers_A") 194 | c.Assert(ok, Equals, false) 195 | c.Assert(r, Equals, "") 196 | 197 | r, ok = rra.contextRequestValueBy("foo") 198 | c.Assert(ok, Equals, false) 199 | c.Assert(r, Equals, "") 200 | } 201 | 202 | func (s *ruleReplaceActionTest) Test_contextResponseValueBy(c *C) { 203 | rra := &ruleReplaceAction{ 204 | responseHeader: &http.Header{ 205 | "A": []string{"1"}, 206 | "B": []string{"2"}, 207 | }, 208 | } 209 | r, ok := rra.contextResponseValueBy("header_A") 210 | c.Assert(ok, Equals, true) 211 | c.Assert(r, Equals, "1") 212 | 213 | r, ok = rra.contextResponseValueBy("header_B") 214 | c.Assert(ok, Equals, true) 215 | c.Assert(r, Equals, "2") 216 | 217 | r, ok = rra.contextResponseValueBy("header_C") 218 | c.Assert(ok, Equals, true) 219 | c.Assert(r, Equals, "") 220 | 221 | r, ok = rra.contextResponseValueBy("headers_A") 222 | c.Assert(ok, Equals, false) 223 | c.Assert(r, Equals, "") 224 | 225 | r, ok = rra.contextResponseValueBy("foo") 226 | c.Assert(ok, Equals, false) 227 | c.Assert(r, Equals, "") 228 | } 229 | 230 | func (s *ruleReplaceActionTest) Test_formatTimeBy(c *C) { 231 | rra := &ruleReplaceAction{} 232 | now, err := time.Parse(time.RFC3339Nano, "2017-08-15T14:00:00.123456789+02:00") 233 | c.Assert(err, IsNil) 234 | 235 | c.Assert(rra.formatTimeBy(now, ""), Equals, "2017-08-15T14:00:00+02:00") 236 | c.Assert(rra.formatTimeBy(now, "RFC"), Equals, "2017-08-15T14:00:00+02:00") 237 | c.Assert(rra.formatTimeBy(now, "RFC3339"), Equals, "2017-08-15T14:00:00+02:00") 238 | c.Assert(rra.formatTimeBy(now, "unix"), Equals, "1502798400") 239 | c.Assert(rra.formatTimeBy(now, "timestamp"), Equals, "1502798400123") 240 | c.Assert(rra.formatTimeBy(now, "2006-01-02"), Equals, "2017-08-15") 241 | c.Assert(rra.formatTimeBy(now, "xxx"), Equals, "xxx") 242 | } 243 | -------------------------------------------------------------------------------- /rule_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | "net/http" 6 | "net/url" 7 | "regexp" 8 | ) 9 | 10 | var ( 11 | testUrl1, _ = url.ParseRequestURI("http://foo.bar/my/path.html") 12 | testUrl2, _ = url.ParseRequestURI("http://foo.bar/my/path.txt") 13 | ) 14 | 15 | type ruleTest struct{} 16 | 17 | func init() { 18 | Suite(&ruleTest{}) 19 | } 20 | 21 | func (s *ruleTest) Test_matches_path(c *C) { 22 | req := &http.Request{} 23 | r := &rule{ 24 | path: regexp.MustCompile(".*\\.html"), 25 | } 26 | 27 | req.URL = testUrl1 28 | c.Assert(r.matches(req, nil), Equals, true) 29 | 30 | req.URL = testUrl2 31 | c.Assert(r.matches(req, nil), Equals, false) 32 | } 33 | 34 | func (s *ruleTest) Test_matches_contentType(c *C) { 35 | header := http.Header{} 36 | r := &rule{ 37 | contentType: regexp.MustCompile("text/html.*"), 38 | } 39 | 40 | header.Set("Content-Type", "text/html") 41 | c.Assert(r.matches(nil, &header), Equals, true) 42 | 43 | header.Set("Content-Type", "text/plain") 44 | c.Assert(r.matches(nil, &header), Equals, false) 45 | 46 | header.Del("Content-Type") 47 | c.Assert(r.matches(nil, &header), Equals, false) 48 | } 49 | 50 | func (s *ruleTest) Test_matches_and_combined(c *C) { 51 | req := &http.Request{} 52 | header := http.Header{} 53 | r := &rule{ 54 | path: regexp.MustCompile(".*\\.html"), 55 | contentType: regexp.MustCompile("text/html.*"), 56 | pathAndContentTypeCombination: pathAndContentTypeAndCombination, 57 | } 58 | 59 | req.URL = testUrl1 60 | header.Set("Content-Type", "text/html") 61 | c.Assert(r.matches(req, &header), Equals, true) 62 | c.Assert(r.matches(nil, &header), Equals, false) 63 | c.Assert(r.matches(req, nil), Equals, false) 64 | 65 | req.URL = testUrl2 66 | c.Assert(r.matches(req, &header), Equals, false) 67 | 68 | req.URL = testUrl1 69 | header.Set("Content-Type", "text/plain") 70 | c.Assert(r.matches(req, &header), Equals, false) 71 | } 72 | 73 | func (s *ruleTest) Test_matches_or_combined(c *C) { 74 | req := &http.Request{} 75 | header := http.Header{} 76 | r := &rule{ 77 | path: regexp.MustCompile(".*\\.html"), 78 | contentType: regexp.MustCompile("text/html.*"), 79 | pathAndContentTypeCombination: pathAndContentTypeOrCombination, 80 | } 81 | 82 | req.URL = testUrl1 83 | header.Set("Content-Type", "text/html") 84 | c.Assert(r.matches(req, &header), Equals, true) 85 | c.Assert(r.matches(nil, &header), Equals, true) 86 | c.Assert(r.matches(req, nil), Equals, true) 87 | c.Assert(r.matches(nil, nil), Equals, false) 88 | 89 | req.URL = testUrl2 90 | c.Assert(r.matches(req, &header), Equals, true) 91 | 92 | req.URL = testUrl1 93 | header.Set("Content-Type", "text/plain") 94 | c.Assert(r.matches(req, &header), Equals, true) 95 | 96 | req.URL = testUrl2 97 | c.Assert(r.matches(req, &header), Equals, false) 98 | } 99 | 100 | func (s *ruleTest) Test_execute(c *C) { 101 | req := &http.Request{} 102 | header := http.Header{} 103 | header.Set("Server", "Caddy") 104 | r := &rule{ 105 | searchPattern: regexp.MustCompile("My name is (.*?)\\."), 106 | replacement: []byte("Hi {1}! The name of this server is {response_header_Server}."), 107 | } 108 | 109 | result := r.execute(req, &header, []byte("Hello I'am a test.\nMy name is Test_execute.")) 110 | c.Assert(string(result), Equals, "Hello I'am a test.\nHi Test_execute! The name of this server is Caddy.") 111 | 112 | r.searchPattern = nil 113 | result = r.execute(req, &header, []byte("foobar")) 114 | c.Assert(string(result), Equals, "foobar") 115 | } 116 | -------------------------------------------------------------------------------- /utils/fcgi/child.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package fcgi 6 | 7 | // This file implements FastCGI from the perspective of a child process. 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "net" 15 | "net/http" 16 | "net/http/cgi" 17 | "os" 18 | "strings" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | // Request holds the state for an in-progress request. As soon as it's complete, 24 | // it's converted to an http.Request. 25 | type Request struct { 26 | pw *io.PipeWriter 27 | reqId uint16 28 | params map[string]string 29 | buf [1024]byte 30 | rawParams []byte 31 | keepConn bool 32 | } 33 | 34 | func newRequest(reqId uint16, flags uint8) *Request { 35 | r := &Request{ 36 | reqId: reqId, 37 | params: map[string]string{}, 38 | keepConn: flags&flagKeepConn != 0, 39 | } 40 | r.rawParams = r.buf[:0] 41 | return r 42 | } 43 | 44 | // parseParams reads an encoded []byte into Params. 45 | func (r *Request) parseParams() { 46 | text := r.rawParams 47 | r.rawParams = nil 48 | for len(text) > 0 { 49 | keyLen, n := readSize(text) 50 | if n == 0 { 51 | return 52 | } 53 | text = text[n:] 54 | valLen, n := readSize(text) 55 | if n == 0 { 56 | return 57 | } 58 | text = text[n:] 59 | if int(keyLen)+int(valLen) > len(text) { 60 | return 61 | } 62 | key := readString(text, keyLen) 63 | text = text[keyLen:] 64 | val := readString(text, valLen) 65 | text = text[valLen:] 66 | r.params[key] = val 67 | } 68 | } 69 | 70 | // response implements http.ResponseWriter. 71 | type response struct { 72 | req *Request 73 | header http.Header 74 | w *bufWriter 75 | wErr io.Writer 76 | wroteHeader bool 77 | } 78 | 79 | func newResponse(c *child, req *Request) *response { 80 | return &response{ 81 | req: req, 82 | header: http.Header{}, 83 | w: newWriter(c.conn, typeStdout, req.reqId), 84 | wErr: &streamWriter{c: c.conn, recType: typeStderr, reqId: req.reqId}, 85 | } 86 | } 87 | 88 | func (r *response) Header() http.Header { 89 | return r.header 90 | } 91 | 92 | func (r *response) Write(data []byte) (int, error) { 93 | if !r.wroteHeader { 94 | r.WriteHeader(http.StatusOK) 95 | } 96 | return r.w.Write(data) 97 | } 98 | 99 | func (r *response) WriteErr(data []byte) (int, error) { 100 | return r.wErr.Write(data) 101 | } 102 | 103 | func (r *response) WriteHeader(code int) { 104 | if r.wroteHeader { 105 | return 106 | } 107 | r.wroteHeader = true 108 | if code == http.StatusNotModified { 109 | // Must not have body. 110 | r.header.Del("Content-Type") 111 | r.header.Del("Content-Length") 112 | r.header.Del("Transfer-Encoding") 113 | } else if r.header.Get("Content-Type") == "" { 114 | r.header.Set("Content-Type", "text/html; charset=utf-8") 115 | } 116 | 117 | if r.header.Get("Date") == "" { 118 | r.header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) 119 | } 120 | 121 | fmt.Fprintf(r.w, "Status: %d %s\r\n", code, http.StatusText(code)) 122 | r.header.Write(r.w) 123 | r.w.WriteString("\r\n") 124 | } 125 | 126 | func (r *response) Flush() { 127 | if !r.wroteHeader { 128 | r.WriteHeader(http.StatusOK) 129 | } 130 | r.w.Flush() 131 | } 132 | 133 | func (r *response) Close() error { 134 | r.Flush() 135 | return r.w.Close() 136 | } 137 | 138 | type child struct { 139 | conn *conn 140 | handler http.Handler 141 | 142 | mu sync.Mutex // protects requests: 143 | requests map[uint16]*Request // keyed by request ID 144 | } 145 | 146 | func newChild(rwc io.ReadWriteCloser, handler http.Handler) *child { 147 | return &child{ 148 | conn: newConn(rwc), 149 | handler: handler, 150 | requests: make(map[uint16]*Request), 151 | } 152 | } 153 | 154 | func (c *child) serve() { 155 | defer c.conn.Close() 156 | defer c.cleanUp() 157 | var rec record 158 | for { 159 | if err := rec.read(c.conn.rwc); err != nil { 160 | return 161 | } 162 | if err := c.handleRecord(&rec); err != nil { 163 | return 164 | } 165 | } 166 | } 167 | 168 | var errCloseConn = errors.New("fcgi: connection should be closed") 169 | 170 | var emptyBody = ioutil.NopCloser(strings.NewReader("")) 171 | 172 | // ErrRequestAborted is returned by Read when a handler attempts to read the 173 | // body of a request that has been aborted by the web server. 174 | var ErrRequestAborted = errors.New("fcgi: request aborted by web server") 175 | 176 | // ErrConnClosed is returned by Read when a handler attempts to read the body of 177 | // a request after the connection to the web server has been closed. 178 | var ErrConnClosed = errors.New("fcgi: connection to web server closed") 179 | 180 | func (c *child) handleRecord(rec *record) error { 181 | c.mu.Lock() 182 | req, ok := c.requests[rec.h.Id] 183 | c.mu.Unlock() 184 | if !ok && rec.h.Type != typeBeginRequest && rec.h.Type != typeGetValues { 185 | // The spec says to ignore unknown request IDs. 186 | return nil 187 | } 188 | 189 | switch rec.h.Type { 190 | case typeBeginRequest: 191 | if req != nil { 192 | // The server is trying to begin a request with the same ID 193 | // as an in-progress request. This is an error. 194 | return errors.New("fcgi: received ID that is already in-flight") 195 | } 196 | 197 | var br beginRequest 198 | if err := br.read(rec.content()); err != nil { 199 | return err 200 | } 201 | if br.role != roleResponder { 202 | c.conn.writeEndRequest(rec.h.Id, 0, statusUnknownRole) 203 | return nil 204 | } 205 | req = newRequest(rec.h.Id, br.flags) 206 | c.mu.Lock() 207 | c.requests[rec.h.Id] = req 208 | c.mu.Unlock() 209 | return nil 210 | case typeParams: 211 | // NOTE(eds): Technically a key-value pair can straddle the boundary 212 | // between two packets. We buffer until we've received all parameters. 213 | if len(rec.content()) > 0 { 214 | req.rawParams = append(req.rawParams, rec.content()...) 215 | return nil 216 | } 217 | req.parseParams() 218 | return nil 219 | case typeStdin: 220 | content := rec.content() 221 | if req.pw == nil { 222 | var body io.ReadCloser 223 | if len(content) > 0 { 224 | // body could be an io.LimitReader, but it shouldn't matter 225 | // as long as both sides are behaving. 226 | body, req.pw = io.Pipe() 227 | } else { 228 | body = emptyBody 229 | } 230 | go c.serveRequest(req, body) 231 | } 232 | if len(content) > 0 { 233 | // TODO(eds): This blocks until the handler reads from the pipe. 234 | // If the handler takes a long time, it might be a problem. 235 | req.pw.Write(content) 236 | } else if req.pw != nil { 237 | req.pw.Close() 238 | } 239 | return nil 240 | case typeGetValues: 241 | values := map[string]string{"FCGI_MPXS_CONNS": "1"} 242 | c.conn.writePairs(typeGetValuesResult, 0, values) 243 | return nil 244 | case typeData: 245 | // If the filter role is implemented, read the data stream here. 246 | return nil 247 | case typeAbortRequest: 248 | c.mu.Lock() 249 | delete(c.requests, rec.h.Id) 250 | c.mu.Unlock() 251 | c.conn.writeEndRequest(rec.h.Id, 0, statusRequestComplete) 252 | if req.pw != nil { 253 | req.pw.CloseWithError(ErrRequestAborted) 254 | } 255 | if !req.keepConn { 256 | // connection will close upon return 257 | return errCloseConn 258 | } 259 | return nil 260 | default: 261 | b := make([]byte, 8) 262 | b[0] = byte(rec.h.Type) 263 | c.conn.writeRecord(typeUnknownType, 0, b) 264 | return nil 265 | } 266 | } 267 | 268 | func (c *child) serveRequest(req *Request, body io.ReadCloser) { 269 | r := newResponse(c, req) 270 | httpReq, err := cgi.RequestFromMap(req.params) 271 | if err != nil { 272 | // there was an error reading the request 273 | r.WriteHeader(http.StatusInternalServerError) 274 | c.conn.writeRecord(typeStderr, req.reqId, []byte(err.Error())) 275 | } else { 276 | httpReq.Body = body 277 | c.handler.ServeHTTP(r, httpReq) 278 | } 279 | r.Close() 280 | c.mu.Lock() 281 | delete(c.requests, req.reqId) 282 | c.mu.Unlock() 283 | c.conn.writeEndRequest(req.reqId, 0, statusRequestComplete) 284 | 285 | // Consume the entire body, so the host isn't still writing to 286 | // us when we close the socket below in the !keepConn case, 287 | // otherwise we'd send a RST. (golang.org/issue/4183) 288 | // TODO(bradfitz): also bound this copy in time. Or send 289 | // some sort of abort request to the host, so the host 290 | // can properly cut off the client sending all the data. 291 | // For now just bound it a little and 292 | io.CopyN(ioutil.Discard, body, 100<<20) 293 | body.Close() 294 | 295 | if !req.keepConn { 296 | c.conn.Close() 297 | } 298 | } 299 | 300 | func (c *child) cleanUp() { 301 | c.mu.Lock() 302 | defer c.mu.Unlock() 303 | for _, req := range c.requests { 304 | if req.pw != nil { 305 | // race with call to Close in c.serveRequest doesn't matter because 306 | // Pipe(Reader|Writer).Close are idempotent 307 | req.pw.CloseWithError(ErrConnClosed) 308 | } 309 | } 310 | } 311 | 312 | // Serve accepts incoming FastCGI connections on the listener l, creating a new 313 | // goroutine for each. The goroutine reads requests and then calls handler 314 | // to reply to them. 315 | // If l is nil, Serve accepts connections from os.Stdin. 316 | // If handler is nil, http.DefaultServeMux is used. 317 | func Serve(l net.Listener, handler http.Handler) error { 318 | if l == nil { 319 | var err error 320 | l, err = net.FileListener(os.Stdin) 321 | if err != nil { 322 | return err 323 | } 324 | defer l.Close() 325 | } 326 | if handler == nil { 327 | handler = http.DefaultServeMux 328 | } 329 | for { 330 | rw, err := l.Accept() 331 | if err != nil { 332 | return err 333 | } 334 | c := newChild(rw, handler) 335 | go c.serve() 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /utils/fcgi/fcgi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package fcgi implements the FastCGI protocol. 6 | // Currently only the responder role is supported. 7 | // The protocol is defined at http://www.fastcgi.com/drupal/node/6?q=node/22 8 | package fcgi 9 | 10 | // This file defines the raw protocol and some utilities used by the child and 11 | // the host. 12 | 13 | import ( 14 | "bufio" 15 | "bytes" 16 | "encoding/binary" 17 | "errors" 18 | "io" 19 | "net/http" 20 | "sync" 21 | ) 22 | 23 | // A ResponseWriter interface is used by an HTTP handler to 24 | // construct an HTTP response. 25 | // 26 | // A ResponseWriter may not be used after the Handler.ServeHTTP method 27 | // has returned. 28 | type ResponseWriter interface { 29 | Header() http.Header 30 | Write([]byte) (int, error) 31 | WriteHeader(int) 32 | WriteErr([]byte) (int, error) 33 | } 34 | 35 | // recType is a record type, as defined by 36 | // http://www.fastcgi.com/devkit/doc/fcgi-spec.html#S8 37 | type recType uint8 38 | 39 | const ( 40 | typeBeginRequest recType = 1 41 | typeAbortRequest recType = 2 42 | typeEndRequest recType = 3 43 | typeParams recType = 4 44 | typeStdin recType = 5 45 | typeStdout recType = 6 46 | typeStderr recType = 7 47 | typeData recType = 8 48 | typeGetValues recType = 9 49 | typeGetValuesResult recType = 10 50 | typeUnknownType recType = 11 51 | ) 52 | 53 | // keep the connection between web-server and responder open after request 54 | const flagKeepConn = 1 55 | 56 | const ( 57 | maxWrite = 65535 // maximum record body 58 | maxPad = 255 59 | ) 60 | 61 | const ( 62 | roleResponder = iota + 1 // only Responders are implemented. 63 | roleAuthorizer 64 | roleFilter 65 | ) 66 | 67 | const ( 68 | statusRequestComplete = iota 69 | statusCantMultiplex 70 | statusOverloaded 71 | statusUnknownRole 72 | ) 73 | 74 | type header struct { 75 | Version uint8 76 | Type recType 77 | Id uint16 78 | ContentLength uint16 79 | PaddingLength uint8 80 | Reserved uint8 81 | } 82 | 83 | type beginRequest struct { 84 | role uint16 85 | flags uint8 86 | reserved [5]uint8 87 | } 88 | 89 | func (br *beginRequest) read(content []byte) error { 90 | if len(content) != 8 { 91 | return errors.New("fcgi: invalid begin request record") 92 | } 93 | br.role = binary.BigEndian.Uint16(content) 94 | br.flags = content[2] 95 | return nil 96 | } 97 | 98 | // for padding so we don't have to allocate all the time 99 | // not synchronized because we don't care what the contents are 100 | var pad [maxPad]byte 101 | 102 | func (h *header) init(recType recType, reqId uint16, contentLength int) { 103 | h.Version = 1 104 | h.Type = recType 105 | h.Id = reqId 106 | h.ContentLength = uint16(contentLength) 107 | h.PaddingLength = uint8(-contentLength & 7) 108 | } 109 | 110 | // conn sends records over rwc 111 | type conn struct { 112 | mutex sync.Mutex 113 | rwc io.ReadWriteCloser 114 | 115 | // to avoid allocations 116 | buf bytes.Buffer 117 | h header 118 | } 119 | 120 | func newConn(rwc io.ReadWriteCloser) *conn { 121 | return &conn{rwc: rwc} 122 | } 123 | 124 | func (c *conn) Close() error { 125 | c.mutex.Lock() 126 | defer c.mutex.Unlock() 127 | return c.rwc.Close() 128 | } 129 | 130 | type record struct { 131 | h header 132 | buf [maxWrite + maxPad]byte 133 | } 134 | 135 | func (rec *record) read(r io.Reader) (err error) { 136 | if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil { 137 | return err 138 | } 139 | if rec.h.Version != 1 { 140 | return errors.New("fcgi: invalid header version") 141 | } 142 | n := int(rec.h.ContentLength) + int(rec.h.PaddingLength) 143 | if _, err = io.ReadFull(r, rec.buf[:n]); err != nil { 144 | return err 145 | } 146 | return nil 147 | } 148 | 149 | func (rec *record) content() []byte { 150 | return rec.buf[:rec.h.ContentLength] 151 | } 152 | 153 | // writeRecord writes and sends a single record. 154 | func (c *conn) writeRecord(recType recType, reqId uint16, b []byte) error { 155 | c.mutex.Lock() 156 | defer c.mutex.Unlock() 157 | c.buf.Reset() 158 | c.h.init(recType, reqId, len(b)) 159 | if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil { 160 | return err 161 | } 162 | if _, err := c.buf.Write(b); err != nil { 163 | return err 164 | } 165 | if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil { 166 | return err 167 | } 168 | _, err := c.rwc.Write(c.buf.Bytes()) 169 | return err 170 | } 171 | 172 | func (c *conn) writeEndRequest(reqId uint16, appStatus int, protocolStatus uint8) error { 173 | b := make([]byte, 8) 174 | binary.BigEndian.PutUint32(b, uint32(appStatus)) 175 | b[4] = protocolStatus 176 | return c.writeRecord(typeEndRequest, reqId, b) 177 | } 178 | 179 | func (c *conn) writePairs(recType recType, reqId uint16, pairs map[string]string) error { 180 | w := newWriter(c, recType, reqId) 181 | b := make([]byte, 8) 182 | for k, v := range pairs { 183 | n := encodeSize(b, uint32(len(k))) 184 | n += encodeSize(b[n:], uint32(len(v))) 185 | if _, err := w.Write(b[:n]); err != nil { 186 | return err 187 | } 188 | if _, err := w.WriteString(k); err != nil { 189 | return err 190 | } 191 | if _, err := w.WriteString(v); err != nil { 192 | return err 193 | } 194 | } 195 | w.Close() 196 | return nil 197 | } 198 | 199 | func readSize(s []byte) (uint32, int) { 200 | if len(s) == 0 { 201 | return 0, 0 202 | } 203 | size, n := uint32(s[0]), 1 204 | if size&(1<<7) != 0 { 205 | if len(s) < 4 { 206 | return 0, 0 207 | } 208 | n = 4 209 | size = binary.BigEndian.Uint32(s) 210 | size &^= 1 << 31 211 | } 212 | return size, n 213 | } 214 | 215 | func readString(s []byte, size uint32) string { 216 | if size > uint32(len(s)) { 217 | return "" 218 | } 219 | return string(s[:size]) 220 | } 221 | 222 | func encodeSize(b []byte, size uint32) int { 223 | if size > 127 { 224 | size |= 1 << 31 225 | binary.BigEndian.PutUint32(b, size) 226 | return 4 227 | } 228 | b[0] = byte(size) 229 | return 1 230 | } 231 | 232 | // bufWriter encapsulates bufio.Writer but also closes the underlying stream when 233 | // Closed. 234 | type bufWriter struct { 235 | closer io.Closer 236 | *bufio.Writer 237 | } 238 | 239 | func (w *bufWriter) Close() error { 240 | if err := w.Writer.Flush(); err != nil { 241 | w.closer.Close() 242 | return err 243 | } 244 | return w.closer.Close() 245 | } 246 | 247 | func newWriter(c *conn, recType recType, reqId uint16) *bufWriter { 248 | s := &streamWriter{c: c, recType: recType, reqId: reqId} 249 | w := bufio.NewWriterSize(s, maxWrite) 250 | return &bufWriter{s, w} 251 | } 252 | 253 | // streamWriter abstracts out the separation of a stream into discrete records. 254 | // It only writes maxWrite bytes at a time. 255 | type streamWriter struct { 256 | c *conn 257 | recType recType 258 | reqId uint16 259 | } 260 | 261 | func (w *streamWriter) Write(p []byte) (int, error) { 262 | nn := 0 263 | for len(p) > 0 { 264 | n := len(p) 265 | if n > maxWrite { 266 | n = maxWrite 267 | } 268 | if err := w.c.writeRecord(w.recType, w.reqId, p[:n]); err != nil { 269 | return nn, err 270 | } 271 | nn += n 272 | p = p[n:] 273 | } 274 | return nn, nil 275 | } 276 | 277 | func (w *streamWriter) Close() error { 278 | // send empty record to close the stream 279 | return w.c.writeRecord(w.recType, w.reqId, nil) 280 | } 281 | -------------------------------------------------------------------------------- /utils/test/testingCaddy.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/caddyserver/caddy" 7 | "io" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "time" 12 | ) 13 | 14 | var ( 15 | caddyBinary string 16 | ) 17 | 18 | func init() { 19 | flag.StringVar(&caddyBinary, "integrationTest.caddyBinary", "", "Specifies the caddy binary to use for integration tests instead of embedded version.") 20 | flag.Parse() 21 | if len(caddyBinary) > 0 { 22 | log.Printf("Using caddy binary '%s' for integration tests instead of embedded version.", caddyBinary) 23 | } 24 | } 25 | 26 | // NewTestingCaddy creates a new instance of a caddy with a testing resource for testing purposes. 27 | func NewTestingCaddy(configurationName string) io.Closer { 28 | if len(caddyBinary) <= 0 { 29 | return newEmbeddedTestingCaddy(configurationName) 30 | } 31 | return newExternalTestingCaddy(caddyBinary, configurationName) 32 | } 33 | 34 | type embeddedTestingCaddy struct { 35 | caddy *caddy.Instance 36 | } 37 | 38 | func newEmbeddedTestingCaddy(configurationName string) *embeddedTestingCaddy { 39 | config := TestingResourceOf(configurationName) 40 | defer config.Close() 41 | caddyFile, err := caddy.CaddyfileFromPipe(config, "http") 42 | if err != nil { 43 | panic(fmt.Sprintf("Could not read config '%s'. Got: %v", configurationName, err)) 44 | } 45 | instance, err := caddy.Start(caddyFile) 46 | if err != nil { 47 | panic(fmt.Sprintf("Could not start caddy with config '%s'. Got: %v", configurationName, err)) 48 | } 49 | return &embeddedTestingCaddy{ 50 | caddy: instance, 51 | } 52 | } 53 | 54 | func (instance *embeddedTestingCaddy) Close() error { 55 | return instance.caddy.Stop() 56 | } 57 | 58 | type externalTestingCaddy struct { 59 | cmd *exec.Cmd 60 | } 61 | 62 | func newExternalTestingCaddy(caddyBinary string, configurationName string) *externalTestingCaddy { 63 | cmd := exec.Command(caddyBinary, "-agree", "-conf", TestingPathOfResource(configurationName)) 64 | cmd.Stderr = os.Stderr 65 | cmd.Stdout = os.Stdout 66 | err := cmd.Start() 67 | if err != nil { 68 | panic(fmt.Sprintf("Could not start caddy process. Got: %v", err)) 69 | } 70 | time.Sleep(200 * time.Millisecond) 71 | return &externalTestingCaddy{ 72 | cmd: cmd, 73 | } 74 | } 75 | 76 | func (instance *externalTestingCaddy) Close() error { 77 | return instance.cmd.Process.Kill() 78 | } 79 | -------------------------------------------------------------------------------- /utils/test/testingFcgiServer.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/echocat/caddy-filter/utils/fcgi" 6 | "log" 7 | "net" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | // TestingFcgiServer represents a http server for testing purposes 13 | type TestingFcgiServer struct { 14 | mux *http.ServeMux 15 | listener net.Listener 16 | } 17 | 18 | // NewTestingFcgiServer creates a new http server for testing purposes 19 | func NewTestingFcgiServer(port int) *TestingFcgiServer { 20 | var err error 21 | result := &TestingFcgiServer{} 22 | 23 | result.mux = http.NewServeMux() 24 | result.mux.HandleFunc("/index.cgi", result.handleIndexRequest) 25 | result.mux.HandleFunc("/redirect.cgi", result.handleRedirectRequest) 26 | result.mux.HandleFunc("/another.cgi", result.handleAnotherRequest) 27 | 28 | result.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", port)) 29 | if err != nil { 30 | panic(fmt.Sprintf("Could not start test server. Got: %v", err)) 31 | } 32 | 33 | go func(instance *TestingFcgiServer) { 34 | err := fcgi.Serve(instance.listener, instance.mux) 35 | if err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") { 36 | panic(fmt.Sprintf("Problem while serving. Got: %v", err)) 37 | } 38 | }(result) 39 | return result 40 | } 41 | 42 | func (instance *TestingFcgiServer) handleIndexRequest(resp http.ResponseWriter, req *http.Request) { 43 | fr, ok := resp.(fcgi.ResponseWriter) 44 | if !ok { 45 | log.Fatal("ResponseWriter is not the FCGI specific one.") 46 | } 47 | 48 | resp.WriteHeader(200) 49 | fr.WriteErr([]byte("Hello from FCGI to server.")) 50 | resp.Write([]byte("" + 51 | "Hello world!" + 52 | "

Hello world!

" + 53 | "")) 54 | } 55 | 56 | func (instance *TestingFcgiServer) handleRedirectRequest(resp http.ResponseWriter, req *http.Request) { 57 | resp.Header().Set("Location", "/another.cgi") 58 | resp.WriteHeader(301) 59 | resp.Write([]byte("Moved Permanently.")) 60 | } 61 | 62 | func (instance *TestingFcgiServer) handleAnotherRequest(resp http.ResponseWriter, req *http.Request) { 63 | resp.Write([]byte("" + 64 | "I'am another!" + 65 | "

I'am another!

" + 66 | "")) 67 | } 68 | 69 | // Close closes the testing server graceful. 70 | func (instance *TestingFcgiServer) Close() { 71 | defer func() { 72 | instance.listener = nil 73 | instance.mux = nil 74 | }() 75 | if instance.listener != nil { 76 | instance.listener.Close() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /utils/test/testingHttpServer.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "github.com/NYTimes/gziphandler" 7 | "net" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | // TestingHttpServer represents a http server for testing purposes 13 | type TestingHttpServer struct { 14 | mux *http.ServeMux 15 | server *http.Server 16 | listener net.Listener 17 | } 18 | 19 | // NewTestingHttpServer creates a new http server for testing purposes 20 | func NewTestingHttpServer(port int, gzipEnabled bool) *TestingHttpServer { 21 | var err error 22 | result := &TestingHttpServer{} 23 | 24 | result.mux = http.NewServeMux() 25 | 26 | defaultRequest := http.HandlerFunc(result.handleDefaultRequest) 27 | if gzipEnabled { 28 | handlerFactory, _ := gziphandler.NewGzipLevelAndMinSize(gzip.DefaultCompression, 0) 29 | result.mux.Handle("/default", handlerFactory(defaultRequest)) 30 | } else { 31 | result.mux.HandleFunc("/default", defaultRequest) 32 | } 33 | 34 | result.server = &http.Server{ 35 | Addr: fmt.Sprintf(":%d", port), 36 | Handler: result.mux, 37 | } 38 | 39 | result.listener, err = net.Listen("tcp", result.server.Addr) 40 | if err != nil { 41 | panic(fmt.Sprintf("Could not start test server. Got: %v", err)) 42 | } 43 | 44 | go func(instance *TestingHttpServer) { 45 | err := instance.server.Serve(instance.listener) 46 | if err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") { 47 | panic(fmt.Sprintf("Problem while serving. Got: %v", err)) 48 | } 49 | }(result) 50 | return result 51 | } 52 | 53 | func (instance *TestingHttpServer) handleDefaultRequest(resp http.ResponseWriter, req *http.Request) { 54 | resp.WriteHeader(200) 55 | resp.Write([]byte("Hello world!")) 56 | } 57 | 58 | // Close closes the testing server graceful. 59 | func (instance *TestingHttpServer) Close() { 60 | defer func() { 61 | instance.listener = nil 62 | instance.server = nil 63 | instance.mux = nil 64 | }() 65 | if instance.listener != nil { 66 | instance.listener.Close() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /utils/test/testingResources.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // TestingResourceContentOf load the content of a named testing resource from resources/test/ 11 | func TestingResourceContentOf(name string) []byte { 12 | f := TestingResourceOf(name) 13 | defer f.Close() 14 | bytes, err := ioutil.ReadAll(f) 15 | if err != nil { 16 | panic(fmt.Sprintf("Could not read testing resource '%s'. Got: %v", name, err)) 17 | } 18 | return bytes 19 | } 20 | 21 | // TestingResourceOf load testing resource as stream from resources/test/ 22 | func TestingResourceOf(name string) *os.File { 23 | f, err := os.Open(TestingPathOfResource(name)) 24 | if err != nil { 25 | panic(fmt.Sprintf("Could not open testing resource '%s'. Got: %v", name, err)) 26 | } 27 | return f 28 | } 29 | 30 | // TestingPathOfResource returns a path for a testing resource by resources/test/ 31 | func TestingPathOfResource(name string) string { 32 | return filepath.Join("resources", "test", name) 33 | } 34 | --------------------------------------------------------------------------------