Commit c431a07a authored by Tobias Weingartner's avatar Tobias Weingartner

Merge branch 'master' into md_changes

parents dd4de698 e2234497
*.bash text eol=lf whitespace=blank-at-eol,space-before-tab,tab-in-indent,trailing-space,tabwidth=4 # shell scripts should not use tabs to indent!
*.sh text eol=lf whitespace=blank-at-eol,space-before-tab,tab-in-indent,trailing-space,tabwidth=4 *.bash text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.sh text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
# files for systemd # files for systemd (shell-similar)
*.path text eol=lf whitespace=blank-at-eol,space-before-tab,tab-in-indent,trailing-space,tabwidth=4 *.path text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.service text eol=lf whitespace=blank-at-eol,space-before-tab,tab-in-indent,trailing-space,tabwidth=4 *.service text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.timer text eol=lf whitespace=blank-at-eol,space-before-tab,tab-in-indent,trailing-space,tabwidth=4 *.timer text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
# go fmt will enforce this, but in case a user has not called "go fmt" allow GIT to catch this:
*.go text eol=lf core.whitespace whitespace=indent-with-non-tab,trailing-space,tabwidth=4
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
.git* text eol=auto core.whitespace whitespace=trailing-space
...@@ -20,10 +20,10 @@ anything about Web development ...@@ -20,10 +20,10 @@ anything about Web development
### Bug reports ### Bug reports
First, please [search this repository](https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93) Please [search this repository](https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93)
with a variety of keywords to ensure your bug is not already reported. with a variety of keywords to ensure your bug is not already reported.
If not, [open an issue](https://github.com/mholt/caddy/issues) and answer the If unique, [open an issue](https://github.com/mholt/caddy/issues) and answer the
questions so we can understand and reproduce the problematic behavior. questions so we can understand and reproduce the problematic behavior.
The burden is on you to convince us that it is actually a bug in Caddy. This is The burden is on you to convince us that it is actually a bug in Caddy. This is
...@@ -39,12 +39,16 @@ getting free help. If we helped you, please consider ...@@ -39,12 +39,16 @@ getting free help. If we helped you, please consider
### Minor improvements and new tests ### Minor improvements and new tests
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time. Make Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time for
sure to write tests to assert your change is working properly and is thoroughly minor changes or new tests. Make sure to write tests to assert your change is
covered. We'll ask most pull requests to be working properly and is thoroughly covered. We'll ask most pull requests to be
[squashed](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html), [squashed](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html),
especially with small commits. especially with small commits.
Your pull request may be thoroughly reviewed. This is because if we accept the
PR, we also assume responsibility for it, although we would prefer you to
help maintain your code after it gets merged.
### Proposals, suggestions, ideas, new features ### Proposals, suggestions, ideas, new features
...@@ -54,17 +58,23 @@ with a variety of keywords to ensure your suggestion/proposal is new. ...@@ -54,17 +58,23 @@ with a variety of keywords to ensure your suggestion/proposal is new.
If so, you may open either an issue or a pull request for discussion and If so, you may open either an issue or a pull request for discussion and
feedback. feedback.
The advantage of issues is that you don't have to spend time actually The advantage of issues is that you don't have to spend time implementing your
implementing your idea, but you should still describe it thoroughly. The idea, but you should still describe it thoroughly as if someone reading it would
advantage of a pull request is that we can immediately see the impact the change implement the whole thing starting from scratch.
will have on the project, what the code will look like, and how to improve it.
The disadvantage of pull requests is that they are unlikely to get accepted The advantage of pull requests is that we can immediately see the impact the
without significant changes, or it may be rejected entirely. Don't worry, that change will have on the project, what the code will look like, and how to
won't happen without an open discussion first. improve it. The disadvantage of pull requests is that they are unlikely to get
accepted without significant changes first, or it may be rejected entirely.
Don't worry, that won't happen without an open discussion first.
If you are going to spend significant time implementing code for a pull request, If you are going to spend significant time writing code for a new pull request,
best to open an issue first and "claim" it and get feedback before you invest best to open an issue to "claim" it and get feedback before you invest a lot of
a lot of time. time.
Remember: pull requests should always be thoroughly documented both via godoc
and with at least a rough draft of documentation that might go on the website
for users to read.
### Collaborator status ### Collaborator status
...@@ -75,6 +85,18 @@ push to the repository and merge other pull requests. We hope that you will ...@@ -75,6 +85,18 @@ push to the repository and merge other pull requests. We hope that you will
stay involved by reviewing pull requests, submitting more of your own, and stay involved by reviewing pull requests, submitting more of your own, and
resolving issues as you are able to. Thanks for making Caddy amazing! resolving issues as you are able to. Thanks for making Caddy amazing!
We ask that collaborators will conduct thorough code reviews and be nice to
new contributors. Before merging a PR, it's best to get the approval of
at least one or two other collaborators and/or the project owner. We prefer
squashed commits instead of many little, semantically-unimportant commits. Also,
CI and other post-commit hooks must pass before being merged except in certain
unusual circumstances.
Collaborator status may be removed for inactive users from time to time as
we see fit; this is not an insult, just a basic security precaution in case
the account becomes inactive or abandoned. Privileges can always be restored
later.
### Vulnerabilities ### Vulnerabilities
......
...@@ -18,3 +18,7 @@ ...@@ -18,3 +18,7 @@
#### 6. What did you see instead (give full error messages and/or log)? #### 6. What did you see instead (give full error messages and/or log)?
#### 7. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
...@@ -17,6 +17,7 @@ set -euo pipefail ...@@ -17,6 +17,7 @@ set -euo pipefail
: ${output_filename:="ecaddy"} : ${output_filename:="ecaddy"}
: ${git_repo:="${2:-}"} : ${git_repo:="${2:-}"}
: ${git_repo:="."}
pkg=main pkg=main
ldflags=() ldflags=()
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"errors"
"os" "os"
"runtime" "runtime"
"testing" "testing"
...@@ -95,17 +96,25 @@ func TestSaveAndLoadECCPrivateKey(t *testing.T) { ...@@ -95,17 +96,25 @@ func TestSaveAndLoadECCPrivateKey(t *testing.T) {
// PrivateKeysSame compares the bytes of a and b and returns true if they are the same. // PrivateKeysSame compares the bytes of a and b and returns true if they are the same.
func PrivateKeysSame(a, b crypto.PrivateKey) bool { func PrivateKeysSame(a, b crypto.PrivateKey) bool {
return bytes.Equal(PrivateKeyBytes(a), PrivateKeyBytes(b)) var abytes, bbytes []byte
var err error
if abytes, err = PrivateKeyBytes(a); err != nil {
return false
}
if bbytes, err = PrivateKeyBytes(b); err != nil {
return false
}
return bytes.Equal(abytes, bbytes)
} }
// PrivateKeyBytes returns the bytes of DER-encoded key. // PrivateKeyBytes returns the bytes of DER-encoded key.
func PrivateKeyBytes(key crypto.PrivateKey) []byte { func PrivateKeyBytes(key crypto.PrivateKey) ([]byte, error) {
var keyBytes []byte
switch key := key.(type) { switch key := key.(type) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
keyBytes = x509.MarshalPKCS1PrivateKey(key) return x509.MarshalPKCS1PrivateKey(key), nil
case *ecdsa.PrivateKey: case *ecdsa.PrivateKey:
keyBytes, _ = x509.MarshalECPrivateKey(key) return x509.MarshalECPrivateKey(key)
} }
return keyBytes return nil, errors.New("Unknown private key type")
} }
...@@ -112,12 +112,21 @@ func renewManagedCertificates(allowPrompts bool) (err error) { ...@@ -112,12 +112,21 @@ func renewManagedCertificates(allowPrompts bool) (err error) {
// Apply changes to the cache // Apply changes to the cache
for _, cert := range renewed { for _, cert := range renewed {
if cert.Names[len(cert.Names)-1] == "" {
// Special case: This is the default certificate, so we must
// ensure it gets updated as well, otherwise the renewal
// routine will find it and think it still needs to be renewed,
// even though we already renewed it...
certCacheMu.Lock()
delete(certCache, "")
certCacheMu.Unlock()
}
_, err := cacheManagedCertificate(cert.Names[0], cert.OnDemand) _, err := cacheManagedCertificate(cert.Names[0], cert.OnDemand)
if err != nil { if err != nil {
if client.AllowPrompts { if client.AllowPrompts {
return err // operator is present, so report error immediately return err // operator is present, so report error immediately
} }
log.Printf("[ERROR] %v", err) log.Printf("[ERROR] Caching renewed certificate: %v", err)
} }
} }
for _, cert := range deleted { for _, cert := range deleted {
...@@ -178,7 +187,7 @@ func updateOCSPStaples() { ...@@ -178,7 +187,7 @@ func updateOCSPStaples() {
if err != nil { if err != nil {
if cert.OCSP != nil { if cert.OCSP != nil {
// if it was no staple before, that's fine, otherwise we should log the error // if it was no staple before, that's fine, otherwise we should log the error
log.Printf("[ERROR] Checking OCSP for %s: %v", name, err) log.Printf("[ERROR] Checking OCSP for %v: %v", cert.Names, err)
} }
continue continue
} }
......
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
"net" "net"
"os" "os"
"os/exec" "os/exec"
"path" "path/filepath"
"sync/atomic" "sync/atomic"
"github.com/mholt/caddy/caddy/https" "github.com/mholt/caddy/caddy/https"
...@@ -138,7 +138,7 @@ func Restart(newCaddyfile Input) error { ...@@ -138,7 +138,7 @@ func Restart(newCaddyfile Input) error {
func getCertsForNewCaddyfile(newCaddyfile Input) error { func getCertsForNewCaddyfile(newCaddyfile Input) error {
// parse the new caddyfile only up to (and including) TLS // parse the new caddyfile only up to (and including) TLS
// so we can know what we need to get certs for. // so we can know what we need to get certs for.
configs, _, _, err := loadConfigsUpToIncludingTLS(path.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body())) configs, _, _, err := loadConfigsUpToIncludingTLS(filepath.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body()))
if err != nil { if err != nil {
return errors.New("loading Caddyfile: " + err.Error()) return errors.New("loading Caddyfile: " + err.Error())
} }
......
...@@ -3,6 +3,7 @@ package setup ...@@ -3,6 +3,7 @@ package setup
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http"
"text/template" "text/template"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
...@@ -17,7 +18,6 @@ func Browse(c *Controller) (middleware.Middleware, error) { ...@@ -17,7 +18,6 @@ func Browse(c *Controller) (middleware.Middleware, error) {
} }
browse := browse.Browse{ browse := browse.Browse{
Root: c.Root,
Configs: configs, Configs: configs,
IgnoreIndexes: false, IgnoreIndexes: false,
} }
...@@ -50,6 +50,16 @@ func browseParse(c *Controller) ([]browse.Config, error) { ...@@ -50,6 +50,16 @@ func browseParse(c *Controller) ([]browse.Config, error) {
} else { } else {
bc.PathScope = "/" bc.PathScope = "/"
} }
bc.Root = http.Dir(c.Root)
theRoot, err := bc.Root.Open("/") // catch a missing path early
if err != nil {
return configs, err
}
defer theRoot.Close()
_, err = theRoot.Readdir(-1)
if err != nil {
return configs, err
}
// Second argument would be the template file to use // Second argument would be the template file to use
var tplText string var tplText string
...@@ -85,7 +95,6 @@ const defaultTemplate = `<!DOCTYPE html> ...@@ -85,7 +95,6 @@ const defaultTemplate = `<!DOCTYPE html>
<html> <html>
<head> <head>
<title>{{.Name}}</title> <title>{{.Name}}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<style> <style>
* { padding: 0; margin: 0; } * { padding: 0; margin: 0; }
...@@ -106,7 +115,7 @@ h1 a:hover { ...@@ -106,7 +115,7 @@ h1 a:hover {
} }
header, header,
.content { #summary {
padding-left: 5%; padding-left: 5%;
padding-right: 5%; padding-right: 5%;
} }
...@@ -306,43 +315,49 @@ footer { ...@@ -306,43 +315,49 @@ footer {
</header> </header>
<main> <main>
<div class="meta"> <div class="meta">
<div class="content"> <div id="summary">
<span class="meta-item"><b>{{.NumDirs}}</b> director{{if eq 1 .NumDirs}}y{{else}}ies{{end}}</span> <span class="meta-item"><b>{{.NumDirs}}</b> director{{if eq 1 .NumDirs}}y{{else}}ies{{end}}</span>
<span class="meta-item"><b>{{.NumFiles}}</b> file{{if ne 1 .NumFiles}}s{{end}}</span> <span class="meta-item"><b>{{.NumFiles}}</b> file{{if ne 1 .NumFiles}}s{{end}}</span>
{{- if ne 0 .ItemsLimitedTo}}
<span class="meta-item">(of which only <b>{{.ItemsLimitedTo}}</b> are displayed)</span>
{{- end}}
</div> </div>
</div> </div>
<div class="listing"> <div class="listing">
<table> <table aria-describedby="summary">
<thead>
<tr> <tr>
<th> <th>
{{if and (eq .Sort "name") (ne .Order "desc")}} {{- if and (eq .Sort "name") (ne .Order "desc")}}
<a href="?sort=name&order=desc">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a> <a href="?sort=name&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{else if and (eq .Sort "name") (ne .Order "asc")}} {{- else if and (eq .Sort "name") (ne .Order "asc")}}
<a href="?sort=name&order=asc">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a> <a href="?sort=name&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{else}} {{- else}}
<a href="?sort=name&order=asc">Name</a> <a href="?sort=name&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name</a>
{{end}} {{- end}}
</th> </th>
<th> <th>
{{if and (eq .Sort "size") (ne .Order "desc")}} {{- if and (eq .Sort "size") (ne .Order "desc")}}
<a href="?sort=size&order=desc">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a></a> <a href="?sort=size&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a></a>
{{else if and (eq .Sort "size") (ne .Order "asc")}} {{- else if and (eq .Sort "size") (ne .Order "asc")}}
<a href="?sort=size&order=asc">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a></a> <a href="?sort=size&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a></a>
{{else}} {{- else}}
<a href="?sort=size&order=asc">Size</a> <a href="?sort=size&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size</a>
{{end}} {{- end}}
</th> </th>
<th class="hideable"> <th class="hideable">
{{if and (eq .Sort "time") (ne .Order "desc")}} {{- if and (eq .Sort "time") (ne .Order "desc")}}
<a href="?sort=time&order=desc">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a></a> <a href="?sort=time&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a></a>
{{else if and (eq .Sort "time") (ne .Order "asc")}} {{- else if and (eq .Sort "time") (ne .Order "asc")}}
<a href="?sort=time&order=asc">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a></a> <a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a></a>
{{else}} {{- else}}
<a href="?sort=time&order=asc">Modified</a> <a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified</a>
{{end}} {{- end}}
</th> </th>
</tr> </tr>
{{if .CanGoUp}} </thead>
<tbody>
{{- if .CanGoUp}}
<tr> <tr>
<td> <td>
<a href=".."> <a href="..">
...@@ -350,30 +365,52 @@ footer { ...@@ -350,30 +365,52 @@ footer {
</a> </a>
</td> </td>
<td>&mdash;</td> <td>&mdash;</td>
<td>&mdash;</td> <td class="hideable">&mdash;</td>
</tr> </tr>
{{end}} {{- end}}
{{range .Items}} {{- range .Items}}
<tr> <tr>
<td> <td>
<a href="{{.URL}}"> <a href="{{.URL}}">
{{if .IsDir}} {{- if .IsDir}}
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg> <svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg>
{{else}} {{- else}}
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg> <svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg>
{{end}} {{- end}}
<span class="name">{{.Name}}</span> <span class="name">{{.Name}}</span>
</a> </a>
</td> </td>
<td>{{.HumanSize}}</td> {{- if .IsDir}}
<td class="hideable">{{.HumanModTime "01/02/2006 03:04:05 PM"}}</td> <td data-order="-1">&mdash;</td>
{{- else}}
<td data-order="{{.Size}}">{{.HumanSize}}</td>
{{- end}}
<td class="hideable"><time datetime="{{.HumanModTime "2006-01-02T15:04:05Z"}}">{{.HumanModTime "01/02/2006 03:04:05 PM -07:00"}}</time></td>
</tr> </tr>
{{end}} {{- end}}
</tbody>
</table> </table>
</div> </div>
</main> </main>
<footer> <footer>
Served with <a href="https://caddyserver.com">Caddy</a> Served with <a href="https://caddyserver.com">Caddy</a>
</footer> </footer>
<script type="text/javascript">
function localizeDatetime(e, index, ar) {
if (e.textContent === undefined) {
return;
}
var d = new Date(e.getAttribute('datetime'));
if (isNaN(d)) {
d = new Date(e.textContent);
if (isNaN(d)) {
return;
}
}
e.textContent = d.toLocaleString();
}
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
timeList.forEach(localizeDatetime);
</script>
</body> </body>
</html>` </html>`
...@@ -2,6 +2,7 @@ package setup ...@@ -2,6 +2,7 @@ package setup
import ( import (
"os" "os"
"path/filepath"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/extensions" "github.com/mholt/caddy/middleware/extensions"
...@@ -47,7 +48,7 @@ func extParse(c *Controller) ([]string, error) { ...@@ -47,7 +48,7 @@ func extParse(c *Controller) ([]string, error) {
// resourceExists returns true if the file specified at // resourceExists returns true if the file specified at
// root + path exists; false otherwise. // root + path exists; false otherwise.
func resourceExists(root, path string) bool { func resourceExists(root, path string) bool {
_, err := os.Stat(root + path) _, err := os.Stat(filepath.Join(root, path))
// technically we should use os.IsNotExist(err) // technically we should use os.IsNotExist(err)
// but we don't handle any other kinds of errors anyway // but we don't handle any other kinds of errors anyway
return err == nil return err == nil
......
...@@ -38,14 +38,14 @@ func TestHeadersParse(t *testing.T) { ...@@ -38,14 +38,14 @@ func TestHeadersParse(t *testing.T) {
{`header /foo Foo "Bar Baz"`, {`header /foo Foo "Bar Baz"`,
false, []headers.Rule{ false, []headers.Rule{
{Path: "/foo", Headers: []headers.Header{ {Path: "/foo", Headers: []headers.Header{
{"Foo", "Bar Baz"}, {Name: "Foo", Value: "Bar Baz"},
}}, }},
}}, }},
{`header /bar { Foo "Bar Baz" Baz Qux }`, {`header /bar { Foo "Bar Baz" Baz Qux }`,
false, []headers.Rule{ false, []headers.Rule{
{Path: "/bar", Headers: []headers.Header{ {Path: "/bar", Headers: []headers.Header{
{"Foo", "Bar Baz"}, {Name: "Foo", Value: "Bar Baz"},
{"Baz", "Qux"}, {Name: "Baz", Value: "Qux"},
}}, }},
}}, }},
} }
......
CHANGES CHANGES
<master> <master>
- Built with Go 1.6.1 - ...
- New pprof directive for exposing process performance profile
- New expvar directive for exposing memory/GC performance
0.8.3 (April 26, 2016)
- Built with Go 1.6.2
- New pprof middleware for exposing process profiling endpoints
- New expvar middleware for exposing memory/GC performance
- New -restart option to force in-process restarts on Unix systems - New -restart option to force in-process restarts on Unix systems
- Only fail to start if managed certificate is expired (issue #642) - Only fail to start if managed certificate is expired (issue #642)
- Toggle case-sensitive path matching with environment variable - Toggle case-sensitive path matching with environment variable
- File server now adds ETag header for static files - File server now adds ETag header for static files
- browse: Replace .LinkedPath action with .BreadcrumbMap
- fastcgi: New except clause to exclude paths - fastcgi: New except clause to exclude paths
- proxy: New max_conns setting to limit max connections per upstream - proxy: New max_conns setting to limit max connections per upstream
- proxy: Enables replaceable value for name of upstream host - proxy: New replaceable value for name of upstream host
- templates: New utility actions for dealing with strings
- tls: Customize certificate key with key_type (+ECC) - tls: Customize certificate key with key_type (+ECC)
- Internal improvements and bug fixes - tls: Session ticket keys are now rotated
- Many other minor internal improvements and bug fixes
0.8.2 (February 25, 2016) 0.8.2 (February 25, 2016)
- On-demand TLS can obtain certificates during handshakes - On-demand TLS can obtain certificates during handshakes
......
CADDY 0.8.2 CADDY 0.8.3
Website Website
https://caddyserver.com https://caddyserver.com
......
This diff is collapsed.
...@@ -112,13 +112,12 @@ func TestBrowseHTTPMethods(t *testing.T) { ...@@ -112,13 +112,12 @@ func TestBrowseHTTPMethods(t *testing.T) {
b := Browse{ b := Browse{
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called") return http.StatusTeapot, nil // not t.Fatalf, or we will not see what other methods yield
return 0, nil
}), }),
Root: "./testdata",
Configs: []Config{ Configs: []Config{
{ {
PathScope: "/photos", PathScope: "/photos",
Root: http.Dir("./testdata"),
Template: tmpl, Template: tmpl,
}, },
}, },
...@@ -128,14 +127,8 @@ func TestBrowseHTTPMethods(t *testing.T) { ...@@ -128,14 +127,8 @@ func TestBrowseHTTPMethods(t *testing.T) {
for method, expected := range map[string]int{ for method, expected := range map[string]int{
http.MethodGet: http.StatusOK, http.MethodGet: http.StatusOK,
http.MethodHead: http.StatusOK, http.MethodHead: http.StatusOK,
http.MethodOptions: http.StatusMethodNotAllowed, http.MethodOptions: http.StatusNotImplemented,
http.MethodPost: http.StatusMethodNotAllowed, "PROPFIND": http.StatusNotImplemented,
http.MethodPut: http.StatusMethodNotAllowed,
http.MethodPatch: http.StatusMethodNotAllowed,
http.MethodDelete: http.StatusMethodNotAllowed,
"COPY": http.StatusMethodNotAllowed,
"MOVE": http.StatusMethodNotAllowed,
"MKCOL": http.StatusMethodNotAllowed,
} { } {
req, err := http.NewRequest(method, "/photos/", nil) req, err := http.NewRequest(method, "/photos/", nil)
if err != nil { if err != nil {
...@@ -160,10 +153,10 @@ func TestBrowseTemplate(t *testing.T) { ...@@ -160,10 +153,10 @@ func TestBrowseTemplate(t *testing.T) {
t.Fatalf("Next shouldn't be called") t.Fatalf("Next shouldn't be called")
return 0, nil return 0, nil
}), }),
Root: "./testdata",
Configs: []Config{ Configs: []Config{
{ {
PathScope: "/photos", PathScope: "/photos",
Root: http.Dir("./testdata"),
Template: tmpl, Template: tmpl,
}, },
}, },
...@@ -215,16 +208,16 @@ func TestBrowseJson(t *testing.T) { ...@@ -215,16 +208,16 @@ func TestBrowseJson(t *testing.T) {
t.Fatalf("Next shouldn't be called") t.Fatalf("Next shouldn't be called")
return 0, nil return 0, nil
}), }),
Root: "./testdata",
Configs: []Config{ Configs: []Config{
{ {
PathScope: "/photos/", PathScope: "/photos/",
Root: http.Dir("./testdata"),
}, },
}, },
} }
//Getting the listing from the ./testdata/photos, the listing returned will be used to validate test results //Getting the listing from the ./testdata/photos, the listing returned will be used to validate test results
testDataPath := b.Root + "/photos/" testDataPath := filepath.Join("./testdata", "photos")
file, err := os.Open(testDataPath) file, err := os.Open(testDataPath)
if err != nil { if err != nil {
if os.IsPermission(err) { if os.IsPermission(err) {
...@@ -245,7 +238,7 @@ func TestBrowseJson(t *testing.T) { ...@@ -245,7 +238,7 @@ func TestBrowseJson(t *testing.T) {
// Tests fail in CI environment because all file mod times are the same for // Tests fail in CI environment because all file mod times are the same for
// some reason, making the sorting unpredictable. To hack around this, // some reason, making the sorting unpredictable. To hack around this,
// we ensure here that each file has a different mod time. // we ensure here that each file has a different mod time.
chTime := f.ModTime().Add(-(time.Duration(i) * time.Second)) chTime := f.ModTime().UTC().Add(-(time.Duration(i) * time.Second))
if err := os.Chtimes(filepath.Join(testDataPath, name), chTime, chTime); err != nil { if err := os.Chtimes(filepath.Join(testDataPath, name), chTime, chTime); err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -322,7 +315,7 @@ func TestBrowseJson(t *testing.T) { ...@@ -322,7 +315,7 @@ func TestBrowseJson(t *testing.T) {
code, err := b.ServeHTTP(rec, req) code, err := b.ServeHTTP(rec, req)
if code != http.StatusOK { if code != http.StatusOK {
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code) t.Fatalf("In test %d: Wrong status, expected %d, got %d", i, http.StatusOK, code)
} }
if rec.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { if rec.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
t.Fatalf("Expected Content type to be application/json; charset=utf-8, but got %s ", rec.HeaderMap.Get("Content-Type")) t.Fatalf("Expected Content type to be application/json; charset=utf-8, but got %s ", rec.HeaderMap.Get("Content-Type"))
......
...@@ -235,7 +235,7 @@ func (c Context) ToUpper(s string) string { ...@@ -235,7 +235,7 @@ func (c Context) ToUpper(s string) string {
return strings.ToUpper(s) return strings.ToUpper(s)
} }
// Split is a passthrough to strings.Split. It will split the first argument at each instance of the seperator and return a slice of strings. // Split is a passthrough to strings.Split. It will split the first argument at each instance of the separator and return a slice of strings.
func (c Context) Split(s string, sep string) []string { func (c Context) Split(s string, sep string) []string {
return strings.Split(s, sep) return strings.Split(s, sep)
} }
......
...@@ -200,7 +200,7 @@ func DisabledTest(t *testing.T) { ...@@ -200,7 +200,7 @@ func DisabledTest(t *testing.T) {
listener, err := net.Listen("tcp", ipPort) listener, err := net.Listen("tcp", ipPort)
if err != nil { if err != nil {
// handle error // handle error
log.Println("listener creatation failed: ", err) log.Println("listener creation failed: ", err)
} }
srv := new(FastCGIServer) srv := new(FastCGIServer)
......
...@@ -2,10 +2,12 @@ package middleware ...@@ -2,10 +2,12 @@ package middleware
import ( import (
"fmt" "fmt"
"math/rand"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
) )
...@@ -40,12 +42,11 @@ type fileHandler struct { ...@@ -40,12 +42,11 @@ type fileHandler struct {
} }
func (fh *fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { func (fh *fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
upath := r.URL.Path // r.URL.Path has already been cleaned in caddy/server by path.Clean().
if !strings.HasPrefix(upath, "/") { if r.URL.Path == "" {
upath = "/" + upath r.URL.Path = "/"
r.URL.Path = upath
} }
return fh.serveFile(w, r, path.Clean(upath)) return fh.serveFile(w, r, r.URL.Path)
} }
// serveFile writes the specified file to the HTTP response. // serveFile writes the specified file to the HTTP response.
...@@ -66,7 +67,8 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st ...@@ -66,7 +67,8 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st
return http.StatusForbidden, err return http.StatusForbidden, err
} }
// Likely the server is under load and ran out of file descriptors // Likely the server is under load and ran out of file descriptors
w.Header().Set("Retry-After", "5") // TODO: 5 seconds enough delay? Or too much? backoff := int(3 + rand.Int31()%3) // 3–5 seconds to prevent a stampede
w.Header().Set("Retry-After", strconv.Itoa(backoff))
return http.StatusServiceUnavailable, err return http.StatusServiceUnavailable, err
} }
defer f.Close() defer f.Close()
...@@ -86,13 +88,13 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st ...@@ -86,13 +88,13 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st
url := r.URL.Path url := r.URL.Path
if d.IsDir() { if d.IsDir() {
// Ensure / at end of directory url // Ensure / at end of directory url
if url[len(url)-1] != '/' { if !strings.HasSuffix(url, "/") {
redirect(w, r, path.Base(url)+"/") redirect(w, r, path.Base(url)+"/")
return http.StatusMovedPermanently, nil return http.StatusMovedPermanently, nil
} }
} else { } else {
// Ensure no / at end of file url // Ensure no / at end of file url
if url[len(url)-1] == '/' { if strings.HasSuffix(url, "/") {
redirect(w, r, "../"+path.Base(url)) redirect(w, r, "../"+path.Base(url))
return http.StatusMovedPermanently, nil return http.StatusMovedPermanently, nil
} }
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"errors" "errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
...@@ -11,23 +12,30 @@ import ( ...@@ -11,23 +12,30 @@ import (
"time" "time"
) )
var testDir = filepath.Join(os.TempDir(), "caddy_testdir") var (
var ErrCustom = errors.New("Custom Error") ErrCustom = errors.New("Custom Error")
testDir = filepath.Join(os.TempDir(), "caddy_testdir")
testWebRoot = filepath.Join(testDir, "webroot")
)
// testFiles is a map with relative paths to test files as keys and file content as values. // testFiles is a map with relative paths to test files as keys and file content as values.
// The map represents the following structure: // The map represents the following structure:
// - $TEMP/caddy_testdir/ // - $TEMP/caddy_testdir/
// '-- file1.html // '-- unreachable.html
// '-- dirwithindex/ // '-- webroot/
// '---- index.html // '---- file1.html
// '-- dir/ // '---- dirwithindex/
// '---- file2.html // '------ index.html
// '---- hidden.html // '---- dir/
// '------ file2.html
// '------ hidden.html
var testFiles = map[string]string{ var testFiles = map[string]string{
"file1.html": "<h1>file1.html</h1>", "unreachable.html": "<h1>must not leak</h1>",
filepath.Join("dirwithindex", "index.html"): "<h1>dirwithindex/index.html</h1>", filepath.Join("webroot", "file1.html"): "<h1>file1.html</h1>",
filepath.Join("dir", "file2.html"): "<h1>dir/file2.html</h1>", filepath.Join("webroot", "dirwithindex", "index.html"): "<h1>dirwithindex/index.html</h1>",
filepath.Join("dir", "hidden.html"): "<h1>dir/hidden.html</h1>", filepath.Join("webroot", "dir", "file2.html"): "<h1>dir/file2.html</h1>",
filepath.Join("webroot", "dir", "hidden.html"): "<h1>dir/hidden.html</h1>",
} }
// TestServeHTTP covers positive scenarios when serving files. // TestServeHTTP covers positive scenarios when serving files.
...@@ -36,7 +44,7 @@ func TestServeHTTP(t *testing.T) { ...@@ -36,7 +44,7 @@ func TestServeHTTP(t *testing.T) {
beforeServeHTTPTest(t) beforeServeHTTPTest(t)
defer afterServeHTTPTest(t) defer afterServeHTTPTest(t)
fileserver := FileServer(http.Dir(testDir), []string{"dir/hidden.html"}) fileserver := FileServer(http.Dir(testWebRoot), []string{"dir/hidden.html"})
movedPermanently := "Moved Permanently" movedPermanently := "Moved Permanently"
...@@ -142,11 +150,20 @@ func TestServeHTTP(t *testing.T) { ...@@ -142,11 +150,20 @@ func TestServeHTTP(t *testing.T) {
url: "https://foo/hidden.html", url: "https://foo/hidden.html",
expectedStatus: http.StatusNotFound, expectedStatus: http.StatusNotFound,
}, },
// Test 17 - try to get below the root directory.
{
url: "https://foo/%2f..%2funreachable.html",
expectedStatus: http.StatusNotFound,
},
} }
for i, test := range tests { for i, test := range tests {
responseRecorder := httptest.NewRecorder() responseRecorder := httptest.NewRecorder()
request, err := http.NewRequest("GET", test.url, strings.NewReader("")) request, err := http.NewRequest("GET", test.url, nil)
// prevent any URL sanitization within Go: we need unmodified paths here
if u, _ := url.Parse(test.url); u.RawPath != "" {
request.URL.Path = u.RawPath
}
status, err := fileserver.ServeHTTP(responseRecorder, request) status, err := fileserver.ServeHTTP(responseRecorder, request)
etag := responseRecorder.Header().Get("Etag") etag := responseRecorder.Header().Get("Etag")
...@@ -176,7 +193,7 @@ func TestServeHTTP(t *testing.T) { ...@@ -176,7 +193,7 @@ func TestServeHTTP(t *testing.T) {
// beforeServeHTTPTest creates a test directory with the structure, defined in the variable testFiles // beforeServeHTTPTest creates a test directory with the structure, defined in the variable testFiles
func beforeServeHTTPTest(t *testing.T) { func beforeServeHTTPTest(t *testing.T) {
// make the root test dir // make the root test dir
err := os.Mkdir(testDir, os.ModePerm) err := os.MkdirAll(testWebRoot, os.ModePerm)
if err != nil { if err != nil {
if !os.IsExist(err) { if !os.IsExist(err) {
t.Fatalf("Failed to create test dir. Error was: %v", err) t.Fatalf("Failed to create test dir. Error was: %v", err)
......
...@@ -20,13 +20,14 @@ type Headers struct { ...@@ -20,13 +20,14 @@ type Headers struct {
// ServeHTTP implements the middleware.Handler interface and serves requests, // ServeHTTP implements the middleware.Handler interface and serves requests,
// setting headers on the response according to the configured rules. // setting headers on the response according to the configured rules.
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
replacer := middleware.NewReplacer(r, nil, "")
for _, rule := range h.Rules { for _, rule := range h.Rules {
if middleware.Path(r.URL.Path).Matches(rule.Path) { if middleware.Path(r.URL.Path).Matches(rule.Path) {
for _, header := range rule.Headers { for _, header := range rule.Headers {
if strings.HasPrefix(header.Name, "-") { if strings.HasPrefix(header.Name, "-") {
w.Header().Del(strings.TrimLeft(header.Name, "-")) w.Header().Del(strings.TrimLeft(header.Name, "-"))
} else { } else {
w.Header().Set(header.Name, header.Value) w.Header().Set(header.Name, replacer.Replace(header.Value))
} }
} }
} }
......
...@@ -3,12 +3,17 @@ package headers ...@@ -3,12 +3,17 @@ package headers
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"testing" "testing"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
) )
func TestHeaders(t *testing.T) { func TestHeaders(t *testing.T) {
hostname, err := os.Hostname()
if err != nil {
t.Fatalf("Could not determine hostname: %v", err)
}
for i, test := range []struct { for i, test := range []struct {
from string from string
name string name string
...@@ -17,6 +22,7 @@ func TestHeaders(t *testing.T) { ...@@ -17,6 +22,7 @@ func TestHeaders(t *testing.T) {
{"/a", "Foo", "Bar"}, {"/a", "Foo", "Bar"},
{"/a", "Bar", ""}, {"/a", "Bar", ""},
{"/a", "Baz", ""}, {"/a", "Baz", ""},
{"/a", "ServerName", hostname},
{"/b", "Foo", ""}, {"/b", "Foo", ""},
{"/b", "Bar", "Removed in /a"}, {"/b", "Bar", "Removed in /a"},
} { } {
...@@ -27,6 +33,7 @@ func TestHeaders(t *testing.T) { ...@@ -27,6 +33,7 @@ func TestHeaders(t *testing.T) {
Rules: []Rule{ Rules: []Rule{
{Path: "/a", Headers: []Header{ {Path: "/a", Headers: []Header{
{Name: "Foo", Value: "Bar"}, {Name: "Foo", Value: "Bar"},
{Name: "ServerName", Value: "{hostname}"},
{Name: "-Bar"}, {Name: "-Bar"},
}}, }},
}, },
......
...@@ -3,8 +3,10 @@ package proxy ...@@ -3,8 +3,10 @@ package proxy
import ( import (
"errors" "errors"
"net"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"sync/atomic" "sync/atomic"
"time" "time"
...@@ -41,7 +43,8 @@ type UpstreamHost struct { ...@@ -41,7 +43,8 @@ type UpstreamHost struct {
Fails int32 Fails int32
FailTimeout time.Duration FailTimeout time.Duration
Unhealthy bool Unhealthy bool
ExtraHeaders http.Header UpstreamHeaders http.Header
DownstreamHeaders http.Header
CheckDown UpstreamHostDownFunc CheckDown UpstreamHostDownFunc
WithoutPathPrefix string WithoutPathPrefix string
MaxConns int64 MaxConns int64
...@@ -75,71 +78,160 @@ var tryDuration = 60 * time.Second ...@@ -75,71 +78,160 @@ var tryDuration = 60 * time.Second
// ServeHTTP satisfies the middleware.Handler interface. // ServeHTTP satisfies the middleware.Handler interface.
func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for _, upstream := range p.Upstreams { for _, upstream := range p.Upstreams {
if middleware.Path(r.URL.Path).Matches(upstream.From()) && upstream.AllowedPath(r.URL.Path) { if !middleware.Path(r.URL.Path).Matches(upstream.From()) ||
var replacer middleware.Replacer !upstream.AllowedPath(r.URL.Path) {
start := time.Now() continue
requestHost := r.Host }
// Since Select() should give us "up" hosts, keep retrying var replacer middleware.Replacer
// hosts until timeout (or until we get a nil host). start := time.Now()
for time.Now().Sub(start) < tryDuration {
host := upstream.Select() outreq := createUpstreamRequest(r)
if host == nil {
return http.StatusBadGateway, errUnreachable
}
proxy := host.ReverseProxy
r.Host = host.Name
if rr, ok := w.(*middleware.ResponseRecorder); ok && rr.Replacer != nil {
rr.Replacer.Set("upstream", host.Name)
}
if baseURL, err := url.Parse(host.Name); err == nil { // Since Select() should give us "up" hosts, keep retrying
r.Host = baseURL.Host // hosts until timeout (or until we get a nil host).
if proxy == nil { for time.Now().Sub(start) < tryDuration {
proxy = NewSingleHostReverseProxy(baseURL, host.WithoutPathPrefix) host := upstream.Select()
} if host == nil {
} else if proxy == nil { return http.StatusBadGateway, errUnreachable
return http.StatusInternalServerError, err }
if rr, ok := w.(*middleware.ResponseRecorder); ok && rr.Replacer != nil {
rr.Replacer.Set("upstream", host.Name)
}
outreq.Host = host.Name
if host.UpstreamHeaders != nil {
if replacer == nil {
rHost := r.Host
replacer = middleware.NewReplacer(r, nil, "")
outreq.Host = rHost
} }
var extraHeaders http.Header if v, ok := host.UpstreamHeaders["Host"]; ok {
if host.ExtraHeaders != nil { r.Host = replacer.Replace(v[len(v)-1])
extraHeaders = make(http.Header)
if replacer == nil {
rHost := r.Host
r.Host = requestHost
replacer = middleware.NewReplacer(r, nil, "")
r.Host = rHost
}
for header, values := range host.ExtraHeaders {
for _, value := range values {
extraHeaders.Add(header,
replacer.Replace(value))
if header == "Host" {
r.Host = replacer.Replace(value)
}
}
}
} }
// Modify headers for request that will be sent to the upstream host
upHeaders := createHeadersByRules(host.UpstreamHeaders, r.Header, replacer)
for k, v := range upHeaders {
outreq.Header[k] = v
}
}
atomic.AddInt64(&host.Conns, 1) var downHeaderUpdateFn respUpdateFn
backendErr := proxy.ServeHTTP(w, r, extraHeaders) if host.DownstreamHeaders != nil {
atomic.AddInt64(&host.Conns, -1) if replacer == nil {
if backendErr == nil { rHost := r.Host
return 0, nil replacer = middleware.NewReplacer(r, nil, "")
outreq.Host = rHost
} }
timeout := host.FailTimeout //Creates a function that is used to update headers the response received by the reverse proxy
if timeout == 0 { downHeaderUpdateFn = createRespHeaderUpdateFn(host.DownstreamHeaders, replacer)
timeout = 10 * time.Second }
proxy := host.ReverseProxy
if baseURL, err := url.Parse(host.Name); err == nil {
r.Host = baseURL.Host
if proxy == nil {
proxy = NewSingleHostReverseProxy(baseURL, host.WithoutPathPrefix)
} }
atomic.AddInt32(&host.Fails, 1) } else if proxy == nil {
go func(host *UpstreamHost, timeout time.Duration) { return http.StatusInternalServerError, err
time.Sleep(timeout) }
atomic.AddInt32(&host.Fails, -1)
}(host, timeout) atomic.AddInt64(&host.Conns, 1)
backendErr := proxy.ServeHTTP(w, outreq, downHeaderUpdateFn)
atomic.AddInt64(&host.Conns, -1)
if backendErr == nil {
return 0, nil
}
timeout := host.FailTimeout
if timeout == 0 {
timeout = 10 * time.Second
} }
return http.StatusBadGateway, errUnreachable atomic.AddInt32(&host.Fails, 1)
go func(host *UpstreamHost, timeout time.Duration) {
time.Sleep(timeout)
atomic.AddInt32(&host.Fails, -1)
}(host, timeout)
} }
return http.StatusBadGateway, errUnreachable
} }
return p.Next.ServeHTTP(w, r) return p.Next.ServeHTTP(w, r)
} }
// createUpstremRequest shallow-copies r into a new request
// that can be sent upstream.
func createUpstreamRequest(r *http.Request) *http.Request {
outreq := new(http.Request)
*outreq = *r // includes shallow copies of maps, but okay
// Remove hop-by-hop headers to the backend. Especially
// important is "Connection" because we want a persistent
// connection, regardless of what the client sent to us. This
// is modifying the same underlying map from r (shallow
// copied above) so we only copy it if necessary.
for _, h := range hopHeaders {
if outreq.Header.Get(h) != "" {
outreq.Header = make(http.Header)
copyHeader(outreq.Header, r.Header)
outreq.Header.Del(h)
}
}
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
// If we aren't the first proxy, retain prior
// X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one.
if prior, ok := outreq.Header["X-Forwarded-For"]; ok {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
outreq.Header.Set("X-Forwarded-For", clientIP)
}
return outreq
}
func createRespHeaderUpdateFn(rules http.Header, replacer middleware.Replacer) respUpdateFn {
return func(resp *http.Response) {
newHeaders := createHeadersByRules(rules, resp.Header, replacer)
for h, v := range newHeaders {
resp.Header[h] = v
}
}
}
func createHeadersByRules(rules http.Header, base http.Header, repl middleware.Replacer) http.Header {
newHeaders := make(http.Header)
for header, values := range rules {
if strings.HasPrefix(header, "+") {
header = strings.TrimLeft(header, "+")
add(newHeaders, header, base[header])
applyEach(values, repl.Replace)
add(newHeaders, header, values)
} else if strings.HasPrefix(header, "-") {
base.Del(strings.TrimLeft(header, "-"))
} else if _, ok := base[header]; ok {
applyEach(values, repl.Replace)
for _, v := range values {
newHeaders.Set(header, v)
}
} else {
applyEach(values, repl.Replace)
add(newHeaders, header, values)
add(newHeaders, header, base[header])
}
}
return newHeaders
}
func applyEach(values []string, mapFn func(string) string) {
for i, v := range values {
values[i] = mapFn(v)
}
}
func add(base http.Header, header string, values []string) {
for _, v := range values {
base.Add(header, v)
}
}
...@@ -348,6 +348,141 @@ func TestUnixSocketProxyPaths(t *testing.T) { ...@@ -348,6 +348,141 @@ func TestUnixSocketProxyPaths(t *testing.T) {
} }
} }
func TestUpstreamHeadersUpdate(t *testing.T) {
log.SetOutput(ioutil.Discard)
defer log.SetOutput(os.Stderr)
var actualHeaders http.Header
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, client"))
actualHeaders = r.Header
}))
defer backend.Close()
upstream := newFakeUpstream(backend.URL, false)
upstream.host.UpstreamHeaders = http.Header{
"Connection": {"{>Connection}"},
"Upgrade": {"{>Upgrade}"},
"+Merge-Me": {"Merge-Value"},
"+Add-Me": {"Add-Value"},
"-Remove-Me": {""},
"Replace-Me": {"{hostname}"},
}
// set up proxy
p := &Proxy{
Upstreams: []Upstream{upstream},
}
// create request and response recorder
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
w := httptest.NewRecorder()
//add initial headers
r.Header.Add("Merge-Me", "Initial")
r.Header.Add("Remove-Me", "Remove-Value")
r.Header.Add("Replace-Me", "Replace-Value")
p.ServeHTTP(w, r)
replacer := middleware.NewReplacer(r, nil, "")
headerKey := "Merge-Me"
values, ok := actualHeaders[headerKey]
if !ok {
t.Errorf("Request sent to upstream backend does not contain expected %v header. Expected header to be added", headerKey)
} else if len(values) < 2 && (values[0] != "Initial" || values[1] != replacer.Replace("{hostname}")) {
t.Errorf("Values for proxy header `+Merge-Me` should be merged. Got %v", values)
}
headerKey = "Add-Me"
if _, ok := actualHeaders[headerKey]; !ok {
t.Errorf("Request sent to upstream backend does not contain expected %v header", headerKey)
}
headerKey = "Remove-Me"
if _, ok := actualHeaders[headerKey]; ok {
t.Errorf("Request sent to upstream backend should not contain %v header", headerKey)
}
headerKey = "Replace-Me"
headerValue := replacer.Replace("{hostname}")
value, ok := actualHeaders[headerKey]
if !ok {
t.Errorf("Request sent to upstream backend should not remove %v header", headerKey)
} else if len(value) > 0 && headerValue != value[0] {
t.Errorf("Request sent to upstream backend should replace value of %v header with %v. Instead value was %v", headerKey, headerValue, value)
}
}
func TestDownstreamHeadersUpdate(t *testing.T) {
log.SetOutput(ioutil.Discard)
defer log.SetOutput(os.Stderr)
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Merge-Me", "Initial")
w.Header().Add("Remove-Me", "Remove-Value")
w.Header().Add("Replace-Me", "Replace-Value")
w.Write([]byte("Hello, client"))
}))
defer backend.Close()
upstream := newFakeUpstream(backend.URL, false)
upstream.host.DownstreamHeaders = http.Header{
"+Merge-Me": {"Merge-Value"},
"+Add-Me": {"Add-Value"},
"-Remove-Me": {""},
"Replace-Me": {"{hostname}"},
}
// set up proxy
p := &Proxy{
Upstreams: []Upstream{upstream},
}
// create request and response recorder
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
w := httptest.NewRecorder()
p.ServeHTTP(w, r)
replacer := middleware.NewReplacer(r, nil, "")
actualHeaders := w.Header()
headerKey := "Merge-Me"
values, ok := actualHeaders[headerKey]
if !ok {
t.Errorf("Downstream response does not contain expected %v header. Expected header should be added", headerKey)
} else if len(values) < 2 && (values[0] != "Initial" || values[1] != replacer.Replace("{hostname}")) {
t.Errorf("Values for header `+Merge-Me` should be merged. Got %v", values)
}
headerKey = "Add-Me"
if _, ok := actualHeaders[headerKey]; !ok {
t.Errorf("Downstream response does not contain expected %v header", headerKey)
}
headerKey = "Remove-Me"
if _, ok := actualHeaders[headerKey]; ok {
t.Errorf("Downstream response should not contain %v header received from upstream", headerKey)
}
headerKey = "Replace-Me"
headerValue := replacer.Replace("{hostname}")
value, ok := actualHeaders[headerKey]
if !ok {
t.Errorf("Downstream response should contain %v header and not remove it", headerKey)
} else if len(value) > 0 && headerValue != value[0] {
t.Errorf("Downstream response should have header %v with value %v. Instead value was %v", headerKey, headerValue, value)
}
}
func newFakeUpstream(name string, insecure bool) *fakeUpstream { func newFakeUpstream(name string, insecure bool) *fakeUpstream {
uri, _ := url.Parse(name) uri, _ := url.Parse(name)
u := &fakeUpstream{ u := &fakeUpstream{
...@@ -410,7 +545,7 @@ func (u *fakeWsUpstream) Select() *UpstreamHost { ...@@ -410,7 +545,7 @@ func (u *fakeWsUpstream) Select() *UpstreamHost {
return &UpstreamHost{ return &UpstreamHost{
Name: u.name, Name: u.name,
ReverseProxy: NewSingleHostReverseProxy(uri, u.without), ReverseProxy: NewSingleHostReverseProxy(uri, u.without),
ExtraHeaders: http.Header{ UpstreamHeaders: http.Header{
"Connection": {"{>Connection}"}, "Connection": {"{>Connection}"},
"Upgrade": {"{>Upgrade}"}}, "Upgrade": {"{>Upgrade}"}},
} }
......
...@@ -154,57 +154,25 @@ var InsecureTransport http.RoundTripper = &http.Transport{ ...@@ -154,57 +154,25 @@ var InsecureTransport http.RoundTripper = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
} }
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request, extraHeaders http.Header) error { type respUpdateFn func(resp *http.Response)
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, outreq *http.Request, respUpdateFn respUpdateFn) error {
transport := p.Transport transport := p.Transport
if transport == nil { if transport == nil {
transport = http.DefaultTransport transport = http.DefaultTransport
} }
outreq := new(http.Request)
*outreq = *req // includes shallow copies of maps, but okay
p.Director(outreq) p.Director(outreq)
outreq.Proto = "HTTP/1.1" outreq.Proto = "HTTP/1.1"
outreq.ProtoMajor = 1 outreq.ProtoMajor = 1
outreq.ProtoMinor = 1 outreq.ProtoMinor = 1
outreq.Close = false outreq.Close = false
// Remove hop-by-hop headers to the backend. Especially
// important is "Connection" because we want a persistent
// connection, regardless of what the client sent to us. This
// is modifying the same underlying map from req (shallow
// copied above) so we only copy it if necessary.
copiedHeaders := false
for _, h := range hopHeaders {
if outreq.Header.Get(h) != "" {
if !copiedHeaders {
outreq.Header = make(http.Header)
copyHeader(outreq.Header, req.Header)
copiedHeaders = true
}
outreq.Header.Del(h)
}
}
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
// If we aren't the first proxy retain prior
// X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one.
if prior, ok := outreq.Header["X-Forwarded-For"]; ok {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
outreq.Header.Set("X-Forwarded-For", clientIP)
}
if extraHeaders != nil {
for k, v := range extraHeaders {
outreq.Header[k] = v
}
}
res, err := transport.RoundTrip(outreq) res, err := transport.RoundTrip(outreq)
if err != nil { if err != nil {
return err return err
} else if respUpdateFn != nil {
respUpdateFn(res)
} }
if res.StatusCode == http.StatusSwitchingProtocols && strings.ToLower(res.Header.Get("Upgrade")) == "websocket" { if res.StatusCode == http.StatusSwitchingProtocols && strings.ToLower(res.Header.Get("Upgrade")) == "websocket" {
...@@ -237,9 +205,7 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request, extr ...@@ -237,9 +205,7 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request, extr
for _, h := range hopHeaders { for _, h := range hopHeaders {
res.Header.Del(h) res.Header.Del(h)
} }
copyHeader(rw.Header(), res.Header) copyHeader(rw.Header(), res.Header)
rw.WriteHeader(res.StatusCode) rw.WriteHeader(res.StatusCode)
p.copyResponse(rw, res.Body) p.copyResponse(rw, res.Body)
} }
...@@ -260,7 +226,6 @@ func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) { ...@@ -260,7 +226,6 @@ func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
dst = mlw dst = mlw
} }
} }
io.Copy(dst, src) io.Copy(dst, src)
} }
......
...@@ -20,7 +20,8 @@ var ( ...@@ -20,7 +20,8 @@ var (
type staticUpstream struct { type staticUpstream struct {
from string from string
proxyHeaders http.Header upstreamHeaders http.Header
downstreamHeaders http.Header
Hosts HostPool Hosts HostPool
Policy Policy Policy Policy
insecureSkipVerify bool insecureSkipVerify bool
...@@ -42,13 +43,14 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) { ...@@ -42,13 +43,14 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) {
var upstreams []Upstream var upstreams []Upstream
for c.Next() { for c.Next() {
upstream := &staticUpstream{ upstream := &staticUpstream{
from: "", from: "",
proxyHeaders: make(http.Header), upstreamHeaders: make(http.Header),
Hosts: nil, downstreamHeaders: make(http.Header),
Policy: &Random{}, Hosts: nil,
FailTimeout: 10 * time.Second, Policy: &Random{},
MaxFails: 1, FailTimeout: 10 * time.Second,
MaxConns: 0, MaxFails: 1,
MaxConns: 0,
} }
if !c.Args(&upstream.from) { if !c.Args(&upstream.from) {
...@@ -97,12 +99,13 @@ func (u *staticUpstream) NewHost(host string) (*UpstreamHost, error) { ...@@ -97,12 +99,13 @@ func (u *staticUpstream) NewHost(host string) (*UpstreamHost, error) {
host = "http://" + host host = "http://" + host
} }
uh := &UpstreamHost{ uh := &UpstreamHost{
Name: host, Name: host,
Conns: 0, Conns: 0,
Fails: 0, Fails: 0,
FailTimeout: u.FailTimeout, FailTimeout: u.FailTimeout,
Unhealthy: false, Unhealthy: false,
ExtraHeaders: u.proxyHeaders, UpstreamHeaders: u.upstreamHeaders,
DownstreamHeaders: u.downstreamHeaders,
CheckDown: func(u *staticUpstream) UpstreamHostDownFunc { CheckDown: func(u *staticUpstream) UpstreamHostDownFunc {
return func(uh *UpstreamHost) bool { return func(uh *UpstreamHost) bool {
if uh.Unhealthy { if uh.Unhealthy {
...@@ -182,15 +185,23 @@ func parseBlock(c *parse.Dispenser, u *staticUpstream) error { ...@@ -182,15 +185,23 @@ func parseBlock(c *parse.Dispenser, u *staticUpstream) error {
} }
u.HealthCheck.Interval = dur u.HealthCheck.Interval = dur
} }
case "header_upstream":
fallthrough
case "proxy_header": case "proxy_header":
var header, value string var header, value string
if !c.Args(&header, &value) { if !c.Args(&header, &value) {
return c.ArgErr() return c.ArgErr()
} }
u.proxyHeaders.Add(header, value) u.upstreamHeaders.Add(header, value)
case "header_downstream":
var header, value string
if !c.Args(&header, &value) {
return c.ArgErr()
}
u.downstreamHeaders.Add(header, value)
case "websocket": case "websocket":
u.proxyHeaders.Add("Connection", "{>Connection}") u.upstreamHeaders.Add("Connection", "{>Connection}")
u.proxyHeaders.Add("Upgrade", "{>Upgrade}") u.upstreamHeaders.Add("Upgrade", "{>Upgrade}")
case "without": case "without":
if !c.NextArg() { if !c.NextArg() {
return c.ArgErr() return c.ArgErr()
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path" "path"
"strconv" "strconv"
"strings" "strings"
...@@ -52,6 +53,13 @@ func NewReplacer(r *http.Request, rr *ResponseRecorder, emptyValue string) Repla ...@@ -52,6 +53,13 @@ func NewReplacer(r *http.Request, rr *ResponseRecorder, emptyValue string) Repla
} }
return "http" return "http"
}(), }(),
"{hostname}": func() string {
name, err := os.Hostname()
if err != nil {
return ""
}
return name
}(),
"{host}": r.Host, "{host}": r.Host,
"{path}": r.URL.Path, "{path}": r.URL.Path,
"{path_escaped}": url.QueryEscape(r.URL.Path), "{path_escaped}": url.QueryEscape(r.URL.Path),
......
...@@ -3,6 +3,7 @@ package middleware ...@@ -3,6 +3,7 @@ package middleware
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"strings" "strings"
"testing" "testing"
) )
...@@ -53,6 +54,14 @@ func TestReplace(t *testing.T) { ...@@ -53,6 +54,14 @@ func TestReplace(t *testing.T) {
request.Header.Set("ShorterVal", "1") request.Header.Set("ShorterVal", "1")
repl := NewReplacer(request, recordRequest, "-") repl := NewReplacer(request, recordRequest, "-")
hostname, err := os.Hostname()
if err != nil {
t.Fatal("Failed to determine hostname\n")
}
if expected, actual := "This hostname is "+hostname, repl.Replace("This hostname is {hostname}"); expected != actual {
t.Errorf("{hostname} replacement: expected '%s', got '%s'", expected, actual)
}
if expected, actual := "This host is localhost.", repl.Replace("This host is {host}."); expected != actual { if expected, actual := "This host is localhost.", repl.Replace("This host is {host}."); expected != actual {
t.Errorf("{host} replacement: expected '%s', got '%s'", expected, actual) t.Errorf("{host} replacement: expected '%s', got '%s'", expected, actual)
} }
......
...@@ -14,7 +14,7 @@ import ( ...@@ -14,7 +14,7 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath" "path"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
...@@ -336,11 +336,18 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { ...@@ -336,11 +336,18 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Use URL.RawPath If you need the original, "raw" URL.Path in your middleware. // Use URL.RawPath If you need the original, "raw" URL.Path in your middleware.
// Collapse any ./ ../ /// madness here instead of doing that in every plugin. // Collapse any ./ ../ /// madness here instead of doing that in every plugin.
if r.URL.Path != "/" { if r.URL.Path != "/" {
path := filepath.Clean(r.URL.Path) cleanedPath := path.Clean(r.URL.Path)
if !strings.HasPrefix(path, "/") { if cleanedPath == "." {
path = "/" + path r.URL.Path = "/"
} else {
if !strings.HasPrefix(cleanedPath, "/") {
cleanedPath = "/" + cleanedPath
}
if strings.HasSuffix(r.URL.Path, "/") && !strings.HasSuffix(cleanedPath, "/") {
cleanedPath = cleanedPath + "/"
}
r.URL.Path = cleanedPath
} }
r.URL.Path = path
} }
// Execute the optional request callback if it exists and it's not disabled // Execute the optional request callback if it exists and it's not disabled
...@@ -438,6 +445,7 @@ func standaloneTLSTicketKeyRotation(c *tls.Config, timer *time.Ticker, exitChan ...@@ -438,6 +445,7 @@ func standaloneTLSTicketKeyRotation(c *tls.Config, timer *time.Ticker, exitChan
c.SessionTicketsDisabled = true // bail if we don't have the entropy for the first one c.SessionTicketsDisabled = true // bail if we don't have the entropy for the first one
return return
} }
c.SessionTicketKey = keys[0] // SetSessionTicketKeys doesn't set a 'tls.keysAlreadSet'
c.SetSessionTicketKeys(setSessionTicketKeysTestHook(keys)) c.SetSessionTicketKeys(setSessionTicketKeysTestHook(keys))
for { for {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment