Commit 9b4134b2 authored by Matt Holt's avatar Matt Holt

Merge pull request #866 from mholt/0.9-wip

Merge 0.9 into master (warning: huge diff)
parents ddff0839 71c14fa1
(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. What version of Caddy are you running (`caddy -version`)?
......
package assets package caddy
import ( import (
"os" "os"
...@@ -6,10 +6,15 @@ import ( ...@@ -6,10 +6,15 @@ import (
"runtime" "runtime"
) )
// Path returns the path to the folder // AssetsPath returns the path to the folder
// where the application may store data. This // where the application may store data. If
// currently resolves to ~/.caddy // CADDYPATH env variable is set, that value
func Path() string { // 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") return filepath.Join(userHomeDir(), ".caddy")
} }
......
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", "")
}
This diff is collapsed.
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 @@ ...@@ -7,19 +7,18 @@
# $ ./build.bash [output_filename] [git_repo] # $ ./build.bash [output_filename] [git_repo]
# #
# Outputs compiled program in current directory. # Outputs compiled program in current directory.
# Default file name is 'ecaddy'.
# Default git repo is current directory. # Default git repo is current directory.
# Builds always take place from current directory. # Builds always take place from current directory.
set -euo pipefail set -euo pipefail
: ${output_filename:="${1:-}"} : ${output_filename:="${1:-}"}
: ${output_filename:="ecaddy"} : ${output_filename:="caddy"}
: ${git_repo:="${2:-}"} : ${git_repo:="${2:-}"}
: ${git_repo:="."} : ${git_repo:="."}
pkg=main pkg=github.com/mholt/caddy/caddy/caddymain
ldflags=() ldflags=()
# Timestamp of build # Timestamp of build
......
// 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
}
package main package caddymain
import ( import (
"errors" "errors"
...@@ -7,46 +7,55 @@ import ( ...@@ -7,46 +7,55 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/mholt/caddy/caddy"
"github.com/mholt/caddy/caddy/https"
"github.com/xenolf/lego/acme"
"gopkg.in/natefinch/lumberjack.v2" "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() { func init() {
caddy.TrapSignals() caddy.TrapSignals()
setVersion() 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.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")") // 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(&cpu, "cpu", "100%", "CPU cap")
flag.StringVar(&https.DefaultEmail, "email", "", "Default Let's Encrypt account email address") flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
flag.DurationVar(&caddy.GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown") flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address")
flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host")
flag.BoolVar(&caddy.HTTP2, "http2", true, "Use HTTP/2")
flag.StringVar(&logfile, "log", "", "Process log file") flag.StringVar(&logfile, "log", "", "Process log file")
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid 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)")
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") 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(&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() { // Run is Caddy's main() function.
flag.Parse() // called here in main() to allow other packages to set flags in their inits 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.AppName = appName
caddy.AppVersion = appVersion caddy.AppVersion = appVersion
acme.UserAgent = appName + "/" + appVersion acme.UserAgent = appName + "/" + appVersion
// set up process log before anything bad happens // Set up process log before anything bad happens
switch logfile { switch logfile {
case "stdout": case "stdout":
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
...@@ -63,8 +72,9 @@ func main() { ...@@ -63,8 +72,9 @@ func main() {
}) })
} }
// Check for one-time actions
if revoke != "" { if revoke != "" {
err := https.Revoke(revoke) err := caddytls.Revoke(revoke)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
...@@ -78,10 +88,8 @@ func main() { ...@@ -78,10 +88,8 @@ func main() {
} }
os.Exit(0) os.Exit(0)
} }
if directives { if plugins {
for _, d := range caddy.Directives() { fmt.Println(caddy.DescribePlugins())
fmt.Println(d)
}
os.Exit(0) os.Exit(0)
} }
...@@ -92,77 +100,124 @@ func main() { ...@@ -92,77 +100,124 @@ func main() {
} }
// Get Caddyfile input // Get Caddyfile input
caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile) caddyfile, err := caddy.LoadCaddyfile(serverType)
if err != nil { if err != nil {
mustLogFatal(err) mustLogFatal(err)
} }
// Start your engines // Start your engines
err = caddy.Start(caddyfile) instance, err := caddy.Start(caddyfile)
if err != nil { if err != nil {
mustLogFatal(err) mustLogFatal(err)
} }
// Twiddle your thumbs // 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 // 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 // if the user is still there, even if the process log was not
// enabled. If this process is a restart, however, and the user // enabled. If this process is an upgrade, however, and the user
// might not be there anymore, this just logs to the process log // might not be there anymore, this just logs to the process
// and exits. // log and exits.
func mustLogFatal(args ...interface{}) { func mustLogFatal(args ...interface{}) {
if !caddy.IsRestart() { if !caddy.IsUpgrade() {
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
} }
log.Fatal(args...) log.Fatal(args...)
} }
func loadCaddyfile() (caddy.Input, error) { // confLoader loads the Caddyfile using the -conf flag.
// Try -conf flag func confLoader(serverType string) (caddy.Input, error) {
if conf != "" { if conf == "" {
if conf == "stdin" { return nil, nil
return caddy.CaddyfileFromPipe(os.Stdin) }
}
contents, err := ioutil.ReadFile(conf)
if err != nil {
return nil, err
}
return caddy.CaddyfileInput{ if conf == "stdin" {
Contents: contents, return caddy.CaddyfileFromPipe(os.Stdin)
Filepath: conf,
RealFile: true,
}, nil
} }
// command line args contents, err := ioutil.ReadFile(conf)
if flag.NArg() > 0 { if err != nil {
confBody := caddy.Host + ":" + caddy.Port + "\n" + strings.Join(flag.Args(), "\n") return nil, err
return caddy.CaddyfileInput{
Contents: []byte(confBody),
Filepath: "args",
}, nil
} }
return caddy.CaddyfileInput{
Contents: contents,
Filepath: conf,
ServerTypeName: serverType,
}, nil
}
// Caddyfile in cwd // defaultLoader loads the Caddyfile from the current working directory.
func defaultLoader(serverType string) (caddy.Input, error) {
contents, err := ioutil.ReadFile(caddy.DefaultConfigFile) contents, err := ioutil.ReadFile(caddy.DefaultConfigFile)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return caddy.DefaultInput(), nil return nil, nil
} }
return nil, err return nil, err
} }
return caddy.CaddyfileInput{ return caddy.CaddyfileInput{
Contents: contents, Contents: contents,
Filepath: caddy.DefaultConfigFile, Filepath: caddy.DefaultConfigFile,
RealFile: true, ServerTypeName: serverType,
}, nil }, 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 // setCPU parses string cpu and sets GOMAXPROCS
// according to its value. It accepts either // according to its value. It accepts either
// a number (e.g. 3) or a percent (e.g. 50%). // a number (e.g. 3) or a percent (e.g. 50%).
...@@ -198,33 +253,17 @@ func setCPU(cpu string) error { ...@@ -198,33 +253,17 @@ func setCPU(cpu string) error {
return nil 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" const appName = "Caddy"
// Flags that control program flow or startup // Flags that control program flow or startup
var ( var (
serverType string
conf string conf string
cpu string cpu string
logfile string logfile string
revoke string revoke string
version bool version bool
directives bool plugins bool
) )
// Build information obtained with the help of -ldflags // Build information obtained with the help of -ldflags
......
This diff is collapsed.
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)
}
}
}
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)
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())
}
}
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 }
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)
}
This diff is collapsed.
This diff is collapsed.
package main
import "github.com/mholt/caddy/caddy/caddymain"
func main() {
caddymain.Run()
}
// 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{})
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)
}
}
}
// +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
}
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)
}
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
}
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
}
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
}
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
}
<!DOCTYPE html>
<html>
<head>
<title>{{.Doc.title}}</title>
</head>
<body>
{{.Include "header.html"}}
{{.Doc.body}}
</body>
</html>
package caddy package caddy
import ( import "testing"
"net/http"
"testing"
"time"
)
/*
// TODO
func TestCaddyStartStop(t *testing.T) { func TestCaddyStartStop(t *testing.T) {
caddyfile := "localhost:1984" caddyfile := "localhost:1984"
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
err := Start(CaddyfileInput{Contents: []byte(caddyfile)}) _, err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
if err != nil { if err != nil {
t.Fatalf("Error starting, iteration %d: %v", i, err) t.Fatalf("Error starting, iteration %d: %v", i, err)
} }
...@@ -30,3 +28,31 @@ func TestCaddyStartStop(t *testing.T) { ...@@ -30,3 +28,31 @@ func TestCaddyStartStop(t *testing.T) {
} }
} }
} }
*/
func TestIsLoopback(t *testing.T) {
for i, test := range []struct {
input string
expect bool
}{
{"example.com", false},
{"localhost", true},
{"localhost:1234", true},
{"localhost:", true},
{"127.0.0.1", true},
{"127.0.0.1:443", true},
{"127.0.1.5", true},
{"10.0.0.5", false},
{"12.7.0.1", false},
{"[::1]", true},
{"[::1]:1234", true},
{"::1", true},
{"::", false},
{"[::]", false},
{"local", false},
} {
if got, want := IsLoopback(test.input), test.expect; got != want {
t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got)
}
}
}
package parse package caddyfile
import ( import (
"errors" "errors"
...@@ -12,7 +12,7 @@ import ( ...@@ -12,7 +12,7 @@ import (
// some really convenient methods. // some really convenient methods.
type Dispenser struct { type Dispenser struct {
filename string filename string
tokens []token tokens []Token
cursor int cursor int
nesting int nesting int
} }
...@@ -27,7 +27,7 @@ func NewDispenser(filename string, input io.Reader) Dispenser { ...@@ -27,7 +27,7 @@ func NewDispenser(filename string, input io.Reader) Dispenser {
} }
// NewDispenserTokens returns a Dispenser filled with the given tokens. // NewDispenserTokens returns a Dispenser filled with the given tokens.
func NewDispenserTokens(filename string, tokens []token) Dispenser { func NewDispenserTokens(filename string, tokens []Token) Dispenser {
return Dispenser{ return Dispenser{
filename: filename, filename: filename,
tokens: tokens, tokens: tokens,
...@@ -59,8 +59,8 @@ func (d *Dispenser) NextArg() bool { ...@@ -59,8 +59,8 @@ func (d *Dispenser) NextArg() bool {
return false return false
} }
if d.cursor < len(d.tokens)-1 && if d.cursor < len(d.tokens)-1 &&
d.tokens[d.cursor].file == d.tokens[d.cursor+1].file && d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].line { d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
d.cursor++ d.cursor++
return true return true
} }
...@@ -80,8 +80,8 @@ func (d *Dispenser) NextLine() bool { ...@@ -80,8 +80,8 @@ func (d *Dispenser) NextLine() bool {
return false return false
} }
if d.cursor < len(d.tokens)-1 && if d.cursor < len(d.tokens)-1 &&
(d.tokens[d.cursor].file != d.tokens[d.cursor+1].file || (d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].line) { d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
d.cursor++ d.cursor++
return true return true
} }
...@@ -131,7 +131,7 @@ func (d *Dispenser) Val() string { ...@@ -131,7 +131,7 @@ func (d *Dispenser) Val() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) { if d.cursor < 0 || d.cursor >= len(d.tokens) {
return "" return ""
} }
return d.tokens[d.cursor].text return d.tokens[d.cursor].Text
} }
// Line gets the line number of the current token. If there is no token // Line gets the line number of the current token. If there is no token
...@@ -140,7 +140,7 @@ func (d *Dispenser) Line() int { ...@@ -140,7 +140,7 @@ func (d *Dispenser) Line() int {
if d.cursor < 0 || d.cursor >= len(d.tokens) { if d.cursor < 0 || d.cursor >= len(d.tokens) {
return 0 return 0
} }
return d.tokens[d.cursor].line return d.tokens[d.cursor].Line
} }
// File gets the filename of the current token. If there is no token loaded, // File gets the filename of the current token. If there is no token loaded,
...@@ -149,7 +149,7 @@ func (d *Dispenser) File() string { ...@@ -149,7 +149,7 @@ func (d *Dispenser) File() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) { if d.cursor < 0 || d.cursor >= len(d.tokens) {
return d.filename return d.filename
} }
if tokenFilename := d.tokens[d.cursor].file; tokenFilename != "" { if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
return tokenFilename return tokenFilename
} }
return d.filename return d.filename
...@@ -233,7 +233,7 @@ func (d *Dispenser) numLineBreaks(tknIdx int) int { ...@@ -233,7 +233,7 @@ func (d *Dispenser) numLineBreaks(tknIdx int) int {
if tknIdx < 0 || tknIdx >= len(d.tokens) { if tknIdx < 0 || tknIdx >= len(d.tokens) {
return 0 return 0
} }
return strings.Count(d.tokens[tknIdx].text, "\n") return strings.Count(d.tokens[tknIdx].Text, "\n")
} }
// isNewLine determines whether the current token is on a different // isNewLine determines whether the current token is on a different
...@@ -246,6 +246,6 @@ func (d *Dispenser) isNewLine() bool { ...@@ -246,6 +246,6 @@ func (d *Dispenser) isNewLine() bool {
if d.cursor > len(d.tokens)-1 { if d.cursor > len(d.tokens)-1 {
return false return false
} }
return d.tokens[d.cursor-1].file != d.tokens[d.cursor].file || return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
d.tokens[d.cursor-1].line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].line d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
} }
package parse package caddyfile
import ( import (
"reflect" "reflect"
......
...@@ -4,31 +4,26 @@ import ( ...@@ -4,31 +4,26 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"github.com/mholt/caddy/caddy/parse"
) )
const filename = "Caddyfile" const filename = "Caddyfile"
// ToJSON converts caddyfile to its JSON representation. // ToJSON converts caddyfile to its JSON representation.
func ToJSON(caddyfile []byte) ([]byte, error) { 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 { if err != nil {
return nil, err return nil, err
} }
for _, sb := range serverBlocks { for _, sb := range serverBlocks {
block := ServerBlock{Body: [][]interface{}{}} block := EncodedServerBlock{
Keys: sb.Keys,
// Fill up host list Body: [][]interface{}{},
for _, host := range sb.HostList() {
block.Hosts = append(block.Hosts, standardizeScheme(host))
} }
// Extract directives deterministically by sorting them // Extract directives deterministically by sorting them
...@@ -40,7 +35,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) { ...@@ -40,7 +35,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
// Convert each directive's tokens into our JSON structure // Convert each directive's tokens into our JSON structure
for _, dir := range directives { for _, dir := range directives {
disp := parse.NewDispenserTokens(filename, sb.Tokens[dir]) disp := NewDispenserTokens(filename, sb.Tokens[dir])
for disp.Next() { for disp.Next() {
block.Body = append(block.Body, constructLine(&disp)) block.Body = append(block.Body, constructLine(&disp))
} }
...@@ -62,7 +57,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) { ...@@ -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 // 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 // a server block only (where the first token on each line is a
// directive) - not to be used at any other nesting level. // directive) - not to be used at any other nesting level.
func constructLine(d *parse.Dispenser) []interface{} { func constructLine(d *Dispenser) []interface{} {
var args []interface{} var args []interface{}
args = append(args, d.Val()) args = append(args, d.Val())
...@@ -81,7 +76,7 @@ func constructLine(d *parse.Dispenser) []interface{} { ...@@ -81,7 +76,7 @@ func constructLine(d *parse.Dispenser) []interface{} {
// constructBlock recursively processes tokens into a // constructBlock recursively processes tokens into a
// JSON-encodable structure. To be used in a directive's // JSON-encodable structure. To be used in a directive's
// block. Goes to end of block. // block. Goes to end of block.
func constructBlock(d *parse.Dispenser) [][]interface{} { func constructBlock(d *Dispenser) [][]interface{} {
block := [][]interface{}{} block := [][]interface{}{}
for d.Next() { for d.Next() {
...@@ -96,7 +91,7 @@ func constructBlock(d *parse.Dispenser) [][]interface{} { ...@@ -96,7 +91,7 @@ func constructBlock(d *parse.Dispenser) [][]interface{} {
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text // FromJSON converts JSON-encoded jsonBytes to Caddyfile text
func FromJSON(jsonBytes []byte) ([]byte, error) { func FromJSON(jsonBytes []byte) ([]byte, error) {
var j Caddyfile var j EncodedCaddyfile
var result string var result string
err := json.Unmarshal(jsonBytes, &j) err := json.Unmarshal(jsonBytes, &j)
...@@ -108,11 +103,12 @@ func FromJSON(jsonBytes []byte) ([]byte, error) { ...@@ -108,11 +103,12 @@ func FromJSON(jsonBytes []byte) ([]byte, error) {
if sbPos > 0 { if sbPos > 0 {
result += "\n\n" result += "\n\n"
} }
for i, host := range sb.Hosts { for i, key := range sb.Keys {
if i > 0 { if i > 0 {
result += ", " result += ", "
} }
result += standardizeScheme(host) //result += standardizeScheme(key)
result += key
} }
result += jsonToText(sb.Body, 1) result += jsonToText(sb.Body, 1)
} }
...@@ -164,6 +160,8 @@ func jsonToText(scope interface{}, depth int) string { ...@@ -164,6 +160,8 @@ func jsonToText(scope interface{}, depth int) string {
return result return result
} }
// TODO: Will this function come in handy somewhere else?
/*
// standardizeScheme turns an address like host:https into https://host, // standardizeScheme turns an address like host:https into https://host,
// or "host:" into "host". // or "host:" into "host".
func standardizeScheme(addr string) string { func standardizeScheme(addr string) string {
...@@ -174,12 +172,13 @@ func standardizeScheme(addr string) string { ...@@ -174,12 +172,13 @@ func standardizeScheme(addr string) string {
} }
return strings.TrimSuffix(addr, ":") return strings.TrimSuffix(addr, ":")
} }
*/
// Caddyfile encapsulates a slice of ServerBlocks. // EncodedCaddyfile encapsulates a slice of EncodedServerBlocks.
type Caddyfile []ServerBlock type EncodedCaddyfile []EncodedServerBlock
// ServerBlock represents a server block. // EncodedServerBlock represents a server block ripe for encoding.
type ServerBlock struct { type EncodedServerBlock struct {
Hosts []string `json:"hosts"` Keys []string `json:"keys"`
Body [][]interface{} `json:"body"` Body [][]interface{} `json:"body"`
} }
...@@ -9,7 +9,7 @@ var tests = []struct { ...@@ -9,7 +9,7 @@ var tests = []struct {
caddyfile: `foo { caddyfile: `foo {
root /bar root /bar
}`, }`,
json: `[{"hosts":["foo"],"body":[["root","/bar"]]}]`, json: `[{"keys":["foo"],"body":[["root","/bar"]]}]`,
}, },
{ // 1 { // 1
caddyfile: `host1, host2 { caddyfile: `host1, host2 {
...@@ -17,7 +17,7 @@ var tests = []struct { ...@@ -17,7 +17,7 @@ var tests = []struct {
def def
} }
}`, }`,
json: `[{"hosts":["host1","host2"],"body":[["dir",[["def"]]]]}]`, json: `[{"keys":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
}, },
{ // 2 { // 2
caddyfile: `host1, host2 { caddyfile: `host1, host2 {
...@@ -26,58 +26,58 @@ var tests = []struct { ...@@ -26,58 +26,58 @@ var tests = []struct {
jkl jkl
} }
}`, }`,
json: `[{"hosts":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`, json: `[{"keys":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
}, },
{ // 3 { // 3
caddyfile: `host1:1234, host2:5678 { caddyfile: `host1:1234, host2:5678 {
dir abc { dir abc {
} }
}`, }`,
json: `[{"hosts":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`, json: `[{"keys":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
}, },
{ // 4 { // 4
caddyfile: `host { caddyfile: `host {
foo "bar baz" foo "bar baz"
}`, }`,
json: `[{"hosts":["host"],"body":[["foo","bar baz"]]}]`, json: `[{"keys":["host"],"body":[["foo","bar baz"]]}]`,
}, },
{ // 5 { // 5
caddyfile: `host, host:80 { caddyfile: `host, host:80 {
foo "bar \"baz\"" foo "bar \"baz\""
}`, }`,
json: `[{"hosts":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`, json: `[{"keys":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
}, },
{ // 6 { // 6
caddyfile: `host { caddyfile: `host {
foo "bar foo "bar
baz" baz"
}`, }`,
json: `[{"hosts":["host"],"body":[["foo","bar\nbaz"]]}]`, json: `[{"keys":["host"],"body":[["foo","bar\nbaz"]]}]`,
}, },
{ // 7 { // 7
caddyfile: `host { caddyfile: `host {
dir 123 4.56 true 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 { // 8
caddyfile: `http://host, https://host { 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 { // 9
caddyfile: `host { caddyfile: `host {
dir1 a b dir1 a b
dir2 c d dir2 c d
}`, }`,
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`, json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
}, },
{ // 10 { // 10
caddyfile: `host { caddyfile: `host {
dir a b dir a b
dir c d dir c d
}`, }`,
json: `[{"hosts":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`, json: `[{"keys":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
}, },
{ // 11 { // 11
caddyfile: `host { caddyfile: `host {
...@@ -87,7 +87,7 @@ baz" ...@@ -87,7 +87,7 @@ baz"
d d
} }
}`, }`,
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`, json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
}, },
{ // 12 { // 12
caddyfile: `host1 { caddyfile: `host1 {
...@@ -97,7 +97,7 @@ baz" ...@@ -97,7 +97,7 @@ baz"
host2 { host2 {
dir2 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) { ...@@ -125,17 +125,19 @@ func TestFromJSON(t *testing.T) {
} }
} }
// TODO: Will these tests come in handy somewhere else?
/*
func TestStandardizeAddress(t *testing.T) { func TestStandardizeAddress(t *testing.T) {
// host:https should be converted to https://host // host:https should be converted to https://host
output, err := ToJSON([]byte(`host:https`)) output, err := ToJSON([]byte(`host:https`))
if err != nil { if err != nil {
t.Fatal(err) 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) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -148,10 +150,10 @@ func TestStandardizeAddress(t *testing.T) { ...@@ -148,10 +150,10 @@ func TestStandardizeAddress(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) 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) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -159,3 +161,4 @@ func TestStandardizeAddress(t *testing.T) { ...@@ -159,3 +161,4 @@ func TestStandardizeAddress(t *testing.T) {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual) t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
} }
} }
*/
package parse package caddyfile
import ( import (
"bufio" "bufio"
...@@ -13,15 +13,15 @@ type ( ...@@ -13,15 +13,15 @@ type (
// in quotes if it contains whitespace. // in quotes if it contains whitespace.
lexer struct { lexer struct {
reader *bufio.Reader reader *bufio.Reader
token token token Token
line int line int
} }
// token represents a single parsable unit. // Token represents a single parsable unit.
token struct { Token struct {
file string File string
line int Line int
text string Text string
} }
) )
...@@ -47,7 +47,7 @@ func (l *lexer) next() bool { ...@@ -47,7 +47,7 @@ func (l *lexer) next() bool {
var comment, quoted, escaped bool var comment, quoted, escaped bool
makeToken := func() bool { makeToken := func() bool {
l.token.text = string(val) l.token.Text = string(val)
return true return true
} }
...@@ -110,7 +110,7 @@ func (l *lexer) next() bool { ...@@ -110,7 +110,7 @@ func (l *lexer) next() bool {
} }
if len(val) == 0 { if len(val) == 0 {
l.token = token{line: l.line} l.token = Token{Line: l.line}
if ch == '"' { if ch == '"' {
quoted = true quoted = true
continue continue
......
package parse package caddyfile
import ( import (
"strings" "strings"
...@@ -7,44 +7,44 @@ import ( ...@@ -7,44 +7,44 @@ import (
type lexerTestCase struct { type lexerTestCase struct {
input string input string
expected []token expected []Token
} }
func TestLexer(t *testing.T) { func TestLexer(t *testing.T) {
testCases := []lexerTestCase{ testCases := []lexerTestCase{
{ {
input: `host:123`, input: `host:123`,
expected: []token{ expected: []Token{
{line: 1, text: "host:123"}, {Line: 1, Text: "host:123"},
}, },
}, },
{ {
input: `host:123 input: `host:123
directive`, directive`,
expected: []token{ expected: []Token{
{line: 1, text: "host:123"}, {Line: 1, Text: "host:123"},
{line: 3, text: "directive"}, {Line: 3, Text: "directive"},
}, },
}, },
{ {
input: `host:123 { input: `host:123 {
directive directive
}`, }`,
expected: []token{ expected: []Token{
{line: 1, text: "host:123"}, {Line: 1, Text: "host:123"},
{line: 1, text: "{"}, {Line: 1, Text: "{"},
{line: 2, text: "directive"}, {Line: 2, Text: "directive"},
{line: 3, text: "}"}, {Line: 3, Text: "}"},
}, },
}, },
{ {
input: `host:123 { directive }`, input: `host:123 { directive }`,
expected: []token{ expected: []Token{
{line: 1, text: "host:123"}, {Line: 1, Text: "host:123"},
{line: 1, text: "{"}, {Line: 1, Text: "{"},
{line: 1, text: "directive"}, {Line: 1, Text: "directive"},
{line: 1, text: "}"}, {Line: 1, Text: "}"},
}, },
}, },
{ {
...@@ -54,42 +54,42 @@ func TestLexer(t *testing.T) { ...@@ -54,42 +54,42 @@ func TestLexer(t *testing.T) {
# comment # comment
foobar # another comment foobar # another comment
}`, }`,
expected: []token{ expected: []Token{
{line: 1, text: "host:123"}, {Line: 1, Text: "host:123"},
{line: 1, text: "{"}, {Line: 1, Text: "{"},
{line: 3, text: "directive"}, {Line: 3, Text: "directive"},
{line: 5, text: "foobar"}, {Line: 5, Text: "foobar"},
{line: 6, text: "}"}, {Line: 6, Text: "}"},
}, },
}, },
{ {
input: `a "quoted value" b input: `a "quoted value" b
foobar`, foobar`,
expected: []token{ expected: []Token{
{line: 1, text: "a"}, {Line: 1, Text: "a"},
{line: 1, text: "quoted value"}, {Line: 1, Text: "quoted value"},
{line: 1, text: "b"}, {Line: 1, Text: "b"},
{line: 2, text: "foobar"}, {Line: 2, Text: "foobar"},
}, },
}, },
{ {
input: `A "quoted \"value\" inside" B`, input: `A "quoted \"value\" inside" B`,
expected: []token{ expected: []Token{
{line: 1, text: "A"}, {Line: 1, Text: "A"},
{line: 1, text: `quoted "value" inside`}, {Line: 1, Text: `quoted "value" inside`},
{line: 1, text: "B"}, {Line: 1, Text: "B"},
}, },
}, },
{ {
input: `"don't\escape"`, input: `"don't\escape"`,
expected: []token{ expected: []Token{
{line: 1, text: `don't\escape`}, {Line: 1, Text: `don't\escape`},
}, },
}, },
{ {
input: `"don't\\escape"`, input: `"don't\\escape"`,
expected: []token{ expected: []Token{
{line: 1, text: `don't\\escape`}, {Line: 1, Text: `don't\\escape`},
}, },
}, },
{ {
...@@ -97,35 +97,35 @@ func TestLexer(t *testing.T) { ...@@ -97,35 +97,35 @@ func TestLexer(t *testing.T) {
break inside" { break inside" {
foobar foobar
}`, }`,
expected: []token{ expected: []Token{
{line: 1, text: "A"}, {Line: 1, Text: "A"},
{line: 1, text: "quoted value with line\n\t\t\t\t\tbreak inside"}, {Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"},
{line: 2, text: "{"}, {Line: 2, Text: "{"},
{line: 3, text: "foobar"}, {Line: 3, Text: "foobar"},
{line: 4, text: "}"}, {Line: 4, Text: "}"},
}, },
}, },
{ {
input: `"C:\php\php-cgi.exe"`, input: `"C:\php\php-cgi.exe"`,
expected: []token{ expected: []Token{
{line: 1, text: `C:\php\php-cgi.exe`}, {Line: 1, Text: `C:\php\php-cgi.exe`},
}, },
}, },
{ {
input: `empty "" string`, input: `empty "" string`,
expected: []token{ expected: []Token{
{line: 1, text: `empty`}, {Line: 1, Text: `empty`},
{line: 1, text: ``}, {Line: 1, Text: ``},
{line: 1, text: `string`}, {Line: 1, Text: `string`},
}, },
}, },
{ {
input: "skip those\r\nCR characters", input: "skip those\r\nCR characters",
expected: []token{ expected: []Token{
{line: 1, text: "skip"}, {Line: 1, Text: "skip"},
{line: 1, text: "those"}, {Line: 1, Text: "those"},
{line: 2, text: "CR"}, {Line: 2, Text: "CR"},
{line: 2, text: "characters"}, {Line: 2, Text: "characters"},
}, },
}, },
} }
...@@ -136,7 +136,7 @@ func TestLexer(t *testing.T) { ...@@ -136,7 +136,7 @@ func TestLexer(t *testing.T) {
} }
} }
func tokenize(input string) (tokens []token) { func tokenize(input string) (tokens []Token) {
l := lexer{} l := lexer{}
l.load(strings.NewReader(input)) l.load(strings.NewReader(input))
for l.next() { for l.next() {
...@@ -145,20 +145,20 @@ func tokenize(input string) (tokens []token) { ...@@ -145,20 +145,20 @@ func tokenize(input string) (tokens []token) {
return return
} }
func lexerCompare(t *testing.T, n int, expected, actual []token) { func lexerCompare(t *testing.T, n int, expected, actual []Token) {
if len(expected) != len(actual) { if len(expected) != len(actual) {
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
} }
for i := 0; i < len(actual) && i < len(expected); i++ { for i := 0; i < len(actual) && i < len(expected); i++ {
if actual[i].line != expected[i].line { if actual[i].Line != expected[i].Line {
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d", t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
n, i, expected[i].text, expected[i].line, actual[i].line) n, i, expected[i].Text, expected[i].Line, actual[i].Line)
break break
} }
if actual[i].text != expected[i].text { if actual[i].Text != expected[i].Text {
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'", t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
n, i, expected[i].text, actual[i].text) n, i, expected[i].Text, actual[i].Text)
break break
} }
} }
......
package parse package caddyfile
import ( import (
"fmt" "io"
"net"
"os" "os"
"path/filepath" "path/filepath"
"strings" "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 { type parser struct {
Dispenser Dispenser
block ServerBlock // current server block being parsed 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 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) { func (p *parser) parseAll() ([]ServerBlock, error) {
...@@ -23,7 +46,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) { ...@@ -23,7 +46,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
if err != nil { if err != nil {
return blocks, err return blocks, err
} }
if len(p.block.Addresses) > 0 { if len(p.block.Keys) > 0 {
blocks = append(blocks, p.block) blocks = append(blocks, p.block)
} }
} }
...@@ -32,7 +55,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) { ...@@ -32,7 +55,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
} }
func (p *parser) parseOne() error { func (p *parser) parseOne() error {
p.block = ServerBlock{Tokens: make(map[string][]token)} p.block = ServerBlock{Tokens: make(map[string][]Token)}
err := p.begin() err := p.begin()
if err != nil { if err != nil {
...@@ -89,7 +112,7 @@ func (p *parser) addresses() error { ...@@ -89,7 +112,7 @@ func (p *parser) addresses() error {
break 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 // Trailing comma indicates another address will follow, which
// may possibly be on the next line // may possibly be on the next line
if tkn[len(tkn)-1] == ',' { if tkn[len(tkn)-1] == ',' {
...@@ -99,13 +122,7 @@ func (p *parser) addresses() error { ...@@ -99,13 +122,7 @@ func (p *parser) addresses() error {
expectingAnother = false // but we may still see another one on this line expectingAnother = false // but we may still see another one on this line
} }
// Parse and save this address p.block.Keys = append(p.block.Keys, tkn)
addr, err := standardAddress(tkn)
if err != nil {
return err
}
p.block.Addresses = append(p.block.Addresses, addr)
} }
// Advance token and possibly break out of loop or return error // Advance token and possibly break out of loop or return error
...@@ -207,7 +224,7 @@ func (p *parser) doImport() error { ...@@ -207,7 +224,7 @@ func (p *parser) doImport() error {
tokensAfter := p.tokens[p.cursor+1:] tokensAfter := p.tokens[p.cursor+1:]
// collect all the imported tokens // collect all the imported tokens
var importedTokens []token var importedTokens []Token
for _, importFile := range matches { for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile) newTokens, err := p.doSingleImport(importFile)
if err != nil { if err != nil {
...@@ -226,7 +243,7 @@ func (p *parser) doImport() error { ...@@ -226,7 +243,7 @@ func (p *parser) doImport() error {
// doSingleImport lexes the individual file at importFile and returns // doSingleImport lexes the individual file at importFile and returns
// its tokens or an error, if any. // its tokens or an error, if any.
func (p *parser) doSingleImport(importFile string) ([]token, error) { func (p *parser) doSingleImport(importFile string) ([]Token, error) {
file, err := os.Open(importFile) file, err := os.Open(importFile)
if err != nil { if err != nil {
return nil, p.Errf("Could not import %s: %v", importFile, err) return nil, p.Errf("Could not import %s: %v", importFile, err)
...@@ -237,7 +254,7 @@ func (p *parser) doSingleImport(importFile string) ([]token, error) { ...@@ -237,7 +254,7 @@ func (p *parser) doSingleImport(importFile string) ([]token, error) {
// Tack the filename onto these tokens so errors show the imported file's name // Tack the filename onto these tokens so errors show the imported file's name
filename := filepath.Base(importFile) filename := filepath.Base(importFile)
for i := 0; i < len(importedTokens); i++ { for i := 0; i < len(importedTokens); i++ {
importedTokens[i].file = filename importedTokens[i].File = filename
} }
return importedTokens, nil return importedTokens, nil
...@@ -253,10 +270,9 @@ func (p *parser) directive() error { ...@@ -253,10 +270,9 @@ func (p *parser) directive() error {
dir := p.Val() dir := p.Val()
nesting := 0 nesting := 0
if p.checkDirectives { // TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
if _, ok := ValidDirectives[dir]; !ok { if !p.validDirective(dir) {
return p.Errf("Unknown directive '%s'", dir) return p.Errf("Unknown directive '%s'", dir)
}
} }
// The directive itself is appended as a relevant token // The directive itself is appended as a relevant token
...@@ -273,7 +289,7 @@ func (p *parser) directive() error { ...@@ -273,7 +289,7 @@ func (p *parser) directive() error {
} else if p.Val() == "}" && nesting == 0 { } else if p.Val() == "}" && nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace") return p.Err("Unexpected '}' because no matching opening brace")
} }
p.tokens[p.cursor].text = replaceEnvVars(p.tokens[p.cursor].text) p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
} }
...@@ -305,63 +321,17 @@ func (p *parser) closeCurlyBrace() error { ...@@ -305,63 +321,17 @@ func (p *parser) closeCurlyBrace() error {
return nil return nil
} }
// standardAddress parses an address string into a structured format with separate // validDirective returns true if dir is in p.validDirectives.
// scheme, host, and port portions, as well as the original input string. func (p *parser) validDirective(dir string) bool {
func standardAddress(str string) (address, error) { if p.validDirectives == nil {
var scheme string return true
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:]
}
// 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 {
// "The host subcomponent is case-insensitive." (RFC 3986) if d == dir {
host = strings.ToLower(host) return true
// see if we can set port based off scheme
if port == "" {
if scheme == "http" {
port = "80"
} else if scheme == "https" {
port = "443"
} }
} }
return false
// 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
} }
// replaceEnvVars replaces environment variables that appear in the token // replaceEnvVars replaces environment variables that appear in the token
...@@ -389,27 +359,9 @@ func replaceEnvReferences(s, refStart, refEnd string) string { ...@@ -389,27 +359,9 @@ func replaceEnvReferences(s, refStart, refEnd string) string {
return s return s
} }
type ( // ServerBlock associates any number of keys (usually addresses
// ServerBlock associates tokens with a list of addresses // of some sort) with tokens (grouped by directive name).
// and groups tokens by directive name. type ServerBlock struct {
ServerBlock struct { Keys []string
Addresses []address Tokens map[string][]Token
Tokens map[string][]token
}
address struct {
Original, Scheme, Host, Port string
}
)
// 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
} }
...@@ -13,7 +13,7 @@ import ( ...@@ -13,7 +13,7 @@ import (
"sync" "sync"
"github.com/jimstudt/http-authentication/basic" "github.com/jimstudt/http-authentication/basic"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
// BasicAuth is middleware to protect resources with a username and password. // BasicAuth is middleware to protect resources with a username and password.
...@@ -22,12 +22,12 @@ import ( ...@@ -22,12 +22,12 @@ import (
// security of HTTP Basic Auth is disputed. Use discretion when deciding // security of HTTP Basic Auth is disputed. Use discretion when deciding
// what to protect with BasicAuth. // what to protect with BasicAuth.
type BasicAuth struct { type BasicAuth struct {
Next middleware.Handler Next httpserver.Handler
SiteRoot string SiteRoot string
Rules []Rule Rules []Rule
} }
// ServeHTTP implements the middleware.Handler interface. // ServeHTTP implements the httpserver.Handler interface.
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var hasAuth bool var hasAuth bool
...@@ -35,7 +35,7 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error ...@@ -35,7 +35,7 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
for _, rule := range a.Rules { for _, rule := range a.Rules {
for _, res := range rule.Resources { for _, res := range rule.Resources {
if !middleware.Path(r.URL.Path).Matches(res) { if !httpserver.Path(r.URL.Path).Matches(res) {
continue continue
} }
......
...@@ -10,13 +10,12 @@ import ( ...@@ -10,13 +10,12 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
func TestBasicAuth(t *testing.T) { func TestBasicAuth(t *testing.T) {
rw := BasicAuth{ rw := BasicAuth{
Next: middleware.HandlerFunc(contentHandler), Next: httpserver.HandlerFunc(contentHandler),
Rules: []Rule{ Rules: []Rule{
{Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}}, {Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}},
}, },
...@@ -67,7 +66,7 @@ func TestBasicAuth(t *testing.T) { ...@@ -67,7 +66,7 @@ func TestBasicAuth(t *testing.T) {
func TestMultipleOverlappingRules(t *testing.T) { func TestMultipleOverlappingRules(t *testing.T) {
rw := BasicAuth{ rw := BasicAuth{
Next: middleware.HandlerFunc(contentHandler), Next: httpserver.HandlerFunc(contentHandler),
Rules: []Rule{ Rules: []Rule{
{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}}, {Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}},
{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}}, {Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}},
......
package setup package basicauth
import ( import (
"strings" "strings"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy"
"github.com/mholt/caddy/middleware/basicauth" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
// BasicAuth configures a new BasicAuth middleware instance. func init() {
func BasicAuth(c *Controller) (middleware.Middleware, error) { caddy.RegisterPlugin(caddy.Plugin{
root := c.Root Name: "basicauth",
ServerType: "http",
Action: setup,
})
}
// setup configures a new BasicAuth middleware instance.
func setup(c *caddy.Controller) error {
cfg := httpserver.GetConfig(c.Key)
root := cfg.Root
rules, err := basicAuthParse(c) rules, err := basicAuthParse(c)
if err != nil { if err != nil {
return nil, err return err
} }
basic := basicauth.BasicAuth{Rules: rules} basic := BasicAuth{Rules: rules}
return func(next middleware.Handler) middleware.Handler { cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
basic.Next = next basic.Next = next
basic.SiteRoot = root basic.SiteRoot = root
return basic return basic
}, nil })
return nil
} }
func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
var rules []basicauth.Rule var rules []Rule
cfg := httpserver.GetConfig(c.Key)
var err error var err error
for c.Next() { for c.Next() {
var rule basicauth.Rule var rule Rule
args := c.RemainingArgs() args := c.RemainingArgs()
switch len(args) { switch len(args) {
case 2: case 2:
rule.Username = args[0] rule.Username = args[0]
if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil { if rule.Password, err = passwordMatcher(rule.Username, args[1], cfg.Root); err != nil {
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
} }
...@@ -50,7 +62,7 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { ...@@ -50,7 +62,7 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
case 3: case 3:
rule.Resources = append(rule.Resources, args[0]) rule.Resources = append(rule.Resources, args[0])
rule.Username = args[1] rule.Username = args[1]
if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil { if rule.Password, err = passwordMatcher(rule.Username, args[2], cfg.Root); err != nil {
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
} }
default: default:
...@@ -63,10 +75,9 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { ...@@ -63,10 +75,9 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
return rules, nil return rules, nil
} }
func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) { func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) {
if !strings.HasPrefix(passw, "htpasswd=") { if !strings.HasPrefix(passw, "htpasswd=") {
return basicauth.PlainMatcher(passw), nil return PlainMatcher(passw), nil
} }
return GetHtpasswdMatcher(passw[9:], username, siteRoot)
return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot)
} }
package setup package basicauth
import ( import (
"fmt" "fmt"
...@@ -7,27 +7,27 @@ import ( ...@@ -7,27 +7,27 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/mholt/caddy/middleware/basicauth" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
) )
func TestBasicAuth(t *testing.T) { func TestSetup(t *testing.T) {
c := NewTestController(`basicauth user pwd`) err := setup(caddy.NewTestController(`basicauth user pwd`))
mid, err := BasicAuth(c)
if err != nil { if err != nil {
t.Errorf("Expected no errors, but got: %v", err) t.Errorf("Expected no errors, but got: %v", err)
} }
if mid == nil { mids := httpserver.GetConfig("").Middleware()
t.Fatal("Expected middleware, was nil instead") if len(mids) == 0 {
t.Fatal("Expected middleware, got 0 instead")
} }
handler := mid(EmptyNext) handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(basicauth.BasicAuth) myHandler, ok := handler.(BasicAuth)
if !ok { if !ok {
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler) t.Fatalf("Expected handler to be type BasicAuth, 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") t.Error("'Next' field of handler was not set properly")
} }
} }
...@@ -54,41 +54,40 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` ...@@ -54,41 +54,40 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
input string input string
shouldErr bool shouldErr bool
password string password string
expected []basicauth.Rule expected []Rule
}{ }{
{`basicauth user pwd`, false, "pwd", []basicauth.Rule{ {`basicauth user pwd`, false, "pwd", []Rule{
{Username: "user"}, {Username: "user"},
}}, }},
{`basicauth user pwd { {`basicauth user pwd {
}`, false, "pwd", []basicauth.Rule{ }`, false, "pwd", []Rule{
{Username: "user"}, {Username: "user"},
}}, }},
{`basicauth user pwd { {`basicauth user pwd {
/resource1 /resource1
/resource2 /resource2
}`, false, "pwd", []basicauth.Rule{ }`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1", "/resource2"}}, {Username: "user", Resources: []string{"/resource1", "/resource2"}},
}}, }},
{`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{ {`basicauth /resource user pwd`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource"}}, {Username: "user", Resources: []string{"/resource"}},
}}, }},
{`basicauth /res1 user1 pwd1 {`basicauth /res1 user1 pwd1
basicauth /res2 user2 pwd2`, false, "pwd", []basicauth.Rule{ basicauth /res2 user2 pwd2`, false, "pwd", []Rule{
{Username: "user1", Resources: []string{"/res1"}}, {Username: "user1", Resources: []string{"/res1"}},
{Username: "user2", Resources: []string{"/res2"}}, {Username: "user2", Resources: []string{"/res2"}},
}}, }},
{`basicauth user`, true, "", []basicauth.Rule{}}, {`basicauth user`, true, "", []Rule{}},
{`basicauth`, true, "", []basicauth.Rule{}}, {`basicauth`, true, "", []Rule{}},
{`basicauth /resource user pwd asdf`, true, "", []basicauth.Rule{}}, {`basicauth /resource user pwd asdf`, true, "", []Rule{}},
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []basicauth.Rule{ {`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []Rule{
{Username: "sha1"}, {Username: "sha1"},
}}, }},
} }
for i, test := range tests { for i, test := range tests {
c := NewTestController(test.input) actual, err := basicAuthParse(caddy.NewTestController(test.input))
actual, err := basicAuthParse(c)
if err == nil && test.shouldErr { if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i) t.Errorf("Test %d didn't error, but it should have", i)
......
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
}
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)
}
}
...@@ -16,13 +16,14 @@ import ( ...@@ -16,13 +16,14 @@ import (
"time" "time"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddyhttp/staticfiles"
) )
// Browse is an http.Handler that can show a file listing when // Browse is an http.Handler that can show a file listing when
// directories in the given paths are specified. // directories in the given paths are specified.
type Browse struct { type Browse struct {
Next middleware.Handler Next httpserver.Handler
Configs []Config Configs []Config
IgnoreIndexes bool IgnoreIndexes bool
} }
...@@ -67,7 +68,7 @@ type Listing struct { ...@@ -67,7 +68,7 @@ type Listing struct {
// Optional custom variables for use in browse templates // Optional custom variables for use in browse templates
User interface{} User interface{}
middleware.Context httpserver.Context
} }
// BreadcrumbMap returns l.Path where every element is a map // BreadcrumbMap returns l.Path where every element is a map
...@@ -195,7 +196,7 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listin ...@@ -195,7 +196,7 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listin
for _, f := range files { for _, f := range files {
name := f.Name() name := f.Name()
for _, indexName := range middleware.IndexPages { for _, indexName := range staticfiles.IndexPages {
if name == indexName { if name == indexName {
hasIndexFile = true hasIndexFile = true
break break
...@@ -237,7 +238,7 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { ...@@ -237,7 +238,7 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var bc *Config var bc *Config
// See if there's a browse configuration to match the path // See if there's a browse configuration to match the path
for i := range b.Configs { for i := range b.Configs {
if middleware.Path(r.URL.Path).Matches(b.Configs[i].PathScope) { if httpserver.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
bc = &b.Configs[i] bc = &b.Configs[i]
goto inScope goto inScope
} }
...@@ -370,7 +371,7 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi ...@@ -370,7 +371,7 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi
if containsIndex && !b.IgnoreIndexes { // directory isn't browsable if containsIndex && !b.IgnoreIndexes { // directory isn't browsable
return b.Next.ServeHTTP(w, r) return b.Next.ServeHTTP(w, r)
} }
listing.Context = middleware.Context{ listing.Context = httpserver.Context{
Root: bc.Root, Root: bc.Root,
Req: r, Req: r,
URL: r.URL, URL: r.URL,
......
This diff is collapsed.
This diff is collapsed.
package middleware package httpserver
import ( import (
"bytes" "bytes"
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment