internal/settings/settings_linux.go (117 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
package settings
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/Azure/azure-extension-foundation/errorhelper"
"io/ioutil"
"os/exec"
"path/filepath"
)
const settingsFileSuffix = ".settings"
type handlerSettingsFile struct {
RuntimeSettings []struct {
HandlerSettings handlerSettings `json:"handlerSettings"`
} `json:"runtimeSettings"`
}
type handlerSettings struct {
PublicSettings map[string]interface{} `json:"publicSettings"`
ProtectedSettingsBase64 string `json:"protectedSettings"`
SettingsCertThumbprint string `json:"protectedSettingsCertThumbprint"`
}
func GetExtensionSettings(sequenceNumber int, publicSettings, protectedSettings interface{}) error {
publicSettingsJSON, protectedSettingsJSON, err := readSettings(sequenceNumber)
if err != nil {
return errorhelper.AddStackToError(fmt.Errorf("error reading handler settings: %v", err))
}
if err := unmarshalHandlerSettings(publicSettingsJSON, protectedSettingsJSON, &publicSettings, &protectedSettings); err != nil {
return errorhelper.AddStackToError(fmt.Errorf("error parsing handler settings: %v", err))
}
return nil
}
// ReadSettings locates the .settings file and returns public settings
// JSON, and protected settings JSON (by decrypting it with the keys in
// configFolder).
func readSettings(sequenceNumber int) (public, protected map[string]interface{}, _ error) {
hEnv, err := GetEnvironment()
if err != nil {
return nil, nil, errorhelper.AddStackToError(fmt.Errorf("unable to get handler environment: %v", err))
}
configFolderPath := hEnv.HandlerEnvironment.ConfigFolder
cf, err := settingsFilePath(configFolderPath, sequenceNumber)
if err != nil {
return nil, nil, errorhelper.AddStackToError(fmt.Errorf("cannot locate settings file: %v", err))
}
hs, err := parseHandlerSettingsFile(cf)
if err != nil {
return nil, nil, errorhelper.AddStackToError(fmt.Errorf("error parsing settings file: %v", err))
}
public = hs.PublicSettings
if err := unmarshalProtectedSettings(configFolderPath, hs, &protected); err != nil {
return nil, nil, errorhelper.AddStackToError(fmt.Errorf("failed to parse protected settings: %v", err))
}
return public, protected, nil
}
// unmarshalHandlerSettings unmarshals given publicSettings/protectedSettings types
// assumed underlying values are JSON into references publicV/protectedV respectively
// (of struct types that contain structured fields for settings).
func unmarshalHandlerSettings(publicSettings, protectedSettings map[string]interface{}, publicV, protectedV interface{}) error {
if err := unmarshalSettings(publicSettings, &publicV); err != nil {
return fmt.Errorf("failed to unmarshal public settings: %v", err)
}
if err := unmarshalSettings(protectedSettings, &protectedV); err != nil {
return fmt.Errorf("failed to unmarshal protected settings: %v", err)
}
return nil
}
// settingsFilePath returns the full path to the .settings file with the
// highest sequence number found in configFolder.
func settingsFilePath(configFolder string, sequenceNumber int) (string, error) {
return filepath.Join(configFolder, fmt.Sprintf("%d%s", sequenceNumber, settingsFileSuffix)), nil
}
// parseHandlerSettings parses a handler settings file (e.g. 0.settings) and
// returns it as a structured object.
func parseHandlerSettingsFile(path string) (h handlerSettings, _ error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return h, errorhelper.AddStackToError(fmt.Errorf("error reading setting's file %s: %v", path, err))
}
if len(b) == 0 { // if no config is specified, we get an empty file
return h, nil
}
var f handlerSettingsFile
if err := json.Unmarshal(b, &f); err != nil {
return h, errorhelper.AddStackToError(fmt.Errorf("error parsing json: %v", err))
}
if len(f.RuntimeSettings) != 1 {
return h, errorhelper.AddStackToError(fmt.Errorf("wrong runtimeSettings count. expected:1, got:%d", len(f.RuntimeSettings)))
}
return f.RuntimeSettings[0].HandlerSettings, nil
}
// unmarshalSettings makes a round-trip JSON marshaling and unmarshaling
// from in (assumed map[interface]{}) to v (actual settings type).
func unmarshalSettings(in interface{}, v interface{}) error {
s, err := json.Marshal(in)
if err != nil {
return errorhelper.AddStackToError(fmt.Errorf("failed to marshal into json: %v", err))
}
if err := json.Unmarshal(s, &v); err != nil {
return errorhelper.AddStackToError(fmt.Errorf("failed to unmarshal json: %v", err))
}
return nil
}
// unmarshalProtectedSettings decodes the protected settings from handler
// runtime settings JSON file, decrypts it using the certificates and unmarshals
// into the given struct v.
func unmarshalProtectedSettings(configFolder string, hs handlerSettings, v interface{}) error {
if hs.ProtectedSettingsBase64 == "" {
return nil
}
if hs.SettingsCertThumbprint == "" {
return errorhelper.AddStackToError(fmt.Errorf("handlerSettings has protected settings but no cert thumbprint"))
}
decoded, err := base64.StdEncoding.DecodeString(hs.ProtectedSettingsBase64)
if err != nil {
return errorhelper.AddStackToError(fmt.Errorf("failed to decode base64: %v", err))
}
// go two levels up where certs are placed (/var/lib/waagent)
crt := filepath.Join(configFolder, "..", "..", fmt.Sprintf("%s.crt", hs.SettingsCertThumbprint))
prv := filepath.Join(configFolder, "..", "..", fmt.Sprintf("%s.prv", hs.SettingsCertThumbprint))
// we use os/exec instead of azure-docker-extension/pkg/executil here as
// other extension handlers depend on this package for parsing handler
// settings.
cmd := exec.Command("openssl", "smime", "-inform", "DER", "-decrypt", "-recip", crt, "-inkey", prv)
var bOut, bErr bytes.Buffer
cmd.Stdin = bytes.NewReader(decoded)
cmd.Stdout = &bOut
cmd.Stderr = &bErr
if err := cmd.Run(); err != nil {
return errorhelper.AddStackToError(fmt.Errorf("decrypting protected settings failed: error=%v stderr=%s", err, string(bErr.Bytes())))
}
// decrypted: json object for protected settings
if err := json.Unmarshal(bOut.Bytes(), &v); err != nil {
return errorhelper.AddStackToError(fmt.Errorf("failed to unmarshal decrypted settings json: %v", err))
}
return nil
}