Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
caddy
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
caddy
Commits
c431a07a
Commit
c431a07a
authored
Apr 30, 2016
by
Tobias Weingartner
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into md_changes
parents
dd4de698
e2234497
Changes
27
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
784 additions
and
375 deletions
+784
-375
.gitattributes
.gitattributes
+13
-6
CONTRIBUTING.md
CONTRIBUTING.md
+37
-15
ISSUE_TEMPLATE
ISSUE_TEMPLATE
+4
-0
build.bash
build.bash
+1
-0
caddy/https/crypto_test.go
caddy/https/crypto_test.go
+15
-6
caddy/https/maintain.go
caddy/https/maintain.go
+11
-2
caddy/restart.go
caddy/restart.go
+2
-2
caddy/setup/browse.go
caddy/setup/browse.go
+73
-36
caddy/setup/ext.go
caddy/setup/ext.go
+2
-1
caddy/setup/headers_test.go
caddy/setup/headers_test.go
+3
-3
dist/CHANGES.txt
dist/CHANGES.txt
+13
-5
dist/README.txt
dist/README.txt
+1
-1
middleware/browse/browse.go
middleware/browse/browse.go
+199
-136
middleware/browse/browse_test.go
middleware/browse/browse_test.go
+9
-16
middleware/context.go
middleware/context.go
+1
-1
middleware/fastcgi/fcgiclient_test.go
middleware/fastcgi/fcgiclient_test.go
+1
-1
middleware/fileserver.go
middleware/fileserver.go
+10
-8
middleware/fileserver_test.go
middleware/fileserver_test.go
+32
-15
middleware/headers/headers.go
middleware/headers/headers.go
+2
-1
middleware/headers/headers_test.go
middleware/headers/headers_test.go
+7
-0
middleware/proxy/proxy.go
middleware/proxy/proxy.go
+149
-57
middleware/proxy/proxy_test.go
middleware/proxy/proxy_test.go
+136
-1
middleware/proxy/reverseproxy.go
middleware/proxy/reverseproxy.go
+5
-40
middleware/proxy/upstream.go
middleware/proxy/upstream.go
+28
-17
middleware/replacer.go
middleware/replacer.go
+8
-0
middleware/replacer_test.go
middleware/replacer_test.go
+9
-0
server/server.go
server/server.go
+13
-5
No files found.
.gitattributes
View file @
c431a07a
*.bash text eol=lf whitespace=blank-at-eol,space-before-tab,tab-in-indent,trailing-space,tabwidth=4
*.sh 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!
*.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
*.path text eol=lf whitespace=blank-at-eol,space-before-tab,tab-in-indent,trailing-space,tabwidth=4
*.service text eol=lf whitespace=blank-at-eol,space-before-tab,tab-in-indent,trailing-space,tabwidth=4
*.timer text eol=lf whitespace=blank-at-eol,space-before-tab,tab-in-indent,trailing-space,tabwidth=4
# files for systemd (shell-similar)
*.path text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.service text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.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
CONTRIBUTING.md
View file @
c431a07a
...
...
@@ -20,10 +20,10 @@ anything about Web development
### Bug reports
First, p
lease
[
search this repository
](
https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93
)
P
lease
[
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.
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.
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
Submit
[
pull requests
](
https://github.com/mholt/caddy/pulls
)
at any time
. Make
sure to write tests to assert your change is working properly and is thoroughly
covered. We'll ask most pull requests to be
Submit
[
pull requests
](
https://github.com/mholt/caddy/pulls
)
at any time
for
minor changes or new tests. Make sure to write tests to assert your change is
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
)
,
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
...
...
@@ -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
feedback.
The advantage of issues is that you don't have to spend time actually
implementing your idea, but you should still describe it thoroughly. The
advantage of a pull request is that we can immediately see the impact the change
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.
The advantage of issues is that you don't have to spend time implementing your
idea, but you should still describe it thoroughly as if someone reading it would
implement the whole thing starting from scratch.
The advantage of pull requests is that we can immediately see the impact the
change 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 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,
best to open an issue first and "claim" it and get feedback before you invest
a lot of time.
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
...
...
@@ -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
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
...
...
ISSUE_TEMPLATE
View file @
c431a07a
...
...
@@ -18,3 +18,7 @@
#### 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?
build.bash
View file @
c431a07a
...
...
@@ -17,6 +17,7 @@ set -euo pipefail
:
${
output_filename
:
=
"ecaddy"
}
:
${
git_repo
:
=
"
${
2
:-}
"
}
:
${
git_repo
:
=
"."
}
pkg
=
main
ldflags
=()
...
...
caddy/https/crypto_test.go
View file @
c431a07a
...
...
@@ -8,6 +8,7 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"errors"
"os"
"runtime"
"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.
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.
func
PrivateKeyBytes
(
key
crypto
.
PrivateKey
)
[]
byte
{
var
keyBytes
[]
byte
func
PrivateKeyBytes
(
key
crypto
.
PrivateKey
)
([]
byte
,
error
)
{
switch
key
:=
key
.
(
type
)
{
case
*
rsa
.
PrivateKey
:
keyBytes
=
x509
.
MarshalPKCS1PrivateKey
(
key
)
return
x509
.
MarshalPKCS1PrivateKey
(
key
),
nil
case
*
ecdsa
.
PrivateKey
:
keyBytes
,
_
=
x509
.
MarshalECPrivateKey
(
key
)
return
x509
.
MarshalECPrivateKey
(
key
)
}
return
keyBytes
return
nil
,
errors
.
New
(
"Unknown private key type"
)
}
caddy/https/maintain.go
View file @
c431a07a
...
...
@@ -112,12 +112,21 @@ func renewManagedCertificates(allowPrompts bool) (err error) {
// Apply changes to the cache
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
)
if
err
!=
nil
{
if
client
.
AllowPrompts
{
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
{
...
...
@@ -178,7 +187,7 @@ func updateOCSPStaples() {
if
err
!=
nil
{
if
cert
.
OCSP
!=
nil
{
// 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
}
...
...
caddy/restart.go
View file @
c431a07a
...
...
@@ -11,7 +11,7 @@ import (
"net"
"os"
"os/exec"
"path"
"path
/filepath
"
"sync/atomic"
"github.com/mholt/caddy/caddy/https"
...
...
@@ -138,7 +138,7 @@ func Restart(newCaddyfile Input) error {
func
getCertsForNewCaddyfile
(
newCaddyfile
Input
)
error
{
// parse the new caddyfile only up to (and including) TLS
// 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
(
file
path
.
Base
(
newCaddyfile
.
Path
()),
bytes
.
NewReader
(
newCaddyfile
.
Body
()))
if
err
!=
nil
{
return
errors
.
New
(
"loading Caddyfile: "
+
err
.
Error
())
}
...
...
caddy/setup/browse.go
View file @
c431a07a
...
...
@@ -3,6 +3,7 @@ package setup
import
(
"fmt"
"io/ioutil"
"net/http"
"text/template"
"github.com/mholt/caddy/middleware"
...
...
@@ -17,7 +18,6 @@ func Browse(c *Controller) (middleware.Middleware, error) {
}
browse
:=
browse
.
Browse
{
Root
:
c
.
Root
,
Configs
:
configs
,
IgnoreIndexes
:
false
,
}
...
...
@@ -50,6 +50,16 @@ func browseParse(c *Controller) ([]browse.Config, error) {
}
else
{
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
var
tplText
string
...
...
@@ -85,7 +95,6 @@ const defaultTemplate = `<!DOCTYPE html>
<html>
<head>
<title>{{.Name}}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { padding: 0; margin: 0; }
...
...
@@ -106,7 +115,7 @@ h1 a:hover {
}
header,
.content
{
#summary
{
padding-left: 5%;
padding-right: 5%;
}
...
...
@@ -306,43 +315,49 @@ footer {
</header>
<main>
<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>{{.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 class="listing">
<table>
<table aria-describedby="summary">
<thead>
<tr>
<th>
{{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>
{{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>
{{else}}
<a href="?sort=name&order=asc">Name</a>
{{end}}
{{
-
if and (eq .Sort "name") (ne .Order "desc")}}
<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")}}
<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}}
<a href="?sort=name&order=asc
{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}
">Name</a>
{{
-
end}}
</th>
<th>
{{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>
{{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>
{{else}}
<a href="?sort=size&order=asc">Size</a>
{{end}}
{{
-
if and (eq .Sort "size") (ne .Order "desc")}}
<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")}}
<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}}
<a href="?sort=size&order=asc
{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}
">Size</a>
{{
-
end}}
</th>
<th class="hideable">
{{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>
{{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>
{{else}}
<a href="?sort=time&order=asc">Modified</a>
{{end}}
{{
-
if and (eq .Sort "time") (ne .Order "desc")}}
<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")}}
<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}}
<a href="?sort=time&order=asc
{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}
">Modified</a>
{{
-
end}}
</th>
</tr>
{{if .CanGoUp}}
</thead>
<tbody>
{{- if .CanGoUp}}
<tr>
<td>
<a href="..">
...
...
@@ -350,30 +365,52 @@ footer {
</a>
</td>
<td>—</td>
<td>—</td>
<td
class="hideable"
>—</td>
</tr>
{{end}}
{{range .Items}}
{{
-
end}}
{{
-
range .Items}}
<tr>
<td>
<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>
{{else}}
{{
-
else}}
<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>
</a>
</td>
<td>{{.HumanSize}}</td>
<td class="hideable">{{.HumanModTime "01/02/2006 03:04:05 PM"}}</td>
{{- if .IsDir}}
<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>
{{end}}
{{- end}}
</tbody>
</table>
</div>
</main>
<footer>
Served with <a href="https://caddyserver.com">Caddy</a>
</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>
</html>`
caddy/setup/ext.go
View file @
c431a07a
...
...
@@ -2,6 +2,7 @@ package setup
import
(
"os"
"path/filepath"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/extensions"
...
...
@@ -47,7 +48,7 @@ func extParse(c *Controller) ([]string, error) {
// resourceExists returns true if the file specified at
// root + path exists; false otherwise.
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)
// but we don't handle any other kinds of errors anyway
return
err
==
nil
...
...
caddy/setup/headers_test.go
View file @
c431a07a
...
...
@@ -38,14 +38,14 @@ func TestHeadersParse(t *testing.T) {
{
`header /foo Foo "Bar Baz"`
,
false
,
[]
headers
.
Rule
{
{
Path
:
"/foo"
,
Headers
:
[]
headers
.
Header
{
{
"Foo"
,
"Bar Baz"
},
{
Name
:
"Foo"
,
Value
:
"Bar Baz"
},
}},
}},
{
`header /bar { Foo "Bar Baz" Baz Qux }`
,
false
,
[]
headers
.
Rule
{
{
Path
:
"/bar"
,
Headers
:
[]
headers
.
Header
{
{
"Foo"
,
"Bar Baz"
},
{
"Baz"
,
"Qux"
},
{
Name
:
"Foo"
,
Value
:
"Bar Baz"
},
{
Name
:
"Baz"
,
Value
:
"Qux"
},
}},
}},
}
...
...
dist/CHANGES.txt
View file @
c431a07a
CHANGES
<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
- Only fail to start if managed certificate is expired (issue #642)
- Toggle case-sensitive path matching with environment variable
- File server now adds ETag header for static files
- browse: Replace .LinkedPath action with .BreadcrumbMap
- fastcgi: New except clause to exclude paths
- 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)
- 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)
- On-demand TLS can obtain certificates during handshakes
...
...
dist/README.txt
View file @
c431a07a
CADDY 0.8.
2
CADDY 0.8.
3
Website
https://caddyserver.com
...
...
middleware/browse/browse.go
View file @
c431a07a
This diff is collapsed.
Click to expand it.
middleware/browse/browse_test.go
View file @
c431a07a
...
...
@@ -112,13 +112,12 @@ func TestBrowseHTTPMethods(t *testing.T) {
b
:=
Browse
{
Next
:
middleware
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
(
int
,
error
)
{
t
.
Fatalf
(
"Next shouldn't be called"
)
return
0
,
nil
return
http
.
StatusTeapot
,
nil
// not t.Fatalf, or we will not see what other methods yield
}),
Root
:
"./testdata"
,
Configs
:
[]
Config
{
{
PathScope
:
"/photos"
,
Root
:
http
.
Dir
(
"./testdata"
),
Template
:
tmpl
,
},
},
...
...
@@ -128,14 +127,8 @@ func TestBrowseHTTPMethods(t *testing.T) {
for
method
,
expected
:=
range
map
[
string
]
int
{
http
.
MethodGet
:
http
.
StatusOK
,
http
.
MethodHead
:
http
.
StatusOK
,
http
.
MethodOptions
:
http
.
StatusMethodNotAllowed
,
http
.
MethodPost
:
http
.
StatusMethodNotAllowed
,
http
.
MethodPut
:
http
.
StatusMethodNotAllowed
,
http
.
MethodPatch
:
http
.
StatusMethodNotAllowed
,
http
.
MethodDelete
:
http
.
StatusMethodNotAllowed
,
"COPY"
:
http
.
StatusMethodNotAllowed
,
"MOVE"
:
http
.
StatusMethodNotAllowed
,
"MKCOL"
:
http
.
StatusMethodNotAllowed
,
http
.
MethodOptions
:
http
.
StatusNotImplemented
,
"PROPFIND"
:
http
.
StatusNotImplemented
,
}
{
req
,
err
:=
http
.
NewRequest
(
method
,
"/photos/"
,
nil
)
if
err
!=
nil
{
...
...
@@ -160,10 +153,10 @@ func TestBrowseTemplate(t *testing.T) {
t
.
Fatalf
(
"Next shouldn't be called"
)
return
0
,
nil
}),
Root
:
"./testdata"
,
Configs
:
[]
Config
{
{
PathScope
:
"/photos"
,
Root
:
http
.
Dir
(
"./testdata"
),
Template
:
tmpl
,
},
},
...
...
@@ -215,16 +208,16 @@ func TestBrowseJson(t *testing.T) {
t
.
Fatalf
(
"Next shouldn't be called"
)
return
0
,
nil
}),
Root
:
"./testdata"
,
Configs
:
[]
Config
{
{
PathScope
:
"/photos/"
,
Root
:
http
.
Dir
(
"./testdata"
),
},
},
}
//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
)
if
err
!=
nil
{
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
// some reason, making the sorting unpredictable. To hack around this,
// 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
{
t
.
Fatal
(
err
)
}
...
...
@@ -322,7 +315,7 @@ func TestBrowseJson(t *testing.T) {
code
,
err
:=
b
.
ServeHTTP
(
rec
,
req
)
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"
{
t
.
Fatalf
(
"Expected Content type to be application/json; charset=utf-8, but got %s "
,
rec
.
HeaderMap
.
Get
(
"Content-Type"
))
...
...
middleware/context.go
View file @
c431a07a
...
...
@@ -235,7 +235,7 @@ func (c Context) ToUpper(s string) string {
return
strings
.
ToUpper
(
s
)
}
// Split is a passthrough to strings.Split. It will split the first argument at each instance of the sep
e
rator and return a slice of strings.
// Split is a passthrough to strings.Split. It will split the first argument at each instance of the sep
a
rator and return a slice of strings.
func
(
c
Context
)
Split
(
s
string
,
sep
string
)
[]
string
{
return
strings
.
Split
(
s
,
sep
)
}
...
...
middleware/fastcgi/fcgiclient_test.go
View file @
c431a07a
...
...
@@ -200,7 +200,7 @@ func DisabledTest(t *testing.T) {
listener
,
err
:=
net
.
Listen
(
"tcp"
,
ipPort
)
if
err
!=
nil
{
// handle error
log
.
Println
(
"listener creat
at
ion failed: "
,
err
)
log
.
Println
(
"listener creation failed: "
,
err
)
}
srv
:=
new
(
FastCGIServer
)
...
...
middleware/fileserver.go
View file @
c431a07a
...
...
@@ -2,10 +2,12 @@ package middleware
import
(
"fmt"
"math/rand"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
)
...
...
@@ -40,12 +42,11 @@ type fileHandler struct {
}
func
(
fh
*
fileHandler
)
ServeHTTP
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
(
int
,
error
)
{
upath
:=
r
.
URL
.
Path
if
!
strings
.
HasPrefix
(
upath
,
"/"
)
{
upath
=
"/"
+
upath
r
.
URL
.
Path
=
upath
// r.URL.Path has already been cleaned in caddy/server by path.Clean().
if
r
.
URL
.
Path
==
""
{
r
.
URL
.
Path
=
"/"
}
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.
...
...
@@ -66,7 +67,8 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st
return
http
.
StatusForbidden
,
err
}
// 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
}
defer
f
.
Close
()
...
...
@@ -86,13 +88,13 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st
url
:=
r
.
URL
.
Path
if
d
.
IsDir
()
{
// Ensure / at end of directory url
if
url
[
len
(
url
)
-
1
]
!=
'/'
{
if
!
strings
.
HasSuffix
(
url
,
"/"
)
{
redirect
(
w
,
r
,
path
.
Base
(
url
)
+
"/"
)
return
http
.
StatusMovedPermanently
,
nil
}
}
else
{
// Ensure no / at end of file url
if
url
[
len
(
url
)
-
1
]
==
'/'
{
if
strings
.
HasSuffix
(
url
,
"/"
)
{
redirect
(
w
,
r
,
"../"
+
path
.
Base
(
url
))
return
http
.
StatusMovedPermanently
,
nil
}
...
...
middleware/fileserver_test.go
View file @
c431a07a
...
...
@@ -4,6 +4,7 @@ import (
"errors"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
...
...
@@ -11,23 +12,30 @@ import (
"time"
)
var
testDir
=
filepath
.
Join
(
os
.
TempDir
(),
"caddy_testdir"
)
var
ErrCustom
=
errors
.
New
(
"Custom Error"
)
var
(
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.
// The map represents the following structure:
// - $TEMP/caddy_testdir/
// '-- file1.html
// '-- dirwithindex/
// '---- index.html
// '-- dir/
// '---- file2.html
// '---- hidden.html
// '-- unreachable.html
// '-- webroot/
// '---- file1.html
// '---- dirwithindex/
// '------ index.html
// '---- dir/
// '------ file2.html
// '------ hidden.html
var
testFiles
=
map
[
string
]
string
{
"file1.html"
:
"<h1>file1.html</h1>"
,
filepath
.
Join
(
"dirwithindex"
,
"index.html"
)
:
"<h1>dirwithindex/index.html</h1>"
,
filepath
.
Join
(
"dir"
,
"file2.html"
)
:
"<h1>dir/file2.html</h1>"
,
filepath
.
Join
(
"dir"
,
"hidden.html"
)
:
"<h1>dir/hidden.html</h1>"
,
"unreachable.html"
:
"<h1>must not leak</h1>"
,
filepath
.
Join
(
"webroot"
,
"file1.html"
)
:
"<h1>file1.html</h1>"
,
filepath
.
Join
(
"webroot"
,
"dirwithindex"
,
"index.html"
)
:
"<h1>dirwithindex/index.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.
...
...
@@ -36,7 +44,7 @@ func TestServeHTTP(t *testing.T) {
beforeServeHTTPTest
(
t
)
defer
afterServeHTTPTest
(
t
)
fileserver
:=
FileServer
(
http
.
Dir
(
test
Dir
),
[]
string
{
"dir/hidden.html"
})
fileserver
:=
FileServer
(
http
.
Dir
(
test
WebRoot
),
[]
string
{
"dir/hidden.html"
})
movedPermanently
:=
"Moved Permanently"
...
...
@@ -142,11 +150,20 @@ func TestServeHTTP(t *testing.T) {
url
:
"https://foo/hidden.html"
,
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
{
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
)
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
func
beforeServeHTTPTest
(
t
*
testing
.
T
)
{
// make the root test dir
err
:=
os
.
Mkdir
(
testDir
,
os
.
ModePerm
)
err
:=
os
.
Mkdir
All
(
testWebRoot
,
os
.
ModePerm
)
if
err
!=
nil
{
if
!
os
.
IsExist
(
err
)
{
t
.
Fatalf
(
"Failed to create test dir. Error was: %v"
,
err
)
...
...
middleware/headers/headers.go
View file @
c431a07a
...
...
@@ -20,13 +20,14 @@ type Headers struct {
// ServeHTTP implements the middleware.Handler interface and serves requests,
// setting headers on the response according to the configured rules.
func
(
h
Headers
)
ServeHTTP
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
(
int
,
error
)
{
replacer
:=
middleware
.
NewReplacer
(
r
,
nil
,
""
)
for
_
,
rule
:=
range
h
.
Rules
{
if
middleware
.
Path
(
r
.
URL
.
Path
)
.
Matches
(
rule
.
Path
)
{
for
_
,
header
:=
range
rule
.
Headers
{
if
strings
.
HasPrefix
(
header
.
Name
,
"-"
)
{
w
.
Header
()
.
Del
(
strings
.
TrimLeft
(
header
.
Name
,
"-"
))
}
else
{
w
.
Header
()
.
Set
(
header
.
Name
,
header
.
Value
)
w
.
Header
()
.
Set
(
header
.
Name
,
replacer
.
Replace
(
header
.
Value
)
)
}
}
}
...
...
middleware/headers/headers_test.go
View file @
c431a07a
...
...
@@ -3,12 +3,17 @@ package headers
import
(
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/mholt/caddy/middleware"
)
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
{
from
string
name
string
...
...
@@ -17,6 +22,7 @@ func TestHeaders(t *testing.T) {
{
"/a"
,
"Foo"
,
"Bar"
},
{
"/a"
,
"Bar"
,
""
},
{
"/a"
,
"Baz"
,
""
},
{
"/a"
,
"ServerName"
,
hostname
},
{
"/b"
,
"Foo"
,
""
},
{
"/b"
,
"Bar"
,
"Removed in /a"
},
}
{
...
...
@@ -27,6 +33,7 @@ func TestHeaders(t *testing.T) {
Rules
:
[]
Rule
{
{
Path
:
"/a"
,
Headers
:
[]
Header
{
{
Name
:
"Foo"
,
Value
:
"Bar"
},
{
Name
:
"ServerName"
,
Value
:
"{hostname}"
},
{
Name
:
"-Bar"
},
}},
},
...
...
middleware/proxy/proxy.go
View file @
c431a07a
...
...
@@ -3,8 +3,10 @@ package proxy
import
(
"errors"
"net"
"net/http"
"net/url"
"strings"
"sync/atomic"
"time"
...
...
@@ -41,7 +43,8 @@ type UpstreamHost struct {
Fails
int32
FailTimeout
time
.
Duration
Unhealthy
bool
ExtraHeaders
http
.
Header
UpstreamHeaders
http
.
Header
DownstreamHeaders
http
.
Header
CheckDown
UpstreamHostDownFunc
WithoutPathPrefix
string
MaxConns
int64
...
...
@@ -75,71 +78,160 @@ var tryDuration = 60 * time.Second
// ServeHTTP satisfies the middleware.Handler interface.
func
(
p
Proxy
)
ServeHTTP
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
(
int
,
error
)
{
for
_
,
upstream
:=
range
p
.
Upstreams
{
if
middleware
.
Path
(
r
.
URL
.
Path
)
.
Matches
(
upstream
.
From
())
&&
upstream
.
AllowedPath
(
r
.
URL
.
Path
)
{
var
replacer
middleware
.
Replacer
start
:=
time
.
Now
()
requestHost
:=
r
.
Host
// Since Select() should give us "up" hosts, keep retrying
// hosts until timeout (or until we get a nil host).
for
time
.
Now
()
.
Sub
(
start
)
<
tryDuration
{
host
:=
upstream
.
Select
()
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
!
middleware
.
Path
(
r
.
URL
.
Path
)
.
Matches
(
upstream
.
From
())
||
!
upstream
.
AllowedPath
(
r
.
URL
.
Path
)
{
continue
}
var
replacer
middleware
.
Replacer
start
:=
time
.
Now
()
outreq
:=
createUpstreamRequest
(
r
)
if
baseURL
,
err
:=
url
.
Parse
(
host
.
Name
);
err
==
nil
{
r
.
Host
=
baseURL
.
Host
if
proxy
==
nil
{
proxy
=
NewSingleHostReverseProxy
(
baseURL
,
host
.
WithoutPathPrefix
)
}
}
else
if
proxy
==
nil
{
return
http
.
StatusInternalServerError
,
err
// Since Select() should give us "up" hosts, keep retrying
// hosts until timeout (or until we get a nil host).
for
time
.
Now
()
.
Sub
(
start
)
<
tryDuration
{
host
:=
upstream
.
Select
()
if
host
==
nil
{
return
http
.
StatusBadGateway
,
errUnreachable
}
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
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
)
}
}
}
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
}
}
atomic
.
AddInt64
(
&
host
.
Conns
,
1
)
backendErr
:=
proxy
.
ServeHTTP
(
w
,
r
,
extraHeaders
)
atomic
.
AddInt64
(
&
host
.
Conns
,
-
1
)
if
backendErr
==
nil
{
return
0
,
nil
var
downHeaderUpdateFn
respUpdateFn
if
host
.
DownstreamHeaders
!=
nil
{
if
replacer
==
nil
{
rHost
:=
r
.
Host
replacer
=
middleware
.
NewReplacer
(
r
,
nil
,
""
)
outreq
.
Host
=
rHost
}
timeout
:=
host
.
FailTimeout
if
timeout
==
0
{
timeout
=
10
*
time
.
Second
//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
{
r
.
Host
=
baseURL
.
Host
if
proxy
==
nil
{
proxy
=
NewSingleHostReverseProxy
(
baseURL
,
host
.
WithoutPathPrefix
)
}
atomic
.
AddInt32
(
&
host
.
Fails
,
1
)
go
func
(
host
*
UpstreamHost
,
timeout
time
.
Duration
)
{
time
.
Sleep
(
timeout
)
atomic
.
AddInt32
(
&
host
.
Fails
,
-
1
)
}(
host
,
timeout
)
}
else
if
proxy
==
nil
{
return
http
.
StatusInternalServerError
,
err
}
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
)
}
// 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
)
}
}
middleware/proxy/proxy_test.go
View file @
c431a07a
...
...
@@ -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
{
uri
,
_
:=
url
.
Parse
(
name
)
u
:=
&
fakeUpstream
{
...
...
@@ -410,7 +545,7 @@ func (u *fakeWsUpstream) Select() *UpstreamHost {
return
&
UpstreamHost
{
Name
:
u
.
name
,
ReverseProxy
:
NewSingleHostReverseProxy
(
uri
,
u
.
without
),
Extra
Headers
:
http
.
Header
{
Upstream
Headers
:
http
.
Header
{
"Connection"
:
{
"{>Connection}"
},
"Upgrade"
:
{
"{>Upgrade}"
}},
}
...
...
middleware/proxy/reverseproxy.go
View file @
c431a07a
...
...
@@ -154,57 +154,25 @@ var InsecureTransport http.RoundTripper = &http.Transport{
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
if
transport
==
nil
{
transport
=
http
.
DefaultTransport
}
outreq
:=
new
(
http
.
Request
)
*
outreq
=
*
req
// includes shallow copies of maps, but okay
p
.
Director
(
outreq
)
outreq
.
Proto
=
"HTTP/1.1"
outreq
.
ProtoMajor
=
1
outreq
.
ProtoMinor
=
1
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
)
if
err
!=
nil
{
return
err
}
else
if
respUpdateFn
!=
nil
{
respUpdateFn
(
res
)
}
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
{
res
.
Header
.
Del
(
h
)
}
copyHeader
(
rw
.
Header
(),
res
.
Header
)
rw
.
WriteHeader
(
res
.
StatusCode
)
p
.
copyResponse
(
rw
,
res
.
Body
)
}
...
...
@@ -260,7 +226,6 @@ func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
dst
=
mlw
}
}
io
.
Copy
(
dst
,
src
)
}
...
...
middleware/proxy/upstream.go
View file @
c431a07a
...
...
@@ -20,7 +20,8 @@ var (
type
staticUpstream
struct
{
from
string
proxyHeaders
http
.
Header
upstreamHeaders
http
.
Header
downstreamHeaders
http
.
Header
Hosts
HostPool
Policy
Policy
insecureSkipVerify
bool
...
...
@@ -42,13 +43,14 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) {
var
upstreams
[]
Upstream
for
c
.
Next
()
{
upstream
:=
&
staticUpstream
{
from
:
""
,
proxyHeaders
:
make
(
http
.
Header
),
Hosts
:
nil
,
Policy
:
&
Random
{},
FailTimeout
:
10
*
time
.
Second
,
MaxFails
:
1
,
MaxConns
:
0
,
from
:
""
,
upstreamHeaders
:
make
(
http
.
Header
),
downstreamHeaders
:
make
(
http
.
Header
),
Hosts
:
nil
,
Policy
:
&
Random
{},
FailTimeout
:
10
*
time
.
Second
,
MaxFails
:
1
,
MaxConns
:
0
,
}
if
!
c
.
Args
(
&
upstream
.
from
)
{
...
...
@@ -97,12 +99,13 @@ func (u *staticUpstream) NewHost(host string) (*UpstreamHost, error) {
host
=
"http://"
+
host
}
uh
:=
&
UpstreamHost
{
Name
:
host
,
Conns
:
0
,
Fails
:
0
,
FailTimeout
:
u
.
FailTimeout
,
Unhealthy
:
false
,
ExtraHeaders
:
u
.
proxyHeaders
,
Name
:
host
,
Conns
:
0
,
Fails
:
0
,
FailTimeout
:
u
.
FailTimeout
,
Unhealthy
:
false
,
UpstreamHeaders
:
u
.
upstreamHeaders
,
DownstreamHeaders
:
u
.
downstreamHeaders
,
CheckDown
:
func
(
u
*
staticUpstream
)
UpstreamHostDownFunc
{
return
func
(
uh
*
UpstreamHost
)
bool
{
if
uh
.
Unhealthy
{
...
...
@@ -182,15 +185,23 @@ func parseBlock(c *parse.Dispenser, u *staticUpstream) error {
}
u
.
HealthCheck
.
Interval
=
dur
}
case
"header_upstream"
:
fallthrough
case
"proxy_header"
:
var
header
,
value
string
if
!
c
.
Args
(
&
header
,
&
value
)
{
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"
:
u
.
proxy
Headers
.
Add
(
"Connection"
,
"{>Connection}"
)
u
.
proxy
Headers
.
Add
(
"Upgrade"
,
"{>Upgrade}"
)
u
.
upstream
Headers
.
Add
(
"Connection"
,
"{>Connection}"
)
u
.
upstream
Headers
.
Add
(
"Upgrade"
,
"{>Upgrade}"
)
case
"without"
:
if
!
c
.
NextArg
()
{
return
c
.
ArgErr
()
...
...
middleware/replacer.go
View file @
c431a07a
...
...
@@ -4,6 +4,7 @@ import (
"net"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
...
...
@@ -52,6 +53,13 @@ func NewReplacer(r *http.Request, rr *ResponseRecorder, emptyValue string) Repla
}
return
"http"
}(),
"{hostname}"
:
func
()
string
{
name
,
err
:=
os
.
Hostname
()
if
err
!=
nil
{
return
""
}
return
name
}(),
"{host}"
:
r
.
Host
,
"{path}"
:
r
.
URL
.
Path
,
"{path_escaped}"
:
url
.
QueryEscape
(
r
.
URL
.
Path
),
...
...
middleware/replacer_test.go
View file @
c431a07a
...
...
@@ -3,6 +3,7 @@ package middleware
import
(
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
...
...
@@ -53,6 +54,14 @@ func TestReplace(t *testing.T) {
request
.
Header
.
Set
(
"ShorterVal"
,
"1"
)
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
{
t
.
Errorf
(
"{host} replacement: expected '%s', got '%s'"
,
expected
,
actual
)
}
...
...
server/server.go
View file @
c431a07a
...
...
@@ -14,7 +14,7 @@ import (
"net"
"net/http"
"os"
"path
/filepath
"
"path"
"runtime"
"strings"
"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.
// Collapse any ./ ../ /// madness here instead of doing that in every plugin.
if
r
.
URL
.
Path
!=
"/"
{
path
:=
filepath
.
Clean
(
r
.
URL
.
Path
)
if
!
strings
.
HasPrefix
(
path
,
"/"
)
{
path
=
"/"
+
path
cleanedPath
:=
path
.
Clean
(
r
.
URL
.
Path
)
if
cleanedPath
==
"."
{
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
...
...
@@ -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
return
}
c
.
SessionTicketKey
=
keys
[
0
]
// SetSessionTicketKeys doesn't set a 'tls.keysAlreadSet'
c
.
SetSessionTicketKeys
(
setSessionTicketKeysTestHook
(
keys
))
for
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment