// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package agent
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
osexec "os/exec"
"strconv"
"github.com/armon/circbuf"
"github.com/hashicorp/consul/agent/exec"
"github.com/hashicorp/consul/api/watch"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-hclog"
"golang.org/x/net/context"
)
const (
// Limit the size of a watch handlers's output to the
// last WatchBufSize. Prevents an enormous buffer
// from being captured
WatchBufSize = 4 * 1024 // 4KB
)
// makeWatchHandler returns a handler for the given watch
func makeWatchHandler ( logger hclog . Logger , handler interface { } ) watch . HandlerFunc {
var args [ ] string
var script string
// Figure out whether to run in shell or raw subprocess mode
switch h := handler . ( type ) {
case string :
script = h
case [ ] string :
args = h
default :
panic ( fmt . Errorf ( "unknown handler type %T" , handler ) )
}
fn := func ( idx uint64 , data interface { } ) {
// Create the command
var cmd * osexec . Cmd
var err error
if len ( args ) > 0 {
cmd , err = exec . Subprocess ( args )
} else {
cmd , err = exec . Script ( script )
}
if err != nil {
logger . Error ( "Failed to setup watch" , "error" , err )
return
}
cmd . Env = append ( os . Environ ( ) ,
"CONSUL_INDEX=" + strconv . FormatUint ( idx , 10 ) ,
)
// Collect the output
output , _ := circbuf . NewBuffer ( WatchBufSize )
cmd . Stdout = output
cmd . Stderr = output
// Setup the input
var inp bytes . Buffer
enc := json . NewEncoder ( & inp )
if err := enc . Encode ( data ) ; err != nil {
logger . Error ( "Failed to encode data for watch" ,
"watch" , handler ,
"error" , err ,
)
return
}
cmd . Stdin = & inp
// Run the handler
if err := cmd . Run ( ) ; err != nil {
logger . Error ( "Failed to run watch handler" ,
"watch_handler" , handler ,
"error" , err ,
)
}
// Get the output, add a message about truncation
outputStr := string ( output . Bytes ( ) )
if output . TotalWritten ( ) > output . Size ( ) {
outputStr = fmt . Sprintf ( "Captured %d of %d bytes\n...\n%s" ,
output . Size ( ) , output . TotalWritten ( ) , outputStr )
}
// Log the output
logger . Debug ( "watch handler output" ,
"watch_handler" , handler ,
"output" , outputStr ,
)
}
return fn
}
func makeHTTPWatchHandler ( logger hclog . Logger , config * watch . HttpHandlerConfig ) watch . HandlerFunc {
fn := func ( idx uint64 , data interface { } ) {
trans := cleanhttp . DefaultTransport ( )
// Skip SSL certificate verification if TLSSkipVerify is true
if trans . TLSClientConfig == nil {
trans . TLSClientConfig = & tls . Config {
InsecureSkipVerify : config . TLSSkipVerify ,
}
} else {
trans . TLSClientConfig . InsecureSkipVerify = config . TLSSkipVerify
}
ctx := context . Background ( )
ctx , cancel := context . WithTimeout ( ctx , config . Timeout )
defer cancel ( )
// Create the HTTP client.
httpClient := & http . Client {
Transport : trans ,
}
// Setup the input
var inp bytes . Buffer
enc := json . NewEncoder ( & inp )
if err := enc . Encode ( data ) ; err != nil {
logger . Error ( "Failed to encode data for http watch" ,
"watch" , config . Path ,
"error" , err ,
)
return
}
req , err := http . NewRequest ( config . Method , config . Path , & inp )
if err != nil {
logger . Error ( "Failed to setup http watch" , "error" , err )
return
}
req = req . WithContext ( ctx )
req . Header . Add ( "Content-Type" , "application/json" )
req . Header . Add ( "X-Consul-Index" , strconv . FormatUint ( idx , 10 ) )
for key , values := range config . Header {
for _ , val := range values {
req . Header . Add ( key , val )
}
}
resp , err := httpClient . Do ( req )
if err != nil {
logger . Error ( "Failed to invoke http watch handler" ,
"watch" , config . Path ,
"error" , err ,
)
return
}
defer resp . Body . Close ( )
// Collect the output
output , _ := circbuf . NewBuffer ( WatchBufSize )
io . Copy ( output , resp . Body )
// Get the output, add a message about truncation
outputStr := string ( output . Bytes ( ) )
if output . TotalWritten ( ) > output . Size ( ) {
outputStr = fmt . Sprintf ( "Captured %d of %d bytes\n...\n%s" ,
output . Size ( ) , output . TotalWritten ( ) , outputStr )
}
if resp . StatusCode >= 200 && resp . StatusCode <= 299 {
// Log the output
logger . Trace ( "http watch handler output" ,
"watch" , config . Path ,
"output" , outputStr ,
)
} else {
logger . Error ( "http watch handler failed with output" ,
"watch" , config . Path ,
"status" , resp . Status ,
"output" , outputStr ,
)
}
}
return fn
}
// TODO: return a fully constructed watch.Plan with a Plan.Handler, so that Exempt
// can be ignored by the caller.
func makeWatchPlan ( logger hclog . Logger , params map [ string ] interface { } ) ( * watch . Plan , error ) {
wp , err := watch . ParseExempt ( params , [ ] string { "handler" , "args" } )
if err != nil {
return nil , fmt . Errorf ( "Failed to parse watch (%#v): %v" , params , err )
}
handler , hasHandler := wp . Exempt [ "handler" ]
if hasHandler {
logger . Warn ( "The 'handler' field in watches has been deprecated " +
"and replaced with the 'args' field. See https://www.consul.io/docs/agent/watches.html" )
}
if _ , ok := handler . ( string ) ; hasHandler && ! ok {
return nil , fmt . Errorf ( "Watch handler must be a string" )
}
args , hasArgs := wp . Exempt [ "args" ]
if hasArgs {
wp . Exempt [ "args" ] , err = parseWatchArgs ( args )
if err != nil {
return nil , err
}
}
if hasHandler && hasArgs || hasHandler && wp . HandlerType == "http" || hasArgs && wp . HandlerType == "http" {
return nil , fmt . Errorf ( "Only one watch handler allowed" )
}
if ! hasHandler && ! hasArgs && wp . HandlerType != "http" {
return nil , fmt . Errorf ( "Must define a watch handler" )
}
return wp , nil
}
func parseWatchArgs ( args interface { } ) ( [ ] string , error ) {
switch args := args . ( type ) {
case string :
return [ ] string { args } , nil
case [ ] string :
return args , nil
case [ ] interface { } :
result := make ( [ ] string , 0 , len ( args ) )
for _ , arg := range args {
v , ok := arg . ( string )
if ! ok {
return nil , fmt . Errorf ( "Watch args must be a list of strings" )
}
result = append ( result , v )
}
return result , nil
default :
return nil , fmt . Errorf ( "Watch args must be a list of strings" )
}
}