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
Łukasz Nowak
caddy
Commits
88980664
Commit
88980664
authored
Aug 05, 2016
by
Nimi Wariboko Jr
Committed by
GitHub
Aug 05, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into proxy/single-webconn
parents
6e9439d2
fffc1bed
Changes
14
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
263 additions
and
78 deletions
+263
-78
.travis.yml
.travis.yml
+2
-5
appveyor.yml
appveyor.yml
+3
-4
caddy.go
caddy.go
+0
-20
caddyhttp/proxy/policy.go
caddyhttp/proxy/policy.go
+40
-4
caddyhttp/proxy/policy_test.go
caddyhttp/proxy/policy_test.go
+133
-10
caddyhttp/proxy/proxy.go
caddyhttp/proxy/proxy.go
+3
-3
caddyhttp/proxy/proxy_test.go
caddyhttp/proxy/proxy_test.go
+14
-5
caddyhttp/proxy/reverseproxy.go
caddyhttp/proxy/reverseproxy.go
+5
-2
caddyhttp/proxy/upstream.go
caddyhttp/proxy/upstream.go
+7
-4
caddyhttp/proxy/upstream_test.go
caddyhttp/proxy/upstream_test.go
+9
-7
caddytls/crypto_test.go
caddytls/crypto_test.go
+12
-9
dist/init/linux-sysvinit/caddy
dist/init/linux-sysvinit/caddy
+6
-5
rlimit_posix.go
rlimit_posix.go
+23
-0
rlimit_windows.go
rlimit_windows.go
+6
-0
No files found.
.travis.yml
View file @
88980664
language
:
go
language
:
go
go
:
go
:
-
1.6.
2
-
1.6.
3
-
tip
-
tip
env
:
-
CGO_ENABLED=0
before_install
:
before_install
:
# Decrypts a script that installs an authenticated cookie
# Decrypts a script that installs an authenticated cookie
# for git to use when cloning from googlesource.com.
# for git to use when cloning from googlesource.com.
...
@@ -24,7 +21,7 @@ script:
...
@@ -24,7 +21,7 @@ script:
-
diff <(echo -n) <(gofmt -s -d .)
-
diff <(echo -n) <(gofmt -s -d .)
-
ineffassign .
-
ineffassign .
-
go vet ./...
-
go vet ./...
-
go test ./...
-
go test
-race
./...
after_script
:
after_script
:
-
golint ./...
-
golint ./...
appveyor.yml
View file @
88980664
...
@@ -6,12 +6,11 @@ clone_folder: c:\gopath\src\github.com\mholt\caddy
...
@@ -6,12 +6,11 @@ clone_folder: c:\gopath\src\github.com\mholt\caddy
environment
:
environment
:
GOPATH
:
c:\gopath
GOPATH
:
c:\gopath
CGO_ENABLED
:
0
install
:
install
:
-
rmdir c:\go /s /q
-
rmdir c:\go /s /q
-
appveyor DownloadFile https://storage.googleapis.com/golang/go1.6.
2
.windows-amd64.zip
-
appveyor DownloadFile https://storage.googleapis.com/golang/go1.6.
3
.windows-amd64.zip
-
7z x go1.6.
2
.windows-amd64.zip -y -oC:\ > NUL
-
7z x go1.6.
3
.windows-amd64.zip -y -oC:\ > NUL
-
go version
-
go version
-
go env
-
go env
-
go get -t ./...
-
go get -t ./...
...
@@ -23,7 +22,7 @@ build: off
...
@@ -23,7 +22,7 @@ build: off
test_script
:
test_script
:
-
go vet ./...
-
go vet ./...
-
go test ./...
-
go test
-race
./...
-
ineffassign .
-
ineffassign .
after_test
:
after_test
:
...
...
caddy.go
View file @
88980664
...
@@ -21,8 +21,6 @@ import (
...
@@ -21,8 +21,6 @@ import (
"log"
"log"
"net"
"net"
"os"
"os"
"os/exec"
"runtime"
"strconv"
"strconv"
"strings"
"strings"
"sync"
"sync"
...
@@ -725,24 +723,6 @@ func IsLoopback(addr string) bool {
...
@@ -725,24 +723,6 @@ func IsLoopback(addr string) bool {
strings
.
HasPrefix
(
host
,
"127."
)
strings
.
HasPrefix
(
host
,
"127."
)
}
}
// checkFdlimit issues a warning if the OS limit for
// max file descriptors is below a recommended minimum.
func
checkFdlimit
()
{
const
min
=
8192
// Warn if ulimit is too low for production sites
if
runtime
.
GOOS
==
"linux"
||
runtime
.
GOOS
==
"darwin"
{
out
,
err
:=
exec
.
Command
(
"sh"
,
"-c"
,
"ulimit -n"
)
.
Output
()
// use sh because ulimit isn't in Linux $PATH
if
err
==
nil
{
lim
,
err
:=
strconv
.
Atoi
(
string
(
bytes
.
TrimSpace
(
out
)))
if
err
==
nil
&&
lim
<
min
{
fmt
.
Printf
(
"WARNING: File descriptor limit %d is too low for production servers. "
+
"At least %d is recommended. Fix with
\"
ulimit -n %d
\"
.
\n
"
,
lim
,
min
,
min
)
}
}
}
}
// Upgrade re-launches the process, preserving the listeners
// Upgrade re-launches the process, preserving the listeners
// for a graceful restart. It does NOT load new configuration;
// for a graceful restart. It does NOT load new configuration;
// it only starts the process anew with a fresh binary.
// it only starts the process anew with a fresh binary.
...
...
caddyhttp/proxy/policy.go
View file @
88980664
package
proxy
package
proxy
import
(
import
(
"hash/fnv"
"math"
"math"
"math/rand"
"math/rand"
"net"
"net/http"
"sync"
"sync"
)
)
...
@@ -11,20 +14,21 @@ type HostPool []*UpstreamHost
...
@@ -11,20 +14,21 @@ type HostPool []*UpstreamHost
// Policy decides how a host will be selected from a pool.
// Policy decides how a host will be selected from a pool.
type
Policy
interface
{
type
Policy
interface
{
Select
(
pool
HostPool
)
*
UpstreamHost
Select
(
pool
HostPool
,
r
*
http
.
Request
)
*
UpstreamHost
}
}
func
init
()
{
func
init
()
{
RegisterPolicy
(
"random"
,
func
()
Policy
{
return
&
Random
{}
})
RegisterPolicy
(
"random"
,
func
()
Policy
{
return
&
Random
{}
})
RegisterPolicy
(
"least_conn"
,
func
()
Policy
{
return
&
LeastConn
{}
})
RegisterPolicy
(
"least_conn"
,
func
()
Policy
{
return
&
LeastConn
{}
})
RegisterPolicy
(
"round_robin"
,
func
()
Policy
{
return
&
RoundRobin
{}
})
RegisterPolicy
(
"round_robin"
,
func
()
Policy
{
return
&
RoundRobin
{}
})
RegisterPolicy
(
"ip_hash"
,
func
()
Policy
{
return
&
IPHash
{}
})
}
}
// Random is a policy that selects up hosts from a pool at random.
// Random is a policy that selects up hosts from a pool at random.
type
Random
struct
{}
type
Random
struct
{}
// Select selects an up host at random from the specified pool.
// Select selects an up host at random from the specified pool.
func
(
r
*
Random
)
Select
(
pool
HostPool
)
*
UpstreamHost
{
func
(
r
*
Random
)
Select
(
pool
HostPool
,
request
*
http
.
Request
)
*
UpstreamHost
{
// Because the number of available hosts isn't known
// Because the number of available hosts isn't known
// up front, the host is selected via reservoir sampling
// up front, the host is selected via reservoir sampling
...
@@ -53,7 +57,7 @@ type LeastConn struct{}
...
@@ -53,7 +57,7 @@ type LeastConn struct{}
// Select selects the up host with the least number of connections in the
// Select selects the up host with the least number of connections in the
// pool. If more than one host has the same least number of connections,
// pool. If more than one host has the same least number of connections,
// one of the hosts is chosen at random.
// one of the hosts is chosen at random.
func
(
r
*
LeastConn
)
Select
(
pool
HostPool
)
*
UpstreamHost
{
func
(
r
*
LeastConn
)
Select
(
pool
HostPool
,
request
*
http
.
Request
)
*
UpstreamHost
{
var
bestHost
*
UpstreamHost
var
bestHost
*
UpstreamHost
count
:=
0
count
:=
0
leastConn
:=
int64
(
math
.
MaxInt64
)
leastConn
:=
int64
(
math
.
MaxInt64
)
...
@@ -86,7 +90,7 @@ type RoundRobin struct {
...
@@ -86,7 +90,7 @@ type RoundRobin struct {
}
}
// Select selects an up host from the pool using a round robin ordering scheme.
// Select selects an up host from the pool using a round robin ordering scheme.
func
(
r
*
RoundRobin
)
Select
(
pool
HostPool
)
*
UpstreamHost
{
func
(
r
*
RoundRobin
)
Select
(
pool
HostPool
,
request
*
http
.
Request
)
*
UpstreamHost
{
poolLen
:=
uint32
(
len
(
pool
))
poolLen
:=
uint32
(
len
(
pool
))
r
.
mutex
.
Lock
()
r
.
mutex
.
Lock
()
defer
r
.
mutex
.
Unlock
()
defer
r
.
mutex
.
Unlock
()
...
@@ -100,3 +104,35 @@ func (r *RoundRobin) Select(pool HostPool) *UpstreamHost {
...
@@ -100,3 +104,35 @@ func (r *RoundRobin) Select(pool HostPool) *UpstreamHost {
}
}
return
nil
return
nil
}
}
// IPHash is a policy that selects hosts based on hashing the request ip
type
IPHash
struct
{}
func
hash
(
s
string
)
uint32
{
h
:=
fnv
.
New32a
()
h
.
Write
([]
byte
(
s
))
return
h
.
Sum32
()
}
// Select selects an up host from the pool using a round robin ordering scheme.
func
(
r
*
IPHash
)
Select
(
pool
HostPool
,
request
*
http
.
Request
)
*
UpstreamHost
{
poolLen
:=
uint32
(
len
(
pool
))
clientIP
,
_
,
err
:=
net
.
SplitHostPort
(
request
.
RemoteAddr
)
if
err
!=
nil
{
clientIP
=
request
.
RemoteAddr
}
hash
:=
hash
(
clientIP
)
for
{
if
poolLen
==
0
{
break
}
index
:=
hash
%
poolLen
host
:=
pool
[
index
]
if
host
.
Available
()
{
return
host
}
pool
=
append
(
pool
[
:
index
],
pool
[
index
+
1
:
]
...
)
poolLen
--
}
return
nil
}
caddyhttp/proxy/policy_test.go
View file @
88980664
...
@@ -21,7 +21,7 @@ func TestMain(m *testing.M) {
...
@@ -21,7 +21,7 @@ func TestMain(m *testing.M) {
type
customPolicy
struct
{}
type
customPolicy
struct
{}
func
(
r
*
customPolicy
)
Select
(
pool
HostPool
)
*
UpstreamHost
{
func
(
r
*
customPolicy
)
Select
(
pool
HostPool
,
request
*
http
.
Request
)
*
UpstreamHost
{
return
pool
[
0
]
return
pool
[
0
]
}
}
...
@@ -43,37 +43,39 @@ func testPool() HostPool {
...
@@ -43,37 +43,39 @@ func testPool() HostPool {
func
TestRoundRobinPolicy
(
t
*
testing
.
T
)
{
func
TestRoundRobinPolicy
(
t
*
testing
.
T
)
{
pool
:=
testPool
()
pool
:=
testPool
()
rrPolicy
:=
&
RoundRobin
{}
rrPolicy
:=
&
RoundRobin
{}
h
:=
rrPolicy
.
Select
(
pool
)
request
,
_
:=
http
.
NewRequest
(
"GET"
,
"/"
,
nil
)
h
:=
rrPolicy
.
Select
(
pool
,
request
)
// First selected host is 1, because counter starts at 0
// First selected host is 1, because counter starts at 0
// and increments before host is selected
// and increments before host is selected
if
h
!=
pool
[
1
]
{
if
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected first round robin host to be second host in the pool."
)
t
.
Error
(
"Expected first round robin host to be second host in the pool."
)
}
}
h
=
rrPolicy
.
Select
(
pool
)
h
=
rrPolicy
.
Select
(
pool
,
request
)
if
h
!=
pool
[
2
]
{
if
h
!=
pool
[
2
]
{
t
.
Error
(
"Expected second round robin host to be third host in the pool."
)
t
.
Error
(
"Expected second round robin host to be third host in the pool."
)
}
}
h
=
rrPolicy
.
Select
(
pool
)
h
=
rrPolicy
.
Select
(
pool
,
request
)
if
h
!=
pool
[
0
]
{
if
h
!=
pool
[
0
]
{
t
.
Error
(
"Expected third round robin host to be first host in the pool."
)
t
.
Error
(
"Expected third round robin host to be first host in the pool."
)
}
}
// mark host as down
// mark host as down
pool
[
1
]
.
Unhealthy
=
true
pool
[
1
]
.
Unhealthy
=
true
h
=
rrPolicy
.
Select
(
pool
)
h
=
rrPolicy
.
Select
(
pool
,
request
)
if
h
!=
pool
[
2
]
{
if
h
!=
pool
[
2
]
{
t
.
Error
(
"Expected to skip down host."
)
t
.
Error
(
"Expected to skip down host."
)
}
}
// mark host as up
// mark host as up
pool
[
1
]
.
Unhealthy
=
false
pool
[
1
]
.
Unhealthy
=
false
h
=
rrPolicy
.
Select
(
pool
)
h
=
rrPolicy
.
Select
(
pool
,
request
)
if
h
==
pool
[
2
]
{
if
h
==
pool
[
2
]
{
t
.
Error
(
"Expected to balance evenly among healthy hosts"
)
t
.
Error
(
"Expected to balance evenly among healthy hosts"
)
}
}
// mark host as full
// mark host as full
pool
[
1
]
.
Conns
=
1
pool
[
1
]
.
Conns
=
1
pool
[
1
]
.
MaxConns
=
1
pool
[
1
]
.
MaxConns
=
1
h
=
rrPolicy
.
Select
(
pool
)
h
=
rrPolicy
.
Select
(
pool
,
request
)
if
h
!=
pool
[
2
]
{
if
h
!=
pool
[
2
]
{
t
.
Error
(
"Expected to skip full host."
)
t
.
Error
(
"Expected to skip full host."
)
}
}
...
@@ -82,14 +84,16 @@ func TestRoundRobinPolicy(t *testing.T) {
...
@@ -82,14 +84,16 @@ func TestRoundRobinPolicy(t *testing.T) {
func
TestLeastConnPolicy
(
t
*
testing
.
T
)
{
func
TestLeastConnPolicy
(
t
*
testing
.
T
)
{
pool
:=
testPool
()
pool
:=
testPool
()
lcPolicy
:=
&
LeastConn
{}
lcPolicy
:=
&
LeastConn
{}
request
,
_
:=
http
.
NewRequest
(
"GET"
,
"/"
,
nil
)
pool
[
0
]
.
Conns
=
10
pool
[
0
]
.
Conns
=
10
pool
[
1
]
.
Conns
=
10
pool
[
1
]
.
Conns
=
10
h
:=
lcPolicy
.
Select
(
pool
)
h
:=
lcPolicy
.
Select
(
pool
,
request
)
if
h
!=
pool
[
2
]
{
if
h
!=
pool
[
2
]
{
t
.
Error
(
"Expected least connection host to be third host."
)
t
.
Error
(
"Expected least connection host to be third host."
)
}
}
pool
[
2
]
.
Conns
=
100
pool
[
2
]
.
Conns
=
100
h
=
lcPolicy
.
Select
(
pool
)
h
=
lcPolicy
.
Select
(
pool
,
request
)
if
h
!=
pool
[
0
]
&&
h
!=
pool
[
1
]
{
if
h
!=
pool
[
0
]
&&
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected least connection host to be first or second host."
)
t
.
Error
(
"Expected least connection host to be first or second host."
)
}
}
...
@@ -98,8 +102,127 @@ func TestLeastConnPolicy(t *testing.T) {
...
@@ -98,8 +102,127 @@ func TestLeastConnPolicy(t *testing.T) {
func
TestCustomPolicy
(
t
*
testing
.
T
)
{
func
TestCustomPolicy
(
t
*
testing
.
T
)
{
pool
:=
testPool
()
pool
:=
testPool
()
customPolicy
:=
&
customPolicy
{}
customPolicy
:=
&
customPolicy
{}
h
:=
customPolicy
.
Select
(
pool
)
request
,
_
:=
http
.
NewRequest
(
"GET"
,
"/"
,
nil
)
h
:=
customPolicy
.
Select
(
pool
,
request
)
if
h
!=
pool
[
0
]
{
if
h
!=
pool
[
0
]
{
t
.
Error
(
"Expected custom policy host to be the first host."
)
t
.
Error
(
"Expected custom policy host to be the first host."
)
}
}
}
}
func
TestIPHashPolicy
(
t
*
testing
.
T
)
{
pool
:=
testPool
()
ipHash
:=
&
IPHash
{}
request
,
_
:=
http
.
NewRequest
(
"GET"
,
"/"
,
nil
)
// We should be able to predict where every request is routed.
request
.
RemoteAddr
=
"172.0.0.1:80"
h
:=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected ip hash policy host to be the second host."
)
}
request
.
RemoteAddr
=
"172.0.0.2:80"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected ip hash policy host to be the second host."
)
}
request
.
RemoteAddr
=
"172.0.0.3:80"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
2
]
{
t
.
Error
(
"Expected ip hash policy host to be the third host."
)
}
request
.
RemoteAddr
=
"172.0.0.4:80"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected ip hash policy host to be the second host."
)
}
// we should get the same results without a port
request
.
RemoteAddr
=
"172.0.0.1"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected ip hash policy host to be the second host."
)
}
request
.
RemoteAddr
=
"172.0.0.2"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected ip hash policy host to be the second host."
)
}
request
.
RemoteAddr
=
"172.0.0.3"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
2
]
{
t
.
Error
(
"Expected ip hash policy host to be the third host."
)
}
request
.
RemoteAddr
=
"172.0.0.4"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected ip hash policy host to be the second host."
)
}
// we should get a healthy host if the original host is unhealthy and a
// healthy host is available
request
.
RemoteAddr
=
"172.0.0.1"
pool
[
1
]
.
Unhealthy
=
true
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
0
]
{
t
.
Error
(
"Expected ip hash policy host to be the first host."
)
}
request
.
RemoteAddr
=
"172.0.0.2"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected ip hash policy host to be the second host."
)
}
pool
[
1
]
.
Unhealthy
=
false
request
.
RemoteAddr
=
"172.0.0.3"
pool
[
2
]
.
Unhealthy
=
true
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
0
]
{
t
.
Error
(
"Expected ip hash policy host to be the first host."
)
}
request
.
RemoteAddr
=
"172.0.0.4"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
0
]
{
t
.
Error
(
"Expected ip hash policy host to be the first host."
)
}
// We should be able to resize the host pool and still be able to predict
// where a request will be routed with the same IP's used above
pool
=
[]
*
UpstreamHost
{
{
Name
:
workableServer
.
URL
,
// this should resolve (healthcheck test)
},
{
Name
:
"http://localhost:99998"
,
// this shouldn't
},
}
pool
=
HostPool
(
pool
)
request
.
RemoteAddr
=
"172.0.0.1:80"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
0
]
{
t
.
Error
(
"Expected ip hash policy host to be the first host."
)
}
request
.
RemoteAddr
=
"172.0.0.2:80"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected ip hash policy host to be the second host."
)
}
request
.
RemoteAddr
=
"172.0.0.3:80"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
0
]
{
t
.
Error
(
"Expected ip hash policy host to be the first host."
)
}
request
.
RemoteAddr
=
"172.0.0.4:80"
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
pool
[
1
]
{
t
.
Error
(
"Expected ip hash policy host to be the second host."
)
}
// We should get nil when there are no healthy hosts
pool
[
0
]
.
Unhealthy
=
true
pool
[
1
]
.
Unhealthy
=
true
h
=
ipHash
.
Select
(
pool
,
request
)
if
h
!=
nil
{
t
.
Error
(
"Expected ip hash policy host to be nil."
)
}
}
caddyhttp/proxy/proxy.go
View file @
88980664
...
@@ -27,7 +27,7 @@ type Upstream interface {
...
@@ -27,7 +27,7 @@ type Upstream interface {
// The path this upstream host should be routed on
// The path this upstream host should be routed on
From
()
string
From
()
string
// Selects an upstream host to be routed to.
// Selects an upstream host to be routed to.
Select
()
*
UpstreamHost
Select
(
*
http
.
Request
)
*
UpstreamHost
// Checks if subpath is not an ignored path
// Checks if subpath is not an ignored path
AllowedPath
(
string
)
bool
AllowedPath
(
string
)
bool
}
}
...
@@ -93,7 +93,7 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
...
@@ -93,7 +93,7 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
// hosts until timeout (or until we get a nil host).
// hosts until timeout (or until we get a nil host).
start
:=
time
.
Now
()
start
:=
time
.
Now
()
for
time
.
Now
()
.
Sub
(
start
)
<
tryDuration
{
for
time
.
Now
()
.
Sub
(
start
)
<
tryDuration
{
host
:=
upstream
.
Select
()
host
:=
upstream
.
Select
(
r
)
if
host
==
nil
{
if
host
==
nil
{
return
http
.
StatusBadGateway
,
errUnreachable
return
http
.
StatusBadGateway
,
errUnreachable
}
}
...
@@ -108,7 +108,7 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
...
@@ -108,7 +108,7 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
if
nameURL
,
err
:=
url
.
Parse
(
host
.
Name
);
err
==
nil
{
if
nameURL
,
err
:=
url
.
Parse
(
host
.
Name
);
err
==
nil
{
outreq
.
Host
=
nameURL
.
Host
outreq
.
Host
=
nameURL
.
Host
if
proxy
==
nil
{
if
proxy
==
nil
{
proxy
=
NewSingleHostReverseProxy
(
nameURL
,
host
.
WithoutPathPrefix
,
0
)
proxy
=
NewSingleHostReverseProxy
(
nameURL
,
host
.
WithoutPathPrefix
,
http
.
DefaultMaxIdleConnsPerHost
)
}
}
// use upstream credentials by default
// use upstream credentials by default
...
...
caddyhttp/proxy/proxy_test.go
View file @
88980664
...
@@ -362,9 +362,11 @@ func TestUpstreamHeadersUpdate(t *testing.T) {
...
@@ -362,9 +362,11 @@ func TestUpstreamHeadersUpdate(t *testing.T) {
defer
log
.
SetOutput
(
os
.
Stderr
)
defer
log
.
SetOutput
(
os
.
Stderr
)
var
actualHeaders
http
.
Header
var
actualHeaders
http
.
Header
var
actualHost
string
backend
:=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
backend
:=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
Write
([]
byte
(
"Hello, client"
))
w
.
Write
([]
byte
(
"Hello, client"
))
actualHeaders
=
r
.
Header
actualHeaders
=
r
.
Header
actualHost
=
r
.
Host
}))
}))
defer
backend
.
Close
()
defer
backend
.
Close
()
...
@@ -376,6 +378,7 @@ func TestUpstreamHeadersUpdate(t *testing.T) {
...
@@ -376,6 +378,7 @@ func TestUpstreamHeadersUpdate(t *testing.T) {
"+Add-Me"
:
{
"Add-Value"
},
"+Add-Me"
:
{
"Add-Value"
},
"-Remove-Me"
:
{
""
},
"-Remove-Me"
:
{
""
},
"Replace-Me"
:
{
"{hostname}"
},
"Replace-Me"
:
{
"{hostname}"
},
"Host"
:
{
"{>Host}"
},
}
}
// set up proxy
// set up proxy
p
:=
&
Proxy
{
p
:=
&
Proxy
{
...
@@ -390,10 +393,12 @@ func TestUpstreamHeadersUpdate(t *testing.T) {
...
@@ -390,10 +393,12 @@ func TestUpstreamHeadersUpdate(t *testing.T) {
}
}
w
:=
httptest
.
NewRecorder
()
w
:=
httptest
.
NewRecorder
()
const
expectHost
=
"example.com"
//add initial headers
//add initial headers
r
.
Header
.
Add
(
"Merge-Me"
,
"Initial"
)
r
.
Header
.
Add
(
"Merge-Me"
,
"Initial"
)
r
.
Header
.
Add
(
"Remove-Me"
,
"Remove-Value"
)
r
.
Header
.
Add
(
"Remove-Me"
,
"Remove-Value"
)
r
.
Header
.
Add
(
"Replace-Me"
,
"Replace-Value"
)
r
.
Header
.
Add
(
"Replace-Me"
,
"Replace-Value"
)
r
.
Header
.
Add
(
"Host"
,
expectHost
)
p
.
ServeHTTP
(
w
,
r
)
p
.
ServeHTTP
(
w
,
r
)
...
@@ -426,6 +431,10 @@ func TestUpstreamHeadersUpdate(t *testing.T) {
...
@@ -426,6 +431,10 @@ func TestUpstreamHeadersUpdate(t *testing.T) {
t
.
Errorf
(
"Request sent to upstream backend should replace value of %v header with %v. Instead value was %v"
,
headerKey
,
headerValue
,
value
)
t
.
Errorf
(
"Request sent to upstream backend should replace value of %v header with %v. Instead value was %v"
,
headerKey
,
headerValue
,
value
)
}
}
if
actualHost
!=
expectHost
{
t
.
Errorf
(
"Request sent to upstream backend should have value of Host with %s, but got %s"
,
expectHost
,
actualHost
)
}
}
}
func
TestDownstreamHeadersUpdate
(
t
*
testing
.
T
)
{
func
TestDownstreamHeadersUpdate
(
t
*
testing
.
T
)
{
...
@@ -721,7 +730,7 @@ func newFakeUpstream(name string, insecure bool) *fakeUpstream {
...
@@ -721,7 +730,7 @@ func newFakeUpstream(name string, insecure bool) *fakeUpstream {
from
:
"/"
,
from
:
"/"
,
host
:
&
UpstreamHost
{
host
:
&
UpstreamHost
{
Name
:
name
,
Name
:
name
,
ReverseProxy
:
NewSingleHostReverseProxy
(
uri
,
""
,
0
),
ReverseProxy
:
NewSingleHostReverseProxy
(
uri
,
""
,
http
.
DefaultMaxIdleConnsPerHost
),
},
},
}
}
if
insecure
{
if
insecure
{
...
@@ -741,7 +750,7 @@ func (u *fakeUpstream) From() string {
...
@@ -741,7 +750,7 @@ func (u *fakeUpstream) From() string {
return
u
.
from
return
u
.
from
}
}
func
(
u
*
fakeUpstream
)
Select
()
*
UpstreamHost
{
func
(
u
*
fakeUpstream
)
Select
(
r
*
http
.
Request
)
*
UpstreamHost
{
if
u
.
host
==
nil
{
if
u
.
host
==
nil
{
uri
,
err
:=
url
.
Parse
(
u
.
name
)
uri
,
err
:=
url
.
Parse
(
u
.
name
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -749,7 +758,7 @@ func (u *fakeUpstream) Select() *UpstreamHost {
...
@@ -749,7 +758,7 @@ func (u *fakeUpstream) Select() *UpstreamHost {
}
}
u
.
host
=
&
UpstreamHost
{
u
.
host
=
&
UpstreamHost
{
Name
:
u
.
name
,
Name
:
u
.
name
,
ReverseProxy
:
NewSingleHostReverseProxy
(
uri
,
u
.
without
,
0
),
ReverseProxy
:
NewSingleHostReverseProxy
(
uri
,
u
.
without
,
http
.
DefaultMaxIdleConnsPerHost
),
}
}
}
}
return
u
.
host
return
u
.
host
...
@@ -786,11 +795,11 @@ func (u *fakeWsUpstream) From() string {
...
@@ -786,11 +795,11 @@ func (u *fakeWsUpstream) From() string {
return
"/"
return
"/"
}
}
func
(
u
*
fakeWsUpstream
)
Select
()
*
UpstreamHost
{
func
(
u
*
fakeWsUpstream
)
Select
(
r
*
http
.
Request
)
*
UpstreamHost
{
uri
,
_
:=
url
.
Parse
(
u
.
name
)
uri
,
_
:=
url
.
Parse
(
u
.
name
)
return
&
UpstreamHost
{
return
&
UpstreamHost
{
Name
:
u
.
name
,
Name
:
u
.
name
,
ReverseProxy
:
NewSingleHostReverseProxy
(
uri
,
u
.
without
,
0
),
ReverseProxy
:
NewSingleHostReverseProxy
(
uri
,
u
.
without
,
http
.
DefaultMaxIdleConnsPerHost
),
UpstreamHeaders
:
http
.
Header
{
UpstreamHeaders
:
http
.
Header
{
"Connection"
:
{
"{>Connection}"
},
"Connection"
:
{
"{>Connection}"
},
"Upgrade"
:
{
"{>Upgrade}"
}},
"Upgrade"
:
{
"{>Upgrade}"
}},
...
...
caddyhttp/proxy/reverseproxy.go
View file @
88980664
...
@@ -122,7 +122,10 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) *
...
@@ -122,7 +122,10 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) *
rp
.
Transport
=
&
http
.
Transport
{
rp
.
Transport
=
&
http
.
Transport
{
Dial
:
socketDial
(
target
.
String
()),
Dial
:
socketDial
(
target
.
String
()),
}
}
}
else
if
keepalive
!=
0
{
}
else
if
keepalive
!=
http
.
DefaultMaxIdleConnsPerHost
{
// if keepalive is equal to the default,
// just use default transport, to avoid creating
// a brand new transport
rp
.
Transport
=
&
http
.
Transport
{
rp
.
Transport
=
&
http
.
Transport
{
Proxy
:
http
.
ProxyFromEnvironment
,
Proxy
:
http
.
ProxyFromEnvironment
,
Dial
:
(
&
net
.
Dialer
{
Dial
:
(
&
net
.
Dialer
{
...
@@ -132,7 +135,7 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) *
...
@@ -132,7 +135,7 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) *
TLSHandshakeTimeout
:
10
*
time
.
Second
,
TLSHandshakeTimeout
:
10
*
time
.
Second
,
ExpectContinueTimeout
:
1
*
time
.
Second
,
ExpectContinueTimeout
:
1
*
time
.
Second
,
}
}
if
keepalive
<
0
{
if
keepalive
==
0
{
rp
.
Transport
.
(
*
http
.
Transport
)
.
DisableKeepAlives
=
true
rp
.
Transport
.
(
*
http
.
Transport
)
.
DisableKeepAlives
=
true
}
else
{
}
else
{
rp
.
Transport
.
(
*
http
.
Transport
)
.
MaxIdleConnsPerHost
=
keepalive
rp
.
Transport
.
(
*
http
.
Transport
)
.
MaxIdleConnsPerHost
=
keepalive
...
...
caddyhttp/proxy/upstream.go
View file @
88980664
...
@@ -55,6 +55,7 @@ func NewStaticUpstreams(c caddyfile.Dispenser) ([]Upstream, error) {
...
@@ -55,6 +55,7 @@ func NewStaticUpstreams(c caddyfile.Dispenser) ([]Upstream, error) {
FailTimeout
:
10
*
time
.
Second
,
FailTimeout
:
10
*
time
.
Second
,
MaxFails
:
1
,
MaxFails
:
1
,
MaxConns
:
0
,
MaxConns
:
0
,
KeepAlive
:
http
.
DefaultMaxIdleConnsPerHost
,
}
}
if
!
c
.
Args
(
&
upstream
.
from
)
{
if
!
c
.
Args
(
&
upstream
.
from
)
{
...
@@ -321,6 +322,9 @@ func parseBlock(c *caddyfile.Dispenser, u *staticUpstream) error {
...
@@ -321,6 +322,9 @@ func parseBlock(c *caddyfile.Dispenser, u *staticUpstream) error {
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
if
n
<
0
{
return
c
.
ArgErr
()
}
u
.
KeepAlive
=
n
u
.
KeepAlive
=
n
default
:
default
:
return
c
.
Errf
(
"unknown property '%s'"
,
c
.
Val
())
return
c
.
Errf
(
"unknown property '%s'"
,
c
.
Val
())
...
@@ -356,7 +360,7 @@ func (u *staticUpstream) HealthCheckWorker(stop chan struct{}) {
...
@@ -356,7 +360,7 @@ func (u *staticUpstream) HealthCheckWorker(stop chan struct{}) {
}
}
}
}
func
(
u
*
staticUpstream
)
Select
()
*
UpstreamHost
{
func
(
u
*
staticUpstream
)
Select
(
r
*
http
.
Request
)
*
UpstreamHost
{
pool
:=
u
.
Hosts
pool
:=
u
.
Hosts
if
len
(
pool
)
==
1
{
if
len
(
pool
)
==
1
{
if
!
pool
[
0
]
.
Available
()
{
if
!
pool
[
0
]
.
Available
()
{
...
@@ -374,11 +378,10 @@ func (u *staticUpstream) Select() *UpstreamHost {
...
@@ -374,11 +378,10 @@ func (u *staticUpstream) Select() *UpstreamHost {
if
allUnavailable
{
if
allUnavailable
{
return
nil
return
nil
}
}
if
u
.
Policy
==
nil
{
if
u
.
Policy
==
nil
{
return
(
&
Random
{})
.
Select
(
pool
)
return
(
&
Random
{})
.
Select
(
pool
,
r
)
}
}
return
u
.
Policy
.
Select
(
pool
)
return
u
.
Policy
.
Select
(
pool
,
r
)
}
}
func
(
u
*
staticUpstream
)
AllowedPath
(
requestPath
string
)
bool
{
func
(
u
*
staticUpstream
)
AllowedPath
(
requestPath
string
)
bool
{
...
...
caddyhttp/proxy/upstream_test.go
View file @
88980664
package
proxy
package
proxy
import
(
import
(
"github.com/mholt/caddy/caddyfile"
"net/http"
"strings"
"strings"
"testing"
"testing"
"time"
"time"
"github.com/mholt/caddy/caddyfile"
)
)
func
TestNewHost
(
t
*
testing
.
T
)
{
func
TestNewHost
(
t
*
testing
.
T
)
{
...
@@ -72,14 +72,15 @@ func TestSelect(t *testing.T) {
...
@@ -72,14 +72,15 @@ func TestSelect(t *testing.T) {
FailTimeout
:
10
*
time
.
Second
,
FailTimeout
:
10
*
time
.
Second
,
MaxFails
:
1
,
MaxFails
:
1
,
}
}
r
,
_
:=
http
.
NewRequest
(
"GET"
,
"/"
,
nil
)
upstream
.
Hosts
[
0
]
.
Unhealthy
=
true
upstream
.
Hosts
[
0
]
.
Unhealthy
=
true
upstream
.
Hosts
[
1
]
.
Unhealthy
=
true
upstream
.
Hosts
[
1
]
.
Unhealthy
=
true
upstream
.
Hosts
[
2
]
.
Unhealthy
=
true
upstream
.
Hosts
[
2
]
.
Unhealthy
=
true
if
h
:=
upstream
.
Select
();
h
!=
nil
{
if
h
:=
upstream
.
Select
(
r
);
h
!=
nil
{
t
.
Error
(
"Expected select to return nil as all host are down"
)
t
.
Error
(
"Expected select to return nil as all host are down"
)
}
}
upstream
.
Hosts
[
2
]
.
Unhealthy
=
false
upstream
.
Hosts
[
2
]
.
Unhealthy
=
false
if
h
:=
upstream
.
Select
();
h
==
nil
{
if
h
:=
upstream
.
Select
(
r
);
h
==
nil
{
t
.
Error
(
"Expected select to not return nil"
)
t
.
Error
(
"Expected select to not return nil"
)
}
}
upstream
.
Hosts
[
0
]
.
Conns
=
1
upstream
.
Hosts
[
0
]
.
Conns
=
1
...
@@ -88,11 +89,11 @@ func TestSelect(t *testing.T) {
...
@@ -88,11 +89,11 @@ func TestSelect(t *testing.T) {
upstream
.
Hosts
[
1
]
.
MaxConns
=
1
upstream
.
Hosts
[
1
]
.
MaxConns
=
1
upstream
.
Hosts
[
2
]
.
Conns
=
1
upstream
.
Hosts
[
2
]
.
Conns
=
1
upstream
.
Hosts
[
2
]
.
MaxConns
=
1
upstream
.
Hosts
[
2
]
.
MaxConns
=
1
if
h
:=
upstream
.
Select
();
h
!=
nil
{
if
h
:=
upstream
.
Select
(
r
);
h
!=
nil
{
t
.
Error
(
"Expected select to return nil as all hosts are full"
)
t
.
Error
(
"Expected select to return nil as all hosts are full"
)
}
}
upstream
.
Hosts
[
2
]
.
Conns
=
0
upstream
.
Hosts
[
2
]
.
Conns
=
0
if
h
:=
upstream
.
Select
();
h
==
nil
{
if
h
:=
upstream
.
Select
(
r
);
h
==
nil
{
t
.
Error
(
"Expected select to not return nil"
)
t
.
Error
(
"Expected select to not return nil"
)
}
}
}
}
...
@@ -188,6 +189,7 @@ func TestParseBlockHealthCheck(t *testing.T) {
...
@@ -188,6 +189,7 @@ func TestParseBlockHealthCheck(t *testing.T) {
}
}
func
TestParseBlock
(
t
*
testing
.
T
)
{
func
TestParseBlock
(
t
*
testing
.
T
)
{
r
,
_
:=
http
.
NewRequest
(
"GET"
,
"/"
,
nil
)
tests
:=
[]
struct
{
tests
:=
[]
struct
{
config
string
config
string
}{
}{
...
@@ -207,7 +209,7 @@ func TestParseBlock(t *testing.T) {
...
@@ -207,7 +209,7 @@ func TestParseBlock(t *testing.T) {
t
.
Error
(
"Expected no error. Got:"
,
err
.
Error
())
t
.
Error
(
"Expected no error. Got:"
,
err
.
Error
())
}
}
for
_
,
upstream
:=
range
upstreams
{
for
_
,
upstream
:=
range
upstreams
{
headers
:=
upstream
.
Select
()
.
UpstreamHeaders
headers
:=
upstream
.
Select
(
r
)
.
UpstreamHeaders
if
_
,
ok
:=
headers
[
"Host"
];
!
ok
{
if
_
,
ok
:=
headers
[
"Host"
];
!
ok
{
t
.
Errorf
(
"Test %d: Could not find the Host header"
,
i
+
1
)
t
.
Errorf
(
"Test %d: Could not find the Host header"
,
i
+
1
)
...
...
caddytls/crypto_test.go
View file @
88980664
...
@@ -79,19 +79,22 @@ func PrivateKeyBytes(key crypto.PrivateKey) []byte {
...
@@ -79,19 +79,22 @@ func PrivateKeyBytes(key crypto.PrivateKey) []byte {
}
}
func
TestStandaloneTLSTicketKeyRotation
(
t
*
testing
.
T
)
{
func
TestStandaloneTLSTicketKeyRotation
(
t
*
testing
.
T
)
{
type
syncPkt
struct
{
ticketKey
[
32
]
byte
keysInUse
int
}
tlsGovChan
:=
make
(
chan
struct
{})
tlsGovChan
:=
make
(
chan
struct
{})
defer
close
(
tlsGovChan
)
defer
close
(
tlsGovChan
)
callSync
:=
make
(
chan
bool
,
1
)
callSync
:=
make
(
chan
*
syncPkt
,
1
)
defer
close
(
callSync
)
defer
close
(
callSync
)
oldHook
:=
setSessionTicketKeysTestHook
oldHook
:=
setSessionTicketKeysTestHook
defer
func
()
{
defer
func
()
{
setSessionTicketKeysTestHook
=
oldHook
setSessionTicketKeysTestHook
=
oldHook
}()
}()
var
keysInUse
[][
32
]
byte
setSessionTicketKeysTestHook
=
func
(
keys
[][
32
]
byte
)
[][
32
]
byte
{
setSessionTicketKeysTestHook
=
func
(
keys
[][
32
]
byte
)
[][
32
]
byte
{
keysInUse
=
keys
callSync
<-
&
syncPkt
{
keys
[
0
],
len
(
keys
)}
callSync
<-
true
return
keys
return
keys
}
}
...
@@ -104,17 +107,17 @@ func TestStandaloneTLSTicketKeyRotation(t *testing.T) {
...
@@ -104,17 +107,17 @@ func TestStandaloneTLSTicketKeyRotation(t *testing.T) {
var
lastTicketKey
[
32
]
byte
var
lastTicketKey
[
32
]
byte
for
{
for
{
select
{
select
{
case
<-
callSync
:
case
pkt
:=
<-
callSync
:
if
lastTicketKey
==
keysInUse
[
0
]
{
if
lastTicketKey
==
pkt
.
ticketKey
{
close
(
tlsGovChan
)
close
(
tlsGovChan
)
t
.
Errorf
(
"The same TLS ticket key has been used again (not rotated): %x."
,
lastTicketKey
)
t
.
Errorf
(
"The same TLS ticket key has been used again (not rotated): %x."
,
lastTicketKey
)
return
return
}
}
lastTicketKey
=
keysInUse
[
0
]
lastTicketKey
=
pkt
.
ticketKey
rounds
++
rounds
++
if
rounds
<=
NumTickets
&&
len
(
keysInUse
)
!=
rounds
{
if
rounds
<=
NumTickets
&&
pkt
.
keysInUse
!=
rounds
{
close
(
tlsGovChan
)
close
(
tlsGovChan
)
t
.
Errorf
(
"Expected TLS ticket keys in use: %d; Got instead: %d."
,
rounds
,
len
(
keysInUse
)
)
t
.
Errorf
(
"Expected TLS ticket keys in use: %d; Got instead: %d."
,
rounds
,
pkt
.
keysInUse
)
return
return
}
}
if
c
.
SessionTicketsDisabled
==
true
{
if
c
.
SessionTicketsDisabled
==
true
{
...
...
dist/init/linux-sysvinit/caddy
View file @
88980664
...
@@ -20,9 +20,9 @@ DAEMONUSER=www-data
...
@@ -20,9 +20,9 @@ DAEMONUSER=www-data
PIDFILE
=
/var/run/
$NAME
.pid
PIDFILE
=
/var/run/
$NAME
.pid
LOGFILE
=
/var/log/
$NAME
.log
LOGFILE
=
/var/log/
$NAME
.log
CONFIGFILE
=
/etc/caddy/Caddyfile
CONFIGFILE
=
/etc/caddy/Caddyfile
DAEMONOPTS
=
"-agree=true -
-pidfile=
$PIDFILE
log=
$LOGFILE
-conf=
$CONFIGFILE
"
DAEMONOPTS
=
"-agree=true -
pidfile=
$PIDFILE
-
log=
$LOGFILE
-conf=
$CONFIGFILE
"
USERBIND
=
"
$(
which setcap
)
cap_net_bind_service=+ep"
USERBIND
=
"
setcap
cap_net_bind_service=+ep"
STOP_SCHEDULE
=
"
${
STOP_SCHEDULE
:-
QUIT
/5/TERM/5/KILL/5
}
"
STOP_SCHEDULE
=
"
${
STOP_SCHEDULE
:-
QUIT
/5/TERM/5/KILL/5
}
"
test
-x
$DAEMON
||
exit
0
test
-x
$DAEMON
||
exit
0
...
@@ -37,12 +37,13 @@ ulimit -n 8192
...
@@ -37,12 +37,13 @@ ulimit -n 8192
start
()
{
start
()
{
$USERBIND
$DAEMON
$USERBIND
$DAEMON
start-stop-daemon
--start
--quiet
--make-pidfile
--pidfile
$PIDFILE
\
start-stop-daemon
--start
--quiet
--make-pidfile
--pidfile
$PIDFILE
\
--background
--chuid
$DAEMONUSER
--exec
$DAEMON
--
$DAEMONOPTS
--background
--chuid
$DAEMONUSER
--
oknodo
--
exec
$DAEMON
--
$DAEMONOPTS
}
}
stop
()
{
stop
()
{
start-stop-daemon
--stop
--quiet
--remove-pidfile
--pidfile
$PIDFILE
\
start-stop-daemon
--stop
--quiet
--pidfile
$PIDFILE
--retry
=
$STOP_SCHEDULE
\
--retry
=
$STOP_SCHEDULE
--name
$NAME
--oknodo
--name
$NAME
--oknodo
rm
-f
$PIDFILE
}
}
reload
()
{
reload
()
{
...
...
rlimit_posix.go
0 → 100644
View file @
88980664
// +build !windows
package
caddy
import
(
"fmt"
"syscall"
)
// checkFdlimit issues a warning if the OS limit for
// max file descriptors is below a recommended minimum.
func
checkFdlimit
()
{
const
min
=
8192
// Warn if ulimit is too low for production sites
rlimit
:=
&
syscall
.
Rlimit
{}
err
:=
syscall
.
Getrlimit
(
syscall
.
RLIMIT_NOFILE
,
rlimit
)
if
err
==
nil
&&
rlimit
.
Cur
<
min
{
fmt
.
Printf
(
"WARNING: File descriptor limit %d is too low for production servers. "
+
"At least %d is recommended. Fix with
\"
ulimit -n %d
\"
.
\n
"
,
rlimit
.
Cur
,
min
,
min
)
}
}
rlimit_windows.go
0 → 100644
View file @
88980664
package
caddy
// checkFdlimit issues a warning if the OS limit for
// max file descriptors is below a recommended minimum.
func
checkFdlimit
()
{
}
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