mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-22 10:25:46 +03:00
Merge branch 'master' into md_changes
This commit is contained in:
commit
c431a07af5
27 changed files with 810 additions and 401 deletions
19
.gitattributes
vendored
19
.gitattributes
vendored
|
@ -1,7 +1,14 @@
|
||||||
*.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
|
||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
### 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.
|
||||||
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
|
|
||||||
without significant changes, 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,
|
The advantage of pull requests is that we can immediately see the impact the
|
||||||
best to open an issue first and "claim" it and get feedback before you invest
|
change will have on the project, what the code will look like, and how to
|
||||||
a lot of time.
|
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 writing code for a new pull request,
|
||||||
|
best to open an issue to "claim" it and get feedback before you invest a lot of
|
||||||
|
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
|
||||||
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 @@
|
||||||
|
|
||||||
|
|
||||||
#### 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
|
||||||
: ${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 (
|
||||||
"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) {
|
||||||
|
|
||||||
// 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) {
|
||||||
|
|
||||||
// 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() {
|
||||||
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 (
|
||||||
"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 {
|
||||||
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
|
||||||
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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
} 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>
|
||||||
<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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
header,
|
header,
|
||||||
.content {
|
#summary {
|
||||||
padding-left: 5%;
|
padding-left: 5%;
|
||||||
padding-right: 5%;
|
padding-right: 5%;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>—</td>
|
<td>—</td>
|
||||||
<td>—</td>
|
<td class="hideable">—</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">—</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
|
||||||
|
|
||||||
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) {
|
||||||
// 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) {
|
||||||
{`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"},
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
18
dist/CHANGES.txt
vendored
18
dist/CHANGES.txt
vendored
|
@ -1,18 +1,26 @@
|
||||||
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
|
||||||
|
|
2
dist/README.txt
vendored
2
dist/README.txt
vendored
|
@ -1,4 +1,4 @@
|
||||||
CADDY 0.8.2
|
CADDY 0.8.3
|
||||||
|
|
||||||
Website
|
Website
|
||||||
https://caddyserver.com
|
https://caddyserver.com
|
||||||
|
|
|
@ -5,7 +5,6 @@ package browse
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -24,7 +23,6 @@ import (
|
||||||
// directories in the given paths are specified.
|
// directories in the given paths are specified.
|
||||||
type Browse struct {
|
type Browse struct {
|
||||||
Next middleware.Handler
|
Next middleware.Handler
|
||||||
Root string
|
|
||||||
Configs []Config
|
Configs []Config
|
||||||
IgnoreIndexes bool
|
IgnoreIndexes bool
|
||||||
}
|
}
|
||||||
|
@ -32,6 +30,7 @@ type Browse struct {
|
||||||
// Config is a configuration for browsing in a particular path.
|
// Config is a configuration for browsing in a particular path.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
PathScope string
|
PathScope string
|
||||||
|
Root http.FileSystem
|
||||||
Variables interface{}
|
Variables interface{}
|
||||||
Template *template.Template
|
Template *template.Template
|
||||||
}
|
}
|
||||||
|
@ -62,6 +61,9 @@ type Listing struct {
|
||||||
// And which order
|
// And which order
|
||||||
Order string
|
Order string
|
||||||
|
|
||||||
|
// If ≠0 then Items have been limited to that many elements
|
||||||
|
ItemsLimitedTo int
|
||||||
|
|
||||||
// Optional custom variables for use in browse templates
|
// Optional custom variables for use in browse templates
|
||||||
User interface{}
|
User interface{}
|
||||||
|
|
||||||
|
@ -134,7 +136,18 @@ func (l byName) Less(i, j int) bool {
|
||||||
// By Size
|
// By Size
|
||||||
func (l bySize) Len() int { return len(l.Items) }
|
func (l bySize) Len() int { return len(l.Items) }
|
||||||
func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
||||||
func (l bySize) Less(i, j int) bool { return l.Items[i].Size < l.Items[j].Size }
|
|
||||||
|
const directoryOffset = -1 << 31 // = math.MinInt32
|
||||||
|
func (l bySize) Less(i, j int) bool {
|
||||||
|
iSize, jSize := l.Items[i].Size, l.Items[j].Size
|
||||||
|
if l.Items[i].IsDir {
|
||||||
|
iSize = directoryOffset + iSize
|
||||||
|
}
|
||||||
|
if l.Items[j].IsDir {
|
||||||
|
jSize = directoryOffset + jSize
|
||||||
|
}
|
||||||
|
return iSize < jSize
|
||||||
|
}
|
||||||
|
|
||||||
// By Time
|
// By Time
|
||||||
func (l byTime) Len() int { return len(l.Items) }
|
func (l byTime) Len() int { return len(l.Items) }
|
||||||
|
@ -172,20 +185,20 @@ func (l Listing) applySort() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func directoryListing(files []os.FileInfo, r *http.Request, canGoUp bool, root string, ignoreIndexes bool, vars interface{}) (Listing, error) {
|
func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listing, bool) {
|
||||||
var fileinfos []FileInfo
|
var (
|
||||||
var dirCount, fileCount int
|
fileinfos []FileInfo
|
||||||
var urlPath = r.URL.Path
|
dirCount, fileCount int
|
||||||
|
hasIndexFile bool
|
||||||
|
)
|
||||||
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
name := f.Name()
|
name := f.Name()
|
||||||
|
|
||||||
// Directory is not browsable if it contains index file
|
|
||||||
if !ignoreIndexes {
|
|
||||||
for _, indexName := range middleware.IndexPages {
|
for _, indexName := range middleware.IndexPages {
|
||||||
if name == indexName {
|
if name == indexName {
|
||||||
return Listing{}, errors.New("Directory contains index file, not browsable!")
|
hasIndexFile = true
|
||||||
}
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +216,7 @@ func directoryListing(files []os.FileInfo, r *http.Request, canGoUp bool, root s
|
||||||
Name: f.Name(),
|
Name: f.Name(),
|
||||||
Size: f.Size(),
|
Size: f.Size(),
|
||||||
URL: url.String(),
|
URL: url.String(),
|
||||||
ModTime: f.ModTime(),
|
ModTime: f.ModTime().UTC(),
|
||||||
Mode: f.Mode(),
|
Mode: f.Mode(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -215,143 +228,180 @@ func directoryListing(files []os.FileInfo, r *http.Request, canGoUp bool, root s
|
||||||
Items: fileinfos,
|
Items: fileinfos,
|
||||||
NumDirs: dirCount,
|
NumDirs: dirCount,
|
||||||
NumFiles: fileCount,
|
NumFiles: fileCount,
|
||||||
Context: middleware.Context{
|
}, hasIndexFile
|
||||||
Root: http.Dir(root),
|
|
||||||
Req: r,
|
|
||||||
URL: r.URL,
|
|
||||||
},
|
|
||||||
User: vars,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP implements the middleware.Handler interface.
|
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||||
|
// If so, control is handed over to ServeListing.
|
||||||
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
filename := b.Root + r.URL.Path
|
var bc *Config
|
||||||
info, err := os.Stat(filename)
|
// See if there's a browse configuration to match the path
|
||||||
|
for i := range b.Configs {
|
||||||
|
if middleware.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
|
||||||
|
bc = &b.Configs[i]
|
||||||
|
goto inScope
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.Next.ServeHTTP(w, r)
|
||||||
|
inScope:
|
||||||
|
|
||||||
|
// Browse works on existing directories; delegate everything else
|
||||||
|
requestedFilepath, err := bc.Root.Open(r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case os.IsPermission(err):
|
||||||
|
return http.StatusForbidden, err
|
||||||
|
case os.IsExist(err):
|
||||||
|
return http.StatusNotFound, err
|
||||||
|
default:
|
||||||
return b.Next.ServeHTTP(w, r)
|
return b.Next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
defer requestedFilepath.Close()
|
||||||
|
|
||||||
|
info, err := requestedFilepath.Stat()
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case os.IsPermission(err):
|
||||||
|
return http.StatusForbidden, err
|
||||||
|
case os.IsExist(err):
|
||||||
|
return http.StatusGone, err
|
||||||
|
default:
|
||||||
|
return b.Next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
if !info.IsDir() {
|
if !info.IsDir() {
|
||||||
return b.Next.ServeHTTP(w, r)
|
return b.Next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// See if there's a browse configuration to match the path
|
// Do not reply to anything else because it might be nonsensical
|
||||||
for _, bc := range b.Configs {
|
|
||||||
if !middleware.Path(r.URL.Path).Matches(bc.PathScope) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet, http.MethodHead:
|
case http.MethodGet, http.MethodHead:
|
||||||
|
// proceed, noop
|
||||||
|
case "PROPFIND", http.MethodOptions:
|
||||||
|
return http.StatusNotImplemented, nil
|
||||||
default:
|
default:
|
||||||
return http.StatusMethodNotAllowed, nil
|
return b.Next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browsing navigation gets messed up if browsing a directory
|
// Browsing navigation gets messed up if browsing a directory
|
||||||
// that doesn't end in "/" (which it should, anyway)
|
// that doesn't end in "/" (which it should, anyway)
|
||||||
if r.URL.Path[len(r.URL.Path)-1] != '/' {
|
if !strings.HasSuffix(r.URL.Path, "/") {
|
||||||
http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
|
http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load directory contents
|
return b.ServeListing(w, r, requestedFilepath, bc)
|
||||||
file, err := os.Open(b.Root + r.URL.Path)
|
}
|
||||||
if err != nil {
|
|
||||||
if os.IsPermission(err) {
|
|
||||||
return http.StatusForbidden, err
|
|
||||||
}
|
|
||||||
return http.StatusNotFound, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
files, err := file.Readdir(-1)
|
func (b Browse) loadDirectoryContents(requestedFilepath http.File, urlPath string) (*Listing, bool, error) {
|
||||||
|
files, err := requestedFilepath.Readdir(-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusForbidden, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if user can browse up another folder
|
// Determine if user can browse up another folder
|
||||||
var canGoUp bool
|
var canGoUp bool
|
||||||
curPath := strings.TrimSuffix(r.URL.Path, "/")
|
curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/"))
|
||||||
for _, other := range b.Configs {
|
for _, other := range b.Configs {
|
||||||
if strings.HasPrefix(path.Dir(curPath), other.PathScope) {
|
if strings.HasPrefix(curPathDir, other.PathScope) {
|
||||||
canGoUp = true
|
canGoUp = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assemble listing of directory contents
|
// Assemble listing of directory contents
|
||||||
listing, err := directoryListing(files, r, canGoUp, b.Root, b.IgnoreIndexes, bc.Variables)
|
listing, hasIndex := directoryListing(files, canGoUp, urlPath)
|
||||||
if err != nil { // directory isn't browsable
|
|
||||||
continue
|
return &listing, hasIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
|
||||||
|
// and reads 'limit' if given. The latter is 0 if not given.
|
||||||
|
//
|
||||||
|
// This sets Cookies.
|
||||||
|
func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
|
||||||
|
sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit")
|
||||||
|
|
||||||
|
// If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies
|
||||||
|
switch sort {
|
||||||
|
case "":
|
||||||
|
sort = "name"
|
||||||
|
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
|
||||||
|
sort = sortCookie.Value
|
||||||
|
}
|
||||||
|
case "name", "size", "type":
|
||||||
|
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the query vales and store them in the Listing struct
|
switch order {
|
||||||
listing.Sort, listing.Order = r.URL.Query().Get("sort"), r.URL.Query().Get("order")
|
case "":
|
||||||
|
order = "asc"
|
||||||
// If the query 'sort' or 'order' is empty, check the cookies
|
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
|
||||||
if listing.Sort == "" {
|
order = orderCookie.Value
|
||||||
sortCookie, sortErr := r.Cookie("sort")
|
|
||||||
// if there's no sorting values in the cookies, default to "name" and "asc"
|
|
||||||
if sortErr != nil {
|
|
||||||
listing.Sort = "name"
|
|
||||||
} else { // if we have values in the cookies, use them
|
|
||||||
listing.Sort = sortCookie.Value
|
|
||||||
}
|
}
|
||||||
} else { // save the query value of 'sort' and 'order' as cookies
|
case "asc", "desc":
|
||||||
http.SetCookie(w, &http.Cookie{Name: "sort", Value: listing.Sort, Path: "/"})
|
http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil})
|
||||||
http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, Path: "/"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if listing.Order == "" {
|
if limitQuery != "" {
|
||||||
orderCookie, orderErr := r.Cookie("order")
|
limit, err = strconv.Atoi(limitQuery)
|
||||||
// if there's no sorting values in the cookies, default to "name" and "asc"
|
|
||||||
if orderErr != nil {
|
|
||||||
listing.Order = "asc"
|
|
||||||
} else { // if we have values in the cookies, use them
|
|
||||||
listing.Order = orderCookie.Value
|
|
||||||
}
|
|
||||||
} else { // save the query value of 'sort' and 'order' as cookies
|
|
||||||
http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, Path: "/"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the sorting
|
|
||||||
listing.applySort()
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
// check if we should provide json
|
|
||||||
acceptHeader := strings.Join(r.Header["Accept"], ",")
|
|
||||||
if strings.Contains(strings.ToLower(acceptHeader), "application/json") {
|
|
||||||
var marsh []byte
|
|
||||||
// check if we are limited
|
|
||||||
if limitQuery := r.URL.Query().Get("limit"); limitQuery != "" {
|
|
||||||
limit, err := strconv.Atoi(limitQuery)
|
|
||||||
if err != nil { // if the 'limit' query can't be interpreted as a number, return err
|
if err != nil { // if the 'limit' query can't be interpreted as a number, return err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeListing returns a formatted view of 'requestedFilepath' contents'.
|
||||||
|
func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) {
|
||||||
|
listing, containsIndex, err := b.loadDirectoryContents(requestedFilepath, r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case os.IsPermission(err):
|
||||||
|
return http.StatusForbidden, err
|
||||||
|
case os.IsExist(err):
|
||||||
|
return http.StatusGone, err
|
||||||
|
default:
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if containsIndex && !b.IgnoreIndexes { // directory isn't browsable
|
||||||
|
return b.Next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
listing.Context = middleware.Context{
|
||||||
|
Root: bc.Root,
|
||||||
|
Req: r,
|
||||||
|
URL: r.URL,
|
||||||
|
}
|
||||||
|
listing.User = bc.Variables
|
||||||
|
|
||||||
|
// Copy the query values into the Listing struct
|
||||||
|
var limit int
|
||||||
|
listing.Sort, listing.Order, limit, err = b.handleSortOrder(w, r, bc.PathScope)
|
||||||
|
if err != nil {
|
||||||
return http.StatusBadRequest, err
|
return http.StatusBadRequest, err
|
||||||
}
|
}
|
||||||
// if `limit` is equal or less than len(listing.Items) and bigger than 0, list them
|
|
||||||
if limit <= len(listing.Items) && limit > 0 {
|
listing.applySort()
|
||||||
marsh, err = json.Marshal(listing.Items[:limit])
|
|
||||||
} else { // if the 'limit' query is empty, or has the wrong value, list everything
|
if limit > 0 && limit <= len(listing.Items) {
|
||||||
marsh, err = json.Marshal(listing.Items)
|
listing.Items = listing.Items[:limit]
|
||||||
}
|
listing.ItemsLimitedTo = limit
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
} else { // there's no 'limit' query, list them all
|
|
||||||
marsh, err = json.Marshal(listing.Items)
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the marshaled json to buf
|
var buf *bytes.Buffer
|
||||||
if _, err = buf.Write(marsh); err != nil {
|
acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
|
||||||
|
switch {
|
||||||
|
case strings.Contains(acceptHeader, "application/json"):
|
||||||
|
if buf, err = b.formatAsJSON(listing, bc); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
} else { // there's no 'application/json' in the 'Accept' header, browse normally
|
default: // There's no 'application/json' in the 'Accept' header; browse normally
|
||||||
err = bc.Template.Execute(&buf, listing)
|
if buf, err = b.formatAsHTML(listing, bc); err != nil {
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
@ -361,8 +411,21 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
buf.WriteTo(w)
|
buf.WriteTo(w)
|
||||||
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Browse) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) {
|
||||||
|
marsh, err := json.Marshal(listing.Items)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Didn't qualify; pass-thru
|
buf := new(bytes.Buffer)
|
||||||
return b.Next.ServeHTTP(w, r)
|
_, err = buf.Write(marsh)
|
||||||
|
return buf, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Browse) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err := bc.Template.Execute(buf, listing)
|
||||||
|
return buf, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
// 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) {
|
||||||
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 {
|
||||||
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) {
|
||||||
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
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
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 (
|
||||||
"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 (
|
||||||
"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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
// 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 {
|
||||||
// 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
|
||||||
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) {
|
||||||
{"/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) {
|
||||||
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
|
||||||
|
|
||||||
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 {
|
||||||
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,10 +78,15 @@ 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()) ||
|
||||||
|
!upstream.AllowedPath(r.URL.Path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var replacer middleware.Replacer
|
var replacer middleware.Replacer
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
requestHost := r.Host
|
|
||||||
|
outreq := createUpstreamRequest(r)
|
||||||
|
|
||||||
// Since Select() should give us "up" hosts, keep retrying
|
// Since Select() should give us "up" hosts, keep retrying
|
||||||
// hosts until timeout (or until we get a nil host).
|
// hosts until timeout (or until we get a nil host).
|
||||||
|
@ -87,12 +95,39 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
if host == nil {
|
if host == nil {
|
||||||
return http.StatusBadGateway, errUnreachable
|
return http.StatusBadGateway, errUnreachable
|
||||||
}
|
}
|
||||||
proxy := host.ReverseProxy
|
|
||||||
r.Host = host.Name
|
|
||||||
if rr, ok := w.(*middleware.ResponseRecorder); ok && rr.Replacer != nil {
|
if rr, ok := w.(*middleware.ResponseRecorder); ok && rr.Replacer != nil {
|
||||||
rr.Replacer.Set("upstream", host.Name)
|
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
|
||||||
|
}
|
||||||
|
if v, ok := host.UpstreamHeaders["Host"]; ok {
|
||||||
|
r.Host = replacer.Replace(v[len(v)-1])
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var downHeaderUpdateFn respUpdateFn
|
||||||
|
if host.DownstreamHeaders != nil {
|
||||||
|
if replacer == nil {
|
||||||
|
rHost := r.Host
|
||||||
|
replacer = middleware.NewReplacer(r, nil, "")
|
||||||
|
outreq.Host = rHost
|
||||||
|
}
|
||||||
|
//Creates a function that is used to update headers the response received by the reverse proxy
|
||||||
|
downHeaderUpdateFn = createRespHeaderUpdateFn(host.DownstreamHeaders, replacer)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := host.ReverseProxy
|
||||||
if baseURL, err := url.Parse(host.Name); err == nil {
|
if baseURL, err := url.Parse(host.Name); err == nil {
|
||||||
r.Host = baseURL.Host
|
r.Host = baseURL.Host
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
|
@ -101,28 +136,9 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
} else if proxy == nil {
|
} else if proxy == nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
var extraHeaders http.Header
|
|
||||||
if host.ExtraHeaders != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt64(&host.Conns, 1)
|
atomic.AddInt64(&host.Conns, 1)
|
||||||
backendErr := proxy.ServeHTTP(w, r, extraHeaders)
|
backendErr := proxy.ServeHTTP(w, outreq, downHeaderUpdateFn)
|
||||||
atomic.AddInt64(&host.Conns, -1)
|
atomic.AddInt64(&host.Conns, -1)
|
||||||
if backendErr == nil {
|
if backendErr == nil {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
|
@ -139,7 +155,83 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
}
|
}
|
||||||
return http.StatusBadGateway, errUnreachable
|
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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
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{
|
||||||
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
|
||||||
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) {
|
||||||
dst = mlw
|
dst = mlw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
io.Copy(dst, src)
|
io.Copy(dst, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -43,7 +44,8 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) {
|
||||||
for c.Next() {
|
for c.Next() {
|
||||||
upstream := &staticUpstream{
|
upstream := &staticUpstream{
|
||||||
from: "",
|
from: "",
|
||||||
proxyHeaders: make(http.Header),
|
upstreamHeaders: make(http.Header),
|
||||||
|
downstreamHeaders: make(http.Header),
|
||||||
Hosts: nil,
|
Hosts: nil,
|
||||||
Policy: &Random{},
|
Policy: &Random{},
|
||||||
FailTimeout: 10 * time.Second,
|
FailTimeout: 10 * time.Second,
|
||||||
|
@ -102,7 +104,8 @@ func (u *staticUpstream) NewHost(host string) (*UpstreamHost, error) {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
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 (
|
||||||
"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
|
||||||
}
|
}
|
||||||
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
|
||||||
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) {
|
||||||
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 (
|
||||||
"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) {
|
||||||
// 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
|
||||||
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 {
|
||||||
|
|
Loading…
Reference in a new issue