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 (
const (
defaultHost = "localhost"
defaultPort = "8080"
defaultPort = "2015"
defaultRoot = "."
// The default configuration file to load if none is specified
......@@ -47,9 +47,6 @@ type Config struct {
// these are executed in response to SIGINT and are blocking
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
ConfigFile string
}
......
......@@ -3,9 +3,6 @@ package config
import (
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"github.com/mholt/caddy/middleware"
)
......@@ -74,46 +71,6 @@ func init() {
p.cfg.TLS = tls
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 {
// TODO: This code is duplicated with the shutdown directive below
......
......@@ -149,7 +149,7 @@ func (d *dispenser) ArgErr() error {
if d.Val() == "{" {
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.
......
......@@ -2,6 +2,7 @@ package config
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/basicauth"
"github.com/mholt/caddy/middleware/browse"
"github.com/mholt/caddy/middleware/errors"
"github.com/mholt/caddy/middleware/extensions"
......@@ -45,6 +46,7 @@ func init() {
register("rewrite", rewrite.New)
register("redir", redirect.New)
register("ext", extensions.New)
register("basicauth", basicauth.New)
register("proxy", proxy.New)
register("fastcgi", fastcgi.New)
register("websocket", websockets.New)
......
......@@ -19,6 +19,7 @@ type (
other []locationContext // tokens to be 'parsed' later by middleware generators
scope *locationContext // the current location context (path scope) being populated
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
......
......@@ -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)
}
}
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) {
......
......@@ -38,18 +38,25 @@ func (p *parser) addresses() error {
// address gets host and port in a format accepted by net.Dial
address := func(str string) (host, port string, err error) {
var schemePort string
if strings.HasPrefix(str, "https://") {
port = "https"
host = str[8:]
return
schemePort = "https"
str = str[8:]
} else if strings.HasPrefix(str, "http://") {
port = "http"
host = str[7:]
return
schemePort = "http"
str = str[7:]
} else if !strings.Contains(str, ":") {
str += ":" + defaultPort
}
host, port, err = net.SplitHostPort(str)
if err != nil && schemePort != "" {
host = str
port = schemePort // assume port from scheme
err = nil
}
return
}
......@@ -88,6 +95,10 @@ func (p *parser) addresses() error {
if !expectingAnother && p.line() > startLine {
break
}
if !hasNext {
p.eof = true
break // EOF
}
}
return nil
......@@ -115,6 +126,12 @@ func (p *parser) addressBlock() error {
})
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()
if err != nil {
return err
......
package main
import (
"errors"
"flag"
"fmt"
"log"
"net"
"runtime"
"strconv"
"strings"
"sync"
"github.com/mholt/caddy/config"
......@@ -13,18 +17,27 @@ import (
var (
conf string
http2 bool
http2 bool // TODO: temporary flag until http2 is standard
quiet bool
cpu string
)
func init() {
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() {
var wg sync.WaitGroup
flag.Parse()
// Set CPU cap
err := setCPU(cpu)
if err != nil {
log.Fatal(err)
}
// Load config from file
allConfigs, err := config.Load(conf)
......@@ -60,6 +73,12 @@ func main() {
log.Println(err)
}
}(s)
if !quiet {
for _, config := range configs {
fmt.Println(config.Address())
}
}
}
wg.Wait()
......@@ -102,3 +121,38 @@ func arrangeBindings(allConfigs []config.Config) (map[string][]config.Config, er
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 @@
package browse
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
......@@ -122,8 +123,6 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
}
defer file.Close()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
files, err := file.Readdir(-1)
if err != nil {
return http.StatusForbidden, err
......@@ -182,12 +181,15 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
Items: fileinfos,
}
// TODO: Don't write to w until we know there wasn't an error
err = bc.Template.Execute(w, listing)
var buf bytes.Buffer
err = bc.Template.Execute(&buf, listing)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
buf.WriteTo(w)
return http.StatusOK, nil
}
......
......@@ -11,7 +11,7 @@ const defaultTemplate = `<!DOCTYPE html>
body {
padding: 1% 2%;
font: 16px sans-serif;
font: 16px Arial;
}
header {
......@@ -60,7 +60,7 @@ th {
text-align: left;
}
@media (max-width: 650px) {
@media (max-width: 700px) {
.hideable {
display: none;
}
......@@ -71,7 +71,7 @@ th {
header,
header h1 {
font-size: 14px;
font-size: 16px;
}
header {
......@@ -80,7 +80,7 @@ th {
width: 100%;
background: #333;
color: #FFF;
padding: 10px;
padding: 15px;
text-align: center;
}
......@@ -95,8 +95,8 @@ th {
position: absolute;
left: 0;
top: 0;
width: 35px;
height: 28px;
width: 40px;
height: 48px;
font-size: 35px;
}
......@@ -105,7 +105,7 @@ th {
}
main {
margin-top: 50px;
margin-top: 70px;
}
}
</style>
......
......@@ -88,6 +88,8 @@ func parse(c middleware.Controller) ([]LogRule, error) {
format = commonLogFormat
case "{combined}":
format = combinedLogFormat
default:
format = args[2]
}
}
......
......@@ -135,7 +135,7 @@ func parse(c middleware.Controller) ([]MarkdownConfig, error) {
}
// Get the path scope
if !c.NextArg() {
if !c.NextArg() || c.Val() == "{" {
return mdconfigs, c.ArgErr()
}
md.PathScope = c.Val()
......
package middleware
import "net/http"
import (
"net/http"
"time"
)
// responseRecorder is a type of ResponseWriter that captures
// the status code written to it and also the size of the body
......@@ -12,6 +15,7 @@ type responseRecorder struct {
http.ResponseWriter
status int
size int
start time.Time
}
// NewResponseRecorder makes and returns a new responseRecorder,
......@@ -24,6 +28,7 @@ func NewResponseRecorder(w http.ResponseWriter) *responseRecorder {
return &responseRecorder{
ResponseWriter: w,
status: http.StatusOK,
start: time.Now(),
}
}
......
......@@ -50,8 +50,9 @@ func NewReplacer(r *http.Request, rr *responseRecorder) replacer {
"{when}": func() string {
return time.Now().Format(timeFormat)
}(),
"{status}": strconv.Itoa(rr.status),
"{size}": strconv.Itoa(rr.size),
"{status}": strconv.Itoa(rr.status),
"{size}": strconv.Itoa(rr.size),
"{latency}": time.Since(rr.start).String(),
}
// Header placeholders
......
package templates
import (
"bytes"
"net/http"
"path"
"text/template"
......@@ -47,10 +48,12 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
}
// Execute it
err = tpl.Execute(w, ctx)
var buf bytes.Buffer
err = tpl.Execute(&buf, ctx)
if err != nil {
return http.StatusInternalServerError, err
}
buf.WriteTo(w)
return http.StatusOK, nil
}
......
......@@ -11,7 +11,6 @@ import (
"net/http"
"os"
"os/signal"
"runtime"
"github.com/bradfitz/http2"
"github.com/mholt/caddy/config"
......@@ -20,10 +19,10 @@ import (
// Server represents an instance of a server, which serves
// static content at a particular address (host and port).
type Server struct {
HTTP2 bool // temporary while http2 is not in std lib (TODO: remove flag when part of std lib)
address string
tls bool
vhosts map[string]virtualHost
HTTP2 bool // temporary while http2 is not in std lib (TODO: remove flag when part of std lib)
address string // the actual address for net.Listen to listen on
tls bool // whether this server is serving all HTTPS hosts or not
vhosts map[string]virtualHost // virtual hosts keyed by their address
}
// 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) {
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}
// Build middleware stack
......@@ -73,7 +67,7 @@ func (s *Server) Serve() error {
}
for _, vh := range s.vhosts {
// Execute startup functions
// Execute startup functions now
for _, start := range vh.config.Startup {
err := start()
if err != nil {
......@@ -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)
}
// Execute shutdown commands on exit
if len(vh.config.Shutdown) > 0 {
// Execute shutdown commands on exit
go func() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, os.Kill) // TODO: syscall.SIGQUIT? (Ctrl+\, Unix-only)
......
......@@ -9,7 +9,7 @@ import (
// virtualHost represents a virtual host/server. While a Server
// 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.
type virtualHost struct {
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