mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-27 04:15:55 +03:00
Rewrote Caddy from the ground up; initial commit of 0.9 branch
These changes span work from the last ~4 months in an effort to make Caddy more extensible, reduce the coupling between its components, and lay a more robust foundation of code going forward into 1.0. A bunch of new features have been added, too, with even higher future potential. The most significant design change is an overall inversion of dependencies. Instead of the caddy package knowing about the server and the notion of middleware and config, the caddy package exposes an interface that other components plug into. This does introduce more indirection when reading the code, but every piece is very modular and pluggable. Even the HTTP server is pluggable. The caddy package has been moved to the top level, and main has been pushed into a subfolder called caddy. The actual logic of the main file has been pushed even further into caddy/caddymain/run.go so that custom builds of Caddy can be 'go get'able. The HTTPS logic was surgically separated into two parts to divide the TLS-specific code and the HTTPS-specific code. The caddytls package can now be used by any type of server that needs TLS, not just HTTP. I also added the ability to customize nearly every aspect of TLS at the site level rather than all sites sharing the same TLS configuration. Not all of this flexibility is exposed in the Caddyfile yet, but it may be in the future. Caddy can also generate self-signed certificates in memory for the convenience of a developer working on localhost who wants HTTPS. And Caddy now supports the DNS challenge, assuming at least one DNS provider is plugged in. Dozens, if not hundreds, of other minor changes swept through the code base as I literally started from an empty main function, copying over functions or files as needed, then adjusting them to fit in the new design. Most tests have been restored and adapted to the new API, but more work is needed there. A lot of what was "impossible" before is now possible, or can be made possible with minimal disruption of the code. For example, it's fairly easy to make plugins hook into another part of the code via callbacks. Plugins can do more than just be directives; we now have plugins that customize how the Caddyfile is loaded (useful when you need to get your configuration from a remote store). Site addresses no longer need be just a host and port. They can have a path, allowing you to scope a configuration to a specific path. There is no inheretance, however; each site configuration is distinct. Thanks to amazing work by Lucas Clemente, this commit adds experimental QUIC support. Turn it on using the -quic flag; your browser may have to be configured to enable it. Almost everything is here, but you will notice that most of the middle- ware are missing. After those are transferred over, we'll be ready for beta tests. I'm very excited to get this out. Thanks for everyone's help and patience these last few months. I hope you like it!!
This commit is contained in:
parent
e1a6b60736
commit
ac4fa2c3a9
230 changed files with 5796 additions and 17422 deletions
|
@ -1,4 +1,4 @@
|
|||
(Are you asking for help with Caddy? Please use our forum instead: https://forum.caddyserver.com. If you are filing a bug report, please answer the following questions. If your issue is not a bug report, you do not need to use this template. Either way, please consider donating if we've helped you. Thanks!)
|
||||
(Are you asking for help with using Caddy? Please use our forum instead: https://forum.caddyserver.com. If you are filing a bug report, please answer the following questions. If your issue is not a bug report, you do not need to use this template. Either way, please consider donating if we've helped you. Thanks!)
|
||||
|
||||
#### 1. What version of Caddy are you running (`caddy -version`)?
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package assets
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
@ -6,10 +6,15 @@ import (
|
|||
"runtime"
|
||||
)
|
||||
|
||||
// Path returns the path to the folder
|
||||
// where the application may store data. This
|
||||
// currently resolves to ~/.caddy
|
||||
func Path() string {
|
||||
// AssetsPath returns the path to the folder
|
||||
// where the application may store data. If
|
||||
// CADDYPATH env variable is set, that value
|
||||
// is used. Otherwise, the path is the result
|
||||
// of evaluating "$HOME/.caddy".
|
||||
func AssetsPath() string {
|
||||
if caddyPath := os.Getenv("CADDYPATH"); caddyPath != "" {
|
||||
return caddyPath
|
||||
}
|
||||
return filepath.Join(userHomeDir(), ".caddy")
|
||||
}
|
||||
|
19
assets_test.go
Normal file
19
assets_test.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAssetsPath(t *testing.T) {
|
||||
if actual := AssetsPath(); !strings.HasSuffix(actual, ".caddy") {
|
||||
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
|
||||
}
|
||||
|
||||
os.Setenv("CADDYPATH", "testpath")
|
||||
if actual, expected := AssetsPath(), "testpath"; actual != expected {
|
||||
t.Errorf("Expected path to be %v, got: %v", expected, actual)
|
||||
}
|
||||
os.Setenv("CADDYPATH", "")
|
||||
}
|
752
caddy.go
Normal file
752
caddy.go
Normal file
|
@ -0,0 +1,752 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
var (
|
||||
// AppName is the name of the application.
|
||||
AppName string
|
||||
|
||||
// AppVersion is the version of the application.
|
||||
AppVersion string
|
||||
|
||||
// Quiet mode will not show any informative output on initialization.
|
||||
Quiet bool
|
||||
|
||||
// PidFile is the path to the pidfile to create.
|
||||
PidFile string
|
||||
|
||||
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
||||
GracefulTimeout time.Duration
|
||||
|
||||
// isUpgrade will be set to true if this process
|
||||
// was started as part of an upgrade, where a parent
|
||||
// Caddy process started this one.
|
||||
isUpgrade bool
|
||||
)
|
||||
|
||||
// Instance contains the state of servers created as a result of
|
||||
// calling Start and can be used to access or control those servers.
|
||||
type Instance struct {
|
||||
// serverType is the name of the instance's server type
|
||||
serverType string
|
||||
|
||||
// caddyfileInput is the input configuration text used for this process
|
||||
caddyfileInput Input
|
||||
|
||||
// wg is used to wait for all servers to shut down
|
||||
wg sync.WaitGroup
|
||||
|
||||
// servers is the list of servers with their listeners...
|
||||
servers []serverListener
|
||||
|
||||
// these are callbacks to execute when certain events happen
|
||||
onStartup []func() error
|
||||
onRestart []func() error
|
||||
onShutdown []func() error
|
||||
}
|
||||
|
||||
// Stop stops all servers contained in i. It does NOT
|
||||
// execute shutdown callbacks.
|
||||
func (i *Instance) Stop() error {
|
||||
// stop the servers
|
||||
for _, s := range i.servers {
|
||||
if gs, ok := s.server.(GracefulServer); ok {
|
||||
if err := gs.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", gs.Address(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// splice instance list to delete this one
|
||||
for j, other := range instances {
|
||||
if other == i {
|
||||
instances = append(instances[:j], instances[j+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shutdownCallbacks executes all the shutdown callbacks of i.
|
||||
// An error returned from one does not stop execution of the rest.
|
||||
// All the errors will be returned.
|
||||
func (i *Instance) shutdownCallbacks() []error {
|
||||
var errs []error
|
||||
for _, shutdownFunc := range i.onShutdown {
|
||||
err := shutdownFunc()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Restart replaces the servers in i with new servers created from
|
||||
// executing the newCaddyfile. Upon success, it returns the new
|
||||
// instance to replace i. Upon failure, i will not be replaced.
|
||||
func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
|
||||
log.Println("[INFO] Reloading")
|
||||
|
||||
// run restart callbacks
|
||||
for _, fn := range i.onRestart {
|
||||
err := fn()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
|
||||
if newCaddyfile == nil {
|
||||
newCaddyfile = i.caddyfileInput
|
||||
}
|
||||
|
||||
// Add file descriptors of all the sockets that are capable of it
|
||||
restartFds := make(map[string]restartPair)
|
||||
for _, s := range i.servers {
|
||||
gs, srvOk := s.server.(GracefulServer)
|
||||
ln, lnOk := s.listener.(Listener)
|
||||
if srvOk && lnOk {
|
||||
restartFds[gs.Address()] = restartPair{server: gs, listener: ln}
|
||||
}
|
||||
}
|
||||
|
||||
// create new instance; if the restart fails, it is simply discarded
|
||||
newInst := &Instance{serverType: newCaddyfile.ServerType()}
|
||||
|
||||
// attempt to start new instance
|
||||
err := startWithListenerFds(newCaddyfile, newInst, restartFds)
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
|
||||
// success! bump the old instance out so it will be garbage-collected
|
||||
instancesMu.Lock()
|
||||
for j, other := range instances {
|
||||
if other == i {
|
||||
instances = append(instances[:j], instances[j+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
instancesMu.Unlock()
|
||||
|
||||
log.Println("[INFO] Reloading complete")
|
||||
|
||||
return newInst, nil
|
||||
}
|
||||
|
||||
// SaveServer adds s and its associated listener ln to the
|
||||
// internally-kept list of servers that is running. For
|
||||
// saved servers, graceful restarts will be provided.
|
||||
func (i *Instance) SaveServer(s Server, ln net.Listener) {
|
||||
i.servers = append(i.servers, serverListener{server: s, listener: ln})
|
||||
}
|
||||
|
||||
// HasListenerWithAddress returns whether this package is
|
||||
// tracking a server using a listener with the address
|
||||
// addr.
|
||||
func HasListenerWithAddress(addr string) bool {
|
||||
instancesMu.Lock()
|
||||
defer instancesMu.Unlock()
|
||||
for _, inst := range instances {
|
||||
for _, sln := range inst.servers {
|
||||
if listenerAddrEqual(sln.listener, addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// listenerAddrEqual compares a listener's address with
|
||||
// addr. Extra care is taken to match addresses with an
|
||||
// empty hostname portion, as listeners tend to report
|
||||
// [::]:80, for example, when the matching address that
|
||||
// created the listener might be simply :80.
|
||||
func listenerAddrEqual(ln net.Listener, addr string) bool {
|
||||
lnAddr := ln.Addr().String()
|
||||
hostname, port, err := net.SplitHostPort(addr)
|
||||
if err != nil || hostname != "" {
|
||||
return lnAddr == addr
|
||||
}
|
||||
if lnAddr == net.JoinHostPort("::", port) {
|
||||
return true
|
||||
}
|
||||
if lnAddr == net.JoinHostPort("0.0.0.0", port) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO: We should be able to support UDP servers... I'm considering this pattern.
|
||||
|
||||
type UDPListener struct {
|
||||
*net.UDPConn
|
||||
}
|
||||
|
||||
func (u UDPListener) Accept() (net.Conn, error) {
|
||||
return u.UDPConn, nil
|
||||
}
|
||||
|
||||
func (u UDPListener) Close() error {
|
||||
return u.UDPConn.Close()
|
||||
}
|
||||
|
||||
func (u UDPListener) Addr() net.Addr {
|
||||
return u.UDPConn.LocalAddr()
|
||||
}
|
||||
|
||||
var _ net.Listener = UDPListener{}
|
||||
*/
|
||||
|
||||
// Server is a type that can listen and serve. A Server
|
||||
// must associate with exactly zero or one listeners.
|
||||
type Server interface {
|
||||
// Listen starts listening by creating a new listener
|
||||
// and returning it. It does not start accepting
|
||||
// connections.
|
||||
Listen() (net.Listener, error)
|
||||
|
||||
// Serve starts serving using the provided listener.
|
||||
// Serve must start the server loop nearly immediately,
|
||||
// or at least not return any errors before the server
|
||||
// loop begins. Serve blocks indefinitely, or in other
|
||||
// words, until the server is stopped.
|
||||
Serve(net.Listener) error
|
||||
}
|
||||
|
||||
// Stopper is a type that can stop serving. The stop
|
||||
// does not necessarily have to be graceful.
|
||||
type Stopper interface {
|
||||
// Stop stops the server. It blocks until the
|
||||
// server is completely stopped.
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// GracefulServer is a Server and Stopper, the stopping
|
||||
// of which is graceful (whatever that means for the kind
|
||||
// of server being implemented). It must be able to return
|
||||
// the address it is configured to listen on so that its
|
||||
// listener can be paired with it upon graceful restarts.
|
||||
// The net.Listener that a GracefulServer creates must
|
||||
// implement the Listener interface for restarts to be
|
||||
// graceful (assuming the listener is for TCP).
|
||||
type GracefulServer interface {
|
||||
Server
|
||||
Stopper
|
||||
|
||||
// Address returns the address the server should
|
||||
// listen on; it is used to pair the server to
|
||||
// its listener during a graceful/zero-downtime
|
||||
// restart. Thus when implementing this method,
|
||||
// you must not access a listener to get the
|
||||
// address; you must store the address the
|
||||
// server is to serve on some other way.
|
||||
Address() string
|
||||
}
|
||||
|
||||
// Listener is a net.Listener with an underlying file descriptor.
|
||||
// A server's listener should implement this interface if it is
|
||||
// to support zero-downtime reloads.
|
||||
type Listener interface {
|
||||
net.Listener
|
||||
File() (*os.File, error)
|
||||
}
|
||||
|
||||
// AfterStartup is an interface that can be implemented
|
||||
// by a server type that wants to run some code after all
|
||||
// servers for the same Instance have started.
|
||||
type AfterStartup interface {
|
||||
OnStartupComplete()
|
||||
}
|
||||
|
||||
// LoadCaddyfile loads a Caddyfile by calling the plugged in
|
||||
// Caddyfile loader methods. An error is returned if more than
|
||||
// one loader returns a non-nil Caddyfile input. If no loaders
|
||||
// load a Caddyfile, the default loader is used. If no default
|
||||
// loader is registered or it returns nil, the server type's
|
||||
// default Caddyfile is loaded. If the server type does not
|
||||
// specify any default Caddyfile value, then an empty Caddyfile
|
||||
// is returned. Consequently, this function never returns a nil
|
||||
// value as long as there are no errors.
|
||||
func LoadCaddyfile(serverType string) (Input, error) {
|
||||
// Ask plugged-in loaders for a Caddyfile
|
||||
cdyfile, err := loadCaddyfileInput(serverType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Otherwise revert to default
|
||||
if cdyfile == nil {
|
||||
cdyfile = DefaultInput(serverType)
|
||||
}
|
||||
|
||||
// Still nil? Geez.
|
||||
if cdyfile == nil {
|
||||
cdyfile = CaddyfileInput{ServerTypeName: serverType}
|
||||
}
|
||||
|
||||
return cdyfile, nil
|
||||
}
|
||||
|
||||
// Wait blocks until all of i's servers have stopped.
|
||||
func (i *Instance) Wait() {
|
||||
i.wg.Wait()
|
||||
}
|
||||
|
||||
// CaddyfileFromPipe loads the Caddyfile input from f if f is
|
||||
// not interactive input. f is assumed to be a pipe or stream,
|
||||
// such as os.Stdin. If f is not a pipe, no error is returned
|
||||
// but the Input value will be nil. An error is only returned
|
||||
// if there was an error reading the pipe, even if the length
|
||||
// of what was read is 0.
|
||||
func CaddyfileFromPipe(f *os.File) (Input, error) {
|
||||
fi, err := f.Stat()
|
||||
if err == nil && fi.Mode()&os.ModeCharDevice == 0 {
|
||||
// Note that a non-nil error is not a problem. Windows
|
||||
// will not create a stdin if there is no pipe, which
|
||||
// produces an error when calling Stat(). But Unix will
|
||||
// make one either way, which is why we also check that
|
||||
// bitmask.
|
||||
// NOTE: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X)
|
||||
confBody, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CaddyfileInput{
|
||||
Contents: confBody,
|
||||
Filepath: f.Name(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// not having input from the pipe is not itself an error,
|
||||
// just means no input to return.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Caddyfile returns the Caddyfile used to create i.
|
||||
func (i *Instance) Caddyfile() Input {
|
||||
return i.caddyfileInput
|
||||
}
|
||||
|
||||
// Start starts Caddy with the given Caddyfile.
|
||||
//
|
||||
// This function blocks until all the servers are listening.
|
||||
func Start(cdyfile Input) (*Instance, error) {
|
||||
writePidFile()
|
||||
inst := &Instance{serverType: cdyfile.ServerType()}
|
||||
return inst, startWithListenerFds(cdyfile, inst, nil)
|
||||
}
|
||||
|
||||
func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartPair) error {
|
||||
if cdyfile == nil {
|
||||
cdyfile = CaddyfileInput{}
|
||||
}
|
||||
|
||||
stypeName := cdyfile.ServerType()
|
||||
|
||||
stype, err := getServerType(stypeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inst.caddyfileInput = cdyfile
|
||||
|
||||
sblocks, err := loadServerBlocks(stypeName, path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := stype.NewContext()
|
||||
|
||||
sblocks, err = ctx.InspectServerBlocks(cdyfile.Path(), sblocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = executeDirectives(inst, cdyfile.Path(), stype.Directives, sblocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slist, err := ctx.MakeServers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if restartFds == nil {
|
||||
// run startup callbacks since this is not a restart
|
||||
for _, startupFunc := range inst.onStartup {
|
||||
err := startupFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = startServers(slist, inst, restartFds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instancesMu.Lock()
|
||||
instances = append(instances, inst)
|
||||
instancesMu.Unlock()
|
||||
|
||||
// run any AfterStartup callbacks if this is not
|
||||
// part of a restart; then show file descriptor notice
|
||||
if restartFds == nil {
|
||||
for _, srvln := range inst.servers {
|
||||
if srv, ok := srvln.server.(AfterStartup); ok {
|
||||
srv.OnStartupComplete()
|
||||
}
|
||||
}
|
||||
if !Quiet {
|
||||
for _, srvln := range inst.servers {
|
||||
if !IsLocalhost(srvln.listener.Addr().String()) {
|
||||
checkFdlimit()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeDirectives(inst *Instance, filename string,
|
||||
directives []string, sblocks []caddyfile.ServerBlock) error {
|
||||
|
||||
// map of server block ID to map of directive name to whatever.
|
||||
storages := make(map[int]map[string]interface{})
|
||||
|
||||
// It is crucial that directives are executed in the proper order.
|
||||
// We loop with the directives on the outer loop so we execute
|
||||
// a directive for all server blocks before going to the next directive.
|
||||
// This is important mainly due to the parsing callbacks (below).
|
||||
for _, dir := range directives {
|
||||
for i, sb := range sblocks {
|
||||
var once sync.Once
|
||||
if _, ok := storages[i]; !ok {
|
||||
storages[i] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
for j, key := range sb.Keys {
|
||||
// Execute directive if it is in the server block
|
||||
if tokens, ok := sb.Tokens[dir]; ok {
|
||||
controller := &Controller{
|
||||
instance: inst,
|
||||
Key: key,
|
||||
Dispenser: caddyfile.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockKeyIndex: j,
|
||||
ServerBlockKeys: sb.Keys,
|
||||
ServerBlockStorage: storages[i][dir],
|
||||
}
|
||||
|
||||
setup, err := DirectiveAction(inst.serverType, dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = setup(controller)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storages[i][dir] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See if there are any callbacks to execute after this directive
|
||||
if allCallbacks, ok := parsingCallbacks[inst.serverType]; ok {
|
||||
callbacks := allCallbacks[dir]
|
||||
for _, callback := range callbacks {
|
||||
if err := callback(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startServers(serverList []Server, inst *Instance, restartFds map[string]restartPair) error {
|
||||
errChan := make(chan error, len(serverList))
|
||||
|
||||
for _, s := range serverList {
|
||||
var ln net.Listener
|
||||
var err error
|
||||
|
||||
// If this is a reload and s is a GracefulServer,
|
||||
// reuse the listener for a graceful restart.
|
||||
if gs, ok := s.(GracefulServer); ok && restartFds != nil {
|
||||
addr := gs.Address()
|
||||
if old, ok := restartFds[addr]; ok {
|
||||
file, err := old.listener.File()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ln, err = net.FileListener(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
delete(restartFds, addr)
|
||||
}
|
||||
}
|
||||
|
||||
if ln == nil {
|
||||
ln, err = s.Listen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
inst.wg.Add(1)
|
||||
go func(s Server, ln net.Listener, inst *Instance) {
|
||||
defer inst.wg.Done()
|
||||
errChan <- s.Serve(ln)
|
||||
}(s, ln, inst)
|
||||
|
||||
inst.servers = append(inst.servers, serverListener{server: s, listener: ln})
|
||||
}
|
||||
|
||||
// Close the remaining (unused) file descriptors to free up resources
|
||||
// and stop old servers that aren't used anymore
|
||||
for key, old := range restartFds {
|
||||
if err := old.server.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", old.server.Address(), err)
|
||||
}
|
||||
delete(restartFds, key)
|
||||
}
|
||||
|
||||
// Log errors that may be returned from Serve() calls,
|
||||
// these errors should only be occurring in the server loop.
|
||||
go func() {
|
||||
for err := range errChan {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(err.Error(), "use of closed network connection") {
|
||||
// this error is normal when closing the listener
|
||||
continue
|
||||
}
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServerType(serverType string) (ServerType, error) {
|
||||
stype, ok := serverTypes[serverType]
|
||||
if ok {
|
||||
return stype, nil
|
||||
}
|
||||
if serverType == "" {
|
||||
if len(serverTypes) == 1 {
|
||||
for _, stype := range serverTypes {
|
||||
return stype, nil
|
||||
}
|
||||
}
|
||||
return ServerType{}, fmt.Errorf("multiple server types available; must choose one")
|
||||
}
|
||||
if len(serverTypes) == 0 {
|
||||
return ServerType{}, fmt.Errorf("no server types plugged in")
|
||||
}
|
||||
return ServerType{}, fmt.Errorf("unknown server type '%s'", serverType)
|
||||
}
|
||||
|
||||
func loadServerBlocks(serverType, filename string, input io.Reader) ([]caddyfile.ServerBlock, error) {
|
||||
validDirectives := ValidDirectives(serverType)
|
||||
serverBlocks, err := caddyfile.ServerBlocks(filename, input, validDirectives)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(serverBlocks) == 0 && serverTypes[serverType].DefaultInput != nil {
|
||||
newInput := serverTypes[serverType].DefaultInput()
|
||||
serverBlocks, err = caddyfile.ServerBlocks(newInput.Path(),
|
||||
bytes.NewReader(newInput.Body()), validDirectives)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return serverBlocks, nil
|
||||
}
|
||||
|
||||
// Stop stops ALL servers. It blocks until they are all stopped.
|
||||
// It does NOT execute shutdown callbacks, and it deletes all
|
||||
// instances after stopping is completed. Do not re-use any
|
||||
// references to old instances after calling Stop.
|
||||
func Stop() error {
|
||||
instancesMu.Lock()
|
||||
for _, inst := range instances {
|
||||
if err := inst.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", inst.serverType, err)
|
||||
}
|
||||
}
|
||||
instances = []*Instance{}
|
||||
instancesMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsLocalhost returns true if the hostname of addr looks
|
||||
// explicitly like a common local hostname. addr must only
|
||||
// be a host or a host:port combination.
|
||||
func IsLocalhost(addr string) bool {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr // happens if the addr is just a hostname
|
||||
}
|
||||
return host == "localhost" ||
|
||||
host == "::1" ||
|
||||
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
|
||||
// for a graceful restart. It does NOT load new configuration;
|
||||
// it only starts the process anew with a fresh binary.
|
||||
//
|
||||
// TODO: This is not yet implemented
|
||||
func Upgrade() error {
|
||||
return fmt.Errorf("not implemented")
|
||||
// TODO: have child process set isUpgrade = true
|
||||
}
|
||||
|
||||
// IsUpgrade returns true if this process is part of an upgrade
|
||||
// where a parent caddy process spawned this one to ugprade
|
||||
// the binary.
|
||||
func IsUpgrade() bool {
|
||||
return isUpgrade
|
||||
}
|
||||
|
||||
// CaddyfileInput represents a Caddyfile as input
|
||||
// and is simply a convenient way to implement
|
||||
// the Input interface.
|
||||
type CaddyfileInput struct {
|
||||
Filepath string
|
||||
Contents []byte
|
||||
ServerTypeName string
|
||||
}
|
||||
|
||||
// Body returns c.Contents.
|
||||
func (c CaddyfileInput) Body() []byte { return c.Contents }
|
||||
|
||||
// Path returns c.Filepath.
|
||||
func (c CaddyfileInput) Path() string { return c.Filepath }
|
||||
|
||||
// ServerType returns c.ServerType.
|
||||
func (c CaddyfileInput) ServerType() string { return c.ServerTypeName }
|
||||
|
||||
// Input represents a Caddyfile; its contents and file path
|
||||
// (which should include the file name at the end of the path).
|
||||
// If path does not apply (e.g. piped input) you may use
|
||||
// any understandable value. The path is mainly used for logging,
|
||||
// error messages, and debugging.
|
||||
type Input interface {
|
||||
// Gets the Caddyfile contents
|
||||
Body() []byte
|
||||
|
||||
// Gets the path to the origin file
|
||||
Path() string
|
||||
|
||||
// The type of server this input is intended for
|
||||
ServerType() string
|
||||
}
|
||||
|
||||
// DefaultInput returns the default Caddyfile input
|
||||
// to use when it is otherwise empty or missing.
|
||||
// It uses the default host and port (depends on
|
||||
// host, e.g. localhost is 2015, otherwise 443) and
|
||||
// root.
|
||||
func DefaultInput(serverType string) Input {
|
||||
if _, ok := serverTypes[serverType]; !ok {
|
||||
return nil
|
||||
}
|
||||
if serverTypes[serverType].DefaultInput == nil {
|
||||
return nil
|
||||
}
|
||||
return serverTypes[serverType].DefaultInput()
|
||||
}
|
||||
|
||||
// IsLoopback returns true if host looks explicitly like a loopback address.
|
||||
func IsLoopback(host string) bool {
|
||||
return host == "localhost" ||
|
||||
host == "::1" ||
|
||||
strings.HasPrefix(host, "127.")
|
||||
}
|
||||
|
||||
// writePidFile writes the process ID to the file at PidFile.
|
||||
// It does nothing if PidFile is not set.
|
||||
func writePidFile() error {
|
||||
if PidFile == "" {
|
||||
return nil
|
||||
}
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
return ioutil.WriteFile(PidFile, pid, 0644)
|
||||
}
|
||||
|
||||
type restartPair struct {
|
||||
server GracefulServer
|
||||
listener Listener
|
||||
}
|
||||
|
||||
var (
|
||||
// instances is the list of running Instances.
|
||||
instances []*Instance
|
||||
|
||||
// instancesMu protects instances.
|
||||
instancesMu sync.Mutex
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultConfigFile is the name of the configuration file that is loaded
|
||||
// by default if no other file is specified.
|
||||
DefaultConfigFile = "Caddyfile"
|
||||
)
|
|
@ -1,12 +0,0 @@
|
|||
package assets
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
if actual := Path(); !strings.HasSuffix(actual, ".caddy") {
|
||||
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
|
||||
}
|
||||
}
|
|
@ -7,19 +7,18 @@
|
|||
# $ ./build.bash [output_filename] [git_repo]
|
||||
#
|
||||
# Outputs compiled program in current directory.
|
||||
# Default file name is 'ecaddy'.
|
||||
# Default git repo is current directory.
|
||||
# Builds always take place from current directory.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: ${output_filename:="${1:-}"}
|
||||
: ${output_filename:="ecaddy"}
|
||||
: ${output_filename:="caddy"}
|
||||
|
||||
: ${git_repo:="${2:-}"}
|
||||
: ${git_repo:="."}
|
||||
|
||||
pkg=main
|
||||
pkg=github.com/mholt/caddy/caddy/caddymain
|
||||
ldflags=()
|
||||
|
||||
# Timestamp of build
|
346
caddy/caddy.go
346
caddy/caddy.go
|
@ -1,346 +0,0 @@
|
|||
// Package caddy implements the Caddy web server as a service
|
||||
// in your own Go programs.
|
||||
//
|
||||
// To use this package, follow a few simple steps:
|
||||
//
|
||||
// 1. Set the AppName and AppVersion variables.
|
||||
// 2. Call LoadCaddyfile() to get the Caddyfile.
|
||||
// You should pass in your own Caddyfile loader.
|
||||
// 3. Call caddy.Start() to start Caddy, caddy.Stop()
|
||||
// to stop it, or caddy.Restart() to restart it.
|
||||
//
|
||||
// You should use caddy.Wait() to wait for all Caddy servers
|
||||
// to quit before your process exits.
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
var (
|
||||
// AppName is the name of the application.
|
||||
AppName string
|
||||
|
||||
// AppVersion is the version of the application.
|
||||
AppVersion string
|
||||
|
||||
// Quiet when set to true, will not show any informative output on initialization.
|
||||
Quiet bool
|
||||
|
||||
// HTTP2 indicates whether HTTP2 is enabled or not.
|
||||
HTTP2 bool
|
||||
|
||||
// PidFile is the path to the pidfile to create.
|
||||
PidFile string
|
||||
|
||||
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
||||
GracefulTimeout time.Duration
|
||||
)
|
||||
|
||||
var (
|
||||
// caddyfile is the input configuration text used for this process
|
||||
caddyfile Input
|
||||
|
||||
// caddyfileMu protects caddyfile during changes
|
||||
caddyfileMu sync.Mutex
|
||||
|
||||
// servers is a list of all the currently-listening servers
|
||||
servers []*server.Server
|
||||
|
||||
// serversMu protects the servers slice during changes
|
||||
serversMu sync.Mutex
|
||||
|
||||
// wg is used to wait for all servers to shut down
|
||||
wg sync.WaitGroup
|
||||
|
||||
// restartFds keeps the servers' sockets for graceful in-process restart
|
||||
restartFds = make(map[string]*os.File)
|
||||
|
||||
// startedBefore should be set to true if caddy has been started
|
||||
// at least once (does not indicate whether currently running).
|
||||
startedBefore bool
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultHost is the default host.
|
||||
DefaultHost = ""
|
||||
// DefaultPort is the default port.
|
||||
DefaultPort = "2015"
|
||||
// DefaultRoot is the default root folder.
|
||||
DefaultRoot = "."
|
||||
)
|
||||
|
||||
// Start starts Caddy with the given Caddyfile. If cdyfile
|
||||
// is nil, the LoadCaddyfile function will be called to get
|
||||
// one.
|
||||
//
|
||||
// This function blocks until all the servers are listening.
|
||||
func Start(cdyfile Input) (err error) {
|
||||
// Input must never be nil; try to load something
|
||||
if cdyfile == nil {
|
||||
cdyfile, err = LoadCaddyfile(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
caddyfileMu.Lock()
|
||||
caddyfile = cdyfile
|
||||
caddyfileMu.Unlock()
|
||||
|
||||
// load the server configs (activates Let's Encrypt)
|
||||
configs, err := loadConfigs(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// group virtualhosts by address
|
||||
groupings, err := arrangeBindings(configs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start each server with its one or more configurations
|
||||
err = startServers(groupings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
showInitializationOutput(groupings)
|
||||
|
||||
startedBefore = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// showInitializationOutput just outputs some basic information about
|
||||
// what is being served to stdout, as well as any applicable, non-essential
|
||||
// warnings for the user.
|
||||
func showInitializationOutput(groupings bindingGroup) {
|
||||
// Show initialization output
|
||||
if !Quiet && !IsRestart() {
|
||||
var checkedFdLimit bool
|
||||
for _, group := range groupings {
|
||||
for _, conf := range group.Configs {
|
||||
// Print address of site
|
||||
fmt.Println(conf.Address())
|
||||
|
||||
// Note if non-localhost site resolves to loopback interface
|
||||
if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
|
||||
fmt.Printf("Notice: %s is only accessible on this machine (%s)\n",
|
||||
conf.Host, group.BindAddr.IP.String())
|
||||
}
|
||||
if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
|
||||
checkFdlimit()
|
||||
checkedFdLimit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startServers starts all the servers in groupings,
|
||||
// taking into account whether or not this process is
|
||||
// from a graceful restart or not. It blocks until
|
||||
// the servers are listening.
|
||||
func startServers(groupings bindingGroup) error {
|
||||
var startupWg sync.WaitGroup
|
||||
errChan := make(chan error, len(groupings)) // must be buffered to allow Serve functions below to return if stopped later
|
||||
|
||||
for _, group := range groupings {
|
||||
s, err := server.New(group.BindAddr.String(), group.Configs, GracefulTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.HTTP2 = HTTP2
|
||||
s.ReqCallback = https.RequestCallback // ensures we can solve ACME challenges while running
|
||||
if s.OnDemandTLS {
|
||||
s.TLSConfig.GetCertificate = https.GetOrObtainCertificate // TLS on demand -- awesome!
|
||||
} else {
|
||||
s.TLSConfig.GetCertificate = https.GetCertificate
|
||||
}
|
||||
|
||||
var ln server.ListenerFile
|
||||
if len(restartFds) > 0 {
|
||||
// Reuse the listeners for in-process restart
|
||||
if file, ok := restartFds[s.Addr]; ok {
|
||||
fln, err := net.FileListener(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ln, ok = fln.(server.ListenerFile)
|
||||
if !ok {
|
||||
return errors.New("listener for " + s.Addr + " was not a ListenerFile")
|
||||
}
|
||||
|
||||
file.Close()
|
||||
delete(restartFds, s.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(s *server.Server, ln server.ListenerFile) {
|
||||
defer wg.Done()
|
||||
|
||||
// run startup functions that should only execute when
|
||||
// the original parent process is starting.
|
||||
if !startedBefore {
|
||||
err := s.RunFirstStartupFuncs()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// start the server
|
||||
if ln != nil {
|
||||
errChan <- s.Serve(ln)
|
||||
} else {
|
||||
errChan <- s.ListenAndServe()
|
||||
}
|
||||
}(s, ln)
|
||||
|
||||
startupWg.Add(1)
|
||||
go func(s *server.Server) {
|
||||
defer startupWg.Done()
|
||||
s.WaitUntilStarted()
|
||||
}(s)
|
||||
|
||||
serversMu.Lock()
|
||||
servers = append(servers, s)
|
||||
serversMu.Unlock()
|
||||
}
|
||||
|
||||
// Close the remaining (unused) file descriptors to free up resources
|
||||
if len(restartFds) > 0 {
|
||||
for key, file := range restartFds {
|
||||
file.Close()
|
||||
delete(restartFds, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all servers to finish starting
|
||||
startupWg.Wait()
|
||||
|
||||
// Return the first error, if any
|
||||
select {
|
||||
case err := <-errChan:
|
||||
// "use of closed network connection" is normal if it was a graceful shutdown
|
||||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops all servers. It blocks until they are all stopped.
|
||||
// It does NOT execute shutdown callbacks that may have been
|
||||
// configured by middleware (they must be executed separately).
|
||||
func Stop() error {
|
||||
https.Deactivate()
|
||||
|
||||
serversMu.Lock()
|
||||
for _, s := range servers {
|
||||
if err := s.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", s.Addr, err)
|
||||
}
|
||||
}
|
||||
servers = []*server.Server{} // don't reuse servers
|
||||
serversMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait blocks until all servers are stopped.
|
||||
func Wait() {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// LoadCaddyfile loads a Caddyfile by calling the user's loader function,
|
||||
// and if that returns nil, then this function resorts to the default
|
||||
// configuration. Thus, if there are no other errors, this function
|
||||
// always returns at least the default Caddyfile.
|
||||
func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) {
|
||||
// Try user's loader
|
||||
if cdyfile == nil && loader != nil {
|
||||
cdyfile, err = loader()
|
||||
}
|
||||
|
||||
// Otherwise revert to default
|
||||
if cdyfile == nil {
|
||||
cdyfile = DefaultInput()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CaddyfileFromPipe loads the Caddyfile input from f if f is
|
||||
// not interactive input. f is assumed to be a pipe or stream,
|
||||
// such as os.Stdin. If f is not a pipe, no error is returned
|
||||
// but the Input value will be nil. An error is only returned
|
||||
// if there was an error reading the pipe, even if the length
|
||||
// of what was read is 0.
|
||||
func CaddyfileFromPipe(f *os.File) (Input, error) {
|
||||
fi, err := f.Stat()
|
||||
if err == nil && fi.Mode()&os.ModeCharDevice == 0 {
|
||||
// Note that a non-nil error is not a problem. Windows
|
||||
// will not create a stdin if there is no pipe, which
|
||||
// produces an error when calling Stat(). But Unix will
|
||||
// make one either way, which is why we also check that
|
||||
// bitmask.
|
||||
// BUG: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X)
|
||||
confBody, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CaddyfileInput{
|
||||
Contents: confBody,
|
||||
Filepath: f.Name(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// not having input from the pipe is not itself an error,
|
||||
// just means no input to return.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Caddyfile returns the current Caddyfile
|
||||
func Caddyfile() Input {
|
||||
caddyfileMu.Lock()
|
||||
defer caddyfileMu.Unlock()
|
||||
return caddyfile
|
||||
}
|
||||
|
||||
// Input represents a Caddyfile; its contents and file path
|
||||
// (which should include the file name at the end of the path).
|
||||
// If path does not apply (e.g. piped input) you may use
|
||||
// any understandable value. The path is mainly used for logging,
|
||||
// error messages, and debugging.
|
||||
type Input interface {
|
||||
// Gets the Caddyfile contents
|
||||
Body() []byte
|
||||
|
||||
// Gets the path to the origin file
|
||||
Path() string
|
||||
|
||||
// IsFile returns true if the original input was a file on the file system
|
||||
// that could be loaded again later if requested.
|
||||
IsFile() bool
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCaddyStartStop(t *testing.T) {
|
||||
caddyfile := "localhost:1984"
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting, iteration %d: %v", i, err)
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(2 * time.Second),
|
||||
}
|
||||
resp, err := client.Get("http://localhost:1984")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
err = Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("Error stopping, iteration %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package caddymain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
@ -7,46 +7,55 @@ import (
|
|||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddy"
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/xenolf/lego/acme"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
// plug in the HTTP server type
|
||||
_ "github.com/mholt/caddy/caddyhttp"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
// This is where other plugins get plugged in (imported)
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.TrapSignals()
|
||||
setVersion()
|
||||
flag.BoolVar(&https.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement")
|
||||
flag.StringVar(&https.CAUrl, "ca", "https://acme-v01.api.letsencrypt.org/directory", "Certificate authority ACME server")
|
||||
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")")
|
||||
|
||||
flag.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
|
||||
// TODO: Change from staging to v01
|
||||
flag.StringVar(&caddytls.DefaultCAUrl, "ca", "https://acme-staging.api.letsencrypt.org/directory", "URL to certificate authority's ACME server directory")
|
||||
flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
|
||||
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
|
||||
flag.StringVar(&https.DefaultEmail, "email", "", "Default Let's Encrypt account email address")
|
||||
flag.DurationVar(&caddy.GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown")
|
||||
flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host")
|
||||
flag.BoolVar(&caddy.HTTP2, "http2", true, "Use HTTP/2")
|
||||
flag.BoolVar(&plugins, "plugins", false, "List supported plugins") // TODO: change to plugins
|
||||
flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address")
|
||||
flag.StringVar(&logfile, "log", "", "Process log file")
|
||||
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
|
||||
flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port")
|
||||
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
|
||||
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)") // TODO
|
||||
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
|
||||
flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site")
|
||||
flag.StringVar(&serverType, "type", "http", "Type of server to run")
|
||||
flag.BoolVar(&version, "version", false, "Show version")
|
||||
flag.BoolVar(&directives, "directives", false, "List supported directives")
|
||||
|
||||
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
|
||||
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse() // called here in main() to allow other packages to set flags in their inits
|
||||
// Run is Caddy's main() function.
|
||||
func Run() {
|
||||
flag.Parse()
|
||||
moveStorage() // TODO: This is temporary for the 0.9 release, or until most users upgrade to 0.9+
|
||||
|
||||
caddy.AppName = appName
|
||||
caddy.AppVersion = appVersion
|
||||
acme.UserAgent = appName + "/" + appVersion
|
||||
|
||||
// set up process log before anything bad happens
|
||||
// Set up process log before anything bad happens
|
||||
switch logfile {
|
||||
case "stdout":
|
||||
log.SetOutput(os.Stdout)
|
||||
|
@ -63,8 +72,9 @@ func main() {
|
|||
})
|
||||
}
|
||||
|
||||
// Check for one-time actions
|
||||
if revoke != "" {
|
||||
err := https.Revoke(revoke)
|
||||
err := caddytls.Revoke(revoke)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -78,10 +88,8 @@ func main() {
|
|||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
if directives {
|
||||
for _, d := range caddy.Directives() {
|
||||
fmt.Println(d)
|
||||
}
|
||||
if plugins {
|
||||
fmt.Println(caddy.DescribePlugins())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
|
@ -92,77 +100,124 @@ func main() {
|
|||
}
|
||||
|
||||
// Get Caddyfile input
|
||||
caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile)
|
||||
caddyfile, err := caddy.LoadCaddyfile(serverType)
|
||||
if err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
// Start your engines
|
||||
err = caddy.Start(caddyfile)
|
||||
instance, err := caddy.Start(caddyfile)
|
||||
if err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
// Twiddle your thumbs
|
||||
caddy.Wait()
|
||||
instance.Wait()
|
||||
}
|
||||
|
||||
// mustLogFatal just wraps log.Fatal() in a way that ensures the
|
||||
// mustLogFatal wraps log.Fatal() in a way that ensures the
|
||||
// output is always printed to stderr so the user can see it
|
||||
// if the user is still there, even if the process log was not
|
||||
// enabled. If this process is a restart, however, and the user
|
||||
// might not be there anymore, this just logs to the process log
|
||||
// and exits.
|
||||
// enabled. If this process is an upgrade, however, and the user
|
||||
// might not be there anymore, this just logs to the process
|
||||
// log and exits.
|
||||
func mustLogFatal(args ...interface{}) {
|
||||
if !caddy.IsRestart() {
|
||||
if !caddy.IsUpgrade() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}
|
||||
log.Fatal(args...)
|
||||
}
|
||||
|
||||
func loadCaddyfile() (caddy.Input, error) {
|
||||
// Try -conf flag
|
||||
if conf != "" {
|
||||
if conf == "stdin" {
|
||||
return caddy.CaddyfileFromPipe(os.Stdin)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
RealFile: true,
|
||||
}, nil
|
||||
// confLoader loads the Caddyfile using the -conf flag.
|
||||
func confLoader(serverType string) (caddy.Input, error) {
|
||||
if conf == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// command line args
|
||||
if flag.NArg() > 0 {
|
||||
confBody := caddy.Host + ":" + caddy.Port + "\n" + strings.Join(flag.Args(), "\n")
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: []byte(confBody),
|
||||
Filepath: "args",
|
||||
}, nil
|
||||
if conf == "stdin" {
|
||||
return caddy.CaddyfileFromPipe(os.Stdin)
|
||||
}
|
||||
|
||||
// Caddyfile in cwd
|
||||
contents, err := ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// defaultLoader loads the Caddyfile from the current working directory.
|
||||
func defaultLoader(serverType string) (caddy.Input, error) {
|
||||
contents, err := ioutil.ReadFile(caddy.DefaultConfigFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return caddy.DefaultInput(), nil
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: caddy.DefaultConfigFile,
|
||||
RealFile: true,
|
||||
Contents: contents,
|
||||
Filepath: caddy.DefaultConfigFile,
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// moveStorage moves the old certificate storage location by
|
||||
// renaming the "letsencrypt" folder to the hostname of the
|
||||
// CA URL. This is TEMPORARY until most users have upgraded to 0.9+.
|
||||
func moveStorage() {
|
||||
oldPath := filepath.Join(caddy.AssetsPath(), "letsencrypt")
|
||||
_, err := os.Stat(oldPath)
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
newPath, err := caddytls.StorageFor(caddytls.DefaultCAUrl)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to get new path for certificate storage: %v", err)
|
||||
}
|
||||
err = os.MkdirAll(string(newPath), 0700)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to make new certificate storage path: %v", err)
|
||||
}
|
||||
err = os.Rename(oldPath, string(newPath))
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to migrate certificate storage: %v", err)
|
||||
}
|
||||
// convert mixed case folder and file names to lowercase
|
||||
filepath.Walk(string(newPath), func(path string, info os.FileInfo, err error) error {
|
||||
// must be careful to only lowercase the base of the path, not the whole thing!!
|
||||
base := filepath.Base(path)
|
||||
if lowerBase := strings.ToLower(base); base != lowerBase {
|
||||
lowerPath := filepath.Join(filepath.Dir(path), lowerBase)
|
||||
err = os.Rename(path, lowerPath)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to lower-case: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// setVersion figures out the version information
|
||||
// based on variables set by -ldflags.
|
||||
func setVersion() {
|
||||
// A development build is one that's not at a tag or has uncommitted changes
|
||||
devBuild = gitTag == "" || gitShortStat != ""
|
||||
|
||||
// Only set the appVersion if -ldflags was used
|
||||
if gitNearestTag != "" || gitTag != "" {
|
||||
if devBuild && gitNearestTag != "" {
|
||||
appVersion = fmt.Sprintf("%s (+%s %s)",
|
||||
strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate)
|
||||
} else if gitTag != "" {
|
||||
appVersion = strings.TrimPrefix(gitTag, "v")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setCPU parses string cpu and sets GOMAXPROCS
|
||||
// according to its value. It accepts either
|
||||
// a number (e.g. 3) or a percent (e.g. 50%).
|
||||
|
@ -198,33 +253,17 @@ func setCPU(cpu string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// setVersion figures out the version information based on
|
||||
// variables set by -ldflags.
|
||||
func setVersion() {
|
||||
// A development build is one that's not at a tag or has uncommitted changes
|
||||
devBuild = gitTag == "" || gitShortStat != ""
|
||||
|
||||
// Only set the appVersion if -ldflags was used
|
||||
if gitNearestTag != "" || gitTag != "" {
|
||||
if devBuild && gitNearestTag != "" {
|
||||
appVersion = fmt.Sprintf("%s (+%s %s)",
|
||||
strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate)
|
||||
} else if gitTag != "" {
|
||||
appVersion = strings.TrimPrefix(gitTag, "v")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const appName = "Caddy"
|
||||
|
||||
// Flags that control program flow or startup
|
||||
var (
|
||||
serverType string
|
||||
conf string
|
||||
cpu string
|
||||
logfile string
|
||||
revoke string
|
||||
version bool
|
||||
directives bool
|
||||
plugins bool
|
||||
)
|
||||
|
||||
// Build information obtained with the help of -ldflags
|
348
caddy/config.go
348
caddy/config.go
|
@ -1,348 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultConfigFile is the name of the configuration file that is loaded
|
||||
// by default if no other file is specified.
|
||||
DefaultConfigFile = "Caddyfile"
|
||||
)
|
||||
|
||||
// loadConfigsUpToIncludingTLS loads the configs from input with name filename and returns them,
|
||||
// the parsed server blocks, the index of the last directive it processed, and an error (if any).
|
||||
func loadConfigsUpToIncludingTLS(filename string, input io.Reader) ([]server.Config, []parse.ServerBlock, int, error) {
|
||||
var configs []server.Config
|
||||
|
||||
// Each server block represents similar hosts/addresses, since they
|
||||
// were grouped together in the Caddyfile.
|
||||
serverBlocks, err := parse.ServerBlocks(filename, input, true)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
if len(serverBlocks) == 0 {
|
||||
newInput := DefaultInput()
|
||||
serverBlocks, err = parse.ServerBlocks(newInput.Path(), bytes.NewReader(newInput.Body()), true)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
var lastDirectiveIndex int // we set up directives in two parts; this stores where we left off
|
||||
|
||||
// Iterate each server block and make a config for each one,
|
||||
// executing the directives that were parsed in order up to the tls
|
||||
// directive; this is because we must activate Let's Encrypt.
|
||||
for i, sb := range serverBlocks {
|
||||
onces := makeOnces()
|
||||
storages := makeStorages()
|
||||
|
||||
for j, addr := range sb.Addresses {
|
||||
config := server.Config{
|
||||
Host: addr.Host,
|
||||
Port: addr.Port,
|
||||
Scheme: addr.Scheme,
|
||||
Root: Root,
|
||||
ConfigFile: filename,
|
||||
AppName: AppName,
|
||||
AppVersion: AppVersion,
|
||||
}
|
||||
|
||||
// It is crucial that directives are executed in the proper order.
|
||||
for k, dir := range directiveOrder {
|
||||
// Execute directive if it is in the server block
|
||||
if tokens, ok := sb.Tokens[dir.name]; ok {
|
||||
// Each setup function gets a controller, from which setup functions
|
||||
// get access to the config, tokens, and other state information useful
|
||||
// to set up its own host only.
|
||||
controller := &setup.Controller{
|
||||
Config: &config,
|
||||
Dispenser: parse.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
onces[dir.name].Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockHostIndex: j,
|
||||
ServerBlockHosts: sb.HostList(),
|
||||
ServerBlockStorage: storages[dir.name],
|
||||
}
|
||||
// execute setup function and append middleware handler, if any
|
||||
midware, err := dir.setup(controller)
|
||||
if err != nil {
|
||||
return nil, nil, lastDirectiveIndex, err
|
||||
}
|
||||
if midware != nil {
|
||||
config.Middleware = append(config.Middleware, midware)
|
||||
}
|
||||
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
|
||||
// Stop after TLS setup, since we need to activate Let's Encrypt before continuing;
|
||||
// it makes some changes to the configs that middlewares might want to know about.
|
||||
if dir.name == "tls" {
|
||||
lastDirectiveIndex = k
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
}
|
||||
}
|
||||
|
||||
return configs, serverBlocks, lastDirectiveIndex, nil
|
||||
}
|
||||
|
||||
// loadConfigs reads input (named filename) and parses it, returning the
|
||||
// server configurations in the order they appeared in the input. As part
|
||||
// of this, it activates Let's Encrypt for the configs that are produced.
|
||||
// Thus, the returned configs are already optimally configured for HTTPS.
|
||||
func loadConfigs(filename string, input io.Reader) ([]server.Config, error) {
|
||||
configs, serverBlocks, lastDirectiveIndex, err := loadConfigsUpToIncludingTLS(filename, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now we have all the configs, but they have only been set up to the
|
||||
// point of tls. We need to activate Let's Encrypt before setting up
|
||||
// the rest of the middlewares so they have correct information regarding
|
||||
// TLS configuration, if necessary. (this only appends, so our iterations
|
||||
// over server blocks below shouldn't be affected)
|
||||
if !IsRestart() && !Quiet {
|
||||
fmt.Print("Activating privacy features...")
|
||||
}
|
||||
configs, err = https.Activate(configs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !IsRestart() && !Quiet {
|
||||
fmt.Println(" done.")
|
||||
}
|
||||
|
||||
// Finish setting up the rest of the directives, now that TLS is
|
||||
// optimally configured. These loops are similar to above except
|
||||
// we don't iterate all the directives from the beginning and we
|
||||
// don't create new configs.
|
||||
configIndex := -1
|
||||
for i, sb := range serverBlocks {
|
||||
onces := makeOnces()
|
||||
storages := makeStorages()
|
||||
|
||||
for j := range sb.Addresses {
|
||||
configIndex++
|
||||
|
||||
for k := lastDirectiveIndex + 1; k < len(directiveOrder); k++ {
|
||||
dir := directiveOrder[k]
|
||||
|
||||
if tokens, ok := sb.Tokens[dir.name]; ok {
|
||||
controller := &setup.Controller{
|
||||
Config: &configs[configIndex],
|
||||
Dispenser: parse.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
onces[dir.name].Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockHostIndex: j,
|
||||
ServerBlockHosts: sb.HostList(),
|
||||
ServerBlockStorage: storages[dir.name],
|
||||
}
|
||||
midware, err := dir.setup(controller)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if midware != nil {
|
||||
configs[configIndex].Middleware = append(configs[configIndex].Middleware, midware)
|
||||
}
|
||||
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// makeOnces makes a map of directive name to sync.Once
|
||||
// instance. This is intended to be called once per server
|
||||
// block when setting up configs so that Setup functions
|
||||
// for each directive can perform a task just once per
|
||||
// server block, even if there are multiple hosts on the block.
|
||||
//
|
||||
// We need one Once per directive, otherwise the first
|
||||
// directive to use it would exclude other directives from
|
||||
// using it at all, which would be a bug.
|
||||
func makeOnces() map[string]*sync.Once {
|
||||
onces := make(map[string]*sync.Once)
|
||||
for _, dir := range directiveOrder {
|
||||
onces[dir.name] = new(sync.Once)
|
||||
}
|
||||
return onces
|
||||
}
|
||||
|
||||
// makeStorages makes a map of directive name to interface{}
|
||||
// so that directives' setup functions can persist state
|
||||
// between different hosts on the same server block during the
|
||||
// setup phase.
|
||||
func makeStorages() map[string]interface{} {
|
||||
storages := make(map[string]interface{})
|
||||
for _, dir := range directiveOrder {
|
||||
storages[dir.name] = nil
|
||||
}
|
||||
return storages
|
||||
}
|
||||
|
||||
// arrangeBindings groups configurations by their bind address. For example,
|
||||
// a server that should listen on localhost and another on 127.0.0.1 will
|
||||
// be grouped into the same address: 127.0.0.1. It will return an error
|
||||
// if an address is malformed or a TLS listener is configured on the
|
||||
// same address as a plaintext HTTP listener. The return value is a map of
|
||||
// bind address to list of configs that would become VirtualHosts on that
|
||||
// server. Use the keys of the returned map to create listeners, and use
|
||||
// the associated values to set up the virtualhosts.
|
||||
func arrangeBindings(allConfigs []server.Config) (bindingGroup, error) {
|
||||
var groupings bindingGroup
|
||||
|
||||
// Group configs by bind address
|
||||
for _, conf := range allConfigs {
|
||||
// use default port if none is specified
|
||||
if conf.Port == "" {
|
||||
conf.Port = Port
|
||||
}
|
||||
|
||||
bindAddr, warnErr, fatalErr := resolveAddr(conf)
|
||||
if fatalErr != nil {
|
||||
return groupings, fatalErr
|
||||
}
|
||||
if warnErr != nil {
|
||||
log.Printf("[WARNING] Resolving bind address for %s: %v", conf.Address(), warnErr)
|
||||
}
|
||||
|
||||
// Make sure to compare the string representation of the address,
|
||||
// not the pointer, since a new *TCPAddr is created each time.
|
||||
var existing bool
|
||||
for i := 0; i < len(groupings); i++ {
|
||||
if groupings[i].BindAddr.String() == bindAddr.String() {
|
||||
groupings[i].Configs = append(groupings[i].Configs, conf)
|
||||
existing = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !existing {
|
||||
groupings = append(groupings, bindingMapping{
|
||||
BindAddr: bindAddr,
|
||||
Configs: []server.Config{conf},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow HTTP and HTTPS to be served on the same address
|
||||
for _, group := range groupings {
|
||||
isTLS := group.Configs[0].TLS.Enabled
|
||||
for _, config := range group.Configs {
|
||||
if config.TLS.Enabled != isTLS {
|
||||
thisConfigProto, otherConfigProto := "HTTP", "HTTP"
|
||||
if config.TLS.Enabled {
|
||||
thisConfigProto = "HTTPS"
|
||||
}
|
||||
if group.Configs[0].TLS.Enabled {
|
||||
otherConfigProto = "HTTPS"
|
||||
}
|
||||
return groupings, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address",
|
||||
group.Configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupings, nil
|
||||
}
|
||||
|
||||
// resolveAddr determines the address (host and port) that a config will
|
||||
// bind to. The returned address, resolvAddr, should be used to bind the
|
||||
// listener or group the config with other configs using the same address.
|
||||
// The first error, if not nil, is just a warning and should be reported
|
||||
// but execution may continue. The second error, if not nil, is a real
|
||||
// problem and the server should not be started.
|
||||
//
|
||||
// This function does not handle edge cases like port "http" or "https" if
|
||||
// they are not known to the system. It does, however, serve on the wildcard
|
||||
// host if resolving the address of the specific hostname fails.
|
||||
func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr, fatalErr error) {
|
||||
resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.BindHost, conf.Port))
|
||||
if warnErr != nil {
|
||||
// the hostname probably couldn't be resolved, just bind to wildcard then
|
||||
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("", conf.Port))
|
||||
if fatalErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// validDirective returns true if d is a valid
|
||||
// directive; false otherwise.
|
||||
func validDirective(d string) bool {
|
||||
for _, dir := range directiveOrder {
|
||||
if dir.name == d {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DefaultInput returns the default Caddyfile input
|
||||
// to use when it is otherwise empty or missing.
|
||||
// It uses the default host and port (depends on
|
||||
// host, e.g. localhost is 2015, otherwise 443) and
|
||||
// root.
|
||||
func DefaultInput() CaddyfileInput {
|
||||
port := Port
|
||||
if https.HostQualifies(Host) && port == DefaultPort {
|
||||
port = "443"
|
||||
}
|
||||
return CaddyfileInput{
|
||||
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, port, Root)),
|
||||
}
|
||||
}
|
||||
|
||||
// These defaults are configurable through the command line
|
||||
var (
|
||||
// Root is the site root
|
||||
Root = DefaultRoot
|
||||
|
||||
// Host is the site host
|
||||
Host = DefaultHost
|
||||
|
||||
// Port is the site port
|
||||
Port = DefaultPort
|
||||
)
|
||||
|
||||
// bindingMapping maps a network address to configurations
|
||||
// that will bind to it. The order of the configs is important.
|
||||
type bindingMapping struct {
|
||||
BindAddr *net.TCPAddr
|
||||
Configs []server.Config
|
||||
}
|
||||
|
||||
// bindingGroup maps network addresses to their configurations.
|
||||
// Preserving the order of the groupings is important
|
||||
// (related to graceful shutdown and restart)
|
||||
// so this is a slice, not a literal map.
|
||||
type bindingGroup []bindingMapping
|
|
@ -1,159 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
func TestDefaultInput(t *testing.T) {
|
||||
if actual, expected := string(DefaultInput().Body()), ":2015\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
// next few tests simulate user providing -host and/or -port flags
|
||||
|
||||
Host = "not-localhost.com"
|
||||
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:443\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = "[::1]"
|
||||
if actual, expected := string(DefaultInput().Body()), "[::1]:2015\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = "127.0.1.1"
|
||||
if actual, expected := string(DefaultInput().Body()), "127.0.1.1:2015\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = "not-localhost.com"
|
||||
Port = "1234"
|
||||
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:1234\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = DefaultHost
|
||||
Port = "1234"
|
||||
if actual, expected := string(DefaultInput().Body()), ":1234\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAddr(t *testing.T) {
|
||||
// NOTE: If tests fail due to comparing to string "127.0.0.1",
|
||||
// it's possible that system env resolves with IPv6, or ::1.
|
||||
// If that happens, maybe we should use actualAddr.IP.IsLoopback()
|
||||
// for the assertion, rather than a direct string comparison.
|
||||
|
||||
// NOTE: Tests with {Host: "", Port: ""} and {Host: "localhost", Port: ""}
|
||||
// will not behave the same cross-platform, so they have been omitted.
|
||||
|
||||
for i, test := range []struct {
|
||||
config server.Config
|
||||
shouldWarnErr bool
|
||||
shouldFatalErr bool
|
||||
expectedIP string
|
||||
expectedPort int
|
||||
}{
|
||||
{server.Config{Host: "127.0.0.1", Port: "1234"}, false, false, "<nil>", 1234},
|
||||
{server.Config{Host: "localhost", Port: "80"}, false, false, "<nil>", 80},
|
||||
{server.Config{BindHost: "localhost", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "127.0.0.1", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "should-not-resolve", Port: "1234"}, true, false, "<nil>", 1234},
|
||||
{server.Config{BindHost: "localhost", Port: "http"}, false, false, "127.0.0.1", 80},
|
||||
{server.Config{BindHost: "localhost", Port: "https"}, false, false, "127.0.0.1", 443},
|
||||
{server.Config{BindHost: "", Port: "1234"}, false, false, "<nil>", 1234},
|
||||
{server.Config{BindHost: "localhost", Port: "abcd"}, false, true, "", 0},
|
||||
{server.Config{BindHost: "127.0.0.1", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "localhost", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "should-not-resolve", Host: "localhost", Port: "1234"}, true, false, "<nil>", 1234},
|
||||
} {
|
||||
actualAddr, warnErr, fatalErr := resolveAddr(test.config)
|
||||
|
||||
if test.shouldFatalErr && fatalErr == nil {
|
||||
t.Errorf("Test %d: Expected error, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldFatalErr && fatalErr != nil {
|
||||
t.Errorf("Test %d: Expected no error, but there was one: %v", i, fatalErr)
|
||||
}
|
||||
if fatalErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if test.shouldWarnErr && warnErr == nil {
|
||||
t.Errorf("Test %d: Expected warning, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldWarnErr && warnErr != nil {
|
||||
t.Errorf("Test %d: Expected no warning, but there was one: %v", i, warnErr)
|
||||
}
|
||||
|
||||
if actual, expected := actualAddr.IP.String(), test.expectedIP; actual != expected {
|
||||
t.Errorf("Test %d: IP was %s but expected %s", i, actual, expected)
|
||||
}
|
||||
if actual, expected := actualAddr.Port, test.expectedPort; actual != expected {
|
||||
t.Errorf("Test %d: Port was %d but expected %d", i, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeOnces(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
onces := makeOnces()
|
||||
if len(onces) != len(directives) {
|
||||
t.Errorf("onces had len %d , expected %d", len(onces), len(directives))
|
||||
}
|
||||
expected := map[string]*sync.Once{
|
||||
"dummy": new(sync.Once),
|
||||
"dummy2": new(sync.Once),
|
||||
}
|
||||
if !reflect.DeepEqual(onces, expected) {
|
||||
t.Errorf("onces was %v, expected %v", onces, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeStorages(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
storages := makeStorages()
|
||||
if len(storages) != len(directives) {
|
||||
t.Errorf("storages had len %d , expected %d", len(storages), len(directives))
|
||||
}
|
||||
expected := map[string]interface{}{
|
||||
"dummy": nil,
|
||||
"dummy2": nil,
|
||||
}
|
||||
if !reflect.DeepEqual(storages, expected) {
|
||||
t.Errorf("storages was %v, expected %v", storages, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidDirective(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
for i, test := range []struct {
|
||||
directive string
|
||||
valid bool
|
||||
}{
|
||||
{"dummy", true},
|
||||
{"dummy2", true},
|
||||
{"dummy3", false},
|
||||
} {
|
||||
if actual, expected := validDirective(test.directive), test.valid; actual != expected {
|
||||
t.Errorf("Test %d: valid was %t, expected %t", i, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// The parse package must know which directives
|
||||
// are valid, but it must not import the setup
|
||||
// or config package. To solve this problem, we
|
||||
// fill up this map in our init function here.
|
||||
// The parse package does not need to know the
|
||||
// ordering of the directives.
|
||||
for _, dir := range directiveOrder {
|
||||
parse.ValidDirectives[dir.name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Directives are registered in the order they should be
|
||||
// executed. Middleware (directives that inject a handler)
|
||||
// are executed in the order A-B-C-*-C-B-A, assuming
|
||||
// they all call the Next handler in the chain.
|
||||
//
|
||||
// Ordering is VERY important. Every middleware will
|
||||
// feel the effects of all other middleware below
|
||||
// (after) them during a request, but they must not
|
||||
// care what middleware above them are doing.
|
||||
//
|
||||
// For example, log needs to know the status code and
|
||||
// exactly how many bytes were written to the client,
|
||||
// which every other middleware can affect, so it gets
|
||||
// registered first. The errors middleware does not
|
||||
// care if gzip or log modifies its response, so it
|
||||
// gets registered below them. Gzip, on the other hand,
|
||||
// DOES care what errors does to the response since it
|
||||
// must compress every output to the client, even error
|
||||
// pages, so it must be registered before the errors
|
||||
// middleware and any others that would write to the
|
||||
// response.
|
||||
var directiveOrder = []directive{
|
||||
// Essential directives that initialize vital configuration settings
|
||||
{"root", setup.Root},
|
||||
{"bind", setup.BindHost},
|
||||
{"tls", https.Setup},
|
||||
|
||||
// Other directives that don't create HTTP handlers
|
||||
{"startup", setup.Startup},
|
||||
{"shutdown", setup.Shutdown},
|
||||
|
||||
// Directives that inject handlers (middleware)
|
||||
{"log", setup.Log},
|
||||
{"gzip", setup.Gzip},
|
||||
{"errors", setup.Errors},
|
||||
{"header", setup.Headers},
|
||||
{"rewrite", setup.Rewrite},
|
||||
{"redir", setup.Redir},
|
||||
{"ext", setup.Ext},
|
||||
{"mime", setup.Mime},
|
||||
{"basicauth", setup.BasicAuth},
|
||||
{"internal", setup.Internal},
|
||||
{"pprof", setup.PProf},
|
||||
{"expvar", setup.ExpVar},
|
||||
{"proxy", setup.Proxy},
|
||||
{"fastcgi", setup.FastCGI},
|
||||
{"websocket", setup.WebSocket},
|
||||
{"markdown", setup.Markdown},
|
||||
{"templates", setup.Templates},
|
||||
{"browse", setup.Browse},
|
||||
}
|
||||
|
||||
// Directives returns the list of directives in order of priority.
|
||||
func Directives() []string {
|
||||
directives := make([]string, len(directiveOrder))
|
||||
for i, d := range directiveOrder {
|
||||
directives[i] = d.name
|
||||
}
|
||||
return directives
|
||||
}
|
||||
|
||||
// RegisterDirective adds the given directive to caddy's list of directives.
|
||||
// Pass the name of a directive you want it to be placed after,
|
||||
// otherwise it will be placed at the bottom of the stack.
|
||||
func RegisterDirective(name string, setup SetupFunc, after string) {
|
||||
dir := directive{name: name, setup: setup}
|
||||
idx := len(directiveOrder)
|
||||
for i := range directiveOrder {
|
||||
if directiveOrder[i].name == after {
|
||||
idx = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
newDirectives := append(directiveOrder[:idx], append([]directive{dir}, directiveOrder[idx:]...)...)
|
||||
directiveOrder = newDirectives
|
||||
parse.ValidDirectives[name] = struct{}{}
|
||||
}
|
||||
|
||||
// directive ties together a directive name with its setup function.
|
||||
type directive struct {
|
||||
name string
|
||||
setup SetupFunc
|
||||
}
|
||||
|
||||
// SetupFunc takes a controller and may optionally return a middleware.
|
||||
// If the resulting middleware is not nil, it will be chained into
|
||||
// the HTTP handlers in the order specified in this package.
|
||||
type SetupFunc func(c *setup.Controller) (middleware.Middleware, error)
|
|
@ -1,31 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
RegisterDirective("foo", nil, "dummy")
|
||||
if len(directiveOrder) != 3 {
|
||||
t.Fatal("Should have 3 directives now")
|
||||
}
|
||||
getNames := func() (s []string) {
|
||||
for _, d := range directiveOrder {
|
||||
s = append(s, d.name)
|
||||
}
|
||||
return s
|
||||
}
|
||||
if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2"}) {
|
||||
t.Fatalf("directive order doesn't match: %s", getNames())
|
||||
}
|
||||
RegisterDirective("bar", nil, "ASDASD")
|
||||
if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2", "bar"}) {
|
||||
t.Fatalf("directive order doesn't match: %s", getNames())
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isLocalhost returns true if host looks explicitly like a localhost address.
|
||||
func isLocalhost(host string) bool {
|
||||
return host == "localhost" || host == "::1" || strings.HasPrefix(host, "127.")
|
||||
}
|
||||
|
||||
// checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum.
|
||||
func checkFdlimit() {
|
||||
const min = 4096
|
||||
|
||||
// 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 {
|
||||
// Note that an error here need not be reported
|
||||
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 sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsRestart returns whether this process is, according
|
||||
// to env variables, a fork as part of a graceful restart.
|
||||
func IsRestart() bool {
|
||||
return startedBefore
|
||||
}
|
||||
|
||||
// writePidFile writes the process ID to the file at PidFile, if specified.
|
||||
func writePidFile() error {
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
return ioutil.WriteFile(PidFile, pid, 0644)
|
||||
}
|
||||
|
||||
// CaddyfileInput represents a Caddyfile as input
|
||||
// and is simply a convenient way to implement
|
||||
// the Input interface.
|
||||
type CaddyfileInput struct {
|
||||
Filepath string
|
||||
Contents []byte
|
||||
RealFile bool
|
||||
}
|
||||
|
||||
// Body returns c.Contents.
|
||||
func (c CaddyfileInput) Body() []byte { return c.Contents }
|
||||
|
||||
// Path returns c.Filepath.
|
||||
func (c CaddyfileInput) Path() string { return c.Filepath }
|
||||
|
||||
// IsFile returns true if the original input was a real file on the file system.
|
||||
func (c CaddyfileInput) IsFile() bool { return c.RealFile }
|
|
@ -1,57 +0,0 @@
|
|||
package https
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// loadPrivateKey loads a PEM-encoded ECC/RSA private key from file.
|
||||
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
|
||||
keyBytes, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyBlock, _ := pem.Decode(keyBytes)
|
||||
|
||||
switch keyBlock.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown private key type")
|
||||
}
|
||||
|
||||
// savePrivateKey saves a PEM-encoded ECC/RSA private key to file.
|
||||
func savePrivateKey(key crypto.PrivateKey, file string) error {
|
||||
var pemType string
|
||||
var keyBytes []byte
|
||||
switch key := key.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
var err error
|
||||
pemType = "EC"
|
||||
keyBytes, err = x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case *rsa.PrivateKey:
|
||||
pemType = "RSA"
|
||||
keyBytes = x509.MarshalPKCS1PrivateKey(key)
|
||||
}
|
||||
|
||||
pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes}
|
||||
keyOut, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyOut.Chmod(0600)
|
||||
defer keyOut.Close()
|
||||
return pem.Encode(keyOut, &pemKey)
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package https
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const challengeBasePath = "/.well-known/acme-challenge"
|
||||
|
||||
// RequestCallback proxies challenge requests to ACME client if the
|
||||
// request path starts with challengeBasePath. It returns true if it
|
||||
// handled the request and no more needs to be done; it returns false
|
||||
// if this call was a no-op and the request still needs handling.
|
||||
func RequestCallback(w http.ResponseWriter, r *http.Request) bool {
|
||||
if strings.HasPrefix(r.URL.Path, challengeBasePath) {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
upstream, err := url.Parse(scheme + "://localhost:" + AlternatePort)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("[ERROR] ACME proxy handler: %v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(upstream)
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs
|
||||
}
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -1,411 +0,0 @@
|
|||
// Package https facilitates the management of TLS assets and integrates
|
||||
// Let's Encrypt functionality into Caddy with first-class support for
|
||||
// creating and renewing certificates automatically. It is designed to
|
||||
// configure sites for HTTPS by default.
|
||||
package https
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// Activate sets up TLS for each server config in configs
|
||||
// as needed; this consists of acquiring and maintaining
|
||||
// certificates and keys for qualifying configs and enabling
|
||||
// OCSP stapling for all TLS-enabled configs.
|
||||
//
|
||||
// This function may prompt the user to provide an email
|
||||
// address if none is available through other means. It
|
||||
// prefers the email address specified in the config, but
|
||||
// if that is not available it will check the command line
|
||||
// argument. If absent, it will use the most recent email
|
||||
// address from last time. If there isn't one, the user
|
||||
// will be prompted and shown SA link.
|
||||
//
|
||||
// Also note that calling this function activates asset
|
||||
// management automatically, which keeps certificates
|
||||
// renewed and OCSP stapling updated.
|
||||
//
|
||||
// Activate returns the updated list of configs, since
|
||||
// some may have been appended, for example, to redirect
|
||||
// plaintext HTTP requests to their HTTPS counterpart.
|
||||
// This function only appends; it does not splice.
|
||||
func Activate(configs []server.Config) ([]server.Config, error) {
|
||||
// just in case previous caller forgot...
|
||||
Deactivate()
|
||||
|
||||
// pre-screen each config and earmark the ones that qualify for managed TLS
|
||||
MarkQualified(configs)
|
||||
|
||||
// place certificates and keys on disk
|
||||
err := ObtainCerts(configs, true, false)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// update TLS configurations
|
||||
err = EnableTLS(configs, true)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// set up redirects
|
||||
configs = MakePlaintextRedirects(configs)
|
||||
|
||||
// renew all relevant certificates that need renewal. this is important
|
||||
// to do right away for a couple reasons, mainly because each restart,
|
||||
// the renewal ticker is reset, so if restarts happen more often than
|
||||
// the ticker interval, renewals would never happen. but doing
|
||||
// it right away at start guarantees that renewals aren't missed.
|
||||
err = renewManagedCertificates(true)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// keep certificates renewed and OCSP stapling updated
|
||||
go maintainAssets(stopChan)
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// Deactivate cleans up long-term, in-memory resources
|
||||
// allocated by calling Activate(). Essentially, it stops
|
||||
// the asset maintainer from running, meaning that certificates
|
||||
// will not be renewed, OCSP staples will not be updated, etc.
|
||||
func Deactivate() (err error) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
err = errors.New("already deactivated")
|
||||
}
|
||||
}()
|
||||
close(stopChan)
|
||||
stopChan = make(chan struct{})
|
||||
return
|
||||
}
|
||||
|
||||
// MarkQualified scans each config and, if it qualifies for managed
|
||||
// TLS, it sets the Managed field of the TLSConfig to true.
|
||||
func MarkQualified(configs []server.Config) {
|
||||
for i := 0; i < len(configs); i++ {
|
||||
if ConfigQualifies(configs[i]) {
|
||||
configs[i].TLS.Managed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ObtainCerts obtains certificates for all these configs as long as a
|
||||
// certificate does not already exist on disk. It does not modify the
|
||||
// configs at all; it only obtains and stores certificates and keys to
|
||||
// the disk. If allowPrompts is true, the user may be shown a prompt.
|
||||
// If proxyACME is true, the ACME challenges will be proxied to our alt port.
|
||||
func ObtainCerts(configs []server.Config, allowPrompts, proxyACME bool) error {
|
||||
// We group configs by email so we don't make the same clients over and
|
||||
// over. This has the potential to prompt the user for an email, but we
|
||||
// prevent that by assuming that if we already have a listener that can
|
||||
// proxy ACME challenge requests, then the server is already running and
|
||||
// the operator is no longer present.
|
||||
groupedConfigs := groupConfigsByEmail(configs, allowPrompts)
|
||||
|
||||
for email, group := range groupedConfigs {
|
||||
// Wait as long as we can before creating the client, because it
|
||||
// may not be needed, for example, if we already have what we
|
||||
// need on disk. Creating a client involves the network and
|
||||
// potentially prompting the user, etc., so only do if necessary.
|
||||
var client *ACMEClient
|
||||
|
||||
for _, cfg := range group {
|
||||
if !HostQualifies(cfg.Host) || existingCertAndKey(cfg.Host) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Now we definitely do need a client
|
||||
if client == nil {
|
||||
var err error
|
||||
client, err = NewACMEClient(email, allowPrompts)
|
||||
if err != nil {
|
||||
return errors.New("error creating client: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// c.Configure assumes that allowPrompts == !proxyACME,
|
||||
// but that's not always true. For example, a restart where
|
||||
// the user isn't present and we're not listening on port 80.
|
||||
// TODO: This could probably be refactored better.
|
||||
if proxyACME {
|
||||
client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
|
||||
client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
|
||||
} else {
|
||||
client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, ""))
|
||||
client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, ""))
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.DNS01})
|
||||
}
|
||||
|
||||
err := client.Obtain([]string{cfg.Host})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// groupConfigsByEmail groups configs by the email address to be used by an
|
||||
// ACME client. It only groups configs that have TLS enabled and that are
|
||||
// marked as Managed. If userPresent is true, the operator MAY be prompted
|
||||
// for an email address.
|
||||
func groupConfigsByEmail(configs []server.Config, userPresent bool) map[string][]server.Config {
|
||||
initMap := make(map[string][]server.Config)
|
||||
for _, cfg := range configs {
|
||||
if !cfg.TLS.Managed {
|
||||
continue
|
||||
}
|
||||
leEmail := getEmail(cfg, userPresent)
|
||||
initMap[leEmail] = append(initMap[leEmail], cfg)
|
||||
}
|
||||
return initMap
|
||||
}
|
||||
|
||||
// EnableTLS configures each config to use TLS according to default settings.
|
||||
// It will only change configs that are marked as managed, and assumes that
|
||||
// certificates and keys are already on disk. If loadCertificates is true,
|
||||
// the certificates will be loaded from disk into the cache for this process
|
||||
// to use. If false, TLS will still be enabled and configured with default
|
||||
// settings, but no certificates will be parsed loaded into the cache, and
|
||||
// the returned error value will always be nil.
|
||||
func EnableTLS(configs []server.Config, loadCertificates bool) error {
|
||||
for i := 0; i < len(configs); i++ {
|
||||
if !configs[i].TLS.Managed {
|
||||
continue
|
||||
}
|
||||
configs[i].TLS.Enabled = true
|
||||
if loadCertificates && HostQualifies(configs[i].Host) {
|
||||
_, err := cacheManagedCertificate(configs[i].Host, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
setDefaultTLSParams(&configs[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hostHasOtherPort returns true if there is another config in the list with the same
|
||||
// hostname that has port otherPort, or false otherwise. All the configs are checked
|
||||
// against the hostname of allConfigs[thisConfigIdx].
|
||||
func hostHasOtherPort(allConfigs []server.Config, thisConfigIdx int, otherPort string) bool {
|
||||
for i, otherCfg := range allConfigs {
|
||||
if i == thisConfigIdx {
|
||||
continue // has to be a config OTHER than the one we're comparing against
|
||||
}
|
||||
if otherCfg.Host == allConfigs[thisConfigIdx].Host && otherCfg.Port == otherPort {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MakePlaintextRedirects sets up redirects from port 80 to the relevant HTTPS
|
||||
// hosts. You must pass in all configs, not just configs that qualify, since
|
||||
// we must know whether the same host already exists on port 80, and those would
|
||||
// not be in a list of configs that qualify for automatic HTTPS. This function will
|
||||
// only set up redirects for configs that qualify. It returns the updated list of
|
||||
// all configs.
|
||||
func MakePlaintextRedirects(allConfigs []server.Config) []server.Config {
|
||||
for i, cfg := range allConfigs {
|
||||
if cfg.TLS.Managed &&
|
||||
!hostHasOtherPort(allConfigs, i, "80") &&
|
||||
(cfg.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) {
|
||||
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
|
||||
}
|
||||
}
|
||||
return allConfigs
|
||||
}
|
||||
|
||||
// ConfigQualifies returns true if cfg qualifies for
|
||||
// fully managed TLS (but not on-demand TLS, which is
|
||||
// not considered here). It does NOT check to see if a
|
||||
// cert and key already exist for the config. If the
|
||||
// config does qualify, you should set cfg.TLS.Managed
|
||||
// to true and check that instead, because the process of
|
||||
// setting up the config may make it look like it
|
||||
// doesn't qualify even though it originally did.
|
||||
func ConfigQualifies(cfg server.Config) bool {
|
||||
return (!cfg.TLS.Manual || cfg.TLS.OnDemand) && // user might provide own cert and key
|
||||
|
||||
// user can force-disable automatic HTTPS for this host
|
||||
cfg.Scheme != "http" &&
|
||||
cfg.Port != "80" &&
|
||||
cfg.TLS.LetsEncryptEmail != "off" &&
|
||||
|
||||
// we get can't certs for some kinds of hostnames, but
|
||||
// on-demand TLS allows empty hostnames at startup
|
||||
(HostQualifies(cfg.Host) || cfg.TLS.OnDemand)
|
||||
}
|
||||
|
||||
// HostQualifies returns true if the hostname alone
|
||||
// appears eligible for automatic HTTPS. For example,
|
||||
// localhost, empty hostname, and IP addresses are
|
||||
// not eligible because we cannot obtain certificates
|
||||
// for those names.
|
||||
func HostQualifies(hostname string) bool {
|
||||
return hostname != "localhost" && // localhost is ineligible
|
||||
|
||||
// hostname must not be empty
|
||||
strings.TrimSpace(hostname) != "" &&
|
||||
|
||||
// cannot be an IP address, see
|
||||
// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
|
||||
// (also trim [] from either end, since that special case can sneak through
|
||||
// for IPv6 addresses using the -host flag and with empty/no Caddyfile)
|
||||
net.ParseIP(strings.Trim(hostname, "[]")) == nil
|
||||
}
|
||||
|
||||
// existingCertAndKey returns true if the host has a certificate
|
||||
// and private key in storage already, false otherwise.
|
||||
func existingCertAndKey(host string) bool {
|
||||
_, err := os.Stat(storage.SiteCertFile(host))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = os.Stat(storage.SiteKeyFile(host))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// saveCertResource saves the certificate resource to disk. This
|
||||
// includes the certificate file itself, the private key, and the
|
||||
// metadata file.
|
||||
func saveCertResource(cert acme.CertificateResource) error {
|
||||
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save cert
|
||||
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save private key
|
||||
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save cert metadata
|
||||
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// redirPlaintextHost returns a new plaintext HTTP configuration for
|
||||
// a virtualHost that simply redirects to cfg, which is assumed to
|
||||
// be the HTTPS configuration. The returned configuration is set
|
||||
// to listen on port 80.
|
||||
func redirPlaintextHost(cfg server.Config) server.Config {
|
||||
toURL := "https://{host}" // serve any host, since cfg.Host could be empty
|
||||
if cfg.Port != "443" && cfg.Port != "80" {
|
||||
toURL += ":" + cfg.Port
|
||||
}
|
||||
|
||||
redirMidware := func(next middleware.Handler) middleware.Handler {
|
||||
return redirect.Redirect{Next: next, Rules: []redirect.Rule{
|
||||
{
|
||||
FromScheme: "http",
|
||||
FromPath: "/",
|
||||
To: toURL + "{uri}",
|
||||
Code: http.StatusMovedPermanently,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
return server.Config{
|
||||
Host: cfg.Host,
|
||||
BindHost: cfg.BindHost,
|
||||
Port: "80",
|
||||
Middleware: []middleware.Middleware{redirMidware},
|
||||
}
|
||||
}
|
||||
|
||||
// Revoke revokes the certificate for host via ACME protocol.
|
||||
func Revoke(host string) error {
|
||||
if !existingCertAndKey(host) {
|
||||
return errors.New("no certificate and key for " + host)
|
||||
}
|
||||
|
||||
email := getEmail(server.Config{Host: host}, true)
|
||||
if email == "" {
|
||||
return errors.New("email is required to revoke")
|
||||
}
|
||||
|
||||
client, err := NewACMEClient(email, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certFile := storage.SiteCertFile(host)
|
||||
certBytes, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.RevokeCertificate(certBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Remove(certFile)
|
||||
if err != nil {
|
||||
return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultEmail represents the Let's Encrypt account email to use if none provided
|
||||
DefaultEmail string
|
||||
|
||||
// Agreed indicates whether user has agreed to the Let's Encrypt SA
|
||||
Agreed bool
|
||||
|
||||
// CAUrl represents the base URL to the CA's ACME endpoint
|
||||
CAUrl string
|
||||
)
|
||||
|
||||
// AlternatePort is the port on which the acme client will open a
|
||||
// listener and solve the CA's challenges. If this alternate port
|
||||
// is used instead of the default port (80 or 443), then the
|
||||
// default port for the challenge must be forwarded to this one.
|
||||
const AlternatePort = "5033"
|
||||
|
||||
// KeyType is the type to use for new keys.
|
||||
// This shouldn't need to change except for in tests;
|
||||
// the size can be drastically reduced for speed.
|
||||
var KeyType = acme.RSA2048
|
||||
|
||||
// stopChan is used to signal the maintenance goroutine
|
||||
// to terminate.
|
||||
var stopChan chan struct{}
|
|
@ -1,332 +0,0 @@
|
|||
package https
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
func TestHostQualifies(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
host string
|
||||
expect bool
|
||||
}{
|
||||
{"localhost", false},
|
||||
{"127.0.0.1", false},
|
||||
{"127.0.1.5", false},
|
||||
{"::1", false},
|
||||
{"[::1]", false},
|
||||
{"[::]", false},
|
||||
{"::", false},
|
||||
{"", false},
|
||||
{" ", false},
|
||||
{"0.0.0.0", false},
|
||||
{"192.168.1.3", false},
|
||||
{"10.0.2.1", false},
|
||||
{"169.112.53.4", false},
|
||||
{"foobar.com", true},
|
||||
{"sub.foobar.com", true},
|
||||
} {
|
||||
if HostQualifies(test.host) && !test.expect {
|
||||
t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
|
||||
}
|
||||
if !HostQualifies(test.host) && test.expect {
|
||||
t.Errorf("Test %d: Expected '%s' to qualify, but it did NOT", i, test.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigQualifies(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
cfg server.Config
|
||||
expect bool
|
||||
}{
|
||||
{server.Config{Host: ""}, false},
|
||||
{server.Config{Host: "localhost"}, false},
|
||||
{server.Config{Host: "123.44.3.21"}, false},
|
||||
{server.Config{Host: "example.com"}, true},
|
||||
{server.Config{Host: "example.com", TLS: server.TLSConfig{Manual: true}}, false},
|
||||
{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}}, false},
|
||||
{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}}, true},
|
||||
{server.Config{Host: "example.com", Scheme: "http"}, false},
|
||||
{server.Config{Host: "example.com", Port: "80"}, false},
|
||||
{server.Config{Host: "example.com", Port: "1234"}, true},
|
||||
{server.Config{Host: "example.com", Scheme: "https"}, true},
|
||||
{server.Config{Host: "example.com", Port: "80", Scheme: "https"}, false},
|
||||
} {
|
||||
if test.expect && !ConfigQualifies(test.cfg) {
|
||||
t.Errorf("Test %d: Expected config to qualify, but it did NOT: %#v", i, test.cfg)
|
||||
}
|
||||
if !test.expect && ConfigQualifies(test.cfg) {
|
||||
t.Errorf("Test %d: Expected config to NOT qualify, but it did: %#v", i, test.cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
cfg := redirPlaintextHost(server.Config{
|
||||
Host: "example.com",
|
||||
BindHost: "93.184.216.34",
|
||||
Port: "1234",
|
||||
})
|
||||
|
||||
// Check host and port
|
||||
if actual, expected := cfg.Host, "example.com"; actual != expected {
|
||||
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.BindHost, "93.184.216.34"; actual != expected {
|
||||
t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.Port, "80"; actual != expected {
|
||||
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Make sure redirect handler is set up properly
|
||||
if cfg.Middleware == nil || len(cfg.Middleware) != 1 {
|
||||
t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.Middleware)
|
||||
}
|
||||
|
||||
handler, ok := cfg.Middleware[0](nil).(redirect.Redirect)
|
||||
if !ok {
|
||||
t.Fatalf("Expected a redirect.Redirect middleware, but got: %#v", handler)
|
||||
}
|
||||
if len(handler.Rules) != 1 {
|
||||
t.Fatalf("Expected one redirect rule, got: %#v", handler.Rules)
|
||||
}
|
||||
|
||||
// Check redirect rule for correctness
|
||||
if actual, expected := handler.Rules[0].FromScheme, "http"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be from scheme '%s' but is actually from '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].FromPath, "/"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be for path '%s' but is actually for '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].To, "https://{host}:1234{uri}"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].Code, http.StatusMovedPermanently; actual != expected {
|
||||
t.Errorf("Expected redirect rule to have code %d but was %d", expected, actual)
|
||||
}
|
||||
|
||||
// browsers can infer a default port from scheme, so make sure the port
|
||||
// doesn't get added in explicitly for default ports like 443 for https.
|
||||
cfg = redirPlaintextHost(server.Config{Host: "example.com", Port: "443"})
|
||||
handler, _ = cfg.Middleware[0](nil).(redirect.Redirect)
|
||||
if actual, expected := handler.Rules[0].To, "https://{host}{uri}"; actual != expected {
|
||||
t.Errorf("(Default Port) Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveCertResource(t *testing.T) {
|
||||
storage = Storage("./le_test_save")
|
||||
defer func() {
|
||||
err := os.RemoveAll(string(storage))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
|
||||
}
|
||||
}()
|
||||
|
||||
domain := "example.com"
|
||||
certContents := "certificate"
|
||||
keyContents := "private key"
|
||||
metaContents := `{
|
||||
"domain": "example.com",
|
||||
"certUrl": "https://example.com/cert",
|
||||
"certStableUrl": "https://example.com/cert/stable"
|
||||
}`
|
||||
|
||||
cert := acme.CertificateResource{
|
||||
Domain: domain,
|
||||
CertURL: "https://example.com/cert",
|
||||
CertStableURL: "https://example.com/cert/stable",
|
||||
PrivateKey: []byte(keyContents),
|
||||
Certificate: []byte(certContents),
|
||||
}
|
||||
|
||||
err := saveCertResource(cert)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
certFile, err := ioutil.ReadFile(storage.SiteCertFile(domain))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error reading certificate file, got: %v", err)
|
||||
}
|
||||
if string(certFile) != certContents {
|
||||
t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(certFile))
|
||||
}
|
||||
|
||||
keyFile, err := ioutil.ReadFile(storage.SiteKeyFile(domain))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error reading private key file, got: %v", err)
|
||||
}
|
||||
if string(keyFile) != keyContents {
|
||||
t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(keyFile))
|
||||
}
|
||||
|
||||
metaFile, err := ioutil.ReadFile(storage.SiteMetaFile(domain))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error reading meta file, got: %v", err)
|
||||
}
|
||||
if string(metaFile) != metaContents {
|
||||
t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(metaFile))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistingCertAndKey(t *testing.T) {
|
||||
storage = Storage("./le_test_existing")
|
||||
defer func() {
|
||||
err := os.RemoveAll(string(storage))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
|
||||
}
|
||||
}()
|
||||
|
||||
domain := "example.com"
|
||||
|
||||
if existingCertAndKey(domain) {
|
||||
t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain)
|
||||
}
|
||||
|
||||
err := saveCertResource(acme.CertificateResource{
|
||||
Domain: domain,
|
||||
PrivateKey: []byte("key"),
|
||||
Certificate: []byte("cert"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if !existingCertAndKey(domain) {
|
||||
t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostHasOtherPort(t *testing.T) {
|
||||
configs := []server.Config{
|
||||
{Host: "example.com", Port: "80"},
|
||||
{Host: "sub1.example.com", Port: "80"},
|
||||
{Host: "sub1.example.com", Port: "443"},
|
||||
}
|
||||
|
||||
if hostHasOtherPort(configs, 0, "80") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`)
|
||||
}
|
||||
if hostHasOtherPort(configs, 0, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`)
|
||||
}
|
||||
if !hostHasOtherPort(configs, 1, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePlaintextRedirects(t *testing.T) {
|
||||
configs := []server.Config{
|
||||
// Happy path = standard redirect from 80 to 443
|
||||
{Host: "example.com", TLS: server.TLSConfig{Managed: true}},
|
||||
|
||||
// Host on port 80 already defined; don't change it (no redirect)
|
||||
{Host: "sub1.example.com", Port: "80", Scheme: "http"},
|
||||
{Host: "sub1.example.com", TLS: server.TLSConfig{Managed: true}},
|
||||
|
||||
// Redirect from port 80 to port 5000 in this case
|
||||
{Host: "sub2.example.com", Port: "5000", TLS: server.TLSConfig{Managed: true}},
|
||||
|
||||
// Can redirect from 80 to either 443 or 5001, but choose 443
|
||||
{Host: "sub3.example.com", Port: "443", TLS: server.TLSConfig{Managed: true}},
|
||||
{Host: "sub3.example.com", Port: "5001", Scheme: "https", TLS: server.TLSConfig{Managed: true}},
|
||||
}
|
||||
|
||||
result := MakePlaintextRedirects(configs)
|
||||
expectedRedirCount := 3
|
||||
|
||||
if len(result) != len(configs)+expectedRedirCount {
|
||||
t.Errorf("Expected %d redirect(s) to be added, but got %d",
|
||||
expectedRedirCount, len(result)-len(configs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableTLS(t *testing.T) {
|
||||
configs := []server.Config{
|
||||
{Host: "example.com", TLS: server.TLSConfig{Managed: true}},
|
||||
{}, // not managed - no changes!
|
||||
}
|
||||
|
||||
EnableTLS(configs, false)
|
||||
|
||||
if !configs[0].TLS.Enabled {
|
||||
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
|
||||
}
|
||||
if configs[1].TLS.Enabled {
|
||||
t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupConfigsByEmail(t *testing.T) {
|
||||
if groupConfigsByEmail([]server.Config{}, false) == nil {
|
||||
t.Errorf("With empty input, returned map was nil, but expected non-nil map")
|
||||
}
|
||||
|
||||
configs := []server.Config{
|
||||
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
|
||||
{Host: "sub1.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
|
||||
{Host: "sub2.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
|
||||
{Host: "sub3.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
|
||||
{Host: "sub4.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
|
||||
{Host: "sub5.example.com", TLS: server.TLSConfig{LetsEncryptEmail: ""}}, // not managed
|
||||
}
|
||||
DefaultEmail = "test@example.com"
|
||||
|
||||
groups := groupConfigsByEmail(configs, true)
|
||||
|
||||
if groups == nil {
|
||||
t.Fatalf("Returned map was nil, but expected values")
|
||||
}
|
||||
|
||||
if len(groups) != 2 {
|
||||
t.Errorf("Expected 2 groups, got %d: %#v", len(groups), groups)
|
||||
}
|
||||
if len(groups["foo@bar"]) != 2 {
|
||||
t.Errorf("Expected 2 configs for foo@bar, got %d: %#v", len(groups["foobar"]), groups["foobar"])
|
||||
}
|
||||
if len(groups[DefaultEmail]) != 3 {
|
||||
t.Errorf("Expected 3 configs for %s, got %d: %#v", DefaultEmail, len(groups["foobar"]), groups["foobar"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkQualified(t *testing.T) {
|
||||
// TODO: TestConfigQualifies and this test share the same config list...
|
||||
configs := []server.Config{
|
||||
{Host: ""},
|
||||
{Host: "localhost"},
|
||||
{Host: "123.44.3.21"},
|
||||
{Host: "example.com"},
|
||||
{Host: "example.com", TLS: server.TLSConfig{Manual: true}},
|
||||
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}},
|
||||
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}},
|
||||
{Host: "example.com", Scheme: "http"},
|
||||
{Host: "example.com", Port: "80"},
|
||||
{Host: "example.com", Port: "1234"},
|
||||
{Host: "example.com", Scheme: "https"},
|
||||
{Host: "example.com", Port: "80", Scheme: "https"},
|
||||
}
|
||||
expectedManagedCount := 4
|
||||
|
||||
MarkQualified(configs)
|
||||
|
||||
count := 0
|
||||
for _, cfg := range configs {
|
||||
if cfg.TLS.Managed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if count != expectedManagedCount {
|
||||
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
|
||||
}
|
||||
}
|
|
@ -1,355 +0,0 @@
|
|||
package https
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// Setup sets up the TLS configuration and installs certificates that
|
||||
// are specified by the user in the config file. All the automatic HTTPS
|
||||
// stuff comes later outside of this function.
|
||||
func Setup(c *setup.Controller) (middleware.Middleware, error) {
|
||||
if c.Port == "80" || c.Scheme == "http" {
|
||||
c.TLS.Enabled = false
|
||||
log.Printf("[WARNING] TLS disabled for %s://%s.", c.Scheme, c.Address())
|
||||
return nil, nil
|
||||
}
|
||||
c.TLS.Enabled = true
|
||||
|
||||
for c.Next() {
|
||||
var certificateFile, keyFile, loadDir, maxCerts string
|
||||
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 1:
|
||||
c.TLS.LetsEncryptEmail = args[0]
|
||||
|
||||
// user can force-disable managed TLS this way
|
||||
if c.TLS.LetsEncryptEmail == "off" {
|
||||
c.TLS.Enabled = false
|
||||
return nil, nil
|
||||
}
|
||||
case 2:
|
||||
certificateFile = args[0]
|
||||
keyFile = args[1]
|
||||
c.TLS.Manual = true
|
||||
}
|
||||
|
||||
// Optional block with extra parameters
|
||||
var hadBlock bool
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
switch c.Val() {
|
||||
case "key_type":
|
||||
arg := c.RemainingArgs()
|
||||
value, ok := supportedKeyTypes[strings.ToUpper(arg[0])]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong KeyType name or KeyType not supported '%s'", c.Val())
|
||||
}
|
||||
KeyType = value
|
||||
case "protocols":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
value, ok := supportedProtocols[strings.ToLower(args[0])]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.ProtocolMinVersion = value
|
||||
value, ok = supportedProtocols[strings.ToLower(args[1])]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.ProtocolMaxVersion = value
|
||||
case "ciphers":
|
||||
for c.NextArg() {
|
||||
value, ok := supportedCiphersMap[strings.ToUpper(c.Val())]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.Ciphers = append(c.TLS.Ciphers, value)
|
||||
}
|
||||
case "clients":
|
||||
clientCertList := c.RemainingArgs()
|
||||
if len(clientCertList) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
listStart, mustProvideCA := 1, true
|
||||
switch clientCertList[0] {
|
||||
case "request":
|
||||
c.TLS.ClientAuth = tls.RequestClientCert
|
||||
mustProvideCA = false
|
||||
case "require":
|
||||
c.TLS.ClientAuth = tls.RequireAnyClientCert
|
||||
mustProvideCA = false
|
||||
case "verify_if_given":
|
||||
c.TLS.ClientAuth = tls.VerifyClientCertIfGiven
|
||||
default:
|
||||
c.TLS.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
listStart = 0
|
||||
}
|
||||
if mustProvideCA && len(clientCertList) <= listStart {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
c.TLS.ClientCerts = clientCertList[listStart:]
|
||||
case "load":
|
||||
c.Args(&loadDir)
|
||||
c.TLS.Manual = true
|
||||
case "max_certs":
|
||||
c.Args(&maxCerts)
|
||||
c.TLS.OnDemand = true
|
||||
default:
|
||||
return nil, c.Errf("Unknown keyword '%s'", c.Val())
|
||||
}
|
||||
}
|
||||
|
||||
// tls requires at least one argument if a block is not opened
|
||||
if len(args) == 0 && !hadBlock {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
// set certificate limit if on-demand TLS is enabled
|
||||
if maxCerts != "" {
|
||||
maxCertsNum, err := strconv.Atoi(maxCerts)
|
||||
if err != nil || maxCertsNum < 1 {
|
||||
return nil, c.Err("max_certs must be a positive integer")
|
||||
}
|
||||
if onDemandMaxIssue == 0 || int32(maxCertsNum) < onDemandMaxIssue { // keep the minimum; TODO: We have to do this because it is global; should be per-server or per-vhost...
|
||||
onDemandMaxIssue = int32(maxCertsNum)
|
||||
}
|
||||
}
|
||||
|
||||
// don't try to load certificates unless we're supposed to
|
||||
if !c.TLS.Enabled || !c.TLS.Manual {
|
||||
continue
|
||||
}
|
||||
|
||||
// load a single certificate and key, if specified
|
||||
if certificateFile != "" && keyFile != "" {
|
||||
err := cacheUnmanagedCertificatePEMFile(certificateFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, c.Errf("Unable to load certificate and key files for %s: %v", c.Host, err)
|
||||
}
|
||||
log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile)
|
||||
}
|
||||
|
||||
// load a directory of certificates, if specified
|
||||
if loadDir != "" {
|
||||
err := loadCertsInDir(c, loadDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultTLSParams(c.Config)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// loadCertsInDir loads all the certificates/keys in dir, as long as
|
||||
// the file ends with .pem. This method of loading certificates is
|
||||
// modeled after haproxy, which expects the certificate and key to
|
||||
// be bundled into the same file:
|
||||
// https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#5.1-crt
|
||||
//
|
||||
// This function may write to the log as it walks the directory tree.
|
||||
func loadCertsInDir(c *setup.Controller, dir string) error {
|
||||
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Unable to traverse into %s; skipping", path)
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(info.Name()), ".pem") {
|
||||
certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer)
|
||||
var foundKey bool // use only the first key in the file
|
||||
|
||||
bundle, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
// Decode next block so we can see what type it is
|
||||
var derBlock *pem.Block
|
||||
derBlock, bundle = pem.Decode(bundle)
|
||||
if derBlock == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if derBlock.Type == "CERTIFICATE" {
|
||||
// Re-encode certificate as PEM, appending to certificate chain
|
||||
pem.Encode(certBuilder, derBlock)
|
||||
} else if derBlock.Type == "EC PARAMETERS" {
|
||||
// EC keys generated from openssl can be composed of two blocks:
|
||||
// parameters and key (parameter block should come first)
|
||||
if !foundKey {
|
||||
// Encode parameters
|
||||
pem.Encode(keyBuilder, derBlock)
|
||||
|
||||
// Key must immediately follow
|
||||
derBlock, bundle = pem.Decode(bundle)
|
||||
if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" {
|
||||
return c.Errf("%s: expected elliptic private key to immediately follow EC parameters", path)
|
||||
}
|
||||
pem.Encode(keyBuilder, derBlock)
|
||||
foundKey = true
|
||||
}
|
||||
} else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") {
|
||||
// RSA key
|
||||
if !foundKey {
|
||||
pem.Encode(keyBuilder, derBlock)
|
||||
foundKey = true
|
||||
}
|
||||
} else {
|
||||
return c.Errf("%s: unrecognized PEM block type: %s", path, derBlock.Type)
|
||||
}
|
||||
}
|
||||
|
||||
certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes()
|
||||
if len(certPEMBytes) == 0 {
|
||||
return c.Errf("%s: failed to parse PEM data", path)
|
||||
}
|
||||
if len(keyPEMBytes) == 0 {
|
||||
return c.Errf("%s: no private key block found", path)
|
||||
}
|
||||
|
||||
err = cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes)
|
||||
if err != nil {
|
||||
return c.Errf("%s: failed to load cert and key for %s: %v", path, c.Host, err)
|
||||
}
|
||||
log.Printf("[INFO] Successfully loaded TLS assets from %s", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// setDefaultTLSParams sets the default TLS cipher suites, protocol versions,
|
||||
// and server preferences of a server.Config if they were not previously set
|
||||
// (it does not overwrite; only fills in missing values). It will also set the
|
||||
// port to 443 if not already set, TLS is enabled, TLS is manual, and the host
|
||||
// does not equal localhost.
|
||||
func setDefaultTLSParams(c *server.Config) {
|
||||
// If no ciphers provided, use default list
|
||||
if len(c.TLS.Ciphers) == 0 {
|
||||
c.TLS.Ciphers = defaultCiphers
|
||||
}
|
||||
|
||||
// Not a cipher suite, but still important for mitigating protocol downgrade attacks
|
||||
// (prepend since having it at end breaks http2 due to non-h2-approved suites before it)
|
||||
c.TLS.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, c.TLS.Ciphers...)
|
||||
|
||||
// Set default protocol min and max versions - must balance compatibility and security
|
||||
if c.TLS.ProtocolMinVersion == 0 {
|
||||
c.TLS.ProtocolMinVersion = tls.VersionTLS10
|
||||
}
|
||||
if c.TLS.ProtocolMaxVersion == 0 {
|
||||
c.TLS.ProtocolMaxVersion = tls.VersionTLS12
|
||||
}
|
||||
|
||||
// Prefer server cipher suites
|
||||
c.TLS.PreferServerCipherSuites = true
|
||||
|
||||
// Default TLS port is 443; only use if port is not manually specified,
|
||||
// TLS is enabled, and the host is not localhost
|
||||
if c.Port == "" && c.TLS.Enabled && (!c.TLS.Manual || c.TLS.OnDemand) && c.Host != "localhost" {
|
||||
c.Port = "443"
|
||||
}
|
||||
}
|
||||
|
||||
// Map of supported key types
|
||||
var supportedKeyTypes = map[string]acme.KeyType{
|
||||
"P384": acme.EC384,
|
||||
"P256": acme.EC256,
|
||||
"RSA8192": acme.RSA8192,
|
||||
"RSA4096": acme.RSA4096,
|
||||
"RSA2048": acme.RSA2048,
|
||||
}
|
||||
|
||||
// Map of supported protocols.
|
||||
// SSLv3 will be not supported in future release.
|
||||
// HTTP/2 only supports TLS 1.2 and higher.
|
||||
var supportedProtocols = map[string]uint16{
|
||||
"ssl3.0": tls.VersionSSL30,
|
||||
"tls1.0": tls.VersionTLS10,
|
||||
"tls1.1": tls.VersionTLS11,
|
||||
"tls1.2": tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// Map of supported ciphers, used only for parsing config.
|
||||
//
|
||||
// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites,
|
||||
// including all but two of the suites below (the two GCM suites).
|
||||
// See https://http2.github.io/http2-spec/#BadCipherSuites
|
||||
//
|
||||
// TLS_FALLBACK_SCSV is not in this list because we manually ensure
|
||||
// it is always added (even though it is not technically a cipher suite).
|
||||
//
|
||||
// This map, like any map, is NOT ORDERED. Do not range over this map.
|
||||
var supportedCiphersMap = map[string]uint16{
|
||||
"ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
"ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
"ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
"RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
"RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
}
|
||||
|
||||
// List of supported cipher suites in descending order of preference.
|
||||
// Ordering is very important! Getting the wrong order will break
|
||||
// mainstream clients, especially with HTTP/2.
|
||||
//
|
||||
// Note that TLS_FALLBACK_SCSV is not in this list since it is always
|
||||
// added manually.
|
||||
var supportedCiphers = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
}
|
||||
|
||||
// List of all the ciphers we want to use by default
|
||||
var defaultCiphers = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
}
|
7
caddy/main.go
Normal file
7
caddy/main.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "github.com/mholt/caddy/caddy/caddymain"
|
||||
|
||||
func main() {
|
||||
caddymain.Run()
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
// Package parse provides facilities for parsing configuration files.
|
||||
package parse
|
||||
|
||||
import "io"
|
||||
|
||||
// ServerBlocks parses the input just enough to organize tokens,
|
||||
// in order, by server block. No further parsing is performed.
|
||||
// If checkDirectives is true, only valid directives will be allowed
|
||||
// otherwise we consider it a parse error. Server blocks are returned
|
||||
// in the order in which they appear.
|
||||
func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]ServerBlock, error) {
|
||||
p := parser{Dispenser: NewDispenser(filename, input)}
|
||||
p.checkDirectives = checkDirectives
|
||||
blocks, err := p.parseAll()
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(input io.Reader) (tokens []token) {
|
||||
l := new(lexer)
|
||||
l.load(input)
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ValidDirectives is a set of directives that are valid (unordered). Populated
|
||||
// by config package's init function.
|
||||
var ValidDirectives = make(map[string]struct{})
|
|
@ -1,22 +0,0 @@
|
|||
package parse
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllTokens(t *testing.T) {
|
||||
input := strings.NewReader("a b c\nd e")
|
||||
expected := []string{"a", "b", "c", "d", "e"}
|
||||
tokens := allTokens(input)
|
||||
|
||||
if len(tokens) != len(expected) {
|
||||
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
|
||||
}
|
||||
|
||||
for i, val := range expected {
|
||||
if tokens[i].text != val {
|
||||
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,480 +0,0 @@
|
|||
package parse
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStandardAddress(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
scheme, host, port string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`localhost`, "", "localhost", "", false},
|
||||
{`LOCALHOST`, "", "localhost", "", false},
|
||||
{`localhost:1234`, "", "localhost", "1234", false},
|
||||
{`LOCALHOST:1234`, "", "localhost", "1234", false},
|
||||
{`localhost:`, "", "localhost", "", false},
|
||||
{`0.0.0.0`, "", "0.0.0.0", "", false},
|
||||
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", false},
|
||||
{`:1234`, "", "", "1234", false},
|
||||
{`[::1]`, "", "::1", "", false},
|
||||
{`[::1]:1234`, "", "::1", "1234", false},
|
||||
{`:`, "", "", "", false},
|
||||
{`localhost:http`, "http", "localhost", "80", false},
|
||||
{`localhost:https`, "https", "localhost", "443", false},
|
||||
{`:http`, "http", "", "80", false},
|
||||
{`:https`, "https", "", "443", false},
|
||||
{`http://localhost:https`, "", "", "", true}, // conflict
|
||||
{`http://localhost:http`, "", "", "", true}, // repeated scheme
|
||||
{`http://localhost:443`, "", "", "", true}, // not conventional
|
||||
{`https://localhost:80`, "", "", "", true}, // not conventional
|
||||
{`http://localhost`, "http", "localhost", "80", false},
|
||||
{`https://localhost`, "https", "localhost", "443", false},
|
||||
{`http://127.0.0.1`, "http", "127.0.0.1", "80", false},
|
||||
{`https://127.0.0.1`, "https", "127.0.0.1", "443", false},
|
||||
{`http://[::1]`, "http", "::1", "80", false},
|
||||
{`http://localhost:1234`, "http", "localhost", "1234", false},
|
||||
{`http://LOCALHOST:1234`, "http", "localhost", "1234", false},
|
||||
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", false},
|
||||
{`http://[::1]:1234`, "http", "::1", "1234", false},
|
||||
{``, "", "", "", false},
|
||||
{`::1`, "", "::1", "", true},
|
||||
{`localhost::`, "", "localhost::", "", true},
|
||||
{`#$%@`, "", "#$%@", "", true},
|
||||
} {
|
||||
actual, err := standardAddress(test.input)
|
||||
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err)
|
||||
}
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected error, but had none", i, test.input)
|
||||
}
|
||||
|
||||
if actual.Scheme != test.scheme {
|
||||
t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme)
|
||||
}
|
||||
if actual.Host != test.host {
|
||||
t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host)
|
||||
}
|
||||
if actual.Port != test.port {
|
||||
t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOneAndImport(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
testParseOne := func(input string) (ServerBlock, error) {
|
||||
p := testParser(input)
|
||||
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
err := p.parseOne()
|
||||
return p.block, err
|
||||
}
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
addresses []address
|
||||
tokens map[string]int // map of directive name to number of tokens expected
|
||||
}{
|
||||
{`localhost`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234
|
||||
dir1 foo bar`, false, []address{
|
||||
{"localhost:1234", "", "localhost", "1234"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost {
|
||||
dir1
|
||||
}`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
dir1 foo bar
|
||||
dir2
|
||||
}`, false, []address{
|
||||
{"localhost:1234", "", "localhost", "1234"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost
|
||||
dir1 foo bar`, false, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
{"https://localhost", "https", "localhost", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
{"https://localhost", "https", "localhost", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
{"https://localhost", "https", "localhost", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, {
|
||||
}`, true, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`host1:80, http://host2.com
|
||||
dir1 foo bar
|
||||
dir2 baz`, false, []address{
|
||||
{"host1:80", "", "host1", "80"},
|
||||
{"http://host2.com", "http", "host2.com", "80"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`http://host1.com,
|
||||
http://host2.com,
|
||||
https://host3.com`, false, []address{
|
||||
{"http://host1.com", "http", "host1.com", "80"},
|
||||
{"http://host2.com", "http", "host2.com", "80"},
|
||||
{"https://host3.com", "https", "host3.com", "443"},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`http://host1.com:1234, https://host2.com
|
||||
dir1 foo {
|
||||
bar baz
|
||||
}
|
||||
dir2`, false, []address{
|
||||
{"http://host1.com:1234", "http", "host1.com", "1234"},
|
||||
{"https://host2.com", "https", "host2.com", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 6,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
dir1 {
|
||||
bar baz
|
||||
}
|
||||
dir2 {
|
||||
foo bar
|
||||
}`, false, []address{
|
||||
{"127.0.0.1", "", "127.0.0.1", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 5,
|
||||
"dir2": 5,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
unknown_directive`, true, []address{
|
||||
{"127.0.0.1", "", "127.0.0.1", ""},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
foo`, true, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
}`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
} }`, true, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
nested {
|
||||
foo
|
||||
}
|
||||
}
|
||||
dir2 foo bar`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 7,
|
||||
"dir2": 3,
|
||||
}},
|
||||
|
||||
{``, false, []address{}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 arg1
|
||||
import import_test1.txt`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 2,
|
||||
"dir2": 3,
|
||||
"dir3": 1,
|
||||
}},
|
||||
|
||||
{`import import_test2.txt`, false, []address{
|
||||
{"host1", "", "host1", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}},
|
||||
|
||||
{`import not_found.txt`, true, []address{}, map[string]int{}},
|
||||
|
||||
{`""`, false, []address{}, map[string]int{}},
|
||||
|
||||
{``, false, []address{}, map[string]int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(result.Addresses) != len(test.addresses) {
|
||||
t.Errorf("Test %d: Expected %d addresses, got %d",
|
||||
i, len(test.addresses), len(result.Addresses))
|
||||
continue
|
||||
}
|
||||
for j, addr := range result.Addresses {
|
||||
if addr.Host != test.addresses[j].Host {
|
||||
t.Errorf("Test %d, address %d: Expected host to be '%s', but was '%s'",
|
||||
i, j, test.addresses[j].Host, addr.Host)
|
||||
}
|
||||
if addr.Port != test.addresses[j].Port {
|
||||
t.Errorf("Test %d, address %d: Expected port to be '%s', but was '%s'",
|
||||
i, j, test.addresses[j].Port, addr.Port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Tokens) != len(test.tokens) {
|
||||
t.Errorf("Test %d: Expected %d directives, had %d",
|
||||
i, len(test.tokens), len(result.Tokens))
|
||||
continue
|
||||
}
|
||||
for directive, tokens := range result.Tokens {
|
||||
if len(tokens) != test.tokens[directive] {
|
||||
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
|
||||
i, directive, test.tokens[directive], len(tokens))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAll(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
addresses [][]address // addresses per server block, in order
|
||||
}{
|
||||
{`localhost`, false, [][]address{
|
||||
{{"localhost", "", "localhost", ""}},
|
||||
}},
|
||||
|
||||
{`localhost:1234`, false, [][]address{
|
||||
{{"localhost:1234", "", "localhost", "1234"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
}
|
||||
localhost:2015 {
|
||||
}`, false, [][]address{
|
||||
{{"localhost:1234", "", "localhost", "1234"}},
|
||||
{{"localhost:2015", "", "localhost", "2015"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2`, false, [][]address{
|
||||
{{"localhost:1234", "", "localhost", "1234"}, {"http://host2", "http", "host2", "80"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2,`, true, [][]address{}},
|
||||
|
||||
{`http://host1.com, http://host2.com {
|
||||
}
|
||||
https://host3.com, https://host4.com {
|
||||
}`, false, [][]address{
|
||||
{{"http://host1.com", "http", "host1.com", "80"}, {"http://host2.com", "http", "host2.com", "80"}},
|
||||
{{"https://host3.com", "https", "host3.com", "443"}, {"https://host4.com", "https", "host4.com", "443"}},
|
||||
}},
|
||||
|
||||
{`import import_glob*.txt`, false, [][]address{
|
||||
{{"glob0.host0", "", "glob0.host0", ""}},
|
||||
{{"glob0.host1", "", "glob0.host1", ""}},
|
||||
{{"glob1.host0", "", "glob1.host0", ""}},
|
||||
{{"glob2.host0", "", "glob2.host0", ""}},
|
||||
}},
|
||||
} {
|
||||
p := testParser(test.input)
|
||||
blocks, err := p.parseAll()
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(blocks) != len(test.addresses) {
|
||||
t.Errorf("Test %d: Expected %d server blocks, got %d",
|
||||
i, len(test.addresses), len(blocks))
|
||||
continue
|
||||
}
|
||||
for j, block := range blocks {
|
||||
if len(block.Addresses) != len(test.addresses[j]) {
|
||||
t.Errorf("Test %d: Expected %d addresses in block %d, got %d",
|
||||
i, len(test.addresses[j]), j, len(block.Addresses))
|
||||
continue
|
||||
}
|
||||
for k, addr := range block.Addresses {
|
||||
if addr.Host != test.addresses[j][k].Host {
|
||||
t.Errorf("Test %d, block %d, address %d: Expected host to be '%s', but was '%s'",
|
||||
i, j, k, test.addresses[j][k].Host, addr.Host)
|
||||
}
|
||||
if addr.Port != test.addresses[j][k].Port {
|
||||
t.Errorf("Test %d, block %d, address %d: Expected port to be '%s', but was '%s'",
|
||||
i, j, k, test.addresses[j][k].Port, addr.Port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentReplacement(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
os.Setenv("PORT", "8080")
|
||||
os.Setenv("ADDRESS", "servername.com")
|
||||
os.Setenv("FOOBAR", "foobar")
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p := testParser(`{$ADDRESS}`)
|
||||
blocks, _ := p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// multiple vars per token
|
||||
p = testParser(`{$ADDRESS}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// windows-style var and unix style in same token
|
||||
p = testParser(`{%ADDRESS%}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// reverse order
|
||||
p = testParser(`{$ADDRESS}:{%PORT%}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// env var in server block body as argument
|
||||
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// combined windows env vars in argument
|
||||
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "servername.com/foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed env var (windows)
|
||||
p = testParser(":1234\ndir1 {%ADDRESS}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "{%ADDRESS}"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed (non-existent) env var (unix)
|
||||
p = testParser(`:{$PORT$}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Port, ""; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// in quoted field
|
||||
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "Test foobar test"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func setupParseTests() {
|
||||
// Set up some bogus directives for testing
|
||||
ValidDirectives = map[string]struct{}{
|
||||
"dir1": {},
|
||||
"dir2": {},
|
||||
"dir3": {},
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
buf := strings.NewReader(input)
|
||||
p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true}
|
||||
return p
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
// +build !windows
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
)
|
||||
|
||||
// Restart restarts the entire application; gracefully with zero
|
||||
// downtime if on a POSIX-compatible system, or forcefully if on
|
||||
// Windows but with imperceptibly-short downtime.
|
||||
//
|
||||
// The behavior can be controlled by the RestartMode variable,
|
||||
// where "inproc" will restart forcefully in process same as
|
||||
// Windows on a POSIX-compatible system.
|
||||
//
|
||||
// The restarted application will use newCaddyfile as its input
|
||||
// configuration. If newCaddyfile is nil, the current (existing)
|
||||
// Caddyfile configuration will be used.
|
||||
//
|
||||
// Note: The process must exist in the same place on the disk in
|
||||
// order for this to work. Thus, multiple graceful restarts don't
|
||||
// work if executing with `go run`, since the binary is cleaned up
|
||||
// when `go run` sees the initial parent process exit.
|
||||
func Restart(newCaddyfile Input) error {
|
||||
log.Println("[INFO] Restarting")
|
||||
|
||||
if newCaddyfile == nil {
|
||||
caddyfileMu.Lock()
|
||||
newCaddyfile = caddyfile
|
||||
caddyfileMu.Unlock()
|
||||
}
|
||||
|
||||
// Get certificates for any new hosts in the new Caddyfile without causing downtime
|
||||
err := getCertsForNewCaddyfile(newCaddyfile)
|
||||
if err != nil {
|
||||
return errors.New("TLS preload: " + err.Error())
|
||||
}
|
||||
|
||||
// Add file descriptors of all the sockets for new instance
|
||||
serversMu.Lock()
|
||||
for _, s := range servers {
|
||||
restartFds[s.Addr] = s.ListenerFd()
|
||||
}
|
||||
serversMu.Unlock()
|
||||
|
||||
return restartInProc(newCaddyfile)
|
||||
}
|
||||
|
||||
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(filepath.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body()))
|
||||
if err != nil {
|
||||
return errors.New("loading Caddyfile: " + err.Error())
|
||||
}
|
||||
|
||||
// first mark the configs that are qualified for managed TLS
|
||||
https.MarkQualified(configs)
|
||||
|
||||
// since we group by bind address to obtain certs, we must call
|
||||
// EnableTLS to make sure the port is set properly first
|
||||
// (can ignore error since we aren't actually using the certs)
|
||||
https.EnableTLS(configs, false)
|
||||
|
||||
// find out if we can let the acme package start its own challenge listener
|
||||
// on port 80
|
||||
var proxyACME bool
|
||||
serversMu.Lock()
|
||||
for _, s := range servers {
|
||||
_, port, _ := net.SplitHostPort(s.Addr)
|
||||
if port == "80" {
|
||||
proxyACME = true
|
||||
break
|
||||
}
|
||||
}
|
||||
serversMu.Unlock()
|
||||
|
||||
// place certs on the disk
|
||||
err = https.ObtainCerts(configs, false, proxyACME)
|
||||
if err != nil {
|
||||
return errors.New("obtaining certs: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import "log"
|
||||
|
||||
// Restart restarts Caddy forcefully using newCaddyfile,
|
||||
// or, if nil, the current/existing Caddyfile is reused.
|
||||
func Restart(newCaddyfile Input) error {
|
||||
log.Println("[INFO] Restarting")
|
||||
|
||||
if newCaddyfile == nil {
|
||||
caddyfileMu.Lock()
|
||||
newCaddyfile = caddyfile
|
||||
caddyfileMu.Unlock()
|
||||
}
|
||||
|
||||
return restartInProc(newCaddyfile)
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import "log"
|
||||
|
||||
// restartInProc restarts Caddy forcefully in process using newCaddyfile.
|
||||
func restartInProc(newCaddyfile Input) error {
|
||||
wg.Add(1) // barrier so Wait() doesn't unblock
|
||||
defer wg.Done()
|
||||
|
||||
err := Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caddyfileMu.Lock()
|
||||
oldCaddyfile := caddyfile
|
||||
caddyfileMu.Unlock()
|
||||
|
||||
err = Start(newCaddyfile)
|
||||
if err != nil {
|
||||
// revert to old Caddyfile
|
||||
if oldErr := Start(oldCaddyfile); oldErr != nil {
|
||||
log.Printf("[ERROR] Restart: in-process restart failed and cannot revert to old Caddyfile: %v", oldErr)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/basicauth"
|
||||
)
|
||||
|
||||
// BasicAuth configures a new BasicAuth middleware instance.
|
||||
func BasicAuth(c *Controller) (middleware.Middleware, error) {
|
||||
root := c.Root
|
||||
|
||||
rules, err := basicAuthParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
basic := basicauth.BasicAuth{Rules: rules}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
basic.Next = next
|
||||
basic.SiteRoot = root
|
||||
return basic
|
||||
}, nil
|
||||
}
|
||||
|
||||
func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
|
||||
var rules []basicauth.Rule
|
||||
|
||||
var err error
|
||||
for c.Next() {
|
||||
var rule basicauth.Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 2:
|
||||
rule.Username = args[0]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil {
|
||||
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
rule.Resources = append(rule.Resources, c.Val())
|
||||
if c.NextArg() {
|
||||
return rules, c.Errf("Expecting only one resource per line (extra '%s')", c.Val())
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
rule.Resources = append(rule.Resources, args[0])
|
||||
rule.Username = args[1]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil {
|
||||
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
|
||||
}
|
||||
default:
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) {
|
||||
if !strings.HasPrefix(passw, "htpasswd=") {
|
||||
return basicauth.PlainMatcher(passw), nil
|
||||
}
|
||||
|
||||
return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot)
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/basicauth"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
c := NewTestController(`basicauth user pwd`)
|
||||
|
||||
mid, err := BasicAuth(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(basicauth.BasicAuth)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthParse(t *testing.T) {
|
||||
htpasswdPasswd := "IedFOuGmTpT8"
|
||||
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
|
||||
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
|
||||
var skipHtpassword bool
|
||||
htfh, err := ioutil.TempFile(".", "basicauth-")
|
||||
if err != nil {
|
||||
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
|
||||
skipHtpassword = true
|
||||
} else {
|
||||
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
|
||||
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
|
||||
}
|
||||
htfh.Close()
|
||||
defer os.Remove(htfh.Name())
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
password string
|
||||
expected []basicauth.Rule
|
||||
}{
|
||||
{`basicauth user pwd`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
}`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
/resource2
|
||||
}`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
|
||||
}},
|
||||
{`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user", Resources: []string{"/resource"}},
|
||||
}},
|
||||
{`basicauth /res1 user1 pwd1
|
||||
basicauth /res2 user2 pwd2`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user1", Resources: []string{"/res1"}},
|
||||
{Username: "user2", Resources: []string{"/res2"}},
|
||||
}},
|
||||
{`basicauth user`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth /resource user pwd asdf`, true, "", []basicauth.Rule{}},
|
||||
|
||||
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []basicauth.Rule{
|
||||
{Username: "sha1"},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := basicAuthParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, expectedRule := range test.expected {
|
||||
actualRule := actual[j]
|
||||
|
||||
if actualRule.Username != expectedRule.Username {
|
||||
t.Errorf("Test %d, rule %d: Expected username '%s', got '%s'",
|
||||
i, j, expectedRule.Username, actualRule.Username)
|
||||
}
|
||||
|
||||
if strings.Contains(test.input, "htpasswd=") && skipHtpassword {
|
||||
continue
|
||||
}
|
||||
pwd := test.password
|
||||
if len(actual) > 1 {
|
||||
pwd = fmt.Sprintf("%s%d", pwd, j+1)
|
||||
}
|
||||
if !actualRule.Password(pwd) || actualRule.Password(test.password+"!") {
|
||||
t.Errorf("Test %d, rule %d: Expected password '%v', got '%v'",
|
||||
i, j, test.password, actualRule.Password(""))
|
||||
}
|
||||
|
||||
expectedRes := fmt.Sprintf("%v", expectedRule.Resources)
|
||||
actualRes := fmt.Sprintf("%v", actualRule.Resources)
|
||||
if actualRes != expectedRes {
|
||||
t.Errorf("Test %d, rule %d: Expected resource list %s, but got %s",
|
||||
i, j, expectedRes, actualRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package setup
|
||||
|
||||
import "github.com/mholt/caddy/middleware"
|
||||
|
||||
// BindHost sets the host to bind the listener to.
|
||||
func BindHost(c *Controller) (middleware.Middleware, error) {
|
||||
for c.Next() {
|
||||
if !c.Args(&c.BindHost) {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
|
@ -1,416 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/browse"
|
||||
)
|
||||
|
||||
// Browse configures a new Browse middleware instance.
|
||||
func Browse(c *Controller) (middleware.Middleware, error) {
|
||||
configs, err := browseParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
browse := browse.Browse{
|
||||
Configs: configs,
|
||||
IgnoreIndexes: false,
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
browse.Next = next
|
||||
return browse
|
||||
}, nil
|
||||
}
|
||||
|
||||
func browseParse(c *Controller) ([]browse.Config, error) {
|
||||
var configs []browse.Config
|
||||
|
||||
appendCfg := func(bc browse.Config) error {
|
||||
for _, c := range configs {
|
||||
if c.PathScope == bc.PathScope {
|
||||
return fmt.Errorf("duplicate browsing config for %s", c.PathScope)
|
||||
}
|
||||
}
|
||||
configs = append(configs, bc)
|
||||
return nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
var bc browse.Config
|
||||
|
||||
// First argument is directory to allow browsing; default is site root
|
||||
if c.NextArg() {
|
||||
bc.PathScope = c.Val()
|
||||
} 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
|
||||
if c.NextArg() {
|
||||
tplBytes, err := ioutil.ReadFile(c.Val())
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
tplText = string(tplBytes)
|
||||
} else {
|
||||
tplText = defaultTemplate
|
||||
}
|
||||
|
||||
// Build the template
|
||||
tpl, err := template.New("listing").Parse(tplText)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
bc.Template = tpl
|
||||
|
||||
// Save configuration
|
||||
err = appendCfg(bc)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// The default template to use when serving up directory listings
|
||||
const defaultTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Name}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* { padding: 0; margin: 0; }
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-rendering: optimizespeed;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #006ed3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
h1 a:hover {
|
||||
color: #319cff;
|
||||
}
|
||||
|
||||
header,
|
||||
#summary {
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 5%;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 5%;
|
||||
}
|
||||
|
||||
header {
|
||||
padding-top: 25px;
|
||||
padding-bottom: 15px;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h1 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 12px;
|
||||
font-family: Verdana, sans-serif;
|
||||
border-bottom: 1px solid #9C9C9C;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px dashed #dadada;
|
||||
}
|
||||
|
||||
tr:not(:first-child):hover {
|
||||
background-color: #ffffec;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
th {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
th svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td:first-child svg {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
td .name,
|
||||
td .goup {
|
||||
margin-left: 1.75em;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 40px 20px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hideable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
padding-right: 5%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="0" width="0" style="position: absolute;">
|
||||
<defs>
|
||||
<!-- Folder -->
|
||||
<linearGradient id="f" y2="640" gradientUnits="userSpaceOnUse" x2="244.84" gradientTransform="matrix(.97319 0 0 1.0135 -.50695 -13.679)" y1="415.75" x1="244.84">
|
||||
<stop stop-color="#b3ddfd" offset="0"/>
|
||||
<stop stop-color="#69c" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="e" y2="571.06" gradientUnits="userSpaceOnUse" x2="238.03" gradientTransform="translate(0,2)" y1="346.05" x1="236.26">
|
||||
<stop stop-color="#ace" offset="0"/>
|
||||
<stop stop-color="#369" offset="1"/>
|
||||
</linearGradient>
|
||||
<g id="folder" transform="translate(-266.06 -193.36)">
|
||||
<g transform="matrix(.066019 0 0 .066019 264.2 170.93)">
|
||||
<g transform="matrix(1.4738 0 0 1.4738 -52.053 -166.93)">
|
||||
<path fill="#69c" d="m98.424 343.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
|
||||
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="409.69" x="54.428" fill="#369"/>
|
||||
<path fill="url(#e)" d="m98.424 345.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
|
||||
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="407.69" x="54.428" fill="url(#f)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- File -->
|
||||
<linearGradient id="a">
|
||||
<stop stop-color="#cbcbcb" offset="0"/>
|
||||
<stop stop-color="#f0f0f0" offset=".34923"/>
|
||||
<stop stop-color="#e2e2e2" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="d" y2="686.15" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="207.83" gradientTransform="matrix(.28346 0 0 .31053 -608.52 485.11)" x2="380.1" x1="749.25"/>
|
||||
<linearGradient id="c" y2="287.74" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="169.44" gradientTransform="matrix(.28342 0 0 .31057 -608.52 485.11)" x2="622.33" x1="741.64"/>
|
||||
<linearGradient id="b" y2="418.54" gradientUnits="userSpaceOnUse" y1="236.13" gradientTransform="matrix(.29343 0 0 .29999 -608.52 485.11)" x2="330.88" x1="687.96">
|
||||
<stop stop-color="#fff" offset="0"/>
|
||||
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
<g id="file" transform="translate(-278.15 -216.59)">
|
||||
<g fill-rule="evenodd" transform="matrix(.19775 0 0 .19775 381.05 112.68)">
|
||||
<path d="m-520.17 525.5v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke-width=".42649" fill="#fff"/>
|
||||
<g>
|
||||
<path d="m-520.11 525.68v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke="#000" stroke-width=".42649" fill="url(#d)"/>
|
||||
<path d="m-386 562.42c-10.108-2.9925-23.206-2.5682-33.101-0.86253 1.7084-10.962 1.922-24.701-0.4271-35.877l33.528 36.739z" stroke-width=".95407pt" fill="url(#c)"/>
|
||||
<path d="m-519.13 537-0.60402 134.7h131.68l0.0755-33.296c-2.9446 1.1325-32.692-40.998-70.141-39.186-37.483 1.8137-27.785-56.777-61.006-62.214z" stroke-width="1pt" fill="url(#b)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Up arrow -->
|
||||
<g id="up-arrow" transform="translate(-279.22 -208.12)">
|
||||
<path transform="matrix(.22413 0 0 .12089 335.67 164.35)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/>
|
||||
</g>
|
||||
|
||||
<!-- Down arrow -->
|
||||
<g id="down-arrow" transform="translate(-279.22 -208.12)">
|
||||
<path transform="matrix(.22413 0 0 -.12089 335.67 257.93)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/>
|
||||
</g>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<header>
|
||||
<h1>
|
||||
{{range $url, $name := .BreadcrumbMap}}<a href="{{$url}}">{{$name}}</a>{{if ne $url "/"}}/{{end}}{{end}}
|
||||
</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div class="meta">
|
||||
<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 aria-describedby="summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{- 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{{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>
|
||||
{{- 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>
|
||||
{{- 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{{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>
|
||||
{{- 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>
|
||||
{{- else}}
|
||||
<a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified</a>
|
||||
{{- end}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{- if .CanGoUp}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="..">
|
||||
<span class="goup">Go up</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>—</td>
|
||||
<td class="hideable">—</td>
|
||||
</tr>
|
||||
{{- end}}
|
||||
{{- range .Items}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{.URL}}">
|
||||
{{- 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}}
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg>
|
||||
{{- end}}
|
||||
<span class="name">{{.Name}}</span>
|
||||
</a>
|
||||
</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}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Served with <a rel="noopener noreferrer" 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>`
|
|
@ -1,65 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/middleware/browse"
|
||||
)
|
||||
|
||||
func TestBrowse(t *testing.T) {
|
||||
|
||||
tempDirPath, err := getTempDirPath()
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
|
||||
}
|
||||
nonExistantDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano())))
|
||||
|
||||
tempTemplate, err := ioutil.TempFile(".", "tempTemplate")
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to create a temporary file in the working directory! Error was: %v", err)
|
||||
}
|
||||
defer os.Remove(tempTemplate.Name())
|
||||
|
||||
tempTemplatePath := filepath.Join(".", tempTemplate.Name())
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
expectedPathScope []string
|
||||
shouldErr bool
|
||||
}{
|
||||
// test case #0 tests handling of multiple pathscopes
|
||||
{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false},
|
||||
|
||||
// test case #1 tests instantiation of browse.Config with default values
|
||||
{"browse /", []string{"/"}, false},
|
||||
|
||||
// test case #2 tests detectaction of custom template
|
||||
{"browse . " + tempTemplatePath, []string{"."}, false},
|
||||
|
||||
// test case #3 tests detection of non-existent template
|
||||
{"browse . " + nonExistantDirPath, nil, true},
|
||||
|
||||
// test case #4 tests detection of duplicate pathscopes
|
||||
{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true},
|
||||
} {
|
||||
|
||||
recievedFunc, err := Browse(NewTestController(test.input))
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test case #%d recieved an error of %v", i, err)
|
||||
}
|
||||
if test.expectedPathScope == nil {
|
||||
continue
|
||||
}
|
||||
recievedConfigs := recievedFunc(nil).(browse.Browse).Configs
|
||||
for j, config := range recievedConfigs {
|
||||
if config.PathScope != test.expectedPathScope[j] {
|
||||
t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Controller is given to the setup function of middlewares which
|
||||
// gives them access to be able to read tokens and set config. Each
|
||||
// virtualhost gets their own server config and dispenser.
|
||||
type Controller struct {
|
||||
*server.Config
|
||||
parse.Dispenser
|
||||
|
||||
// OncePerServerBlock is a function that executes f
|
||||
// exactly once per server block, no matter how many
|
||||
// hosts are associated with it. If it is the first
|
||||
// time, the function f is executed immediately
|
||||
// (not deferred) and may return an error which is
|
||||
// returned by OncePerServerBlock.
|
||||
OncePerServerBlock func(f func() error) error
|
||||
|
||||
// ServerBlockIndex is the 0-based index of the
|
||||
// server block as it appeared in the input.
|
||||
ServerBlockIndex int
|
||||
|
||||
// ServerBlockHostIndex is the 0-based index of this
|
||||
// host as it appeared in the input at the head of the
|
||||
// server block.
|
||||
ServerBlockHostIndex int
|
||||
|
||||
// ServerBlockHosts is a list of hosts that are
|
||||
// associated with this server block. All these
|
||||
// hosts, consequently, share the same tokens.
|
||||
ServerBlockHosts []string
|
||||
|
||||
// ServerBlockStorage is used by a directive's
|
||||
// setup function to persist state between all
|
||||
// the hosts on a server block.
|
||||
ServerBlockStorage interface{}
|
||||
}
|
||||
|
||||
// NewTestController creates a new *Controller for
|
||||
// the input specified, with a filename of "Testfile".
|
||||
// The Config is bare, consisting only of a Root of cwd.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience. Does not initialize
|
||||
// the server-block-related fields.
|
||||
func NewTestController(input string) *Controller {
|
||||
return &Controller{
|
||||
Config: &server.Config{
|
||||
Root: ".",
|
||||
},
|
||||
Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
return f()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EmptyNext is a no-op function that can be passed into
|
||||
// middleware.Middleware functions so that the assignment
|
||||
// to the Next field of the Handler can be tested.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience.
|
||||
var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
|
||||
// SameNext does a pointer comparison between next1 and next2.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience.
|
||||
func SameNext(next1, next2 middleware.Handler) bool {
|
||||
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/errors"
|
||||
)
|
||||
|
||||
// Errors configures a new errors middleware instance.
|
||||
func Errors(c *Controller) (middleware.Middleware, error) {
|
||||
handler, err := errorsParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open the log file for writing when the server starts
|
||||
c.Startup = append(c.Startup, func() error {
|
||||
var err error
|
||||
var writer io.Writer
|
||||
|
||||
switch handler.LogFile {
|
||||
case "visible":
|
||||
handler.Debug = true
|
||||
case "stdout":
|
||||
writer = os.Stdout
|
||||
case "stderr":
|
||||
writer = os.Stderr
|
||||
case "syslog":
|
||||
writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if handler.LogFile == "" {
|
||||
writer = os.Stderr // default
|
||||
break
|
||||
}
|
||||
|
||||
var file *os.File
|
||||
file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if handler.LogRoller != nil {
|
||||
file.Close()
|
||||
|
||||
handler.LogRoller.Filename = handler.LogFile
|
||||
|
||||
writer = handler.LogRoller.GetLogWriter()
|
||||
} else {
|
||||
writer = file
|
||||
}
|
||||
}
|
||||
|
||||
handler.Log = log.New(writer, "", 0)
|
||||
return nil
|
||||
})
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
handler.Next = next
|
||||
return handler
|
||||
}, nil
|
||||
}
|
||||
|
||||
func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
|
||||
// Very important that we make a pointer because the Startup
|
||||
// function that opens the log file must have access to the
|
||||
// same instance of the handler, not a copy.
|
||||
handler := &errors.ErrorHandler{ErrorPages: make(map[int]string)}
|
||||
|
||||
optionalBlock := func() (bool, error) {
|
||||
var hadBlock bool
|
||||
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
|
||||
what := c.Val()
|
||||
if !c.NextArg() {
|
||||
return hadBlock, c.ArgErr()
|
||||
}
|
||||
where := c.Val()
|
||||
|
||||
if what == "log" {
|
||||
if where == "visible" {
|
||||
handler.Debug = true
|
||||
} else {
|
||||
handler.LogFile = where
|
||||
if c.NextArg() {
|
||||
if c.Val() == "{" {
|
||||
c.IncrNest()
|
||||
logRoller, err := parseRoller(c)
|
||||
if err != nil {
|
||||
return hadBlock, err
|
||||
}
|
||||
handler.LogRoller = logRoller
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Error page; ensure it exists
|
||||
where = filepath.Join(c.Root, where)
|
||||
f, err := os.Open(where)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Unable to open error page '%s': %v", where, err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
whatInt, err := strconv.Atoi(what)
|
||||
if err != nil {
|
||||
return hadBlock, c.Err("Expecting a numeric status code, got '" + what + "'")
|
||||
}
|
||||
handler.ErrorPages[whatInt] = where
|
||||
}
|
||||
}
|
||||
return hadBlock, nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
// weird hack to avoid having the handler values overwritten.
|
||||
if c.Val() == "}" {
|
||||
continue
|
||||
}
|
||||
// Configuration may be in a block
|
||||
hadBlock, err := optionalBlock()
|
||||
if err != nil {
|
||||
return handler, err
|
||||
}
|
||||
|
||||
// Otherwise, the only argument would be an error log file name or 'visible'
|
||||
if !hadBlock {
|
||||
if c.NextArg() {
|
||||
if c.Val() == "visible" {
|
||||
handler.Debug = true
|
||||
} else {
|
||||
handler.LogFile = c.Val()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/errors"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
c := NewTestController(`errors`)
|
||||
mid, err := Errors(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(*errors.ErrorHandler)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.LogFile != "" {
|
||||
t.Errorf("Expected '%s' as the default LogFile", "")
|
||||
}
|
||||
if myHandler.LogRoller != nil {
|
||||
t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller)
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
// Test Startup function
|
||||
if len(c.Startup) == 0 {
|
||||
t.Fatal("Expected 1 startup function, had 0")
|
||||
}
|
||||
c.Startup[0]()
|
||||
if myHandler.Log == nil {
|
||||
t.Error("Expected Log to be non-nil after startup because Debug is not enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorsParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputErrorsRules string
|
||||
shouldErr bool
|
||||
expectedErrorHandler errors.ErrorHandler
|
||||
}{
|
||||
{`errors`, false, errors.ErrorHandler{
|
||||
LogFile: "",
|
||||
}},
|
||||
{`errors errors.txt`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
}},
|
||||
{`errors visible`, false, errors.ErrorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log visible }`, false, errors.ErrorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log errors.txt
|
||||
404 404.html
|
||||
500 500.html
|
||||
}`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
500: "500.html",
|
||||
},
|
||||
}},
|
||||
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
LogRoller: &middleware.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
LocalTime: true,
|
||||
},
|
||||
}},
|
||||
{`errors { log errors.txt {
|
||||
size 3
|
||||
age 11
|
||||
keep 5
|
||||
}
|
||||
404 404.html
|
||||
503 503.html
|
||||
}`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
503: "503.html",
|
||||
},
|
||||
LogRoller: &middleware.LogRoller{
|
||||
MaxSize: 3,
|
||||
MaxAge: 11,
|
||||
MaxBackups: 5,
|
||||
LocalTime: true,
|
||||
},
|
||||
}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputErrorsRules)
|
||||
actualErrorsRule, err := errorsParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if actualErrorsRule.LogFile != test.expectedErrorHandler.LogFile {
|
||||
t.Errorf("Test %d expected LogFile to be %s, but got %s",
|
||||
i, test.expectedErrorHandler.LogFile, actualErrorsRule.LogFile)
|
||||
}
|
||||
if actualErrorsRule.Debug != test.expectedErrorHandler.Debug {
|
||||
t.Errorf("Test %d expected Debug to be %v, but got %v",
|
||||
i, test.expectedErrorHandler.Debug, actualErrorsRule.Debug)
|
||||
}
|
||||
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller == nil || actualErrorsRule.LogRoller == nil && test.expectedErrorHandler.LogRoller != nil {
|
||||
t.Fatalf("Test %d expected LogRoller to be %v, but got %v",
|
||||
i, test.expectedErrorHandler.LogRoller, actualErrorsRule.LogRoller)
|
||||
}
|
||||
if len(actualErrorsRule.ErrorPages) != len(test.expectedErrorHandler.ErrorPages) {
|
||||
t.Fatalf("Test %d expected %d no of Error pages, but got %d ",
|
||||
i, len(test.expectedErrorHandler.ErrorPages), len(actualErrorsRule.ErrorPages))
|
||||
}
|
||||
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller != nil {
|
||||
if actualErrorsRule.LogRoller.Filename != test.expectedErrorHandler.LogRoller.Filename {
|
||||
t.Fatalf("Test %d expected LogRoller Filename to be %s, but got %s",
|
||||
i, test.expectedErrorHandler.LogRoller.Filename, actualErrorsRule.LogRoller.Filename)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.MaxAge != test.expectedErrorHandler.LogRoller.MaxAge {
|
||||
t.Fatalf("Test %d expected LogRoller MaxAge to be %d, but got %d",
|
||||
i, test.expectedErrorHandler.LogRoller.MaxAge, actualErrorsRule.LogRoller.MaxAge)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.MaxBackups != test.expectedErrorHandler.LogRoller.MaxBackups {
|
||||
t.Fatalf("Test %d expected LogRoller MaxBackups to be %d, but got %d",
|
||||
i, test.expectedErrorHandler.LogRoller.MaxBackups, actualErrorsRule.LogRoller.MaxBackups)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.MaxSize != test.expectedErrorHandler.LogRoller.MaxSize {
|
||||
t.Fatalf("Test %d expected LogRoller MaxSize to be %d, but got %d",
|
||||
i, test.expectedErrorHandler.LogRoller.MaxSize, actualErrorsRule.LogRoller.MaxSize)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.LocalTime != test.expectedErrorHandler.LogRoller.LocalTime {
|
||||
t.Fatalf("Test %d expected LogRoller LocalTime to be %t, but got %t",
|
||||
i, test.expectedErrorHandler.LogRoller.LocalTime, actualErrorsRule.LogRoller.LocalTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
stdexpvar "expvar"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/expvar"
|
||||
)
|
||||
|
||||
// ExpVar configures a new ExpVar middleware instance.
|
||||
func ExpVar(c *Controller) (middleware.Middleware, error) {
|
||||
resource, err := expVarParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// publish any extra information/metrics we may want to capture
|
||||
publishExtraVars()
|
||||
|
||||
expvar := expvar.ExpVar{Resource: resource}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
expvar.Next = next
|
||||
return expvar
|
||||
}, nil
|
||||
}
|
||||
|
||||
func expVarParse(c *Controller) (expvar.Resource, error) {
|
||||
var resource expvar.Resource
|
||||
var err error
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 0:
|
||||
resource = expvar.Resource(defaultExpvarPath)
|
||||
case 1:
|
||||
resource = expvar.Resource(args[0])
|
||||
default:
|
||||
return resource, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
return resource, err
|
||||
}
|
||||
|
||||
func publishExtraVars() {
|
||||
// By using sync.Once instead of an init() function, we don't clutter
|
||||
// the app's expvar export unnecessarily, or risk colliding with it.
|
||||
publishOnce.Do(func() {
|
||||
stdexpvar.Publish("Goroutines", stdexpvar.Func(func() interface{} {
|
||||
return runtime.NumGoroutine()
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
var publishOnce sync.Once // publishing variables should only be done once
|
||||
var defaultExpvarPath = "/debug/vars"
|
|
@ -1,39 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/expvar"
|
||||
)
|
||||
|
||||
func TestExpvar(t *testing.T) {
|
||||
c := NewTestController(`expvar`)
|
||||
mid, err := ExpVar(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
c = NewTestController(`expvar /d/v`)
|
||||
mid, err = ExpVar(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(expvar.ExpVar)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler)
|
||||
}
|
||||
if myHandler.Resource != "/d/v" {
|
||||
t.Errorf("Expected /d/v as expvar resource")
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/extensions"
|
||||
)
|
||||
|
||||
// Ext configures a new instance of 'extensions' middleware for clean URLs.
|
||||
func Ext(c *Controller) (middleware.Middleware, error) {
|
||||
root := c.Root
|
||||
|
||||
exts, err := extParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return extensions.Ext{
|
||||
Next: next,
|
||||
Extensions: exts,
|
||||
Root: root,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extParse sets up an instance of extension middleware
|
||||
// from a middleware controller and returns a list of extensions.
|
||||
func extParse(c *Controller) ([]string, error) {
|
||||
var exts []string
|
||||
|
||||
for c.Next() {
|
||||
// At least one extension is required
|
||||
if !c.NextArg() {
|
||||
return exts, c.ArgErr()
|
||||
}
|
||||
exts = append(exts, c.Val())
|
||||
|
||||
// Tack on any other extensions that may have been listed
|
||||
exts = append(exts, c.RemainingArgs()...)
|
||||
}
|
||||
|
||||
return exts, nil
|
||||
}
|
||||
|
||||
// resourceExists returns true if the file specified at
|
||||
// root + path exists; false otherwise.
|
||||
func resourceExists(root, path string) bool {
|
||||
_, 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
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/extensions"
|
||||
)
|
||||
|
||||
func TestExt(t *testing.T) {
|
||||
c := NewTestController(`ext .html .htm .php`)
|
||||
|
||||
mid, err := Ext(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(extensions.Ext)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Ext, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Extensions[0] != ".html" {
|
||||
t.Errorf("Expected .html in the list of Extensions")
|
||||
}
|
||||
if myHandler.Extensions[1] != ".htm" {
|
||||
t.Errorf("Expected .htm in the list of Extensions")
|
||||
}
|
||||
if myHandler.Extensions[2] != ".php" {
|
||||
t.Errorf("Expected .php in the list of Extensions")
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExtParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputExts string
|
||||
shouldErr bool
|
||||
expectedExts []string
|
||||
}{
|
||||
{`ext .html .htm .php`, false, []string{".html", ".htm", ".php"}},
|
||||
{`ext .php .html .xml`, false, []string{".php", ".html", ".xml"}},
|
||||
{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputExts)
|
||||
actualExts, err := extParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actualExts) != len(test.expectedExts) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expectedExts), len(actualExts))
|
||||
}
|
||||
for j, actualExt := range actualExts {
|
||||
if actualExt != test.expectedExts[j] {
|
||||
t.Fatalf("Test %d expected %dth extension to be %s , but got %s",
|
||||
i, j, test.expectedExts[j], actualExt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/fastcgi"
|
||||
)
|
||||
|
||||
// FastCGI configures a new FastCGI middleware instance.
|
||||
func FastCGI(c *Controller) (middleware.Middleware, error) {
|
||||
absRoot, err := filepath.Abs(c.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules, err := fastcgiParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return fastcgi.Handler{
|
||||
Next: next,
|
||||
Rules: rules,
|
||||
Root: c.Root,
|
||||
AbsRoot: absRoot,
|
||||
FileSys: http.Dir(c.Root),
|
||||
SoftwareName: c.AppName,
|
||||
SoftwareVersion: c.AppVersion,
|
||||
ServerName: c.Host,
|
||||
ServerPort: c.Port,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
|
||||
var rules []fastcgi.Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule fastcgi.Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return rules, c.ArgErr()
|
||||
case 1:
|
||||
rule.Path = "/"
|
||||
rule.Address = args[0]
|
||||
case 2:
|
||||
rule.Path = args[0]
|
||||
rule.Address = args[1]
|
||||
case 3:
|
||||
rule.Path = args[0]
|
||||
rule.Address = args[1]
|
||||
err := fastcgiPreset(args[2], &rule)
|
||||
if err != nil {
|
||||
return rules, c.Err("Invalid fastcgi rule preset '" + args[2] + "'")
|
||||
}
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "ext":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.Ext = c.Val()
|
||||
case "split":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.SplitPath = c.Val()
|
||||
case "index":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.IndexFiles = args
|
||||
case "env":
|
||||
envArgs := c.RemainingArgs()
|
||||
if len(envArgs) < 2 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]})
|
||||
case "except":
|
||||
ignoredPaths := c.RemainingArgs()
|
||||
if len(ignoredPaths) == 0 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.IgnoredSubPaths = ignoredPaths
|
||||
}
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
// fastcgiPreset configures rule according to name. It returns an error if
|
||||
// name is not a recognized preset name.
|
||||
func fastcgiPreset(name string, rule *fastcgi.Rule) error {
|
||||
switch name {
|
||||
case "php":
|
||||
rule.Ext = ".php"
|
||||
rule.SplitPath = ".php"
|
||||
rule.IndexFiles = []string{"index.php"}
|
||||
default:
|
||||
return errors.New(name + " is not a valid preset name")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/fastcgi"
|
||||
)
|
||||
|
||||
func TestFastCGI(t *testing.T) {
|
||||
|
||||
c := NewTestController(`fastcgi / 127.0.0.1:9000`)
|
||||
|
||||
mid, err := FastCGI(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(fastcgi.Handler)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type , got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].Path != "/" {
|
||||
t.Errorf("Expected / as the Path")
|
||||
}
|
||||
if myHandler.Rules[0].Address != "127.0.0.1:9000" {
|
||||
t.Errorf("Expected 127.0.0.1:9000 as the Address")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFastcgiParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
shouldErr bool
|
||||
expectedFastcgiConfig []fastcgi.Rule
|
||||
}{
|
||||
|
||||
{`fastcgi /blog 127.0.0.1:9000 php`,
|
||||
false, []fastcgi.Rule{{
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000",
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
IndexFiles: []string{"index.php"},
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
}`,
|
||||
false, []fastcgi.Rule{{
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:9001",
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
except /admin /user
|
||||
}`,
|
||||
false, []fastcgi.Rule{{
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:9001",
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
IgnoredSubPaths: []string{"/admin", "/user"},
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputFastcgiConfig)
|
||||
actualFastcgiConfigs, err := fastcgiParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualFastcgiConfigs) != len(test.expectedFastcgiConfig) {
|
||||
t.Fatalf("Test %d expected %d no of FastCGI configs, but got %d ",
|
||||
i, len(test.expectedFastcgiConfig), len(actualFastcgiConfigs))
|
||||
}
|
||||
for j, actualFastcgiConfig := range actualFastcgiConfigs {
|
||||
|
||||
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Address != test.expectedFastcgiConfig[j].Address {
|
||||
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Address, actualFastcgiConfig.Address)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
|
||||
t.Errorf("Test %d expected %dth FastCGI Ext to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Ext, actualFastcgiConfig.Ext)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.SplitPath != test.expectedFastcgiConfig[j].SplitPath {
|
||||
t.Errorf("Test %d expected %dth FastCGI SplitPath to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualFastcgiConfig.IndexFiles) != fmt.Sprint(test.expectedFastcgiConfig[j].IndexFiles) {
|
||||
t.Errorf("Test %d expected %dth FastCGI IndexFiles to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].IndexFiles, actualFastcgiConfig.IndexFiles)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualFastcgiConfig.IgnoredSubPaths) != fmt.Sprint(test.expectedFastcgiConfig[j].IgnoredSubPaths) {
|
||||
t.Errorf("Test %d expected %dth FastCGI IgnoredSubPaths to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].IgnoredSubPaths, actualFastcgiConfig.IgnoredSubPaths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/headers"
|
||||
)
|
||||
|
||||
// Headers configures a new Headers middleware instance.
|
||||
func Headers(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := headersParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return headers.Headers{Next: next, Rules: rules}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func headersParse(c *Controller) ([]headers.Rule, error) {
|
||||
var rules []headers.Rule
|
||||
|
||||
for c.NextLine() {
|
||||
var head headers.Rule
|
||||
var isNewPattern bool
|
||||
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
pattern := c.Val()
|
||||
|
||||
// See if we already have a definition for this Path pattern...
|
||||
for _, h := range rules {
|
||||
if h.Path == pattern {
|
||||
head = h
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ...otherwise, this is a new pattern
|
||||
if head.Path == "" {
|
||||
head.Path = pattern
|
||||
isNewPattern = true
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
// A block of headers was opened...
|
||||
|
||||
h := headers.Header{Name: c.Val()}
|
||||
|
||||
if c.NextArg() {
|
||||
h.Value = c.Val()
|
||||
}
|
||||
|
||||
head.Headers = append(head.Headers, h)
|
||||
}
|
||||
if c.NextArg() {
|
||||
// ... or single header was defined as an argument instead.
|
||||
|
||||
h := headers.Header{Name: c.Val()}
|
||||
|
||||
h.Value = c.Val()
|
||||
|
||||
if c.NextArg() {
|
||||
h.Value = c.Val()
|
||||
}
|
||||
|
||||
head.Headers = append(head.Headers, h)
|
||||
}
|
||||
|
||||
if isNewPattern {
|
||||
rules = append(rules, head)
|
||||
} else {
|
||||
for i := 0; i < len(rules); i++ {
|
||||
if rules[i].Path == pattern {
|
||||
rules[i] = head
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/headers"
|
||||
)
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
c := NewTestController(`header / Foo Bar`)
|
||||
|
||||
mid, err := Headers(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(headers.Headers)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Headers, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadersParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []headers.Rule
|
||||
}{
|
||||
{`header /foo Foo "Bar Baz"`,
|
||||
false, []headers.Rule{
|
||||
{Path: "/foo", Headers: []headers.Header{
|
||||
{Name: "Foo", Value: "Bar Baz"},
|
||||
}},
|
||||
}},
|
||||
{`header /bar { Foo "Bar Baz" Baz Qux }`,
|
||||
false, []headers.Rule{
|
||||
{Path: "/bar", Headers: []headers.Header{
|
||||
{Name: "Foo", Value: "Bar Baz"},
|
||||
{Name: "Baz", Value: "Qux"},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := headersParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, expectedRule := range test.expected {
|
||||
actualRule := actual[j]
|
||||
|
||||
if actualRule.Path != expectedRule.Path {
|
||||
t.Errorf("Test %d, rule %d: Expected path %s, but got %s",
|
||||
i, j, expectedRule.Path, actualRule.Path)
|
||||
}
|
||||
|
||||
expectedHeaders := fmt.Sprintf("%v", expectedRule.Headers)
|
||||
actualHeaders := fmt.Sprintf("%v", actualRule.Headers)
|
||||
|
||||
if actualHeaders != expectedHeaders {
|
||||
t.Errorf("Test %d, rule %d: Expected headers %s, but got %s",
|
||||
i, j, expectedHeaders, actualHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/inner"
|
||||
)
|
||||
|
||||
// Internal configures a new Internal middleware instance.
|
||||
func Internal(c *Controller) (middleware.Middleware, error) {
|
||||
paths, err := internalParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return inner.Internal{Next: next, Paths: paths}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func internalParse(c *Controller) ([]string, error) {
|
||||
var paths []string
|
||||
|
||||
for c.Next() {
|
||||
if !c.NextArg() {
|
||||
return paths, c.ArgErr()
|
||||
}
|
||||
paths = append(paths, c.Val())
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/inner"
|
||||
)
|
||||
|
||||
func TestInternal(t *testing.T) {
|
||||
c := NewTestController(`internal /internal`)
|
||||
|
||||
mid, err := Internal(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(inner.Internal)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Internal, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Paths[0] != "/internal" {
|
||||
t.Errorf("Expected internal in the list of internal Paths")
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInternalParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputInternalPaths string
|
||||
shouldErr bool
|
||||
expectedInternalPaths []string
|
||||
}{
|
||||
{`internal /internal`, false, []string{"/internal"}},
|
||||
|
||||
{`internal /internal1
|
||||
internal /internal2`, false, []string{"/internal1", "/internal2"}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputInternalPaths)
|
||||
actualInternalPaths, err := internalParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actualInternalPaths) != len(test.expectedInternalPaths) {
|
||||
t.Fatalf("Test %d expected %d InternalPaths, but got %d",
|
||||
i, len(test.expectedInternalPaths), len(actualInternalPaths))
|
||||
}
|
||||
for j, actualInternalPath := range actualInternalPaths {
|
||||
if actualInternalPath != test.expectedInternalPaths[j] {
|
||||
t.Fatalf("Test %d expected %dth Internal Path to be %s , but got %s",
|
||||
i, j, test.expectedInternalPaths[j], actualInternalPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/markdown"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
// Markdown configures a new Markdown middleware instance.
|
||||
func Markdown(c *Controller) (middleware.Middleware, error) {
|
||||
mdconfigs, err := markdownParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md := markdown.Markdown{
|
||||
Root: c.Root,
|
||||
FileSys: http.Dir(c.Root),
|
||||
Configs: mdconfigs,
|
||||
IndexFiles: []string{"index.md"},
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
md.Next = next
|
||||
return md
|
||||
}, nil
|
||||
}
|
||||
|
||||
func markdownParse(c *Controller) ([]*markdown.Config, error) {
|
||||
var mdconfigs []*markdown.Config
|
||||
|
||||
for c.Next() {
|
||||
md := &markdown.Config{
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
Extensions: make(map[string]struct{}),
|
||||
Template: markdown.GetDefaultTemplate(),
|
||||
}
|
||||
|
||||
// Get the path scope
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 0:
|
||||
md.PathScope = "/"
|
||||
case 1:
|
||||
md.PathScope = args[0]
|
||||
default:
|
||||
return mdconfigs, c.ArgErr()
|
||||
}
|
||||
|
||||
// Load any other configuration parameters
|
||||
for c.NextBlock() {
|
||||
if err := loadParams(c, md); err != nil {
|
||||
return mdconfigs, err
|
||||
}
|
||||
}
|
||||
|
||||
// If no extensions were specified, assume some defaults
|
||||
if len(md.Extensions) == 0 {
|
||||
md.Extensions[".md"] = struct{}{}
|
||||
md.Extensions[".markdown"] = struct{}{}
|
||||
md.Extensions[".mdown"] = struct{}{}
|
||||
}
|
||||
|
||||
mdconfigs = append(mdconfigs, md)
|
||||
}
|
||||
|
||||
return mdconfigs, nil
|
||||
}
|
||||
|
||||
func loadParams(c *Controller, mdc *markdown.Config) error {
|
||||
switch c.Val() {
|
||||
case "ext":
|
||||
for _, ext := range c.RemainingArgs() {
|
||||
mdc.Extensions[ext] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
case "css":
|
||||
if !c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
mdc.Styles = append(mdc.Styles, c.Val())
|
||||
return nil
|
||||
case "js":
|
||||
if !c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
mdc.Scripts = append(mdc.Scripts, c.Val())
|
||||
return nil
|
||||
case "template":
|
||||
tArgs := c.RemainingArgs()
|
||||
switch len(tArgs) {
|
||||
default:
|
||||
return c.ArgErr()
|
||||
case 1:
|
||||
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0]))
|
||||
|
||||
if err := markdown.SetTemplate(mdc.Template, "", fpath); err != nil {
|
||||
c.Errf("default template parse error: %v", err)
|
||||
}
|
||||
return nil
|
||||
case 2:
|
||||
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1]))
|
||||
|
||||
if err := markdown.SetTemplate(mdc.Template, tArgs[0], fpath); err != nil {
|
||||
c.Errf("template parse error: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case "templatedir":
|
||||
if !c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
_, err := mdc.Template.ParseGlob(c.Val())
|
||||
if err != nil {
|
||||
c.Errf("template load error: %v", err)
|
||||
}
|
||||
if c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return c.Err("Expected valid markdown configuration property")
|
||||
}
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/markdown"
|
||||
)
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
|
||||
c := NewTestController(`markdown /blog`)
|
||||
|
||||
mid, err := Markdown(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(markdown.Markdown)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Markdown, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Configs[0].PathScope != "/blog" {
|
||||
t.Errorf("Expected /blog as the Path Scope")
|
||||
}
|
||||
if len(myHandler.Configs[0].Extensions) != 3 {
|
||||
t.Error("Expected 3 markdown extensions")
|
||||
}
|
||||
for _, key := range []string{".md", ".markdown", ".mdown"} {
|
||||
if ext, ok := myHandler.Configs[0].Extensions[key]; !ok {
|
||||
t.Errorf("Expected extensions to contain %v", ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputMarkdownConfig string
|
||||
shouldErr bool
|
||||
expectedMarkdownConfig []markdown.Config
|
||||
}{
|
||||
|
||||
{`markdown /blog {
|
||||
ext .md .txt
|
||||
css /resources/css/blog.css
|
||||
js /resources/js/blog.js
|
||||
}`, false, []markdown.Config{{
|
||||
PathScope: "/blog",
|
||||
Extensions: map[string]struct{}{
|
||||
".md": {},
|
||||
".txt": {},
|
||||
},
|
||||
Styles: []string{"/resources/css/blog.css"},
|
||||
Scripts: []string{"/resources/js/blog.js"},
|
||||
Template: markdown.GetDefaultTemplate(),
|
||||
}}},
|
||||
{`markdown /blog {
|
||||
ext .md
|
||||
template tpl_with_include.html
|
||||
}`, false, []markdown.Config{{
|
||||
PathScope: "/blog",
|
||||
Extensions: map[string]struct{}{
|
||||
".md": {},
|
||||
},
|
||||
Template: markdown.GetDefaultTemplate(),
|
||||
}}},
|
||||
}
|
||||
// Setup the extra template
|
||||
tmpl := tests[1].expectedMarkdownConfig[0].Template
|
||||
markdown.SetTemplate(tmpl, "", "./testdata/tpl_with_include.html")
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputMarkdownConfig)
|
||||
c.Root = "./testdata"
|
||||
actualMarkdownConfigs, err := markdownParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualMarkdownConfigs) != len(test.expectedMarkdownConfig) {
|
||||
t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
|
||||
i, len(test.expectedMarkdownConfig), len(actualMarkdownConfigs))
|
||||
}
|
||||
for j, actualMarkdownConfig := range actualMarkdownConfigs {
|
||||
|
||||
if actualMarkdownConfig.PathScope != test.expectedMarkdownConfig[j].PathScope {
|
||||
t.Errorf("Test %d expected %dth Markdown PathScope to be %s , but got %s",
|
||||
i, j, test.expectedMarkdownConfig[j].PathScope, actualMarkdownConfig.PathScope)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualMarkdownConfig.Styles) != fmt.Sprint(test.expectedMarkdownConfig[j].Styles) {
|
||||
t.Errorf("Test %d expected %dth Markdown Config Styles to be %s , but got %s",
|
||||
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Styles), fmt.Sprint(actualMarkdownConfig.Styles))
|
||||
}
|
||||
if fmt.Sprint(actualMarkdownConfig.Scripts) != fmt.Sprint(test.expectedMarkdownConfig[j].Scripts) {
|
||||
t.Errorf("Test %d expected %dth Markdown Config Scripts to be %s , but got %s",
|
||||
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Scripts), fmt.Sprint(actualMarkdownConfig.Scripts))
|
||||
}
|
||||
if ok, tx, ty := equalTemplates(actualMarkdownConfig.Template, test.expectedMarkdownConfig[j].Template); !ok {
|
||||
t.Errorf("Test %d the %dth Markdown Config Templates did not match, expected %s to be %s", i, j, tx, ty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func equalTemplates(i, j *template.Template) (bool, string, string) {
|
||||
// Just in case :)
|
||||
if i == j {
|
||||
return true, "", ""
|
||||
}
|
||||
|
||||
// We can't do much here, templates can't really be compared. However,
|
||||
// we can execute the templates and compare their outputs to be reasonably
|
||||
// sure that they're the same.
|
||||
|
||||
// This is exceedingly ugly.
|
||||
ctx := middleware.Context{
|
||||
Root: http.Dir("./testdata"),
|
||||
}
|
||||
|
||||
md := markdown.Data{
|
||||
Context: ctx,
|
||||
Doc: make(map[string]string),
|
||||
DocFlags: make(map[string]bool),
|
||||
Styles: []string{"style1"},
|
||||
Scripts: []string{"js1"},
|
||||
}
|
||||
md.Doc["title"] = "some title"
|
||||
md.Doc["body"] = "some body"
|
||||
|
||||
bufi := new(bytes.Buffer)
|
||||
bufj := new(bytes.Buffer)
|
||||
|
||||
if err := i.Execute(bufi, md); err != nil {
|
||||
return false, fmt.Sprintf("%v", err), ""
|
||||
}
|
||||
if err := j.Execute(bufj, md); err != nil {
|
||||
return false, "", fmt.Sprintf("%v", err)
|
||||
}
|
||||
|
||||
return bytes.Equal(bufi.Bytes(), bufj.Bytes()), string(bufi.Bytes()), string(bufj.Bytes())
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/mime"
|
||||
)
|
||||
|
||||
// Mime configures a new mime middleware instance.
|
||||
func Mime(c *Controller) (middleware.Middleware, error) {
|
||||
configs, err := mimeParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return mime.Mime{Next: next, Configs: configs}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mimeParse(c *Controller) (mime.Config, error) {
|
||||
configs := mime.Config{}
|
||||
|
||||
for c.Next() {
|
||||
// At least one extension is required
|
||||
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 2:
|
||||
if err := validateExt(configs, args[0]); err != nil {
|
||||
return configs, err
|
||||
}
|
||||
configs[args[0]] = args[1]
|
||||
case 1:
|
||||
return configs, c.ArgErr()
|
||||
case 0:
|
||||
for c.NextBlock() {
|
||||
ext := c.Val()
|
||||
if err := validateExt(configs, ext); err != nil {
|
||||
return configs, err
|
||||
}
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
configs[ext] = c.Val()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// validateExt checks for valid file name extension.
|
||||
func validateExt(configs mime.Config, ext string) error {
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
return fmt.Errorf(`mime: invalid extension "%v" (must start with dot)`, ext)
|
||||
}
|
||||
if _, ok := configs[ext]; ok {
|
||||
return fmt.Errorf(`mime: duplicate extension "%v" found`, ext)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/mime"
|
||||
)
|
||||
|
||||
func TestMime(t *testing.T) {
|
||||
|
||||
c := NewTestController(`mime .txt text/plain`)
|
||||
|
||||
mid, err := Mime(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(mime.Mime)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Mime, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`mime {`, true},
|
||||
{`mime {}`, true},
|
||||
{`mime a b`, true},
|
||||
{`mime a {`, true},
|
||||
{`mime { txt f } `, true},
|
||||
{`mime { html } `, true},
|
||||
{`mime {
|
||||
.html text/html
|
||||
.txt text/plain
|
||||
} `, false},
|
||||
{`mime {
|
||||
.foo text/foo
|
||||
.bar text/bar
|
||||
.foo text/foobar
|
||||
} `, true},
|
||||
{`mime { .html text/html } `, false},
|
||||
{`mime { .html
|
||||
} `, true},
|
||||
{`mime .txt text/plain`, false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
m, err := mimeParse(c)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil %v", i, m)
|
||||
} else if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/pprof"
|
||||
)
|
||||
|
||||
//PProf returns a new instance of a pprof handler. It accepts no arguments or options.
|
||||
func PProf(c *Controller) (middleware.Middleware, error) {
|
||||
found := false
|
||||
for c.Next() {
|
||||
if found {
|
||||
return nil, c.Err("pprof can only be specified once")
|
||||
}
|
||||
if len(c.RemainingArgs()) != 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
if c.NextBlock() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
found = true
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return &pprof.Handler{Next: next, Mux: pprof.NewMux()}
|
||||
}, nil
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package setup
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPProf(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`pprof`, false},
|
||||
{`pprof {}`, true},
|
||||
{`pprof /foo`, true},
|
||||
{`pprof {
|
||||
a b
|
||||
}`, true},
|
||||
{`pprof
|
||||
pprof`, true},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
_, err := PProf(c)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil", i)
|
||||
} else if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/proxy"
|
||||
)
|
||||
|
||||
// Proxy configures a new Proxy middleware instance.
|
||||
func Proxy(c *Controller) (middleware.Middleware, error) {
|
||||
upstreams, err := proxy.NewStaticUpstreams(c.Dispenser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return proxy.Proxy{Next: next, Upstreams: upstreams}
|
||||
}, nil
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/proxy"
|
||||
)
|
||||
|
||||
func TestUpstream(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedHosts map[string]struct{}
|
||||
}{
|
||||
// test #0 test usual to destination still works normally
|
||||
{
|
||||
"proxy / localhost:80",
|
||||
false,
|
||||
map[string]struct{}{
|
||||
"http://localhost:80": {},
|
||||
},
|
||||
},
|
||||
|
||||
// test #1 test usual to destination with port range
|
||||
{
|
||||
"proxy / localhost:8080-8082",
|
||||
false,
|
||||
map[string]struct{}{
|
||||
"http://localhost:8080": {},
|
||||
"http://localhost:8081": {},
|
||||
"http://localhost:8082": {},
|
||||
},
|
||||
},
|
||||
|
||||
// test #2 test upstream directive
|
||||
{
|
||||
"proxy / {\n upstream localhost:8080\n}",
|
||||
false,
|
||||
map[string]struct{}{
|
||||
"http://localhost:8080": {},
|
||||
},
|
||||
},
|
||||
|
||||
// test #3 test upstream directive with port range
|
||||
{
|
||||
"proxy / {\n upstream localhost:8080-8081\n}",
|
||||
false,
|
||||
map[string]struct{}{
|
||||
"http://localhost:8080": {},
|
||||
"http://localhost:8081": {},
|
||||
},
|
||||
},
|
||||
|
||||
// test #4 test to destination with upstream directive
|
||||
{
|
||||
"proxy / localhost:8080 {\n upstream localhost:8081-8082\n}",
|
||||
false,
|
||||
map[string]struct{}{
|
||||
"http://localhost:8080": {},
|
||||
"http://localhost:8081": {},
|
||||
"http://localhost:8082": {},
|
||||
},
|
||||
},
|
||||
|
||||
// test #5 test with unix sockets
|
||||
{
|
||||
"proxy / localhost:8080 {\n upstream unix:/var/foo\n}",
|
||||
false,
|
||||
map[string]struct{}{
|
||||
"http://localhost:8080": {},
|
||||
"unix:/var/foo": {},
|
||||
},
|
||||
},
|
||||
|
||||
// test #6 test fail on malformed port range
|
||||
{
|
||||
"proxy / localhost:8090-8080",
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
|
||||
// test #7 test fail on malformed port range 2
|
||||
{
|
||||
"proxy / {\n upstream localhost:80-A\n}",
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
|
||||
// test #8 test upstreams without ports work correctly
|
||||
{
|
||||
"proxy / http://localhost {\n upstream testendpoint\n}",
|
||||
false,
|
||||
map[string]struct{}{
|
||||
"http://localhost": {},
|
||||
"http://testendpoint": {},
|
||||
},
|
||||
},
|
||||
|
||||
// test #9 test several upstream directives
|
||||
{
|
||||
"proxy / localhost:8080 {\n upstream localhost:8081-8082\n upstream localhost:8083-8085\n}",
|
||||
false,
|
||||
map[string]struct{}{
|
||||
"http://localhost:8080": {},
|
||||
"http://localhost:8081": {},
|
||||
"http://localhost:8082": {},
|
||||
"http://localhost:8083": {},
|
||||
"http://localhost:8084": {},
|
||||
"http://localhost:8085": {},
|
||||
},
|
||||
},
|
||||
} {
|
||||
receivedFunc, err := Proxy(NewTestController(test.input))
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test case #%d received an error of %v", i, err)
|
||||
} else if test.shouldErr {
|
||||
continue
|
||||
}
|
||||
|
||||
upstreams := receivedFunc(nil).(proxy.Proxy).Upstreams
|
||||
for _, upstream := range upstreams {
|
||||
val := reflect.ValueOf(upstream).Elem()
|
||||
hosts := val.FieldByName("Hosts").Interface().(proxy.HostPool)
|
||||
if len(hosts) != len(test.expectedHosts) {
|
||||
t.Errorf("Test case #%d expected %d hosts but received %d", i, len(test.expectedHosts), len(hosts))
|
||||
} else {
|
||||
for _, host := range hosts {
|
||||
if _, found := test.expectedHosts[host.Name]; !found {
|
||||
t.Errorf("Test case #%d has an unexpected host %s", i, host.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
)
|
||||
|
||||
// Redir configures a new Redirect middleware instance.
|
||||
func Redir(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := redirParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return redirect.Redirect{Next: next, Rules: rules}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func redirParse(c *Controller) ([]redirect.Rule, error) {
|
||||
var redirects []redirect.Rule
|
||||
|
||||
// setRedirCode sets the redirect code for rule if it can, or returns an error
|
||||
setRedirCode := func(code string, rule *redirect.Rule) error {
|
||||
if code == "meta" {
|
||||
rule.Meta = true
|
||||
} else if codeNumber, ok := httpRedirs[code]; ok {
|
||||
rule.Code = codeNumber
|
||||
} else {
|
||||
return c.Errf("Invalid redirect code '%v'", code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkAndSaveRule checks the rule for validity (except the redir code)
|
||||
// and saves it if it's valid, or returns an error.
|
||||
checkAndSaveRule := func(rule redirect.Rule) error {
|
||||
if rule.FromPath == rule.To {
|
||||
return c.Err("'from' and 'to' values of redirect rule cannot be the same")
|
||||
}
|
||||
|
||||
for _, otherRule := range redirects {
|
||||
if otherRule.FromPath == rule.FromPath {
|
||||
return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.FromPath, otherRule.To)
|
||||
}
|
||||
}
|
||||
|
||||
redirects = append(redirects, rule)
|
||||
return nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
|
||||
var hadOptionalBlock bool
|
||||
for c.NextBlock() {
|
||||
hadOptionalBlock = true
|
||||
|
||||
var rule redirect.Rule
|
||||
|
||||
if c.Config.TLS.Enabled {
|
||||
rule.FromScheme = "https"
|
||||
} else {
|
||||
rule.FromScheme = "http"
|
||||
}
|
||||
|
||||
// Set initial redirect code
|
||||
// BUG: If the code is specified for a whole block and that code is invalid,
|
||||
// the line number will appear on the first line inside the block, even if that
|
||||
// line overwrites the block-level code with a valid redirect code. The program
|
||||
// still functions correctly, but the line number in the error reporting is
|
||||
// misleading to the user.
|
||||
if len(args) == 1 {
|
||||
err := setRedirCode(args[0], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
} else {
|
||||
rule.Code = http.StatusMovedPermanently // default code
|
||||
}
|
||||
|
||||
// RemainingArgs only gets the values after the current token, but in our
|
||||
// case we want to include the current token to get an accurate count.
|
||||
insideArgs := append([]string{c.Val()}, c.RemainingArgs()...)
|
||||
|
||||
switch len(insideArgs) {
|
||||
case 1:
|
||||
// To specified (catch-all redirect)
|
||||
// Not sure why user is doing this in a table, as it causes all other redirects to be ignored.
|
||||
// As such, this feature remains undocumented.
|
||||
rule.FromPath = "/"
|
||||
rule.To = insideArgs[0]
|
||||
case 2:
|
||||
// From and To specified
|
||||
rule.FromPath = insideArgs[0]
|
||||
rule.To = insideArgs[1]
|
||||
case 3:
|
||||
// From, To, and Code specified
|
||||
rule.FromPath = insideArgs[0]
|
||||
rule.To = insideArgs[1]
|
||||
err := setRedirCode(insideArgs[2], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
default:
|
||||
return redirects, c.ArgErr()
|
||||
}
|
||||
|
||||
err := checkAndSaveRule(rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
}
|
||||
|
||||
if !hadOptionalBlock {
|
||||
var rule redirect.Rule
|
||||
|
||||
if c.Config.TLS.Enabled {
|
||||
rule.FromScheme = "https"
|
||||
} else {
|
||||
rule.FromScheme = "http"
|
||||
}
|
||||
|
||||
rule.Code = http.StatusMovedPermanently // default
|
||||
|
||||
switch len(args) {
|
||||
case 1:
|
||||
// To specified (catch-all redirect)
|
||||
rule.FromPath = "/"
|
||||
rule.To = args[0]
|
||||
case 2:
|
||||
// To and Code specified (catch-all redirect)
|
||||
rule.FromPath = "/"
|
||||
rule.To = args[0]
|
||||
err := setRedirCode(args[1], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
case 3:
|
||||
// From, To, and Code specified
|
||||
rule.FromPath = args[0]
|
||||
rule.To = args[1]
|
||||
err := setRedirCode(args[2], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
default:
|
||||
return redirects, c.ArgErr()
|
||||
}
|
||||
|
||||
err := checkAndSaveRule(rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return redirects, nil
|
||||
}
|
||||
|
||||
// httpRedirs is a list of supported HTTP redirect codes.
|
||||
var httpRedirs = map[string]int{
|
||||
"300": http.StatusMultipleChoices,
|
||||
"301": http.StatusMovedPermanently,
|
||||
"302": http.StatusFound, // (NOT CORRECT for "Temporary Redirect", see 307)
|
||||
"303": http.StatusSeeOther,
|
||||
"304": http.StatusNotModified,
|
||||
"305": http.StatusUseProxy,
|
||||
"307": http.StatusTemporaryRedirect,
|
||||
"308": 308, // Permanent Redirect
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
)
|
||||
|
||||
func TestRedir(t *testing.T) {
|
||||
|
||||
for j, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedRules []redirect.Rule
|
||||
}{
|
||||
// test case #0 tests the recognition of a valid HTTP status code defined outside of block statement
|
||||
{"redir 300 {\n/ /foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 300}}},
|
||||
|
||||
// test case #1 tests the recognition of an invalid HTTP status code defined outside of block statement
|
||||
{"redir 9000 {\n/ /foo\n}", true, []redirect.Rule{{}}},
|
||||
|
||||
// test case #2 tests the detection of a valid HTTP status code outside of a block statement being overriden by an invalid HTTP status code inside statement of a block statement
|
||||
{"redir 300 {\n/ /foo 9000\n}", true, []redirect.Rule{{}}},
|
||||
|
||||
// test case #3 tests the detection of an invalid HTTP status code outside of a block statement being overriden by a valid HTTP status code inside statement of a block statement
|
||||
{"redir 9000 {\n/ /foo 300\n}", true, []redirect.Rule{{}}},
|
||||
|
||||
// test case #4 tests the recognition of a TO redirection in a block statement.The HTTP status code is set to the default of 301 - MovedPermanently
|
||||
{"redir 302 {\n/foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 302}}},
|
||||
|
||||
// test case #5 tests the recognition of a TO and From redirection in a block statement
|
||||
{"redir {\n/bar /foo 303\n}", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}},
|
||||
|
||||
// test case #6 tests the recognition of a TO redirection in a non-block statement. The HTTP status code is set to the default of 301 - MovedPermanently
|
||||
{"redir /foo", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 301}}},
|
||||
|
||||
// test case #7 tests the recognition of a TO and From redirection in a non-block statement
|
||||
{"redir /bar /foo 303", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}},
|
||||
|
||||
// test case #8 tests the recognition of multiple redirections
|
||||
{"redir {\n / /foo 304 \n} \n redir {\n /bar /foobar 305 \n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 304}, {FromPath: "/bar", To: "/foobar", Code: 305}}},
|
||||
|
||||
// test case #9 tests the detection of duplicate redirections
|
||||
{"redir {\n /bar /foo 304 \n} redir {\n /bar /foo 304 \n}", true, []redirect.Rule{{}}},
|
||||
} {
|
||||
recievedFunc, err := Redir(NewTestController(test.input))
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test case #%d recieved an error of %v", j, err)
|
||||
} else if test.shouldErr {
|
||||
continue
|
||||
}
|
||||
recievedRules := recievedFunc(nil).(redirect.Redirect).Rules
|
||||
|
||||
for i, recievedRule := range recievedRules {
|
||||
if recievedRule.FromPath != test.expectedRules[i].FromPath {
|
||||
t.Errorf("Test case #%d.%d expected a from path of %s, but recieved a from path of %s", j, i, test.expectedRules[i].FromPath, recievedRule.FromPath)
|
||||
}
|
||||
if recievedRule.To != test.expectedRules[i].To {
|
||||
t.Errorf("Test case #%d.%d expected a TO path of %s, but recieved a TO path of %s", j, i, test.expectedRules[i].To, recievedRule.To)
|
||||
}
|
||||
if recievedRule.Code != test.expectedRules[i].Code {
|
||||
t.Errorf("Test case #%d.%d expected a HTTP status code of %d, but recieved a code of %d", j, i, test.expectedRules[i].Code, recievedRule.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/rewrite"
|
||||
)
|
||||
|
||||
// Rewrite configures a new Rewrite middleware instance.
|
||||
func Rewrite(c *Controller) (middleware.Middleware, error) {
|
||||
rewrites, err := rewriteParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return rewrite.Rewrite{
|
||||
Next: next,
|
||||
FileSys: http.Dir(c.Root),
|
||||
Rules: rewrites,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
|
||||
var simpleRules []rewrite.Rule
|
||||
var regexpRules []rewrite.Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule rewrite.Rule
|
||||
var err error
|
||||
var base = "/"
|
||||
var pattern, to string
|
||||
var status int
|
||||
var ext []string
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
var ifs []rewrite.If
|
||||
|
||||
switch len(args) {
|
||||
case 1:
|
||||
base = args[0]
|
||||
fallthrough
|
||||
case 0:
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "r", "regexp":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
pattern = c.Val()
|
||||
case "to":
|
||||
args1 := c.RemainingArgs()
|
||||
if len(args1) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
to = strings.Join(args1, " ")
|
||||
case "ext":
|
||||
args1 := c.RemainingArgs()
|
||||
if len(args1) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
ext = args1
|
||||
case "if":
|
||||
args1 := c.RemainingArgs()
|
||||
if len(args1) != 3 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
ifCond, err := rewrite.NewIf(args1[0], args1[1], args1[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ifs = append(ifs, ifCond)
|
||||
case "status":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
status, _ = strconv.Atoi(c.Val())
|
||||
if status < 200 || (status > 299 && status < 400) || status > 499 {
|
||||
return nil, c.Err("status must be 2xx or 4xx")
|
||||
}
|
||||
default:
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
// ensure to or status is specified
|
||||
if to == "" && status == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
if rule, err = rewrite.NewComplexRule(base, pattern, to, status, ext, ifs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
regexpRules = append(regexpRules, rule)
|
||||
|
||||
// the only unhandled case is 2 and above
|
||||
default:
|
||||
rule = rewrite.NewSimpleRule(args[0], strings.Join(args[1:], " "))
|
||||
simpleRules = append(simpleRules, rule)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// put simple rules in front to avoid regexp computation for them
|
||||
return append(simpleRules, regexpRules...), nil
|
||||
}
|
|
@ -1,241 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/rewrite"
|
||||
)
|
||||
|
||||
func TestRewrite(t *testing.T) {
|
||||
c := NewTestController(`rewrite /from /to`)
|
||||
|
||||
mid, err := Rewrite(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(rewrite.Rewrite)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Rewrite, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
if len(myHandler.Rules) != 1 {
|
||||
t.Errorf("Expected handler to have %d rule, has %d instead", 1, len(myHandler.Rules))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteParse(t *testing.T) {
|
||||
simpleTests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []rewrite.Rule
|
||||
}{
|
||||
{`rewrite /from /to`, false, []rewrite.Rule{
|
||||
rewrite.SimpleRule{From: "/from", To: "/to"},
|
||||
}},
|
||||
{`rewrite /from /to
|
||||
rewrite a b`, false, []rewrite.Rule{
|
||||
rewrite.SimpleRule{From: "/from", To: "/to"},
|
||||
rewrite.SimpleRule{From: "a", To: "b"},
|
||||
}},
|
||||
{`rewrite a`, true, []rewrite.Rule{}},
|
||||
{`rewrite`, true, []rewrite.Rule{}},
|
||||
{`rewrite a b c`, false, []rewrite.Rule{
|
||||
rewrite.SimpleRule{From: "a", To: "b c"},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range simpleTests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := rewriteParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
} else if err != nil && test.shouldErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, e := range test.expected {
|
||||
actualRule := actual[j].(rewrite.SimpleRule)
|
||||
expectedRule := e.(rewrite.SimpleRule)
|
||||
|
||||
if actualRule.From != expectedRule.From {
|
||||
t.Errorf("Test %d, rule %d: Expected From=%s, got %s",
|
||||
i, j, expectedRule.From, actualRule.From)
|
||||
}
|
||||
|
||||
if actualRule.To != expectedRule.To {
|
||||
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
|
||||
i, j, expectedRule.To, actualRule.To)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
regexpTests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []rewrite.Rule
|
||||
}{
|
||||
{`rewrite {
|
||||
r .*
|
||||
to /to /index.php?
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to /index.php?", Regexp: regexp.MustCompile(".*")},
|
||||
}},
|
||||
{`rewrite {
|
||||
regexp .*
|
||||
to /to
|
||||
ext / html txt
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to", Exts: []string{"/", "html", "txt"}, Regexp: regexp.MustCompile(".*")},
|
||||
}},
|
||||
{`rewrite /path {
|
||||
r rr
|
||||
to /dest
|
||||
}
|
||||
rewrite / {
|
||||
regexp [a-z]+
|
||||
to /to /to2
|
||||
}
|
||||
`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/path", To: "/dest", Regexp: regexp.MustCompile("rr")},
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to /to2", Regexp: regexp.MustCompile("[a-z]+")},
|
||||
}},
|
||||
{`rewrite {
|
||||
r .*
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite /`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
to /to
|
||||
if {path} is a
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to", Ifs: []rewrite.If{{A: "{path}", Operator: "is", B: "a"}}},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 500
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 400
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", Status: 400},
|
||||
}},
|
||||
{`rewrite {
|
||||
to /to
|
||||
status 400
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to", Status: 400},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 399
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 200
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", Status: 200},
|
||||
}},
|
||||
{`rewrite {
|
||||
to /to
|
||||
status 200
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to", Status: 200},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 199
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 0
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
to /to
|
||||
status 0
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range regexpTests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := rewriteParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
} else if err != nil && test.shouldErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, e := range test.expected {
|
||||
actualRule := actual[j].(*rewrite.ComplexRule)
|
||||
expectedRule := e.(*rewrite.ComplexRule)
|
||||
|
||||
if actualRule.Base != expectedRule.Base {
|
||||
t.Errorf("Test %d, rule %d: Expected Base=%s, got %s",
|
||||
i, j, expectedRule.Base, actualRule.Base)
|
||||
}
|
||||
|
||||
if actualRule.To != expectedRule.To {
|
||||
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
|
||||
i, j, expectedRule.To, actualRule.To)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualRule.Exts) != fmt.Sprint(expectedRule.Exts) {
|
||||
t.Errorf("Test %d, rule %d: Expected Ext=%v, got %v",
|
||||
i, j, expectedRule.To, actualRule.To)
|
||||
}
|
||||
|
||||
if actualRule.Regexp != nil {
|
||||
if actualRule.String() != expectedRule.String() {
|
||||
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
|
||||
i, j, expectedRule.String(), actualRule.String())
|
||||
}
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualRule.Ifs) != fmt.Sprint(expectedRule.Ifs) {
|
||||
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
|
||||
i, j, fmt.Sprint(expectedRule.Ifs), fmt.Sprint(actualRule.Ifs))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
// Root sets up the root file path of the server.
|
||||
func Root(c *Controller) (middleware.Middleware, error) {
|
||||
for c.Next() {
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
c.Root = c.Val()
|
||||
}
|
||||
|
||||
// Check if root path exists
|
||||
_, err := os.Stat(c.Root)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Allow this, because the folder might appear later.
|
||||
// But make sure the user knows!
|
||||
log.Printf("[WARNING] Root path does not exist: %s", c.Root)
|
||||
} else {
|
||||
return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/templates"
|
||||
)
|
||||
|
||||
// Templates configures a new Templates middleware instance.
|
||||
func Templates(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := templatesParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tmpls := templates.Templates{
|
||||
Rules: rules,
|
||||
Root: c.Root,
|
||||
FileSys: http.Dir(c.Root),
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
tmpls.Next = next
|
||||
return tmpls
|
||||
}, nil
|
||||
}
|
||||
|
||||
func templatesParse(c *Controller) ([]templates.Rule, error) {
|
||||
var rules []templates.Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule templates.Rule
|
||||
|
||||
rule.Path = defaultTemplatePath
|
||||
rule.Extensions = defaultTemplateExtensions
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
// Optional block
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "path":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
rule.Path = args[0]
|
||||
|
||||
case "ext":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
rule.Extensions = args
|
||||
|
||||
case "between":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
rule.Delims[0] = args[0]
|
||||
rule.Delims[1] = args[1]
|
||||
}
|
||||
}
|
||||
default:
|
||||
// First argument would be the path
|
||||
rule.Path = args[0]
|
||||
|
||||
// Any remaining arguments are extensions
|
||||
rule.Extensions = args[1:]
|
||||
if len(rule.Extensions) == 0 {
|
||||
rule.Extensions = defaultTemplateExtensions
|
||||
}
|
||||
}
|
||||
|
||||
for _, ext := range rule.Extensions {
|
||||
rule.IndexFiles = append(rule.IndexFiles, "index"+ext)
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
const defaultTemplatePath = "/"
|
||||
|
||||
var defaultTemplateExtensions = []string{".html", ".htm", ".tmpl", ".tpl", ".txt"}
|
|
@ -1,112 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/templates"
|
||||
)
|
||||
|
||||
func TestTemplates(t *testing.T) {
|
||||
|
||||
c := NewTestController(`templates`)
|
||||
|
||||
mid, err := Templates(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(templates.Templates)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Templates, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].Path != defaultTemplatePath {
|
||||
t.Errorf("Expected / as the default Path")
|
||||
}
|
||||
if fmt.Sprint(myHandler.Rules[0].Extensions) != fmt.Sprint(defaultTemplateExtensions) {
|
||||
t.Errorf("Expected %v to be the Default Extensions", defaultTemplateExtensions)
|
||||
}
|
||||
var indexFiles []string
|
||||
for _, extension := range defaultTemplateExtensions {
|
||||
indexFiles = append(indexFiles, "index"+extension)
|
||||
}
|
||||
if fmt.Sprint(myHandler.Rules[0].IndexFiles) != fmt.Sprint(indexFiles) {
|
||||
t.Errorf("Expected %v to be the Default Index files", indexFiles)
|
||||
}
|
||||
if myHandler.Rules[0].Delims != [2]string{} {
|
||||
t.Errorf("Expected %v to be the Default Delims", [2]string{})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplatesParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputTemplateConfig string
|
||||
shouldErr bool
|
||||
expectedTemplateConfig []templates.Rule
|
||||
}{
|
||||
{`templates /api1`, false, []templates.Rule{{
|
||||
Path: "/api1",
|
||||
Extensions: defaultTemplateExtensions,
|
||||
Delims: [2]string{},
|
||||
}}},
|
||||
{`templates /api2 .txt .htm`, false, []templates.Rule{{
|
||||
Path: "/api2",
|
||||
Extensions: []string{".txt", ".htm"},
|
||||
Delims: [2]string{},
|
||||
}}},
|
||||
|
||||
{`templates /api3 .htm .html
|
||||
templates /api4 .txt .tpl `, false, []templates.Rule{{
|
||||
Path: "/api3",
|
||||
Extensions: []string{".htm", ".html"},
|
||||
Delims: [2]string{},
|
||||
}, {
|
||||
Path: "/api4",
|
||||
Extensions: []string{".txt", ".tpl"},
|
||||
Delims: [2]string{},
|
||||
}}},
|
||||
{`templates {
|
||||
path /api5
|
||||
ext .html
|
||||
between {% %}
|
||||
}`, false, []templates.Rule{{
|
||||
Path: "/api5",
|
||||
Extensions: []string{".html"},
|
||||
Delims: [2]string{"{%", "%}"},
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputTemplateConfig)
|
||||
actualTemplateConfigs, err := templatesParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualTemplateConfigs) != len(test.expectedTemplateConfig) {
|
||||
t.Fatalf("Test %d expected %d no of Template configs, but got %d ",
|
||||
i, len(test.expectedTemplateConfig), len(actualTemplateConfigs))
|
||||
}
|
||||
for j, actualTemplateConfig := range actualTemplateConfigs {
|
||||
|
||||
if actualTemplateConfig.Path != test.expectedTemplateConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth Template Config Path to be %s , but got %s",
|
||||
i, j, test.expectedTemplateConfig[j].Path, actualTemplateConfig.Path)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualTemplateConfig.Extensions) != fmt.Sprint(test.expectedTemplateConfig[j].Extensions) {
|
||||
t.Errorf("Expected %v to be the Extensions , but got %v instead", test.expectedTemplateConfig[j].Extensions, actualTemplateConfig.Extensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
1
caddy/setup/testdata/blog/first_post.md
vendored
1
caddy/setup/testdata/blog/first_post.md
vendored
|
@ -1 +0,0 @@
|
|||
# Test h1
|
1
caddy/setup/testdata/header.html
vendored
1
caddy/setup/testdata/header.html
vendored
|
@ -1 +0,0 @@
|
|||
<h1>Header title</h1>
|
10
caddy/setup/testdata/tpl_with_include.html
vendored
10
caddy/setup/testdata/tpl_with_include.html
vendored
|
@ -1,10 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Doc.title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{.Include "header.html"}}
|
||||
{{.Doc.body}}
|
||||
</body>
|
||||
</html>
|
|
@ -1,87 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/websocket"
|
||||
)
|
||||
|
||||
// WebSocket configures a new WebSocket middleware instance.
|
||||
func WebSocket(c *Controller) (middleware.Middleware, error) {
|
||||
|
||||
websocks, err := webSocketParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websocket.GatewayInterface = c.AppName + "-CGI/1.1"
|
||||
websocket.ServerSoftware = c.AppName + "/" + c.AppVersion
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return websocket.WebSocket{Next: next, Sockets: websocks}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func webSocketParse(c *Controller) ([]websocket.Config, error) {
|
||||
var websocks []websocket.Config
|
||||
var respawn bool
|
||||
|
||||
optionalBlock := func() (hadBlock bool, err error) {
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
if c.Val() == "respawn" {
|
||||
respawn = true
|
||||
} else {
|
||||
return true, c.Err("Expected websocket configuration parameter in block")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
var val, path, command string
|
||||
|
||||
// Path or command; not sure which yet
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
val = c.Val()
|
||||
|
||||
// Extra configuration may be in a block
|
||||
hadBlock, err := optionalBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !hadBlock {
|
||||
// The next argument on this line will be the command or an open curly brace
|
||||
if c.NextArg() {
|
||||
path = val
|
||||
command = c.Val()
|
||||
} else {
|
||||
path = "/"
|
||||
command = val
|
||||
}
|
||||
|
||||
// Okay, check again for optional block
|
||||
_, err = optionalBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Split command into the actual command and its arguments
|
||||
cmd, args, err := middleware.SplitCommandAndArgs(command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
websocks = append(websocks, websocket.Config{
|
||||
Path: path,
|
||||
Command: cmd,
|
||||
Arguments: args,
|
||||
Respawn: respawn, // TODO: This isn't used currently
|
||||
})
|
||||
}
|
||||
|
||||
return websocks, nil
|
||||
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/websocket"
|
||||
)
|
||||
|
||||
func TestWebSocket(t *testing.T) {
|
||||
|
||||
c := NewTestController(`websocket cat`)
|
||||
|
||||
mid, err := WebSocket(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(websocket.WebSocket)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Sockets[0].Path != "/" {
|
||||
t.Errorf("Expected / as the default Path")
|
||||
}
|
||||
if myHandler.Sockets[0].Command != "cat" {
|
||||
t.Errorf("Expected %s as the command", "cat")
|
||||
}
|
||||
|
||||
}
|
||||
func TestWebSocketParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputWebSocketConfig string
|
||||
shouldErr bool
|
||||
expectedWebSocketConfig []websocket.Config
|
||||
}{
|
||||
{`websocket /api1 cat`, false, []websocket.Config{{
|
||||
Path: "/api1",
|
||||
Command: "cat",
|
||||
}}},
|
||||
|
||||
{`websocket /api3 cat
|
||||
websocket /api4 cat `, false, []websocket.Config{{
|
||||
Path: "/api3",
|
||||
Command: "cat",
|
||||
}, {
|
||||
Path: "/api4",
|
||||
Command: "cat",
|
||||
}}},
|
||||
|
||||
{`websocket /api5 "cmd arg1 arg2 arg3"`, false, []websocket.Config{{
|
||||
Path: "/api5",
|
||||
Command: "cmd",
|
||||
Arguments: []string{"arg1", "arg2", "arg3"},
|
||||
}}},
|
||||
|
||||
// accept respawn
|
||||
{`websocket /api6 cat {
|
||||
respawn
|
||||
}`, false, []websocket.Config{{
|
||||
Path: "/api6",
|
||||
Command: "cat",
|
||||
}}},
|
||||
|
||||
// invalid configuration
|
||||
{`websocket /api7 cat {
|
||||
invalid
|
||||
}`, true, []websocket.Config{}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputWebSocketConfig)
|
||||
actualWebSocketConfigs, err := webSocketParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualWebSocketConfigs) != len(test.expectedWebSocketConfig) {
|
||||
t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
|
||||
i, len(test.expectedWebSocketConfig), len(actualWebSocketConfigs))
|
||||
}
|
||||
for j, actualWebSocketConfig := range actualWebSocketConfigs {
|
||||
|
||||
if actualWebSocketConfig.Path != test.expectedWebSocketConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth WebSocket Config Path to be %s , but got %s",
|
||||
i, j, test.expectedWebSocketConfig[j].Path, actualWebSocketConfig.Path)
|
||||
}
|
||||
|
||||
if actualWebSocketConfig.Command != test.expectedWebSocketConfig[j].Command {
|
||||
t.Errorf("Test %d expected %dth WebSocket Config Command to be %s , but got %s",
|
||||
i, j, test.expectedWebSocketConfig[j].Command, actualWebSocketConfig.Command)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"errors"
|
|
@ -1,4 +1,4 @@
|
|||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"reflect"
|
|
@ -4,31 +4,26 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
)
|
||||
|
||||
const filename = "Caddyfile"
|
||||
|
||||
// ToJSON converts caddyfile to its JSON representation.
|
||||
func ToJSON(caddyfile []byte) ([]byte, error) {
|
||||
var j Caddyfile
|
||||
var j EncodedCaddyfile
|
||||
|
||||
serverBlocks, err := parse.ServerBlocks(filename, bytes.NewReader(caddyfile), false)
|
||||
serverBlocks, err := ServerBlocks(filename, bytes.NewReader(caddyfile), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sb := range serverBlocks {
|
||||
block := ServerBlock{Body: [][]interface{}{}}
|
||||
|
||||
// Fill up host list
|
||||
for _, host := range sb.HostList() {
|
||||
block.Hosts = append(block.Hosts, standardizeScheme(host))
|
||||
block := EncodedServerBlock{
|
||||
Keys: sb.Keys,
|
||||
Body: [][]interface{}{},
|
||||
}
|
||||
|
||||
// Extract directives deterministically by sorting them
|
||||
|
@ -40,7 +35,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
|
|||
|
||||
// Convert each directive's tokens into our JSON structure
|
||||
for _, dir := range directives {
|
||||
disp := parse.NewDispenserTokens(filename, sb.Tokens[dir])
|
||||
disp := NewDispenserTokens(filename, sb.Tokens[dir])
|
||||
for disp.Next() {
|
||||
block.Body = append(block.Body, constructLine(&disp))
|
||||
}
|
||||
|
@ -62,7 +57,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
|
|||
// but only one line at a time, to be used at the top-level of
|
||||
// a server block only (where the first token on each line is a
|
||||
// directive) - not to be used at any other nesting level.
|
||||
func constructLine(d *parse.Dispenser) []interface{} {
|
||||
func constructLine(d *Dispenser) []interface{} {
|
||||
var args []interface{}
|
||||
|
||||
args = append(args, d.Val())
|
||||
|
@ -81,7 +76,7 @@ func constructLine(d *parse.Dispenser) []interface{} {
|
|||
// constructBlock recursively processes tokens into a
|
||||
// JSON-encodable structure. To be used in a directive's
|
||||
// block. Goes to end of block.
|
||||
func constructBlock(d *parse.Dispenser) [][]interface{} {
|
||||
func constructBlock(d *Dispenser) [][]interface{} {
|
||||
block := [][]interface{}{}
|
||||
|
||||
for d.Next() {
|
||||
|
@ -96,7 +91,7 @@ func constructBlock(d *parse.Dispenser) [][]interface{} {
|
|||
|
||||
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
|
||||
func FromJSON(jsonBytes []byte) ([]byte, error) {
|
||||
var j Caddyfile
|
||||
var j EncodedCaddyfile
|
||||
var result string
|
||||
|
||||
err := json.Unmarshal(jsonBytes, &j)
|
||||
|
@ -108,11 +103,12 @@ func FromJSON(jsonBytes []byte) ([]byte, error) {
|
|||
if sbPos > 0 {
|
||||
result += "\n\n"
|
||||
}
|
||||
for i, host := range sb.Hosts {
|
||||
for i, key := range sb.Keys {
|
||||
if i > 0 {
|
||||
result += ", "
|
||||
}
|
||||
result += standardizeScheme(host)
|
||||
//result += standardizeScheme(key)
|
||||
result += key
|
||||
}
|
||||
result += jsonToText(sb.Body, 1)
|
||||
}
|
||||
|
@ -164,6 +160,8 @@ func jsonToText(scope interface{}, depth int) string {
|
|||
return result
|
||||
}
|
||||
|
||||
// TODO: Will this function come in handy somewhere else?
|
||||
/*
|
||||
// standardizeScheme turns an address like host:https into https://host,
|
||||
// or "host:" into "host".
|
||||
func standardizeScheme(addr string) string {
|
||||
|
@ -174,12 +172,13 @@ func standardizeScheme(addr string) string {
|
|||
}
|
||||
return strings.TrimSuffix(addr, ":")
|
||||
}
|
||||
*/
|
||||
|
||||
// Caddyfile encapsulates a slice of ServerBlocks.
|
||||
type Caddyfile []ServerBlock
|
||||
// EncodedCaddyfile encapsulates a slice of EncodedServerBlocks.
|
||||
type EncodedCaddyfile []EncodedServerBlock
|
||||
|
||||
// ServerBlock represents a server block.
|
||||
type ServerBlock struct {
|
||||
Hosts []string `json:"hosts"`
|
||||
Body [][]interface{} `json:"body"`
|
||||
// EncodedServerBlock represents a server block ripe for encoding.
|
||||
type EncodedServerBlock struct {
|
||||
Keys []string `json:"keys"`
|
||||
Body [][]interface{} `json:"body"`
|
||||
}
|
|
@ -9,7 +9,7 @@ var tests = []struct {
|
|||
caddyfile: `foo {
|
||||
root /bar
|
||||
}`,
|
||||
json: `[{"hosts":["foo"],"body":[["root","/bar"]]}]`,
|
||||
json: `[{"keys":["foo"],"body":[["root","/bar"]]}]`,
|
||||
},
|
||||
{ // 1
|
||||
caddyfile: `host1, host2 {
|
||||
|
@ -17,7 +17,7 @@ var tests = []struct {
|
|||
def
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
|
||||
json: `[{"keys":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
|
||||
},
|
||||
{ // 2
|
||||
caddyfile: `host1, host2 {
|
||||
|
@ -26,58 +26,58 @@ var tests = []struct {
|
|||
jkl
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
|
||||
json: `[{"keys":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
|
||||
},
|
||||
{ // 3
|
||||
caddyfile: `host1:1234, host2:5678 {
|
||||
dir abc {
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
|
||||
json: `[{"keys":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
|
||||
},
|
||||
{ // 4
|
||||
caddyfile: `host {
|
||||
foo "bar baz"
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["foo","bar baz"]]}]`,
|
||||
json: `[{"keys":["host"],"body":[["foo","bar baz"]]}]`,
|
||||
},
|
||||
{ // 5
|
||||
caddyfile: `host, host:80 {
|
||||
foo "bar \"baz\""
|
||||
}`,
|
||||
json: `[{"hosts":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
|
||||
json: `[{"keys":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
|
||||
},
|
||||
{ // 6
|
||||
caddyfile: `host {
|
||||
foo "bar
|
||||
baz"
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["foo","bar\nbaz"]]}]`,
|
||||
json: `[{"keys":["host"],"body":[["foo","bar\nbaz"]]}]`,
|
||||
},
|
||||
{ // 7
|
||||
caddyfile: `host {
|
||||
dir 123 4.56 true
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
|
||||
json: `[{"keys":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
|
||||
},
|
||||
{ // 8
|
||||
caddyfile: `http://host, https://host {
|
||||
}`,
|
||||
json: `[{"hosts":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
|
||||
json: `[{"keys":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
|
||||
},
|
||||
{ // 9
|
||||
caddyfile: `host {
|
||||
dir1 a b
|
||||
dir2 c d
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
|
||||
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
|
||||
},
|
||||
{ // 10
|
||||
caddyfile: `host {
|
||||
dir a b
|
||||
dir c d
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
|
||||
json: `[{"keys":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
|
||||
},
|
||||
{ // 11
|
||||
caddyfile: `host {
|
||||
|
@ -87,7 +87,7 @@ baz"
|
|||
d
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
|
||||
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
|
||||
},
|
||||
{ // 12
|
||||
caddyfile: `host1 {
|
||||
|
@ -97,7 +97,7 @@ baz"
|
|||
host2 {
|
||||
dir2
|
||||
}`,
|
||||
json: `[{"hosts":["host1"],"body":[["dir1"]]},{"hosts":["host2"],"body":[["dir2"]]}]`,
|
||||
json: `[{"keys":["host1"],"body":[["dir1"]]},{"keys":["host2"],"body":[["dir2"]]}]`,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -125,17 +125,19 @@ func TestFromJSON(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Will these tests come in handy somewhere else?
|
||||
/*
|
||||
func TestStandardizeAddress(t *testing.T) {
|
||||
// host:https should be converted to https://host
|
||||
output, err := ToJSON([]byte(`host:https`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := `[{"hosts":["https://host"],"body":[]}]`, string(output); expected != actual {
|
||||
if expected, actual := `[{"keys":["https://host"],"body":[]}]`, string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
|
||||
output, err = FromJSON([]byte(`[{"hosts":["https://host"],"body":[]}]`))
|
||||
output, err = FromJSON([]byte(`[{"keys":["https://host"],"body":[]}]`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -148,10 +150,10 @@ func TestStandardizeAddress(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := `[{"hosts":["host"],"body":[]}]`, string(output); expected != actual {
|
||||
if expected, actual := `[{"keys":["host"],"body":[]}]`, string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
output, err = FromJSON([]byte(`[{"hosts":["host:"],"body":[]}]`))
|
||||
output, err = FromJSON([]byte(`[{"keys":["host:"],"body":[]}]`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -159,3 +161,4 @@ func TestStandardizeAddress(t *testing.T) {
|
|||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -1,4 +1,4 @@
|
|||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bufio"
|
|
@ -1,4 +1,4 @@
|
|||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"strings"
|
|
@ -1,18 +1,41 @@
|
|||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ServerBlocks parses the input just enough to group tokens,
|
||||
// in order, by server block. No further parsing is performed.
|
||||
// Server blocks are returned in the order in which they appear.
|
||||
// Directives that do not appear in validDirectives will cause
|
||||
// an error. If you do not want to check for valid directives,
|
||||
// pass in nil instead.
|
||||
func ServerBlocks(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
|
||||
p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
|
||||
blocks, err := p.parseAll()
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(input io.Reader) (tokens []token) {
|
||||
l := new(lexer)
|
||||
l.load(input)
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
Dispenser
|
||||
block ServerBlock // current server block being parsed
|
||||
validDirectives []string // a directive must be valid or it's an error
|
||||
eof bool // if we encounter a valid EOF in a hard place
|
||||
checkDirectives bool // if true, directives must be known
|
||||
}
|
||||
|
||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
|
@ -23,7 +46,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
|
|||
if err != nil {
|
||||
return blocks, err
|
||||
}
|
||||
if len(p.block.Addresses) > 0 {
|
||||
if len(p.block.Keys) > 0 {
|
||||
blocks = append(blocks, p.block)
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +112,7 @@ func (p *parser) addresses() error {
|
|||
break
|
||||
}
|
||||
|
||||
if tkn != "" { // empty token possible if user typed "" in Caddyfile
|
||||
if tkn != "" { // empty token possible if user typed ""
|
||||
// Trailing comma indicates another address will follow, which
|
||||
// may possibly be on the next line
|
||||
if tkn[len(tkn)-1] == ',' {
|
||||
|
@ -99,13 +122,7 @@ func (p *parser) addresses() error {
|
|||
expectingAnother = false // but we may still see another one on this line
|
||||
}
|
||||
|
||||
// Parse and save this address
|
||||
addr, err := standardAddress(tkn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.block.Addresses = append(p.block.Addresses, addr)
|
||||
p.block.Keys = append(p.block.Keys, tkn)
|
||||
}
|
||||
|
||||
// Advance token and possibly break out of loop or return error
|
||||
|
@ -253,10 +270,9 @@ func (p *parser) directive() error {
|
|||
dir := p.Val()
|
||||
nesting := 0
|
||||
|
||||
if p.checkDirectives {
|
||||
if _, ok := ValidDirectives[dir]; !ok {
|
||||
return p.Errf("Unknown directive '%s'", dir)
|
||||
}
|
||||
// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
|
||||
if !p.validDirective(dir) {
|
||||
return p.Errf("Unknown directive '%s'", dir)
|
||||
}
|
||||
|
||||
// The directive itself is appended as a relevant token
|
||||
|
@ -305,63 +321,17 @@ func (p *parser) closeCurlyBrace() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// standardAddress parses an address string into a structured format with separate
|
||||
// scheme, host, and port portions, as well as the original input string.
|
||||
func standardAddress(str string) (address, error) {
|
||||
var scheme string
|
||||
var err error
|
||||
|
||||
// first check for scheme and strip it off
|
||||
input := str
|
||||
if strings.HasPrefix(str, "https://") {
|
||||
scheme = "https"
|
||||
str = str[8:]
|
||||
} else if strings.HasPrefix(str, "http://") {
|
||||
scheme = "http"
|
||||
str = str[7:]
|
||||
// validDirective returns true if dir is in p.validDirectives.
|
||||
func (p *parser) validDirective(dir string) bool {
|
||||
if p.validDirectives == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// separate host and port
|
||||
host, port, err := net.SplitHostPort(str)
|
||||
if err != nil {
|
||||
host, port, err = net.SplitHostPort(str + ":")
|
||||
if err != nil {
|
||||
host = str
|
||||
for _, d := range p.validDirectives {
|
||||
if d == dir {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// "The host subcomponent is case-insensitive." (RFC 3986)
|
||||
host = strings.ToLower(host)
|
||||
|
||||
// see if we can set port based off scheme
|
||||
if port == "" {
|
||||
if scheme == "http" {
|
||||
port = "80"
|
||||
} else if scheme == "https" {
|
||||
port = "443"
|
||||
}
|
||||
}
|
||||
|
||||
// repeated or conflicting scheme is confusing, so error
|
||||
if scheme != "" && (port == "http" || port == "https") {
|
||||
return address{}, fmt.Errorf("[%s] scheme specified twice in address", input)
|
||||
}
|
||||
|
||||
// error if scheme and port combination violate convention
|
||||
if (scheme == "http" && port == "443") || (scheme == "https" && port == "80") {
|
||||
return address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
|
||||
}
|
||||
|
||||
// standardize http and https ports to their respective port numbers
|
||||
if port == "http" {
|
||||
scheme = "http"
|
||||
port = "80"
|
||||
} else if port == "https" {
|
||||
scheme = "https"
|
||||
port = "443"
|
||||
}
|
||||
|
||||
return address{Original: input, Scheme: scheme, Host: host, Port: port}, err
|
||||
return false
|
||||
}
|
||||
|
||||
// replaceEnvVars replaces environment variables that appear in the token
|
||||
|
@ -390,26 +360,10 @@ func replaceEnvReferences(s, refStart, refEnd string) string {
|
|||
}
|
||||
|
||||
type (
|
||||
// ServerBlock associates tokens with a list of addresses
|
||||
// and groups tokens by directive name.
|
||||
// ServerBlock associates any number of keys (usually addresses
|
||||
// of some sort) with tokens (grouped by directive name).
|
||||
ServerBlock struct {
|
||||
Addresses []address
|
||||
Tokens map[string][]token
|
||||
}
|
||||
|
||||
address struct {
|
||||
Original, Scheme, Host, Port string
|
||||
Keys []string
|
||||
Tokens map[string][]token
|
||||
}
|
||||
)
|
||||
|
||||
// HostList converts the list of addresses that are
|
||||
// associated with this server block into a slice of
|
||||
// strings, where each address is as it was originally
|
||||
// read from the input.
|
||||
func (sb ServerBlock) HostList() []string {
|
||||
sbHosts := make([]string, len(sb.Addresses))
|
||||
for j, addr := range sb.Addresses {
|
||||
sbHosts[j] = addr.Original
|
||||
}
|
||||
return sbHosts
|
||||
}
|
410
caddyfile/parse_test.go
Normal file
410
caddyfile/parse_test.go
Normal file
|
@ -0,0 +1,410 @@
|
|||
package caddyfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllTokens(t *testing.T) {
|
||||
input := strings.NewReader("a b c\nd e")
|
||||
expected := []string{"a", "b", "c", "d", "e"}
|
||||
tokens := allTokens(input)
|
||||
|
||||
if len(tokens) != len(expected) {
|
||||
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
|
||||
}
|
||||
|
||||
for i, val := range expected {
|
||||
if tokens[i].text != val {
|
||||
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOneAndImport(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
testParseOne := func(input string) (ServerBlock, error) {
|
||||
p := testParser(input)
|
||||
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
err := p.parseOne()
|
||||
return p.block, err
|
||||
}
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
keys []string
|
||||
tokens map[string]int // map of directive name to number of tokens expected
|
||||
}{
|
||||
{`localhost`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234
|
||||
dir1 foo bar`, false, []string{
|
||||
"localhost:1234",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost {
|
||||
dir1
|
||||
}`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
dir1 foo bar
|
||||
dir2
|
||||
}`, false, []string{
|
||||
"localhost:1234",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost
|
||||
dir1 foo bar`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, {
|
||||
}`, true, []string{
|
||||
"http://localhost",
|
||||
}, map[string]int{}},
|
||||
|
||||
{`host1:80, http://host2.com
|
||||
dir1 foo bar
|
||||
dir2 baz`, false, []string{
|
||||
"host1:80",
|
||||
"http://host2.com",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`http://host1.com,
|
||||
http://host2.com,
|
||||
https://host3.com`, false, []string{
|
||||
"http://host1.com",
|
||||
"http://host2.com",
|
||||
"https://host3.com",
|
||||
}, map[string]int{}},
|
||||
|
||||
{`http://host1.com:1234, https://host2.com
|
||||
dir1 foo {
|
||||
bar baz
|
||||
}
|
||||
dir2`, false, []string{
|
||||
"http://host1.com:1234",
|
||||
"https://host2.com",
|
||||
}, map[string]int{
|
||||
"dir1": 6,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
dir1 {
|
||||
bar baz
|
||||
}
|
||||
dir2 {
|
||||
foo bar
|
||||
}`, false, []string{
|
||||
"127.0.0.1",
|
||||
}, map[string]int{
|
||||
"dir1": 5,
|
||||
"dir2": 5,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
foo`, true, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
}`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
} }`, true, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
nested {
|
||||
foo
|
||||
}
|
||||
}
|
||||
dir2 foo bar`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 7,
|
||||
"dir2": 3,
|
||||
}},
|
||||
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 arg1
|
||||
import testdata/import_test1.txt`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 2,
|
||||
"dir2": 3,
|
||||
"dir3": 1,
|
||||
}},
|
||||
|
||||
{`import testdata/import_test2.txt`, false, []string{
|
||||
"host1",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, map[string]int{}},
|
||||
|
||||
{`import testdata/not_found.txt`, true, []string{}, map[string]int{}},
|
||||
|
||||
{`""`, false, []string{}, map[string]int{}},
|
||||
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(result.Keys) != len(test.keys) {
|
||||
t.Errorf("Test %d: Expected %d keys, got %d",
|
||||
i, len(test.keys), len(result.Keys))
|
||||
continue
|
||||
}
|
||||
for j, addr := range result.Keys {
|
||||
if addr != test.keys[j] {
|
||||
t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
|
||||
i, j, test.keys[j], addr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Tokens) != len(test.tokens) {
|
||||
t.Errorf("Test %d: Expected %d directives, had %d",
|
||||
i, len(test.tokens), len(result.Tokens))
|
||||
continue
|
||||
}
|
||||
for directive, tokens := range result.Tokens {
|
||||
if len(tokens) != test.tokens[directive] {
|
||||
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
|
||||
i, directive, test.tokens[directive], len(tokens))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAll(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
keys [][]string // keys per server block, in order
|
||||
}{
|
||||
{`localhost`, false, [][]string{
|
||||
{"localhost"},
|
||||
}},
|
||||
|
||||
{`localhost:1234`, false, [][]string{
|
||||
{"localhost:1234"},
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
}
|
||||
localhost:2015 {
|
||||
}`, false, [][]string{
|
||||
{"localhost:1234"},
|
||||
{"localhost:2015"},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2`, false, [][]string{
|
||||
{"localhost:1234", "http://host2"},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2,`, true, [][]string{}},
|
||||
|
||||
{`http://host1.com, http://host2.com {
|
||||
}
|
||||
https://host3.com, https://host4.com {
|
||||
}`, false, [][]string{
|
||||
{"http://host1.com", "http://host2.com"},
|
||||
{"https://host3.com", "https://host4.com"},
|
||||
}},
|
||||
|
||||
{`import testdata/import_glob*.txt`, false, [][]string{
|
||||
{"glob0.host0"},
|
||||
{"glob0.host1"},
|
||||
{"glob1.host0"},
|
||||
{"glob2.host0"},
|
||||
}},
|
||||
} {
|
||||
p := testParser(test.input)
|
||||
blocks, err := p.parseAll()
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(blocks) != len(test.keys) {
|
||||
t.Errorf("Test %d: Expected %d server blocks, got %d",
|
||||
i, len(test.keys), len(blocks))
|
||||
continue
|
||||
}
|
||||
for j, block := range blocks {
|
||||
if len(block.Keys) != len(test.keys[j]) {
|
||||
t.Errorf("Test %d: Expected %d keys in block %d, got %d",
|
||||
i, len(test.keys[j]), j, len(block.Keys))
|
||||
continue
|
||||
}
|
||||
for k, addr := range block.Keys {
|
||||
if addr != test.keys[j][k] {
|
||||
t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
|
||||
i, j, k, test.keys[j][k], addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentReplacement(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
os.Setenv("PORT", "8080")
|
||||
os.Setenv("ADDRESS", "servername.com")
|
||||
os.Setenv("FOOBAR", "foobar")
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p := testParser(`{$ADDRESS}`)
|
||||
blocks, _ := p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// multiple vars per token
|
||||
p = testParser(`{$ADDRESS}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// windows-style var and unix style in same token
|
||||
p = testParser(`{%ADDRESS%}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// reverse order
|
||||
p = testParser(`{$ADDRESS}:{%PORT%}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// env var in server block body as argument
|
||||
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// combined windows env vars in argument
|
||||
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "servername.com/foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed env var (windows)
|
||||
p = testParser(":1234\ndir1 {%ADDRESS}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "{%ADDRESS}"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed (non-existent) env var (unix)
|
||||
p = testParser(`:{$PORT$}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], ":"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// in quoted field
|
||||
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "Test foobar test"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func setupParseTests() {
|
||||
// Set up some bogus directives for testing
|
||||
//directives = []string{"dir1", "dir2", "dir3"}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
buf := strings.NewReader(input)
|
||||
p := parser{Dispenser: NewDispenser("Test", buf)}
|
||||
return p
|
||||
}
|
25
caddyhttp/bind/bind.go
Normal file
25
caddyhttp/bind/bind.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package bind
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "bind",
|
||||
ServerType: "http",
|
||||
Action: setupBind,
|
||||
})
|
||||
}
|
||||
|
||||
func setupBind(c *caddy.Controller) error {
|
||||
config := httpserver.GetConfig(c.Key)
|
||||
for c.Next() {
|
||||
if !c.Args(&config.ListenHost) {
|
||||
return c.ArgErr()
|
||||
}
|
||||
config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
|
||||
}
|
||||
return nil
|
||||
}
|
23
caddyhttp/bind/bind_test.go
Normal file
23
caddyhttp/bind/bind_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package bind
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetupBind(t *testing.T) {
|
||||
err := setupBind(caddy.NewTestController(`bind 1.2.3.4`))
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
|
||||
cfg := httpserver.GetConfig("")
|
||||
if got, want := cfg.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
if got, want := cfg.TLS.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
}
|
13
caddyhttp/caddyhttp.go
Normal file
13
caddyhttp/caddyhttp.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package caddyhttp
|
||||
|
||||
import (
|
||||
// plug in the server
|
||||
_ "github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
|
||||
// plug in the standard directives
|
||||
_ "github.com/mholt/caddy/caddyhttp/bind"
|
||||
_ "github.com/mholt/caddy/caddyhttp/gzip"
|
||||
_ "github.com/mholt/caddy/caddyhttp/log"
|
||||
_ "github.com/mholt/caddy/caddyhttp/root"
|
||||
_ "github.com/mholt/caddy/startupshutdown"
|
||||
)
|
|
@ -1,4 +1,4 @@
|
|||
// Package gzip provides a simple middleware layer that performs
|
||||
// Package gzip provides a middleware layer that performs
|
||||
// gzip compression on the response.
|
||||
package gzip
|
||||
|
||||
|
@ -12,15 +12,24 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "gzip",
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// Gzip is a middleware type which gzips HTTP responses. It is
|
||||
// imperative that any handler which writes to a gzipped response
|
||||
// specifies the Content-Type, otherwise some clients will assume
|
||||
// application/x-gzip and try to download a file.
|
||||
type Gzip struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
Configs []Config
|
||||
}
|
||||
|
||||
|
@ -36,7 +45,6 @@ func (g Gzip) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
|||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
return g.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
outer:
|
||||
for _, c := range g.Configs {
|
||||
|
||||
|
@ -79,9 +87,7 @@ outer:
|
|||
// to send something back before gzipWriter gets closed at
|
||||
// the return of this method!
|
||||
if status >= 400 {
|
||||
gz.Header().Set("Content-Type", "text/plain") // very necessary
|
||||
gz.WriteHeader(status)
|
||||
fmt.Fprintf(gz, "%d %s", status, http.StatusText(status))
|
||||
httpserver.DefaultErrorFunc(w, r, status)
|
||||
return 0, err
|
||||
}
|
||||
return status, err
|
|
@ -8,11 +8,10 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestGzipHandler(t *testing.T) {
|
||||
|
||||
pathFilter := PathFilter{make(Set)}
|
||||
badPaths := []string{"/bad", "/nogzip", "/nongzip"}
|
||||
for _, p := range badPaths {
|
||||
|
@ -80,9 +79,8 @@ func TestGzipHandler(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func nextFunc(shouldGzip bool) middleware.Handler {
|
||||
return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
|
||||
func nextFunc(shouldGzip bool) httpserver.Handler {
|
||||
return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// write a relatively large text file
|
||||
b, err := ioutil.ReadFile("testdata/test.txt")
|
||||
if err != nil {
|
|
@ -4,7 +4,7 @@ import (
|
|||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// RequestFilter determines if a request should be gzipped.
|
||||
|
@ -15,7 +15,8 @@ type RequestFilter interface {
|
|||
}
|
||||
|
||||
// defaultExtensions is the list of default extensions for which to enable gzipping.
|
||||
var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json", ".md", ".xml", ".svg"}
|
||||
var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json",
|
||||
".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp"}
|
||||
|
||||
// DefaultExtFilter creates an ExtFilter with default extensions.
|
||||
func DefaultExtFilter() ExtFilter {
|
||||
|
@ -54,7 +55,7 @@ type PathFilter struct {
|
|||
// is found and true otherwise.
|
||||
func (p PathFilter) ShouldCompress(r *http.Request) bool {
|
||||
return !p.IgnoredPaths.ContainsFunc(func(value string) bool {
|
||||
return middleware.Path(r.URL.Path).Matches(value)
|
||||
return httpserver.Path(r.URL.Path).Matches(value)
|
||||
})
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestLengthFilter(t *testing.T) {
|
||||
|
@ -61,7 +61,7 @@ func TestResponseFilterWriter(t *testing.T) {
|
|||
}}
|
||||
|
||||
for i, ts := range tests {
|
||||
server.Next = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Header().Set("Content-Length", fmt.Sprint(len(ts.body)))
|
||||
w.Write([]byte(ts.body))
|
||||
return 200, nil
|
|
@ -1,38 +1,40 @@
|
|||
package setup
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/gzip"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Gzip configures a new gzip middleware instance.
|
||||
func Gzip(c *Controller) (middleware.Middleware, error) {
|
||||
// setup configures a new gzip middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := gzipParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return gzip.Gzip{Next: next, Configs: configs}
|
||||
}, nil
|
||||
httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Gzip{Next: next, Configs: configs}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gzipParse(c *Controller) ([]gzip.Config, error) {
|
||||
var configs []gzip.Config
|
||||
func gzipParse(c *caddy.Controller) ([]Config, error) {
|
||||
var configs []Config
|
||||
|
||||
for c.Next() {
|
||||
config := gzip.Config{}
|
||||
config := Config{}
|
||||
|
||||
// Request Filters
|
||||
pathFilter := gzip.PathFilter{IgnoredPaths: make(gzip.Set)}
|
||||
extFilter := gzip.ExtFilter{Exts: make(gzip.Set)}
|
||||
pathFilter := PathFilter{IgnoredPaths: make(Set)}
|
||||
extFilter := ExtFilter{Exts: make(Set)}
|
||||
|
||||
// Response Filters
|
||||
lengthFilter := gzip.LengthFilter(0)
|
||||
lengthFilter := LengthFilter(0)
|
||||
|
||||
// No extra args expected
|
||||
if len(c.RemainingArgs()) > 0 {
|
||||
|
@ -47,7 +49,7 @@ func gzipParse(c *Controller) ([]gzip.Config, error) {
|
|||
return configs, c.ArgErr()
|
||||
}
|
||||
for _, e := range exts {
|
||||
if !strings.HasPrefix(e, ".") && e != gzip.ExtWildCard && e != "" {
|
||||
if !strings.HasPrefix(e, ".") && e != ExtWildCard && e != "" {
|
||||
return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e)
|
||||
}
|
||||
extFilter.Exts.Add(e)
|
||||
|
@ -82,18 +84,18 @@ func gzipParse(c *Controller) ([]gzip.Config, error) {
|
|||
} else if length == 0 {
|
||||
return configs, fmt.Errorf(`gzip: min_length must be greater than 0`)
|
||||
}
|
||||
lengthFilter = gzip.LengthFilter(length)
|
||||
lengthFilter = LengthFilter(length)
|
||||
default:
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
// Request Filters
|
||||
config.RequestFilters = []gzip.RequestFilter{}
|
||||
config.RequestFilters = []RequestFilter{}
|
||||
|
||||
// If ignored paths are specified, put in front to filter with path first
|
||||
if len(pathFilter.IgnoredPaths) > 0 {
|
||||
config.RequestFilters = []gzip.RequestFilter{pathFilter}
|
||||
config.RequestFilters = []RequestFilter{pathFilter}
|
||||
}
|
||||
|
||||
// Then, if extensions are specified, use those to filter.
|
||||
|
@ -101,7 +103,7 @@ func gzipParse(c *Controller) ([]gzip.Config, error) {
|
|||
if len(extFilter.Exts) > 0 {
|
||||
config.RequestFilters = append(config.RequestFilters, extFilter)
|
||||
} else {
|
||||
config.RequestFilters = append(config.RequestFilters, gzip.DefaultExtFilter())
|
||||
config.RequestFilters = append(config.RequestFilters, DefaultExtFilter())
|
||||
}
|
||||
|
||||
// Response Filters
|
|
@ -1,29 +1,29 @@
|
|||
package setup
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/gzip"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestGzip(t *testing.T) {
|
||||
c := NewTestController(`gzip`)
|
||||
|
||||
mid, err := Gzip(c)
|
||||
func TestSetup(t *testing.T) {
|
||||
err := setup(caddy.NewTestController(`gzip`))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
mids := httpserver.GetConfig("").Middleware()
|
||||
if mids == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(gzip.Gzip)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Gzip)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Gzip, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
|
@ -90,8 +90,7 @@ func TestGzip(t *testing.T) {
|
|||
`, false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
_, err := gzipParse(c)
|
||||
_, err := gzipParse(caddy.NewTestController(test.input))
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil", i)
|
||||
} else if !test.shouldErr && err != nil {
|
|
@ -1,4 +1,4 @@
|
|||
package server
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
@ -6,16 +6,20 @@ import (
|
|||
"syscall"
|
||||
)
|
||||
|
||||
// TODO: Should this be a generic graceful listener available in its own package or something?
|
||||
// Also, passing in a WaitGroup is a little awkward. Why can't this listener just keep
|
||||
// the waitgroup internal to itself?
|
||||
|
||||
// newGracefulListener returns a gracefulListener that wraps l and
|
||||
// uses wg (stored in the host server) to count connections.
|
||||
func newGracefulListener(l ListenerFile, wg *sync.WaitGroup) *gracefulListener {
|
||||
gl := &gracefulListener{ListenerFile: l, stop: make(chan error), httpWg: wg}
|
||||
func newGracefulListener(l net.Listener, wg *sync.WaitGroup) *gracefulListener {
|
||||
gl := &gracefulListener{Listener: l, stop: make(chan error), connWg: wg}
|
||||
go func() {
|
||||
<-gl.stop
|
||||
gl.Lock()
|
||||
gl.stopped = true
|
||||
gl.Unlock()
|
||||
gl.stop <- gl.ListenerFile.Close()
|
||||
gl.stop <- gl.Listener.Close()
|
||||
}()
|
||||
return gl
|
||||
}
|
||||
|
@ -24,21 +28,21 @@ func newGracefulListener(l ListenerFile, wg *sync.WaitGroup) *gracefulListener {
|
|||
// count the number of connections on it. Its
|
||||
// methods mainly wrap net.Listener to be graceful.
|
||||
type gracefulListener struct {
|
||||
ListenerFile
|
||||
net.Listener
|
||||
stop chan error
|
||||
stopped bool
|
||||
sync.Mutex // protects the stopped flag
|
||||
httpWg *sync.WaitGroup // pointer to the host's wg used for counting connections
|
||||
connWg *sync.WaitGroup // pointer to the host's wg used for counting connections
|
||||
}
|
||||
|
||||
// Accept accepts a connection.
|
||||
func (gl *gracefulListener) Accept() (c net.Conn, err error) {
|
||||
c, err = gl.ListenerFile.Accept()
|
||||
c, err = gl.Listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c = gracefulConn{Conn: c, httpWg: gl.httpWg}
|
||||
gl.httpWg.Add(1)
|
||||
c = gracefulConn{Conn: c, connWg: gl.connWg}
|
||||
gl.connWg.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -60,7 +64,7 @@ func (gl *gracefulListener) Close() error {
|
|||
// a graceful shutdown.
|
||||
type gracefulConn struct {
|
||||
net.Conn
|
||||
httpWg *sync.WaitGroup // pointer to the host server's connection waitgroup
|
||||
connWg *sync.WaitGroup // pointer to the host server's connection waitgroup
|
||||
}
|
||||
|
||||
// Close closes c's underlying connection while updating the wg count.
|
||||
|
@ -71,6 +75,6 @@ func (c gracefulConn) Close() error {
|
|||
}
|
||||
// close can fail on http2 connections (as of Oct. 2015, before http2 in std lib)
|
||||
// so don't decrement count unless close succeeds
|
||||
c.httpWg.Done()
|
||||
c.connWg.Done()
|
||||
return nil
|
||||
}
|
154
caddyhttp/httpserver/https.go
Normal file
154
caddyhttp/httpserver/https.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
func activateHTTPS() error {
|
||||
// TODO: Is this loop a bug? Should we scope this method to just a single context? (restarts...?)
|
||||
for _, ctx := range contexts {
|
||||
// pre-screen each config and earmark the ones that qualify for managed TLS
|
||||
markQualifiedForAutoHTTPS(ctx.siteConfigs)
|
||||
|
||||
// place certificates and keys on disk
|
||||
for _, c := range ctx.siteConfigs {
|
||||
err := c.TLS.ObtainCert(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update TLS configurations
|
||||
err := enableAutoHTTPS(ctx.siteConfigs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set up redirects
|
||||
ctx.siteConfigs = makePlaintextRedirects(ctx.siteConfigs)
|
||||
}
|
||||
|
||||
// renew all relevant certificates that need renewal. this is important
|
||||
// to do right away so we guarantee that renewals aren't missed, and
|
||||
// also the user can respond to any potential errors that occur.
|
||||
err := caddytls.RenewManagedCertificates(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// markQualifiedForAutoHTTPS scans each config and, if it
|
||||
// qualifies for managed TLS, it sets the Managed field of
|
||||
// the TLS config to true.
|
||||
func markQualifiedForAutoHTTPS(configs []*SiteConfig) {
|
||||
for _, cfg := range configs {
|
||||
if caddytls.QualifiesForManagedTLS(cfg) && cfg.Addr.Scheme != "http" {
|
||||
cfg.TLS.Managed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// enableAutoHTTPS configures each config to use TLS according to default settings.
|
||||
// It will only change configs that are marked as managed, and assumes that
|
||||
// certificates and keys are already on disk. If loadCertificates is true,
|
||||
// the certificates will be loaded from disk into the cache for this process
|
||||
// to use. If false, TLS will still be enabled and configured with default
|
||||
// settings, but no certificates will be parsed loaded into the cache, and
|
||||
// the returned error value will always be nil.
|
||||
func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
|
||||
for _, cfg := range configs {
|
||||
if cfg == nil || cfg.TLS == nil || !cfg.TLS.Managed {
|
||||
continue
|
||||
}
|
||||
cfg.TLS.Enabled = true
|
||||
cfg.Addr.Scheme = "https"
|
||||
if loadCertificates && caddytls.HostQualifies(cfg.Addr.Host) {
|
||||
_, err := caddytls.CacheManagedCertificate(cfg.Addr.Host, cfg.TLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure any config values not explicitly set are set to default
|
||||
caddytls.SetDefaultTLSParams(cfg.TLS)
|
||||
|
||||
// Set default port of 443 if not explicitly set
|
||||
if cfg.Addr.Port == "" &&
|
||||
cfg.TLS.Enabled &&
|
||||
(!cfg.TLS.Manual || cfg.TLS.OnDemand) &&
|
||||
cfg.Addr.Host != "localhost" {
|
||||
cfg.Addr.Port = "443"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// makePlaintextRedirects sets up redirects from port 80 to the relevant HTTPS
|
||||
// hosts. You must pass in all configs, not just configs that qualify, since
|
||||
// we must know whether the same host already exists on port 80, and those would
|
||||
// not be in a list of configs that qualify for automatic HTTPS. This function will
|
||||
// only set up redirects for configs that qualify. It returns the updated list of
|
||||
// all configs.
|
||||
func makePlaintextRedirects(allConfigs []*SiteConfig) []*SiteConfig {
|
||||
for i, cfg := range allConfigs {
|
||||
if cfg.TLS.Managed &&
|
||||
!hostHasOtherPort(allConfigs, i, "80") &&
|
||||
(cfg.Addr.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) {
|
||||
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
|
||||
}
|
||||
}
|
||||
return allConfigs
|
||||
}
|
||||
|
||||
// hostHasOtherPort returns true if there is another config in the list with the same
|
||||
// hostname that has port otherPort, or false otherwise. All the configs are checked
|
||||
// against the hostname of allConfigs[thisConfigIdx].
|
||||
func hostHasOtherPort(allConfigs []*SiteConfig, thisConfigIdx int, otherPort string) bool {
|
||||
for i, otherCfg := range allConfigs {
|
||||
if i == thisConfigIdx {
|
||||
continue // has to be a config OTHER than the one we're comparing against
|
||||
}
|
||||
if otherCfg.Addr.Host == allConfigs[thisConfigIdx].Addr.Host &&
|
||||
otherCfg.Addr.Port == otherPort {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// redirPlaintextHost returns a new plaintext HTTP configuration for
|
||||
// a virtualHost that simply redirects to cfg, which is assumed to
|
||||
// be the HTTPS configuration. The returned configuration is set
|
||||
// to listen on port 80. The TLS field of cfg must not be nil.
|
||||
func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
|
||||
redirPort := cfg.Addr.Port
|
||||
if redirPort == "443" {
|
||||
// default port is redundant
|
||||
redirPort = ""
|
||||
}
|
||||
redirMiddleware := func(next Handler) Handler {
|
||||
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
toURL := "https://" + r.Host
|
||||
if redirPort != "" {
|
||||
toURL += ":" + redirPort
|
||||
}
|
||||
toURL += r.URL.RequestURI()
|
||||
http.Redirect(w, r, toURL, http.StatusMovedPermanently)
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
host := cfg.Addr.Host
|
||||
port := "80"
|
||||
addr := net.JoinHostPort(host, port)
|
||||
return &SiteConfig{
|
||||
Addr: Address{Original: addr, Host: host, Port: port},
|
||||
ListenHost: cfg.ListenHost,
|
||||
middleware: []Middleware{redirMiddleware},
|
||||
TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort},
|
||||
}
|
||||
}
|
178
caddyhttp/httpserver/https_test.go
Normal file
178
caddyhttp/httpserver/https_test.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
cfg := redirPlaintextHost(&SiteConfig{
|
||||
Addr: Address{
|
||||
Host: "example.com",
|
||||
Port: "1234",
|
||||
},
|
||||
ListenHost: "93.184.216.34",
|
||||
TLS: new(caddytls.Config),
|
||||
})
|
||||
|
||||
// Check host and port
|
||||
if actual, expected := cfg.Addr.Host, "example.com"; actual != expected {
|
||||
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.ListenHost, "93.184.216.34"; actual != expected {
|
||||
t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.Addr.Port, "80"; actual != expected {
|
||||
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Make sure redirect handler is set up properly
|
||||
if cfg.middleware == nil || len(cfg.middleware) != 1 {
|
||||
t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.middleware)
|
||||
}
|
||||
|
||||
handler := cfg.middleware[0](nil)
|
||||
|
||||
// Check redirect for correctness
|
||||
rec := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://foo/bar?q=1", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status, err := handler.ServeHTTP(rec, req)
|
||||
if status != 0 {
|
||||
t.Errorf("Expected status return to be 0, but was %d", status)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected returned error to be nil, but was %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusMovedPermanently {
|
||||
t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code)
|
||||
}
|
||||
if got, want := rec.Header().Get("Location"), "https://foo:1234/bar?q=1"; got != want {
|
||||
t.Errorf("Expected Location: '%s' but got '%s'", want, got)
|
||||
}
|
||||
|
||||
// browsers can infer a default port from scheme, so make sure the port
|
||||
// doesn't get added in explicitly for default ports like 443 for https.
|
||||
cfg = redirPlaintextHost(&SiteConfig{Addr: Address{Host: "example.com", Port: "443"}, TLS: new(caddytls.Config)})
|
||||
handler = cfg.middleware[0](nil)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "http://foo/bar?q=1", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status, err = handler.ServeHTTP(rec, req)
|
||||
if status != 0 {
|
||||
t.Errorf("Expected status return to be 0, but was %d", status)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected returned error to be nil, but was %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusMovedPermanently {
|
||||
t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code)
|
||||
}
|
||||
if got, want := rec.Header().Get("Location"), "https://foo/bar?q=1"; got != want {
|
||||
t.Errorf("Expected Location: '%s' but got '%s'", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostHasOtherPort(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: "example.com", Port: "80"}},
|
||||
{Addr: Address{Host: "sub1.example.com", Port: "80"}},
|
||||
{Addr: Address{Host: "sub1.example.com", Port: "443"}},
|
||||
}
|
||||
|
||||
if hostHasOtherPort(configs, 0, "80") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`)
|
||||
}
|
||||
if hostHasOtherPort(configs, 0, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`)
|
||||
}
|
||||
if !hostHasOtherPort(configs, 1, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePlaintextRedirects(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
// Happy path = standard redirect from 80 to 443
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
|
||||
// Host on port 80 already defined; don't change it (no redirect)
|
||||
{Addr: Address{Host: "sub1.example.com", Port: "80", Scheme: "http"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "sub1.example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
|
||||
// Redirect from port 80 to port 5000 in this case
|
||||
{Addr: Address{Host: "sub2.example.com", Port: "5000"}, TLS: &caddytls.Config{Managed: true}},
|
||||
|
||||
// Can redirect from 80 to either 443 or 5001, but choose 443
|
||||
{Addr: Address{Host: "sub3.example.com", Port: "443"}, TLS: &caddytls.Config{Managed: true}},
|
||||
{Addr: Address{Host: "sub3.example.com", Port: "5001", Scheme: "https"}, TLS: &caddytls.Config{Managed: true}},
|
||||
}
|
||||
|
||||
result := makePlaintextRedirects(configs)
|
||||
expectedRedirCount := 3
|
||||
|
||||
if len(result) != len(configs)+expectedRedirCount {
|
||||
t.Errorf("Expected %d redirect(s) to be added, but got %d",
|
||||
expectedRedirCount, len(result)-len(configs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableAutoHTTPS(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
{}, // not managed - no changes!
|
||||
}
|
||||
|
||||
enableAutoHTTPS(configs, false)
|
||||
|
||||
if !configs[0].TLS.Enabled {
|
||||
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
|
||||
}
|
||||
if configs[0].Addr.Scheme != "https" {
|
||||
t.Errorf("Expected config 0 to have Addr.Scheme == \"https\", but it was \"%s\"",
|
||||
configs[0].Addr.Scheme)
|
||||
}
|
||||
if configs[1].TLS != nil && configs[1].TLS.Enabled {
|
||||
t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
|
||||
// TODO: caddytls.TestQualifiesForManagedTLS and this test share nearly the same config list...
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: ""}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "localhost"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "123.44.3.21"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Manual: true}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "off"}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "foo@bar.com"}},
|
||||
{Addr: Address{Host: "example.com", Scheme: "http"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "80"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "1234"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Scheme: "https"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: new(caddytls.Config)},
|
||||
}
|
||||
expectedManagedCount := 4
|
||||
|
||||
markQualifiedForAutoHTTPS(configs)
|
||||
|
||||
count := 0
|
||||
for _, cfg := range configs {
|
||||
if cfg.TLS.Managed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if count != expectedManagedCount {
|
||||
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
|
||||
}
|
||||
}
|
|
@ -1,12 +1,18 @@
|
|||
// Package middleware provides some types and functions common among middleware.
|
||||
package middleware
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initCaseSettings()
|
||||
}
|
||||
|
||||
type (
|
||||
// Middleware is the middle layer which represents the traditional
|
||||
// idea of middleware: it chains one Handler to the next by being
|
||||
|
@ -96,8 +102,55 @@ func SetLastModifiedHeader(w http.ResponseWriter, modTime time.Time) {
|
|||
w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
// CaseSensitivePath determines if paths should be case sensitive.
|
||||
// This is configurable via CASE_SENSITIVE_PATH environment variable.
|
||||
var CaseSensitivePath = true
|
||||
|
||||
const caseSensitivePathEnv = "CASE_SENSITIVE_PATH"
|
||||
|
||||
// initCaseSettings loads case sensitivity config from environment variable.
|
||||
//
|
||||
// This could have been in init, but init cannot be called from tests.
|
||||
func initCaseSettings() {
|
||||
switch os.Getenv(caseSensitivePathEnv) {
|
||||
case "0", "false":
|
||||
CaseSensitivePath = false
|
||||
default:
|
||||
CaseSensitivePath = true
|
||||
}
|
||||
}
|
||||
|
||||
// Path represents a URI path.
|
||||
type Path string
|
||||
|
||||
// Matches checks to see if other matches p.
|
||||
//
|
||||
// Path matching will probably not always be a direct
|
||||
// comparison; this method assures that paths can be
|
||||
// easily and consistently matched.
|
||||
func (p Path) Matches(other string) bool {
|
||||
if CaseSensitivePath {
|
||||
return strings.HasPrefix(string(p), other)
|
||||
}
|
||||
return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other))
|
||||
}
|
||||
|
||||
// currentTime, as it is defined here, returns time.Now().
|
||||
// It's defined as a variable for mocking time in tests.
|
||||
var currentTime = func() time.Time {
|
||||
return time.Now()
|
||||
var currentTime = func() time.Time { return time.Now() }
|
||||
|
||||
// EmptyNext is a no-op function that can be passed into
|
||||
// Middleware functions so that the assignment to the
|
||||
// Next field of the Handler can be tested.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// plugins can use this as a convenience.
|
||||
var EmptyNext = HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { return 0, nil })
|
||||
|
||||
// SameNext does a pointer comparison between next1 and next2.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// plugins can use this as a convenience.
|
||||
func SameNext(next1, next2 Handler) bool {
|
||||
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package middleware
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"os"
|
367
caddyhttp/httpserver/plugin.go
Normal file
367
caddyhttp/httpserver/plugin.go
Normal file
|
@ -0,0 +1,367 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
const serverType = "http"
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&Host, "host", DefaultHost, "Default host")
|
||||
flag.StringVar(&Port, "port", DefaultPort, "Default port")
|
||||
flag.StringVar(&Root, "root", DefaultRoot, "Root path of default site")
|
||||
flag.DurationVar(&GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown") // TODO
|
||||
flag.BoolVar(&HTTP2, "http2", true, "Use HTTP/2")
|
||||
flag.BoolVar(&QUIC, "quic", false, "Use experimental QUIC")
|
||||
|
||||
caddy.RegisterServerType(serverType, caddy.ServerType{
|
||||
Directives: directives,
|
||||
DefaultInput: func() caddy.Input {
|
||||
if Port == DefaultPort && Host != "" {
|
||||
// by leaving the port blank in this case we give auto HTTPS
|
||||
// a chance to set the port to 443 for us
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: []byte(fmt.Sprintf("%s\nroot %s", Host, Root)),
|
||||
ServerTypeName: serverType,
|
||||
}
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)),
|
||||
ServerTypeName: serverType,
|
||||
}
|
||||
},
|
||||
NewContext: newContext,
|
||||
})
|
||||
caddy.RegisterCaddyfileLoader("short", caddy.LoaderFunc(shortCaddyfileLoader))
|
||||
caddy.RegisterParsingCallback(serverType, "tls", activateHTTPS)
|
||||
caddytls.RegisterConfigGetter(serverType, func(key string) *caddytls.Config { return GetConfig(key).TLS })
|
||||
}
|
||||
|
||||
var contexts []*httpContext
|
||||
|
||||
func newContext() caddy.Context {
|
||||
context := &httpContext{keysToSiteConfigs: make(map[string]*SiteConfig)}
|
||||
contexts = append(contexts, context)
|
||||
return context
|
||||
}
|
||||
|
||||
type httpContext struct {
|
||||
// keysToSiteConfigs maps an address at the top of a
|
||||
// server block (a "key") to its SiteConfig. Not all
|
||||
// SiteConfigs will be represented here, only ones
|
||||
// that appeared in the Caddyfile.
|
||||
keysToSiteConfigs map[string]*SiteConfig
|
||||
|
||||
// siteConfigs is the master list of all site configs.
|
||||
siteConfigs []*SiteConfig
|
||||
}
|
||||
|
||||
// InspectServerBlocks make sure that everything checks out before
|
||||
// executing directives and otherwise prepares the directives to
|
||||
// be parsed and executed.
|
||||
func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
|
||||
// TODO: Here we can inspect the server blocks
|
||||
// and make changes to them, like adding a directive
|
||||
// that must always be present (e.g. 'errors discard`?) -
|
||||
// totally optional; server types need not register this
|
||||
// function.
|
||||
|
||||
// For each address in each server block, make a new config
|
||||
for _, sb := range serverBlocks {
|
||||
for _, key := range sb.Keys {
|
||||
key = strings.ToLower(key)
|
||||
if _, dup := h.keysToSiteConfigs[key]; dup {
|
||||
return serverBlocks, fmt.Errorf("duplicate site address: %s", key)
|
||||
}
|
||||
addr, err := standardizeAddress(key)
|
||||
if err != nil {
|
||||
return serverBlocks, err
|
||||
}
|
||||
// Save the config to our master list, and key it for lookups
|
||||
cfg := &SiteConfig{
|
||||
Addr: addr,
|
||||
TLS: &caddytls.Config{Hostname: addr.Host},
|
||||
HiddenFiles: []string{sourceFile},
|
||||
}
|
||||
h.siteConfigs = append(h.siteConfigs, cfg)
|
||||
h.keysToSiteConfigs[key] = cfg
|
||||
}
|
||||
}
|
||||
|
||||
return serverBlocks, nil
|
||||
}
|
||||
|
||||
// MakeServers uses the newly-created siteConfigs to
|
||||
// create and return a list of server instances.
|
||||
func (h *httpContext) MakeServers() ([]caddy.Server, error) {
|
||||
// make sure TLS is disabled for explicitly-HTTP sites
|
||||
// (necessary when HTTP address shares a block containing tls)
|
||||
for _, cfg := range h.siteConfigs {
|
||||
if cfg.TLS.Enabled && (cfg.Addr.Port == "80" || cfg.Addr.Scheme == "http") {
|
||||
cfg.TLS.Enabled = false
|
||||
log.Printf("[WARNING] TLS disabled for %s", cfg.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
// we must map (group) each config to a bind address
|
||||
groups, err := groupSiteConfigsByListenAddr(h.siteConfigs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// then we create a server for each group
|
||||
var servers []caddy.Server
|
||||
for addr, group := range groups {
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// GetConfig gets a SiteConfig that is keyed by addrKey.
|
||||
// It creates an empty one in the latest context if
|
||||
// the key does not exist in any context, so it
|
||||
// will never return nil. If no contexts exist (which
|
||||
// should never happen except in tests), it creates a
|
||||
// new context in which to put it.
|
||||
func GetConfig(addrKey string) *SiteConfig {
|
||||
for _, context := range contexts {
|
||||
if cfg, ok := context.keysToSiteConfigs[addrKey]; ok {
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
if len(contexts) == 0 {
|
||||
// this shouldn't happen except in tests
|
||||
newContext()
|
||||
}
|
||||
cfg := new(SiteConfig)
|
||||
cfg.TLS = new(caddytls.Config)
|
||||
defaultCtx := contexts[len(contexts)-1]
|
||||
defaultCtx.siteConfigs = append(defaultCtx.siteConfigs, cfg)
|
||||
defaultCtx.keysToSiteConfigs[addrKey] = cfg
|
||||
return cfg
|
||||
}
|
||||
|
||||
// shortCaddyfileLoader loads a Caddyfile if positional arguments are
|
||||
// detected, or, in other words, if un-named arguments are provided to
|
||||
// the program. A "short Caddyfile" is one in which each argument
|
||||
// is a line of the Caddyfile. The default host and port are prepended
|
||||
// according to the Host and Port values.
|
||||
func shortCaddyfileLoader(serverType string) (caddy.Input, error) {
|
||||
if flag.NArg() > 0 && serverType == "http" {
|
||||
confBody := fmt.Sprintf("%s:%s\n%s", Host, Port, strings.Join(flag.Args(), "\n"))
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: []byte(confBody),
|
||||
Filepath: "args",
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// groupSiteConfigsByListenAddr groups site configs by their listen
|
||||
// (bind) address, so sites that use the same listener can be served
|
||||
// on the same server instance. The return value maps the listen
|
||||
// address (what you pass into net.Listen) to the list of site configs.
|
||||
// This function does NOT vet the configs to ensure they are compatible.
|
||||
func groupSiteConfigsByListenAddr(configs []*SiteConfig) (map[string][]*SiteConfig, error) {
|
||||
groups := make(map[string][]*SiteConfig)
|
||||
|
||||
for _, conf := range configs {
|
||||
if caddy.IsLoopback(conf.Addr.Host) && conf.ListenHost == "" {
|
||||
// special case: one would not expect a site served
|
||||
// at loopback to be connected to from the outside.
|
||||
conf.ListenHost = conf.Addr.Host
|
||||
}
|
||||
if conf.Addr.Port == "" {
|
||||
conf.Addr.Port = Port
|
||||
}
|
||||
addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.ListenHost, conf.Addr.Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrstr := addr.String()
|
||||
groups[addrstr] = append(groups[addrstr], conf)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// AddMiddleware adds a middleware to a site's middleware stack.
|
||||
func (sc *SiteConfig) AddMiddleware(m Middleware) {
|
||||
sc.middleware = append(sc.middleware, m)
|
||||
}
|
||||
|
||||
// Address represents a site address. It contains
|
||||
// the original input value, and the component
|
||||
// parts of an address.
|
||||
type Address struct {
|
||||
Original, Scheme, Host, Port, Path string
|
||||
}
|
||||
|
||||
// String returns a human-friendly print of the address.
|
||||
func (a Address) String() string {
|
||||
scheme := a.Scheme
|
||||
if scheme == "" {
|
||||
if a.Port == "80" {
|
||||
scheme = "http"
|
||||
} else if a.Port == "443" {
|
||||
scheme = "https"
|
||||
}
|
||||
}
|
||||
s := scheme
|
||||
if s != "" {
|
||||
s += "://"
|
||||
}
|
||||
s += a.Host
|
||||
if (scheme == "https" && a.Port != "443") ||
|
||||
(scheme == "http" && a.Port != "80") {
|
||||
s += ":" + a.Port
|
||||
}
|
||||
if a.Path != "" {
|
||||
s += "/" + a.Path
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// VHost returns a sensible concatenation of Host:Port/Path from a.
|
||||
// It's basically the a.Original but without the scheme.
|
||||
func (a Address) VHost() string {
|
||||
if idx := strings.Index(a.Original, "://"); idx > -1 {
|
||||
return a.Original[idx+3:]
|
||||
}
|
||||
return a.Original
|
||||
}
|
||||
|
||||
// standardizeAddress parses an address string into a structured format with separate
|
||||
// scheme, host, and port portions, as well as the original input string.
|
||||
func standardizeAddress(str string) (Address, error) {
|
||||
input := str
|
||||
|
||||
// Split input into components (prepend with // to assert host by default)
|
||||
if !strings.Contains(str, "//") {
|
||||
str = "//" + str
|
||||
}
|
||||
u, err := url.Parse(str)
|
||||
if err != nil {
|
||||
return Address{}, err
|
||||
}
|
||||
|
||||
// separate host and port
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
host, port, err = net.SplitHostPort(u.Host + ":")
|
||||
if err != nil {
|
||||
host = u.Host
|
||||
}
|
||||
}
|
||||
|
||||
// see if we can set port based off scheme
|
||||
if port == "" {
|
||||
if u.Scheme == "http" {
|
||||
port = "80"
|
||||
} else if u.Scheme == "https" {
|
||||
port = "443"
|
||||
}
|
||||
}
|
||||
|
||||
// repeated or conflicting scheme is confusing, so error
|
||||
if u.Scheme != "" && (port == "http" || port == "https") {
|
||||
return Address{}, fmt.Errorf("[%s] scheme specified twice in address", input)
|
||||
}
|
||||
|
||||
// error if scheme and port combination violate convention
|
||||
if (u.Scheme == "http" && port == "443") || (u.Scheme == "https" && port == "80") {
|
||||
return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
|
||||
}
|
||||
|
||||
// standardize http and https ports to their respective port numbers
|
||||
if port == "http" {
|
||||
u.Scheme = "http"
|
||||
port = "80"
|
||||
} else if port == "https" {
|
||||
u.Scheme = "https"
|
||||
port = "443"
|
||||
}
|
||||
|
||||
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err
|
||||
}
|
||||
|
||||
// directives is the list of all directives known to exist for the
|
||||
// http server type, including non-standard (3rd-party) directives.
|
||||
// The ordering of this list is important.
|
||||
var directives = []string{
|
||||
// primitive actions that set up the basics of each config
|
||||
"root",
|
||||
"bind",
|
||||
"tls",
|
||||
|
||||
// these don't inject middleware handlers
|
||||
"startup",
|
||||
"shutdown",
|
||||
|
||||
// these add middleware to the stack
|
||||
"log",
|
||||
"gzip",
|
||||
"errors",
|
||||
"header",
|
||||
"rewrite",
|
||||
"redir",
|
||||
"ext",
|
||||
"mime",
|
||||
"basicauth",
|
||||
"internal",
|
||||
"pprof",
|
||||
"expvar",
|
||||
"proxy",
|
||||
"fastcgi",
|
||||
"websocket",
|
||||
"markdown",
|
||||
"templates",
|
||||
"browse",
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultHost is the default host.
|
||||
DefaultHost = ""
|
||||
// DefaultPort is the default port.
|
||||
DefaultPort = "2015"
|
||||
// DefaultRoot is the default root folder.
|
||||
DefaultRoot = "."
|
||||
)
|
||||
|
||||
// These "soft defaults" are configurable by
|
||||
// command line flags, etc.
|
||||
var (
|
||||
// Root is the site root
|
||||
Root = DefaultRoot
|
||||
|
||||
// Host is the site host
|
||||
Host = DefaultHost
|
||||
|
||||
// Port is the site port
|
||||
Port = DefaultPort
|
||||
|
||||
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
||||
GracefulTimeout time.Duration
|
||||
|
||||
// HTTP2 indicates whether HTTP2 is enabled or not.
|
||||
HTTP2 bool
|
||||
|
||||
// QUIC indicates whether QUIC is enabled or not.
|
||||
QUIC bool
|
||||
)
|
92
caddyhttp/httpserver/plugin_test.go
Normal file
92
caddyhttp/httpserver/plugin_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package httpserver
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStandardizeAddress(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
scheme, host, port, path string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`localhost`, "", "localhost", "", "", false},
|
||||
{`localhost:1234`, "", "localhost", "1234", "", false},
|
||||
{`localhost:`, "", "localhost", "", "", false},
|
||||
{`0.0.0.0`, "", "0.0.0.0", "", "", false},
|
||||
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", "", false},
|
||||
{`:1234`, "", "", "1234", "", false},
|
||||
{`[::1]`, "", "::1", "", "", false},
|
||||
{`[::1]:1234`, "", "::1", "1234", "", false},
|
||||
{`:`, "", "", "", "", false},
|
||||
{`localhost:http`, "http", "localhost", "80", "", false},
|
||||
{`localhost:https`, "https", "localhost", "443", "", false},
|
||||
{`:http`, "http", "", "80", "", false},
|
||||
{`:https`, "https", "", "443", "", false},
|
||||
{`http://localhost:https`, "", "", "", "", true}, // conflict
|
||||
{`http://localhost:http`, "", "", "", "", true}, // repeated scheme
|
||||
{`http://localhost:443`, "", "", "", "", true}, // not conventional
|
||||
{`https://localhost:80`, "", "", "", "", true}, // not conventional
|
||||
{`http://localhost`, "http", "localhost", "80", "", false},
|
||||
{`https://localhost`, "https", "localhost", "443", "", false},
|
||||
{`http://127.0.0.1`, "http", "127.0.0.1", "80", "", false},
|
||||
{`https://127.0.0.1`, "https", "127.0.0.1", "443", "", false},
|
||||
{`http://[::1]`, "http", "::1", "80", "", false},
|
||||
{`http://localhost:1234`, "http", "localhost", "1234", "", false},
|
||||
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false},
|
||||
{`http://[::1]:1234`, "http", "::1", "1234", "", false},
|
||||
{``, "", "", "", "", false},
|
||||
{`::1`, "", "::1", "", "", true},
|
||||
{`localhost::`, "", "localhost::", "", "", true},
|
||||
{`#$%@`, "", "", "", "", true},
|
||||
{`host/path`, "", "host", "", "/path", false},
|
||||
{`http://host/`, "http", "host", "80", "/", false},
|
||||
{`//asdf`, "", "asdf", "", "", false},
|
||||
{`:1234/asdf`, "", "", "1234", "/asdf", false},
|
||||
{`http://host/path`, "http", "host", "80", "/path", false},
|
||||
{`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false},
|
||||
{`host:80/path`, "", "host", "80", "/path", false},
|
||||
{`host:https/path`, "https", "host", "443", "/path", false},
|
||||
} {
|
||||
actual, err := standardizeAddress(test.input)
|
||||
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err)
|
||||
}
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected error, but had none", i, test.input)
|
||||
}
|
||||
|
||||
if !test.shouldErr && actual.Original != test.input {
|
||||
t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original)
|
||||
}
|
||||
if actual.Scheme != test.scheme {
|
||||
t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme)
|
||||
}
|
||||
if actual.Host != test.host {
|
||||
t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host)
|
||||
}
|
||||
if actual.Port != test.port {
|
||||
t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port)
|
||||
}
|
||||
if actual.Path != test.path {
|
||||
t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressVHost(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
addr Address
|
||||
expected string
|
||||
}{
|
||||
{Address{Original: "host:1234"}, "host:1234"},
|
||||
{Address{Original: "host:1234/foo"}, "host:1234/foo"},
|
||||
{Address{Original: "host/foo"}, "host/foo"},
|
||||
{Address{Original: "http://host/foo"}, "host/foo"},
|
||||
{Address{Original: "https://host/foo"}, "host/foo"},
|
||||
} {
|
||||
actual := test.addr.VHost()
|
||||
if actual != test.expected {
|
||||
t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package middleware
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bufio"
|
|
@ -1,4 +1,4 @@
|
|||
package middleware
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
|
@ -1,4 +1,4 @@
|
|||
package middleware
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net"
|
|
@ -1,4 +1,4 @@
|
|||
package middleware
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue