Commit 118cf5f2 authored by elcore's avatar elcore Committed by Matt Holt

Implement 'http.on' plugin and replace UUID lib (#1864)

* Implement 'command' plugin

* Rename 'command' to 'on'

* Split this PR
parent f9cba03d
......@@ -45,5 +45,6 @@ import (
_ "github.com/mholt/caddy/caddyhttp/templates"
_ "github.com/mholt/caddy/caddyhttp/timeouts"
_ "github.com/mholt/caddy/caddyhttp/websocket"
_ "github.com/mholt/caddy/onevent"
_ "github.com/mholt/caddy/startupshutdown"
)
......@@ -25,7 +25,7 @@ import (
// ensure that the standard plugins are in fact plugged in
// and registered properly; this is a quick/naive way to do it.
func TestStandardPlugins(t *testing.T) {
numStandardPlugins := 32 // importing caddyhttp plugs in this many plugins
numStandardPlugins := 33 // importing caddyhttp plugs in this many plugins
s := caddy.DescribePlugins()
if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want {
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
......
......@@ -455,8 +455,9 @@ var directives = []string{
"tls",
// services/utilities, or other directives that don't necessarily inject handlers
"startup",
"shutdown",
"startup", // TODO: Deprecate this directive
"shutdown", // TODO: Deprecate this directive
"on",
"request_id",
"realip", // github.com/captncraig/caddy-realip
"git", // github.com/abiosoft/caddy-git
......
......@@ -16,11 +16,10 @@ package requestid
import (
"context"
"log"
"net/http"
"github.com/google/uuid"
"github.com/mholt/caddy/caddyhttp/httpserver"
uuid "github.com/nu7hatch/gouuid"
)
// Handler is a middleware handler
......@@ -29,20 +28,9 @@ type Handler struct {
}
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
reqid := UUID()
reqid := uuid.New().String()
c := context.WithValue(r.Context(), httpserver.RequestIDCtxKey, reqid)
r = r.WithContext(c)
return h.Next.ServeHTTP(w, r)
}
// UUID returns U4 UUID
func UUID() string {
u4, err := uuid.NewV4()
if err != nil {
log.Printf("[ERROR] generating request ID: %v", err)
return ""
}
return u4.String()
}
......@@ -19,6 +19,7 @@ import (
"net/http"
"testing"
"github.com/google/uuid"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
......@@ -28,7 +29,7 @@ func TestRequestID(t *testing.T) {
t.Fatal("Could not create HTTP request:", err)
}
reqid := UUID()
reqid := uuid.New().String()
c := context.WithValue(request.Context(), httpserver.RequestIDCtxKey, reqid)
......
package hook
import (
"github.com/mholt/caddy"
)
// Config describes how Hook should be configured and used.
type Config struct {
ID string
Event caddy.EventName
Command string
Args []string
}
// SupportedEvents is a map of supported events.
var SupportedEvents = map[string]caddy.EventName{
"startup": caddy.InstanceStartupEvent,
"shutdown": caddy.ShutdownEvent,
"certrenew": caddy.CertRenewEvent,
}
package hook
import (
"log"
"os"
"os/exec"
"strings"
"github.com/mholt/caddy"
)
// Hook executes a command.
func (cfg *Config) Hook(event caddy.EventName, info interface{}) error {
if event != cfg.Event {
return nil
}
nonblock := false
if len(cfg.Args) > 1 && cfg.Args[len(cfg.Args)-1] == "&" {
// Run command in background; non-blocking
nonblock = true
cfg.Args = cfg.Args[:len(cfg.Args)-1]
}
// Execute command.
cmd := exec.Command(cfg.Command, cfg.Args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if nonblock {
log.Printf("[INFO] Nonblocking Command with ID %s: \"%s %s\"", cfg.ID, cfg.Command, strings.Join(cfg.Args, " "))
return cmd.Start()
}
log.Printf("[INFO] Blocking Command with ID %s: \"%s %s\"", cfg.ID, cfg.Command, strings.Join(cfg.Args, " "))
err := cmd.Run()
if err != nil {
return err
}
return nil
}
package hook
import (
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/google/uuid"
"github.com/mholt/caddy"
)
func TestHook(t *testing.T) {
tempDirPath := os.TempDir()
testDir := filepath.Join(tempDirPath, "temp_dir_for_testing_command")
defer func() {
// clean up after non-blocking startup function quits
time.Sleep(500 * time.Millisecond)
os.RemoveAll(testDir)
}()
osSenitiveTestDir := filepath.FromSlash(testDir)
os.RemoveAll(osSenitiveTestDir) // start with a clean slate
tests := []struct {
name string
event caddy.EventName
command string
args []string
shouldErr bool
shouldRemoveErr bool
}{
{name: "blocking", event: caddy.InstanceStartupEvent, command: "mkdir", args: []string{osSenitiveTestDir}, shouldErr: false, shouldRemoveErr: false},
{name: "nonBlocking", event: caddy.ShutdownEvent, command: "mkdir", args: []string{osSenitiveTestDir, "&"}, shouldErr: false, shouldRemoveErr: true},
{name: "nonExistent", event: caddy.CertRenewEvent, command: strconv.Itoa(int(time.Now().UnixNano())), shouldErr: true, shouldRemoveErr: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg := new(Config)
cfg.ID = uuid.New().String()
cfg.Event = test.event
cfg.Command = test.command
cfg.Args = test.args
err := cfg.Hook(test.event, nil)
if err == nil && test.shouldErr {
t.Error("Test didn't error, but it should have")
} else if err != nil && !test.shouldErr {
t.Errorf("Test errored, but it shouldn't have; got '%v'", err)
}
err = os.Remove(osSenitiveTestDir)
if err != nil && !test.shouldRemoveErr {
t.Errorf("Test received an error of:\n%v", err)
}
})
}
}
package onevent
import (
"strings"
"github.com/google/uuid"
"github.com/mholt/caddy"
"github.com/mholt/caddy/onevent/hook"
)
func init() {
// Register Directive.
caddy.RegisterPlugin("on", caddy.Plugin{Action: setup})
}
func setup(c *caddy.Controller) error {
config, err := onParse(c)
if err != nil {
return err
}
// Register Event Hooks.
for _, cfg := range config {
caddy.RegisterEventHook("on-"+cfg.ID, cfg.Hook)
}
return nil
}
func onParse(c *caddy.Controller) ([]*hook.Config, error) {
var config []*hook.Config
for c.Next() {
cfg := new(hook.Config)
if !c.NextArg() {
return config, c.ArgErr()
}
// Configure Event.
event, ok := hook.SupportedEvents[strings.ToLower(c.Val())]
if !ok {
return config, c.Errf("Wrong event name or event not supported: '%s'", c.Val())
}
cfg.Event = event
// Assign an unique ID.
cfg.ID = uuid.New().String()
args := c.RemainingArgs()
// Extract command and arguments.
command, args, err := caddy.SplitCommandAndArgs(strings.Join(args, " "))
if err != nil {
return config, c.Err(err.Error())
}
cfg.Command = command
cfg.Args = args
config = append(config, cfg)
}
return config, nil
}
package onevent
import (
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/onevent/hook"
)
func TestSetup(t *testing.T) {
tests := []struct {
name string
input string
shouldErr bool
}{
{name: "noInput", input: "on", shouldErr: true},
{name: "nonExistent", input: "on xyz cmd arg", shouldErr: true},
{name: "startup", input: "on startup cmd arg", shouldErr: false},
{name: "shutdown", input: "on shutdown cmd arg &", shouldErr: false},
{name: "certrenew", input: "on certrenew cmd arg", shouldErr: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := caddy.NewTestController("http", test.input)
c.Key = test.name
err := setup(c)
if err == nil && test.shouldErr {
t.Error("Test didn't error, but it should have")
} else if err != nil && !test.shouldErr {
t.Errorf("Test errored, but it shouldn't have; got '%v'", err)
}
})
}
}
func TestCommandParse(t *testing.T) {
tests := []struct {
name string
input string
shouldErr bool
config hook.Config
}{
{name: "noInput", input: `on`, shouldErr: true},
{name: "nonExistent", input: "on xyz cmd arg", shouldErr: true},
{name: "startup", input: `on startup cmd arg1 arg2`, shouldErr: false, config: hook.Config{Event: caddy.InstanceStartupEvent, Command: "cmd", Args: []string{"arg1", "arg2"}}},
{name: "shutdown", input: `on shutdown cmd arg1 arg2 &`, shouldErr: false, config: hook.Config{Event: caddy.ShutdownEvent, Command: "cmd", Args: []string{"arg1", "arg2", "&"}}},
{name: "certrenew", input: `on certrenew cmd arg1 arg2`, shouldErr: false, config: hook.Config{Event: caddy.CertRenewEvent, Command: "cmd", Args: []string{"arg1", "arg2"}}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
config, err := onParse(caddy.NewTestController("http", test.input))
if err == nil && test.shouldErr {
t.Error("Test didn't error, but it should have")
} else if err != nil && !test.shouldErr {
t.Errorf("Test errored, but it shouldn't have; got '%v'", err)
}
for _, cfg := range config {
if cfg.Event != test.config.Event {
t.Errorf("Expected event %s; got %s", test.config.Event, cfg.Event)
}
if cfg.Command != test.config.Command {
t.Errorf("Expected command %s; got %s", test.config.Command, cfg.Command)
}
for i, arg := range cfg.Args {
if arg != test.config.Args[i] {
t.Errorf("Expected arg in position %d to be %s, got %s", i, test.config.Args[i], arg)
}
}
}
})
}
}
Copyright (c) 2009,2014 Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"fmt"
"os"
)
// A Domain represents a Version 2 domain
type Domain byte
// Domain constants for DCE Security (Version 2) UUIDs.
const (
Person = Domain(0)
Group = Domain(1)
Org = Domain(2)
)
// NewDCESecurity returns a DCE Security (Version 2) UUID.
//
// The domain should be one of Person, Group or Org.
// On a POSIX system the id should be the users UID for the Person
// domain and the users GID for the Group. The meaning of id for
// the domain Org or on non-POSIX systems is site defined.
//
// For a given domain/id pair the same token may be returned for up to
// 7 minutes and 10 seconds.
func NewDCESecurity(domain Domain, id uint32) (UUID, error) {
uuid, err := NewUUID()
if err == nil {
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
uuid[9] = byte(domain)
binary.BigEndian.PutUint32(uuid[0:], id)
}
return uuid, err
}
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
// domain with the id returned by os.Getuid.
//
// NewDCESecurity(Person, uint32(os.Getuid()))
func NewDCEPerson() (UUID, error) {
return NewDCESecurity(Person, uint32(os.Getuid()))
}
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
// domain with the id returned by os.Getgid.
//
// NewDCESecurity(Group, uint32(os.Getgid()))
func NewDCEGroup() (UUID, error) {
return NewDCESecurity(Group, uint32(os.Getgid()))
}
// Domain returns the domain for a Version 2 UUID. Domains are only defined
// for Version 2 UUIDs.
func (uuid UUID) Domain() Domain {
return Domain(uuid[9])
}
// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2
// UUIDs.
func (uuid UUID) ID() uint32 {
return binary.BigEndian.Uint32(uuid[0:4])
}
func (d Domain) String() string {
switch d {
case Person:
return "Person"
case Group:
return "Group"
case Org:
return "Org"
}
return fmt.Sprintf("Domain%d", int(d))
}
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package uuid generates and inspects UUIDs.
//
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security
// Services.
//
// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to
// maps or compared directly.
package uuid
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"crypto/md5"
"crypto/sha1"
"hash"
)
// Well known namespace IDs and UUIDs
var (
NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
Nil UUID // empty UUID, all zeros
)
// NewHash returns a new UUID derived from the hash of space concatenated with
// data generated by h. The hash should be at least 16 byte in length. The
// first 16 bytes of the hash are used to form the UUID. The version of the
// UUID will be the lower 4 bits of version. NewHash is used to implement
// NewMD5 and NewSHA1.
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
h.Reset()
h.Write(space[:])
h.Write([]byte(data))
s := h.Sum(nil)
var uuid UUID
copy(uuid[:], s)
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
return uuid
}
// NewMD5 returns a new MD5 (Version 3) UUID based on the
// supplied name space and data. It is the same as calling:
//
// NewHash(md5.New(), space, data, 3)
func NewMD5(space UUID, data []byte) UUID {
return NewHash(md5.New(), space, data, 3)
}
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
// supplied name space and data. It is the same as calling:
//
// NewHash(sha1.New(), space, data, 5)
func NewSHA1(space UUID, data []byte) UUID {
return NewHash(sha1.New(), space, data, 5)
}
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "fmt"
// MarshalText implements encoding.TextMarshaler.
func (uuid UUID) MarshalText() ([]byte, error) {
var js [36]byte
encodeHex(js[:], uuid)
return js[:], nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (uuid *UUID) UnmarshalText(data []byte) error {
// See comment in ParseBytes why we do this.
// id, err := ParseBytes(data)
id, err := ParseBytes(data)
if err == nil {
*uuid = id
}
return err
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (uuid UUID) MarshalBinary() ([]byte, error) {
return uuid[:], nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (uuid *UUID) UnmarshalBinary(data []byte) error {
if len(data) != 16 {
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
}
copy(uuid[:], data)
return nil
}
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"net"
"sync"
)
var (
nodeMu sync.Mutex
interfaces []net.Interface // cached list of interfaces
ifname string // name of interface being used
nodeID [6]byte // hardware for version 1 UUIDs
zeroID [6]byte // nodeID with only 0's
)
// NodeInterface returns the name of the interface from which the NodeID was
// derived. The interface "user" is returned if the NodeID was set by
// SetNodeID.
func NodeInterface() string {
defer nodeMu.Unlock()
nodeMu.Lock()
return ifname
}
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
// If name is "" then the first usable interface found will be used or a random
// Node ID will be generated. If a named interface cannot be found then false
// is returned.
//
// SetNodeInterface never fails when name is "".
func SetNodeInterface(name string) bool {
defer nodeMu.Unlock()
nodeMu.Lock()
return setNodeInterface(name)
}
func setNodeInterface(name string) bool {
if interfaces == nil {
var err error
interfaces, err = net.Interfaces()
if err != nil && name != "" {
return false
}
}
for _, ifs := range interfaces {
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
copy(nodeID[:], ifs.HardwareAddr)
ifname = ifs.Name
return true
}
}
// We found no interfaces with a valid hardware address. If name
// does not specify a specific interface generate a random Node ID
// (section 4.1.6)
if name == "" {
randomBits(nodeID[:])
return true
}
return false
}
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
// if not already set.
func NodeID() []byte {
defer nodeMu.Unlock()
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
nid := nodeID
return nid[:]
}
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
// of id are used. If id is less than 6 bytes then false is returned and the
// Node ID is not set.
func SetNodeID(id []byte) bool {
if len(id) < 6 {
return false
}
defer nodeMu.Unlock()
nodeMu.Lock()
copy(nodeID[:], id)
ifname = "user"
return true
}
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) NodeID() []byte {
if len(uuid) != 16 {
return nil
}
var node [6]byte
copy(node[:], uuid[10:])
return node[:]
}
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"database/sql/driver"
"fmt"
)
// Scan implements sql.Scanner so UUIDs can be read from databases transparently
// Currently, database types that map to string and []byte are supported. Please
// consult database-specific driver documentation for matching types.
func (uuid *UUID) Scan(src interface{}) error {
switch src := src.(type) {
case nil:
return nil
case string:
// if an empty UUID comes from a table, we return a null UUID
if src == "" {
return nil
}
// see Parse for required string format
u, err := Parse(src)
if err != nil {
return fmt.Errorf("Scan: %v", err)
}
*uuid = u