mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01: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
 | |
| }
 |