Commit 9e12c45d authored by Thomas Hansen's avatar Thomas Hansen

Merge branch 'master' of https://github.com/mholt/caddy

parents 96985fb3 24d9d237
...@@ -12,7 +12,7 @@ import ( ...@@ -12,7 +12,7 @@ import (
const ( const (
defaultHost = "localhost" defaultHost = "localhost"
defaultPort = "8080" defaultPort = "2015"
defaultRoot = "." defaultRoot = "."
// The default configuration file to load if none is specified // The default configuration file to load if none is specified
...@@ -47,9 +47,6 @@ type Config struct { ...@@ -47,9 +47,6 @@ type Config struct {
// these are executed in response to SIGINT and are blocking // these are executed in response to SIGINT and are blocking
Shutdown []func() error Shutdown []func() error
// MaxCPU is the maximum number of cores for the whole process to use
MaxCPU int
// The path to the configuration file from which this was loaded // The path to the configuration file from which this was loaded
ConfigFile string ConfigFile string
} }
......
...@@ -3,9 +3,6 @@ package config ...@@ -3,9 +3,6 @@ package config
import ( import (
"os" "os"
"os/exec" "os/exec"
"runtime"
"strconv"
"strings"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
) )
...@@ -74,46 +71,6 @@ func init() { ...@@ -74,46 +71,6 @@ func init() {
p.cfg.TLS = tls p.cfg.TLS = tls
return nil return nil
}, },
"cpu": func(p *parser) error {
sysCores := runtime.NumCPU()
if !p.nextArg() {
return p.argErr()
}
strNum := p.tkn()
setCPU := func(val int) {
if val < 1 {
val = 1
}
if val > sysCores {
val = sysCores
}
if val > p.cfg.MaxCPU {
p.cfg.MaxCPU = val
}
}
if strings.HasSuffix(strNum, "%") {
// Percent
var percent float32
pctStr := strNum[:len(strNum)-1]
pctInt, err := strconv.Atoi(pctStr)
if err != nil || pctInt < 1 || pctInt > 100 {
return p.err("Parse", "Invalid number '"+strNum+"' (must be a positive percentage between 1 and 100)")
}
percent = float32(pctInt) / 100
setCPU(int(float32(sysCores) * percent))
} else {
// Number
num, err := strconv.Atoi(strNum)
if err != nil || num < 0 {
return p.err("Parse", "Invalid number '"+strNum+"' (requires positive integer or percent)")
}
setCPU(num)
}
return nil
},
"startup": func(p *parser) error { "startup": func(p *parser) error {
// TODO: This code is duplicated with the shutdown directive below // TODO: This code is duplicated with the shutdown directive below
......
...@@ -149,7 +149,7 @@ func (d *dispenser) ArgErr() error { ...@@ -149,7 +149,7 @@ func (d *dispenser) ArgErr() error {
if d.Val() == "{" { if d.Val() == "{" {
return d.Err("Unexpected token '{', expecting argument") return d.Err("Unexpected token '{', expecting argument")
} }
return d.Err("Unexpected line ending after '" + d.Val() + "' (missing arguments?)") return d.Err("Wrong argument count or unexpected line ending after '" + d.Val() + "'")
} }
// Err generates a custom parse error with a message of msg. // Err generates a custom parse error with a message of msg.
......
...@@ -2,6 +2,7 @@ package config ...@@ -2,6 +2,7 @@ package config
import ( import (
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/basicauth"
"github.com/mholt/caddy/middleware/browse" "github.com/mholt/caddy/middleware/browse"
"github.com/mholt/caddy/middleware/errors" "github.com/mholt/caddy/middleware/errors"
"github.com/mholt/caddy/middleware/extensions" "github.com/mholt/caddy/middleware/extensions"
...@@ -45,6 +46,7 @@ func init() { ...@@ -45,6 +46,7 @@ func init() {
register("rewrite", rewrite.New) register("rewrite", rewrite.New)
register("redir", redirect.New) register("redir", redirect.New)
register("ext", extensions.New) register("ext", extensions.New)
register("basicauth", basicauth.New)
register("proxy", proxy.New) register("proxy", proxy.New)
register("fastcgi", fastcgi.New) register("fastcgi", fastcgi.New)
register("websocket", websockets.New) register("websocket", websockets.New)
......
...@@ -19,6 +19,7 @@ type ( ...@@ -19,6 +19,7 @@ type (
other []locationContext // tokens to be 'parsed' later by middleware generators other []locationContext // tokens to be 'parsed' later by middleware generators
scope *locationContext // the current location context (path scope) being populated scope *locationContext // the current location context (path scope) being populated
unused *token // sometimes a token will be read but not immediately consumed unused *token // sometimes a token will be read but not immediately consumed
eof bool // if we encounter a valid EOF in a hard place
} }
// locationContext represents a location context // locationContext represents a location context
......
...@@ -211,6 +211,53 @@ func TestParserBasicWithAlternateAddressStyles(t *testing.T) { ...@@ -211,6 +211,53 @@ func TestParserBasicWithAlternateAddressStyles(t *testing.T) {
t.Fatalf("Expected root for conf of %s to be '/test/www', but got: %s", conf.Address(), conf.Root) t.Fatalf("Expected root for conf of %s to be '/test/www', but got: %s", conf.Address(), conf.Root)
} }
} }
p = &parser{filename: "test"}
input = `host:port, http://host:port, http://host, https://host:port, host`
p.lexer.load(strings.NewReader(input))
confs, err = p.parse()
if err != nil {
t.Fatalf("Expected no errors, but got '%s'", err)
}
if len(confs) != 5 {
t.Fatalf("Expected 5 configurations, but got %d: %#v", len(confs), confs)
}
if confs[0].Host != "host" {
t.Errorf("Expected conf[0] Host='host', got '%#v'", confs[0])
}
if confs[0].Port != "port" {
t.Errorf("Expected conf[0] Port='port', got '%#v'", confs[0])
}
if confs[1].Host != "host" {
t.Errorf("Expected conf[1] Host='host', got '%#v'", confs[1])
}
if confs[1].Port != "port" {
t.Errorf("Expected conf[1] Port='port', got '%#v'", confs[1])
}
if confs[2].Host != "host" {
t.Errorf("Expected conf[2] Host='host', got '%#v'", confs[2])
}
if confs[2].Port != "http" {
t.Errorf("Expected conf[2] Port='http', got '%#v'", confs[2])
}
if confs[3].Host != "host" {
t.Errorf("Expected conf[3] Host='host', got '%#v'", confs[3])
}
if confs[3].Port != "port" {
t.Errorf("Expected conf[3] Port='port', got '%#v'", confs[3])
}
if confs[4].Host != "host" {
t.Errorf("Expected conf[4] Host='host', got '%#v'", confs[4])
}
if confs[4].Port != defaultPort {
t.Errorf("Expected conf[4] Port='%s', got '%#v'", defaultPort, confs[4].Port)
}
} }
func TestParserImport(t *testing.T) { func TestParserImport(t *testing.T) {
......
...@@ -38,18 +38,25 @@ func (p *parser) addresses() error { ...@@ -38,18 +38,25 @@ func (p *parser) addresses() error {
// address gets host and port in a format accepted by net.Dial // address gets host and port in a format accepted by net.Dial
address := func(str string) (host, port string, err error) { address := func(str string) (host, port string, err error) {
var schemePort string
if strings.HasPrefix(str, "https://") { if strings.HasPrefix(str, "https://") {
port = "https" schemePort = "https"
host = str[8:] str = str[8:]
return
} else if strings.HasPrefix(str, "http://") { } else if strings.HasPrefix(str, "http://") {
port = "http" schemePort = "http"
host = str[7:] str = str[7:]
return
} else if !strings.Contains(str, ":") { } else if !strings.Contains(str, ":") {
str += ":" + defaultPort str += ":" + defaultPort
} }
host, port, err = net.SplitHostPort(str) host, port, err = net.SplitHostPort(str)
if err != nil && schemePort != "" {
host = str
port = schemePort // assume port from scheme
err = nil
}
return return
} }
...@@ -88,6 +95,10 @@ func (p *parser) addresses() error { ...@@ -88,6 +95,10 @@ func (p *parser) addresses() error {
if !expectingAnother && p.line() > startLine { if !expectingAnother && p.line() > startLine {
break break
} }
if !hasNext {
p.eof = true
break // EOF
}
} }
return nil return nil
...@@ -115,6 +126,12 @@ func (p *parser) addressBlock() error { ...@@ -115,6 +126,12 @@ func (p *parser) addressBlock() error {
}) })
p.scope = &p.other[0] p.scope = &p.other[0]
if p.eof {
// this happens if the Caddyfile consists of only
// a line of addresses and nothing else
return nil
}
err := p.directives() err := p.directives()
if err != nil { if err != nil {
return err return err
......
package main package main
import ( import (
"errors"
"flag" "flag"
"fmt" "fmt"
"log" "log"
"net" "net"
"runtime"
"strconv"
"strings"
"sync" "sync"
"github.com/mholt/caddy/config" "github.com/mholt/caddy/config"
...@@ -13,18 +17,27 @@ import ( ...@@ -13,18 +17,27 @@ import (
var ( var (
conf string conf string
http2 bool http2 bool // TODO: temporary flag until http2 is standard
quiet bool
cpu string
) )
func init() { func init() {
flag.StringVar(&conf, "conf", config.DefaultConfigFile, "the configuration file to use") flag.StringVar(&conf, "conf", config.DefaultConfigFile, "the configuration file to use")
flag.BoolVar(&http2, "http2", true, "enable HTTP/2 support") // temporary flag until http2 merged into std lib flag.BoolVar(&http2, "http2", true, "enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib
flag.BoolVar(&quiet, "quiet", false, "quiet mode (no initialization output)")
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
flag.Parse()
} }
func main() { func main() {
var wg sync.WaitGroup var wg sync.WaitGroup
flag.Parse() // Set CPU cap
err := setCPU(cpu)
if err != nil {
log.Fatal(err)
}
// Load config from file // Load config from file
allConfigs, err := config.Load(conf) allConfigs, err := config.Load(conf)
...@@ -60,6 +73,12 @@ func main() { ...@@ -60,6 +73,12 @@ func main() {
log.Println(err) log.Println(err)
} }
}(s) }(s)
if !quiet {
for _, config := range configs {
fmt.Println(config.Address())
}
}
} }
wg.Wait() wg.Wait()
...@@ -102,3 +121,38 @@ func arrangeBindings(allConfigs []config.Config) (map[string][]config.Config, er ...@@ -102,3 +121,38 @@ func arrangeBindings(allConfigs []config.Config) (map[string][]config.Config, er
return addresses, nil return addresses, nil
} }
// 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%).
func setCPU(cpu string) error {
var numCPU int
availCPU := runtime.NumCPU()
if strings.HasSuffix(cpu, "%") {
// Percent
var percent float32
pctStr := cpu[:len(cpu)-1]
pctInt, err := strconv.Atoi(pctStr)
if err != nil || pctInt < 1 || pctInt > 100 {
return errors.New("Invalid CPU value: percentage must be between 1-100")
}
percent = float32(pctInt) / 100
numCPU = int(float32(availCPU) * percent)
} else {
// Number
num, err := strconv.Atoi(cpu)
if err != nil || num < 1 {
return errors.New("Invalid CPU value: provide a number or percent greater than 0")
}
numCPU = num
}
if numCPU > availCPU {
numCPU = availCPU
}
runtime.GOMAXPROCS(numCPU)
return nil
}
package basicauth
import (
"net/http"
"github.com/mholt/caddy/middleware"
)
// New constructs a new BasicAuth middleware instance.
func New(c middleware.Controller) (middleware.Middleware, error) {
rules, err := parse(c)
if err != nil {
return nil, err
}
basic := BasicAuth{
Rules: rules,
}
return func(next middleware.Handler) middleware.Handler {
basic.Next = next
return basic
}, nil
}
// ServeHTTP implements the middleware.Handler interface.
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for _, rule := range a.Rules {
for _, res := range rule.Resources {
if !middleware.Path(r.URL.Path).Matches(res) {
continue
}
// Path matches; parse auth header
username, password, ok := r.BasicAuth()
// Check credentials
if !ok || username != rule.Username || password != rule.Password {
w.Header().Set("WWW-Authenticate", "Basic")
return http.StatusUnauthorized, nil
}
// "It's an older code, sir, but it checks out. I was about to clear them."
return a.Next.ServeHTTP(w, r)
}
}
// Pass-thru when no paths match
return a.Next.ServeHTTP(w, r)
}
func parse(c middleware.Controller) ([]Rule, error) {
var rules []Rule
for c.Next() {
var rule Rule
args := c.RemainingArgs()
switch len(args) {
case 2:
rule.Username = args[0]
rule.Password = args[1]
for c.NextBlock() {
rule.Resources = append(rule.Resources, c.Val())
if c.NextArg() {
return rules, c.Err("Expecting only one resource per line (extra '" + c.Val() + "')")
}
}
case 3:
rule.Resources = append(rule.Resources, args[0])
rule.Username = args[1]
rule.Password = args[2]
default:
return rules, c.ArgErr()
}
rules = append(rules, rule)
}
return rules, nil
}
// BasicAuth is middleware to protect resources with a username and password.
// Note that HTTP Basic Authentication is not secure by itself and should
// not be used to protect important assets without HTTPS. Even then, the
// security of HTTP Basic Auth is disputed. Use discretion when deciding
// what to protect with BasicAuth.
type BasicAuth struct {
Next middleware.Handler
Rules []Rule
}
// Rule represents a BasicAuth rule. A username and password
// combination protect the associated resources, which are
// file or directory paths.
type Rule struct {
Username string
Password string
Resources []string
}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
package browse package browse
import ( import (
"bytes"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
...@@ -122,8 +123,6 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { ...@@ -122,8 +123,6 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
} }
defer file.Close() defer file.Close()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
files, err := file.Readdir(-1) files, err := file.Readdir(-1)
if err != nil { if err != nil {
return http.StatusForbidden, err return http.StatusForbidden, err
...@@ -182,12 +181,15 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { ...@@ -182,12 +181,15 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
Items: fileinfos, Items: fileinfos,
} }
// TODO: Don't write to w until we know there wasn't an error var buf bytes.Buffer
err = bc.Template.Execute(w, listing) err = bc.Template.Execute(&buf, listing)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8")
buf.WriteTo(w)
return http.StatusOK, nil return http.StatusOK, nil
} }
......
...@@ -11,7 +11,7 @@ const defaultTemplate = `<!DOCTYPE html> ...@@ -11,7 +11,7 @@ const defaultTemplate = `<!DOCTYPE html>
body { body {
padding: 1% 2%; padding: 1% 2%;
font: 16px sans-serif; font: 16px Arial;
} }
header { header {
...@@ -60,7 +60,7 @@ th { ...@@ -60,7 +60,7 @@ th {
text-align: left; text-align: left;
} }
@media (max-width: 650px) { @media (max-width: 700px) {
.hideable { .hideable {
display: none; display: none;
} }
...@@ -71,7 +71,7 @@ th { ...@@ -71,7 +71,7 @@ th {
header, header,
header h1 { header h1 {
font-size: 14px; font-size: 16px;
} }
header { header {
...@@ -80,7 +80,7 @@ th { ...@@ -80,7 +80,7 @@ th {
width: 100%; width: 100%;
background: #333; background: #333;
color: #FFF; color: #FFF;
padding: 10px; padding: 15px;
text-align: center; text-align: center;
} }
...@@ -95,8 +95,8 @@ th { ...@@ -95,8 +95,8 @@ th {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 35px; width: 40px;
height: 28px; height: 48px;
font-size: 35px; font-size: 35px;
} }
...@@ -105,7 +105,7 @@ th { ...@@ -105,7 +105,7 @@ th {
} }
main { main {
margin-top: 50px; margin-top: 70px;
} }
} }
</style> </style>
......
...@@ -88,6 +88,8 @@ func parse(c middleware.Controller) ([]LogRule, error) { ...@@ -88,6 +88,8 @@ func parse(c middleware.Controller) ([]LogRule, error) {
format = commonLogFormat format = commonLogFormat
case "{combined}": case "{combined}":
format = combinedLogFormat format = combinedLogFormat
default:
format = args[2]
} }
} }
......
...@@ -135,7 +135,7 @@ func parse(c middleware.Controller) ([]MarkdownConfig, error) { ...@@ -135,7 +135,7 @@ func parse(c middleware.Controller) ([]MarkdownConfig, error) {
} }
// Get the path scope // Get the path scope
if !c.NextArg() { if !c.NextArg() || c.Val() == "{" {
return mdconfigs, c.ArgErr() return mdconfigs, c.ArgErr()
} }
md.PathScope = c.Val() md.PathScope = c.Val()
......
package middleware package middleware
import "net/http" import (
"net/http"
"time"
)
// responseRecorder is a type of ResponseWriter that captures // responseRecorder is a type of ResponseWriter that captures
// the status code written to it and also the size of the body // the status code written to it and also the size of the body
...@@ -12,6 +15,7 @@ type responseRecorder struct { ...@@ -12,6 +15,7 @@ type responseRecorder struct {
http.ResponseWriter http.ResponseWriter
status int status int
size int size int
start time.Time
} }
// NewResponseRecorder makes and returns a new responseRecorder, // NewResponseRecorder makes and returns a new responseRecorder,
...@@ -24,6 +28,7 @@ func NewResponseRecorder(w http.ResponseWriter) *responseRecorder { ...@@ -24,6 +28,7 @@ func NewResponseRecorder(w http.ResponseWriter) *responseRecorder {
return &responseRecorder{ return &responseRecorder{
ResponseWriter: w, ResponseWriter: w,
status: http.StatusOK, status: http.StatusOK,
start: time.Now(),
} }
} }
......
...@@ -52,6 +52,7 @@ func NewReplacer(r *http.Request, rr *responseRecorder) replacer { ...@@ -52,6 +52,7 @@ func NewReplacer(r *http.Request, rr *responseRecorder) replacer {
}(), }(),
"{status}": strconv.Itoa(rr.status), "{status}": strconv.Itoa(rr.status),
"{size}": strconv.Itoa(rr.size), "{size}": strconv.Itoa(rr.size),
"{latency}": time.Since(rr.start).String(),
} }
// Header placeholders // Header placeholders
......
package templates package templates
import ( import (
"bytes"
"net/http" "net/http"
"path" "path"
"text/template" "text/template"
...@@ -47,10 +48,12 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error ...@@ -47,10 +48,12 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
} }
// Execute it // Execute it
err = tpl.Execute(w, ctx) var buf bytes.Buffer
err = tpl.Execute(&buf, ctx)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
buf.WriteTo(w)
return http.StatusOK, nil return http.StatusOK, nil
} }
......
...@@ -11,7 +11,6 @@ import ( ...@@ -11,7 +11,6 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"runtime"
"github.com/bradfitz/http2" "github.com/bradfitz/http2"
"github.com/mholt/caddy/config" "github.com/mholt/caddy/config"
...@@ -21,9 +20,9 @@ import ( ...@@ -21,9 +20,9 @@ import (
// static content at a particular address (host and port). // static content at a particular address (host and port).
type Server struct { type Server struct {
HTTP2 bool // temporary while http2 is not in std lib (TODO: remove flag when part of std lib) HTTP2 bool // temporary while http2 is not in std lib (TODO: remove flag when part of std lib)
address string address string // the actual address for net.Listen to listen on
tls bool tls bool // whether this server is serving all HTTPS hosts or not
vhosts map[string]virtualHost vhosts map[string]virtualHost // virtual hosts keyed by their address
} }
// New creates a new Server which will bind to addr and serve // New creates a new Server which will bind to addr and serve
...@@ -41,11 +40,6 @@ func New(addr string, configs []config.Config, tls bool) (*Server, error) { ...@@ -41,11 +40,6 @@ func New(addr string, configs []config.Config, tls bool) (*Server, error) {
return nil, fmt.Errorf("Cannot serve %s - host already defined for address %s", conf.Address(), s.address) return nil, fmt.Errorf("Cannot serve %s - host already defined for address %s", conf.Address(), s.address)
} }
// Use all CPUs (if needed) by default
if conf.MaxCPU == 0 {
conf.MaxCPU = runtime.NumCPU()
}
vh := virtualHost{config: conf} vh := virtualHost{config: conf}
// Build middleware stack // Build middleware stack
...@@ -73,7 +67,7 @@ func (s *Server) Serve() error { ...@@ -73,7 +67,7 @@ func (s *Server) Serve() error {
} }
for _, vh := range s.vhosts { for _, vh := range s.vhosts {
// Execute startup functions // Execute startup functions now
for _, start := range vh.config.Startup { for _, start := range vh.config.Startup {
err := start() err := start()
if err != nil { if err != nil {
...@@ -81,13 +75,8 @@ func (s *Server) Serve() error { ...@@ -81,13 +75,8 @@ func (s *Server) Serve() error {
} }
} }
// Use highest procs value across all configurations
if vh.config.MaxCPU > 0 && vh.config.MaxCPU > runtime.GOMAXPROCS(0) {
runtime.GOMAXPROCS(vh.config.MaxCPU)
}
if len(vh.config.Shutdown) > 0 {
// Execute shutdown commands on exit // Execute shutdown commands on exit
if len(vh.config.Shutdown) > 0 {
go func() { go func() {
interrupt := make(chan os.Signal, 1) interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, os.Kill) // TODO: syscall.SIGQUIT? (Ctrl+\, Unix-only) signal.Notify(interrupt, os.Interrupt, os.Kill) // TODO: syscall.SIGQUIT? (Ctrl+\, Unix-only)
......
...@@ -9,7 +9,7 @@ import ( ...@@ -9,7 +9,7 @@ import (
// virtualHost represents a virtual host/server. While a Server // virtualHost represents a virtual host/server. While a Server
// is what actually binds to the address, a user may want to serve // is what actually binds to the address, a user may want to serve
// multiple sites on a single address, and what is what a // multiple sites on a single address, and this is what a
// virtualHost allows us to do. // virtualHost allows us to do.
type virtualHost struct { type virtualHost struct {
config config.Config config config.Config
......
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