mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-28 20:45:21 +00:00
203 lines
5.0 KiB
Go
203 lines
5.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
|
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
"github.com/juanfont/headscale/hscontrol"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
|
"github.com/rs/zerolog/log"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
|
|
SocketWritePermissions = 0o666
|
|
)
|
|
|
|
func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
|
|
cfg, err := types.LoadServerConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"failed to load configuration while creating headscale instance: %w",
|
|
err,
|
|
)
|
|
}
|
|
|
|
app, err := hscontrol.NewHeadscale(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return app, nil
|
|
}
|
|
|
|
func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
|
|
cfg, err := types.LoadCLIConfig()
|
|
if err != nil {
|
|
log.Fatal().
|
|
Err(err).
|
|
Caller().
|
|
Msgf("Failed to load configuration")
|
|
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
|
|
}
|
|
|
|
log.Debug().
|
|
Dur("timeout", cfg.CLI.Timeout).
|
|
Msgf("Setting timeout")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
|
|
|
|
grpcOptions := []grpc.DialOption{
|
|
grpc.WithBlock(),
|
|
}
|
|
|
|
address := cfg.CLI.Address
|
|
|
|
// If the address is not set, we assume that we are on the server hosting hscontrol.
|
|
if address == "" {
|
|
log.Debug().
|
|
Str("socket", cfg.UnixSocket).
|
|
Msgf("HEADSCALE_CLI_ADDRESS environment is not set, connecting to unix socket.")
|
|
|
|
address = cfg.UnixSocket
|
|
|
|
// Try to give the user better feedback if we cannot write to the headscale
|
|
// socket.
|
|
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) // nolint
|
|
if err != nil {
|
|
if os.IsPermission(err) {
|
|
log.Fatal().
|
|
Err(err).
|
|
Str("socket", cfg.UnixSocket).
|
|
Msgf("Unable to read/write to headscale socket, do you have the correct permissions?")
|
|
}
|
|
}
|
|
socket.Close()
|
|
|
|
grpcOptions = append(
|
|
grpcOptions,
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
grpc.WithContextDialer(util.GrpcSocketDialer),
|
|
)
|
|
} else {
|
|
// If we are not connecting to a local server, require an API key for authentication
|
|
apiKey := cfg.CLI.APIKey
|
|
if apiKey == "" {
|
|
log.Fatal().Caller().Msgf("HEADSCALE_CLI_API_KEY environment variable needs to be set.")
|
|
}
|
|
grpcOptions = append(grpcOptions,
|
|
grpc.WithPerRPCCredentials(tokenAuth{
|
|
token: apiKey,
|
|
}),
|
|
)
|
|
|
|
if cfg.CLI.Insecure {
|
|
tlsConfig := &tls.Config{
|
|
// turn of gosec as we are intentionally setting
|
|
// insecure.
|
|
//nolint:gosec
|
|
InsecureSkipVerify: true,
|
|
}
|
|
|
|
grpcOptions = append(grpcOptions,
|
|
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
|
)
|
|
} else {
|
|
grpcOptions = append(grpcOptions,
|
|
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
|
|
)
|
|
}
|
|
}
|
|
|
|
log.Trace().Caller().Str("address", address).Msg("Connecting via gRPC")
|
|
conn, err := grpc.DialContext(ctx, address, grpcOptions...)
|
|
if err != nil {
|
|
log.Fatal().Caller().Err(err).Msgf("Could not connect: %v", err)
|
|
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
|
|
}
|
|
|
|
client := v1.NewHeadscaleServiceClient(conn)
|
|
|
|
return ctx, client, conn, cancel
|
|
}
|
|
|
|
func output(result interface{}, override string, outputFormat string) string {
|
|
var jsonBytes []byte
|
|
var err error
|
|
switch outputFormat {
|
|
case "json":
|
|
jsonBytes, err = json.MarshalIndent(result, "", "\t")
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to unmarshal output")
|
|
}
|
|
case "json-line":
|
|
jsonBytes, err = json.Marshal(result)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to unmarshal output")
|
|
}
|
|
case "yaml":
|
|
jsonBytes, err = yaml.Marshal(result)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to unmarshal output")
|
|
}
|
|
default:
|
|
// nolint
|
|
return override
|
|
}
|
|
|
|
return string(jsonBytes)
|
|
}
|
|
|
|
// SuccessOutput prints the result to stdout and exits with status code 0.
|
|
func SuccessOutput(result interface{}, override string, outputFormat string) {
|
|
fmt.Println(output(result, override, outputFormat))
|
|
os.Exit(0)
|
|
}
|
|
|
|
// ErrorOutput prints an error message to stderr and exits with status code 1.
|
|
func ErrorOutput(errResult error, override string, outputFormat string) {
|
|
type errOutput struct {
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "%s\n", output(errOutput{errResult.Error()}, override, outputFormat))
|
|
os.Exit(1)
|
|
}
|
|
|
|
func HasMachineOutputFlag() bool {
|
|
for _, arg := range os.Args {
|
|
if arg == "json" || arg == "json-line" || arg == "yaml" {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
type tokenAuth struct {
|
|
token string
|
|
}
|
|
|
|
// Return value is mapped to request headers.
|
|
func (t tokenAuth) GetRequestMetadata(
|
|
ctx context.Context,
|
|
in ...string,
|
|
) (map[string]string, error) {
|
|
return map[string]string{
|
|
"authorization": "Bearer " + t.token,
|
|
}, nil
|
|
}
|
|
|
|
func (tokenAuth) RequireTransportSecurity() bool {
|
|
return true
|
|
}
|