<commit_before>// MIT License
//
// Copyright (c) 2017 Stefan Wichmann
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package main

import (
	log "github.com/Sirupsen/logrus"
	"github.com/stefanwichmann/go.hue"
	"strconv"
	"strings"
	"time"
)

var lightsSupportingDimming = []string{"Dimmable Light", "Color Temperature Light", "Color Light", "Extended Color Light"}
var lightsSupportingColorTemperature = []string{"Color Temperature Light", "Extended Color Light"}
var lightsSupportingXYColor = []string{"Color Light", "Extended Color Light"}

// Light represents a single physical hue light in your system.
// It is used to read and write it's state.
type Light struct {
	id                       int
	name                     string
	hueLight                 hue.Light
	savedLightState          LightState
	currentLightState        LightState
	targetLightState         LightState
	on                       bool
	reachable                bool
	tracking                 bool
	manually                 bool
	dimmable                 bool
	supportsColorTemperature bool
	supportsXYColor          bool
}

const lightUpdateIntervalInSeconds = 1
const lightTransitionIntervalInSeconds = 1
const stateUpdateIntervalInSeconds = 60

func (light *Light) updateCyclic(configuration Configuration) {
	// Filter devices we can't control
	if !light.dimmable && !light.supportsXYColor && !light.supportsColorTemperature {
		log.Printf("💡 Device %v doesn't support any functionality Kelvin uses. Ignoring...", light.name)
		return
	}

	schedule, err := configuration.lightScheduleForDay(light.id, time.Now())
	if err != nil {
		log.Printf("💡 Light %v is not associated to any schedule. Ignoring...", light.name)
		return
	}
	log.Printf("💡 Light %s: Activating schedule for %v (sunrise: %v, sunset: %v)", light.name, schedule.endOfDay.Format("Jan 2 2006"), schedule.sunrise.Time.Format(time.Kitchen), schedule.sunset.Time.Format(time.Kitchen))
	interval, err := schedule.currentInterval(time.Now())
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("💡 Light %s: Activating interval %v - %v", light.name, interval.Start.Time.Format(time.Kitchen), interval.End.Time.Format(time.Kitchen))
	light.targetLightState, err = interval.calculateLightStateInInterval(time.Now())
	if err != nil {
		log.Fatal(err)
	}
	log.Debugf("💡 Light %s: Initializing target state %+v", light.name, light.targetLightState)

	// Start cyclic polling
	log.Debugf("💡 Light %s: Starting cyclic update...", light.name)
	lightUpdateTick := time.Tick(lightUpdateIntervalInSeconds * time.Second)
	stateUpdateTick := time.Tick(stateUpdateIntervalInSeconds * time.Second)
	for {
		select {
		case <-time.After(schedule.endOfDay.Sub(time.Now()) + 1*time.Second):
			// Day has ended, calculate new schedule
			log.Debugf("💡 Light %s: End of day reached: %v)", light.name, schedule.endOfDay.Format("Jan 2 2006"))
			schedule, err = configuration.lightScheduleForDay(light.id, time.Now())
			if err != nil {
				// should not happen
				log.Warningln(err)
				continue
			}
			log.Printf("💡 Light %s: Activating schedule for %v (sunrise: %v, sunset: %v)", light.name, schedule.endOfDay.Format("Jan 2 2006"), schedule.sunrise.Time.Format(time.Kitchen), schedule.sunset.Time.Format(time.Kitchen))
		case <-time.After(interval.End.Time.Sub(time.Now()) + 2*time.Second):
			// interval has ended
			log.Debugf("💡 Light %s: Active interval (%v - %v) ended)", light.name, interval.Start.Time.Format(time.Kitchen), interval.End.Time.Format(time.Kitchen))
			interval, err = schedule.currentInterval(time.Now())
			if err != nil {
				// schedule seems to be wrong.
				log.Warningln(err)
				continue
			}
			log.Printf("💡 Light %s: Activating interval %v - %v", light.name, interval.Start.Time.Format(time.Kitchen), interval.End.Time.Format(time.Kitchen))
		case <-stateUpdateTick:
			// update color every minute
			log.Debugf("💡 Light %s: Updating target lightstate for interval %v - %v (current: %+v)", light.name, interval.Start.Time.Format(time.Kitchen), interval.End.Time.Format(time.Kitchen), light.targetLightState)
			light.targetLightState, err = interval.calculateLightStateInInterval(time.Now())
			if err != nil {
				// interval seems to be wrong. Ignore and try again in one minute.
				log.Debugln(err)
				continue
			}
			log.Debugf("💡 Light %s: Updated target state to %+v", light.name, light.targetLightState)
		case <-lightUpdateTick:
			light.update()
		}
	}
}

func (light *Light) initialize() error {
	attr, err := light.hueLight.GetLightAttributes()
	if err != nil {
		return err
	}

	// initialize non changing values
	light.name = attr.Name
	light.dimmable = containsString(lightsSupportingDimming, attr.Type)
	light.supportsColorTemperature = containsString(lightsSupportingColorTemperature, attr.Type)
	light.supportsXYColor = containsString(lightsSupportingXYColor, attr.Type)

	// initialize changing values
	light.on = attr.State.On
	light.reachable = attr.State.Reachable
	light.currentLightState = lightStateFromHueValues(attr.State.Ct, attr.State.Xy, attr.State.Bri)

	return nil
}

func (light *Light) updateCurrentLightState() error {
	attr, err := light.hueLight.GetLightAttributes()
	if err != nil {
		return err
	}
	light.reachable = attr.State.Reachable
	light.on = attr.State.On
	light.currentLightState = lightStateFromHueValues(attr.State.Ct, attr.State.Xy, attr.State.Bri)

	return nil
}

func (light *Light) update() error {
	// refresh current state
	light.updateCurrentLightState()

	// Light reachable or on?
	if !light.reachable || !light.on {
		if light.tracking {
			log.Printf("💡 Light %s is no longer reachable or turned on. Clearing state.", light.name)
			light.tracking = false
			light.manually = false
			return nil
		}

		// ignore light because we are not tracking it.
		return nil
	}

	// did the light just appear?
	if !light.tracking {
		log.Printf("💡 Light %s just appeared. Initializing state to %vK at %v%%", light.name, light.targetLightState.colorTemperature, light.targetLightState.brightness)

		// For initialization we set the state again and again for 10 seconds
		// because during startup the zigbee communication might be unstable
		for index := 0; index < 9; index++ {
			light.setLightState(light.targetLightState)
		}
		// safe the state of the last iteration
		setLightState, err := light.setLightState(light.targetLightState)
		if err != nil {
			return err
		}
		light.savedLightState = setLightState
		light.targetLightState = setLightState
		light.tracking = true
		return nil
	}

	// light in manual state
	if light.manually {
		return nil
	}

	// did the user manually change the light state?
	if !light.savedLightState.equals(light.currentLightState) {
		if log.GetLevel() == log.DebugLevel {
			log.Debugf("💡 Light %s was manually changed. Current state: %+v - Saved state: %+v", light.name, light.currentLightState, light.savedLightState)
		} else {
			log.Printf("💡 Light %s was manually changed. Disabling updates..", light.name)
		}
		light.manually = true
		return nil
	}

	// Update needed?
	if light.targetLightState.equals(light.currentLightState) {
		return nil
	}

	// Light is reachable, turned on and in automatic state. Update to new state.
	log.Printf("💡 Updating light %s to %vK at %v%%", light.name, light.targetLightState.colorTemperature, light.targetLightState.brightness)
	setLightState, err := light.setLightState(light.targetLightState)
	if err != nil {
		return err
	}

	// Debug: Compare values
	if !setLightState.equals(light.targetLightState) {
		log.Debugf("💡 Light %s - Target and Set state differ: %v, %v", light.name, light.targetLightState, setLightState)
	}

	light.savedLightState = setLightState
	light.targetLightState = setLightState
	return nil
}

func (light *Light) setLightState(state LightState) (LightState, error) {
	// Don't send repeated "On" as this slows the bridge down (see https://developers.meethue.com/faq-page #Performance)
	var hueLightState hue.SetLightState
	colorTemperature, color, brightness := state.convertValuesToHue()
	if light.supportsXYColor && state.color[0] != 0 && state.color[1] != 0 {
		hueLightState.Xy = []float32{color[0], color[1]}
	} else if light.supportsColorTemperature && state.colorTemperature != 0 {
		hueLightState.Ct = strconv.Itoa(colorTemperature)
	}
	if light.dimmable && state.brightness != 0 {
		hueLightState.Bri = strconv.Itoa(brightness)
	}
	hueLightState.TransitionTime = strconv.Itoa(lightTransitionIntervalInSeconds * 10) // conversion to 100ms-value

	results, err := light.hueLight.SetState(hueLightState)
	if err != nil {
		return LightState{0, []float32{0, 0}, 0}, err
	}

	// iterate over result to acquire set values
	for _, result := range results {
		for key, value := range result.Success {
			path := strings.Split(key, "/")

			switch path[len(path)-1] {
			case "xy":
				color = []float32{} // clear old color values
				for _, elem := range value.([]interface{}) {
					color = append(color, float32(elem.(float64)))
				}
			case "bri":
				brightness = int(value.(float64))
			case "ct":
				colorTemperature = int(value.(float64))
			}
		}
	}
	setLightState := lightStateFromHueValues(colorTemperature, color, brightness)

	// wait while the light is in transition before returning
	time.Sleep(lightTransitionIntervalInSeconds + 1*time.Second)
	return setLightState, nil
}
<commit_msg>Make light struct serializable Internal refactoring Allow changes to the configuration during runtime<commit_after>// MIT License
//
// Copyright (c) 2017 Stefan Wichmann
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package main

import (
	log "github.com/Sirupsen/logrus"
	"github.com/stefanwichmann/go.hue"
	"strconv"
	"strings"
	"time"
)

var lightsSupportingDimming = []string{"Dimmable Light", "Color Temperature Light", "Color Light", "Extended Color Light"}
var lightsSupportingColorTemperature = []string{"Color Temperature Light", "Extended Color Light"}
var lightsSupportingXYColor = []string{"Color Light", "Extended Color Light"}

// Light represents a single physical hue light in your system.
// It is used to read and write it's state.
type Light struct {
	ID                       int
	Name                     string
	HueLight                 hue.Light
	SavedLightState          LightState
	CurrentLightState        LightState
	TargetLightState         LightState
	On                       bool
	Reachable                bool
	Tracking                 bool
	Automatic                bool
	Dimmable                 bool
	SupportsColorTemperature bool
	SupportsXYColor          bool
	Configuration            *Configuration
	Schedule                 Schedule
	Interval                 Interval
}

const lightUpdateIntervalInSeconds = 1
const lightTransitionIntervalInSeconds = 1
const stateUpdateIntervalInSeconds = 60

func (light *Light) updateCyclic(configuration *Configuration) {
	light.Configuration = configuration

	// Filter devices we can't control
	if !light.Dimmable && !light.SupportsXYColor && !light.SupportsColorTemperature {
		log.Printf("💡 Device %v doesn't support any functionality Kelvin uses. Ignoring...", light.Name)
		return
	}

	light.updateSchedule()
	light.updateInterval()
	light.updateTargetLightState()

	// Start cyclic polling
	log.Debugf("💡 Light %s: Starting cyclic update...", light.Name)
	lightUpdateTick := time.Tick(lightUpdateIntervalInSeconds * time.Second)
	stateUpdateTick := time.Tick(stateUpdateIntervalInSeconds * time.Second)
	for {
		select {
		case <-time.After(light.Schedule.endOfDay.Sub(time.Now()) + 1*time.Second):
			// Day has ended, calculate new schedule
			light.updateSchedule()
		case <-stateUpdateTick:
			// update interval and color every minute
			light.updateInterval()
			light.updateTargetLightState()
		case <-lightUpdateTick:
			light.update()
		}
	}
}

func (light *Light) initialize() error {
	attr, err := light.HueLight.GetLightAttributes()
	if err != nil {
		return err
	}

	// initialize non changing values
	light.Name = attr.Name
	light.Dimmable = containsString(lightsSupportingDimming, attr.Type)
	light.SupportsColorTemperature = containsString(lightsSupportingColorTemperature, attr.Type)
	light.SupportsXYColor = containsString(lightsSupportingXYColor, attr.Type)

	light.updateCurrentLightState()
	return nil
}

func (light *Light) updateCurrentLightState() error {
	attr, err := light.HueLight.GetLightAttributes()
	if err != nil {
		return err
	}
	light.Reachable = attr.State.Reachable
	if !light.Reachable {
		light.On = false
		light.CurrentLightState = LightState{0, []float32{0,0}, 0}
	} else {
		light.On = attr.State.On
		light.CurrentLightState = lightStateFromHueValues(attr.State.Ct, attr.State.Xy, attr.State.Bri)
	}
	return nil
}

func (light *Light) update() error {
	// refresh current state
	light.updateCurrentLightState()

	// Light reachable or on?
	if !light.Reachable || !light.On {
		if light.Tracking {
			log.Printf("💡 Light %s is no longer reachable or turned on. Clearing state.", light.Name)
			light.Tracking = false
			light.Automatic = false
			return nil
		}

		// ignore light because we are not tracking it.
		return nil
	}

	// did the light just appear?
	if !light.Tracking {
		log.Printf("💡 Light %s just appeared. Initializing state to %vK at %v%% brightness.", light.Name, light.TargetLightState.ColorTemperature, light.TargetLightState.Brightness)

		// For initialization we set the state again and again for 10 seconds
		// because during startup the zigbee communication might be unstable
		for index := 0; index < 9; index++ {
			light.setLightState(light.TargetLightState)
		}
		// safe the state of the last iteration
		setLightState, err := light.setLightState(light.TargetLightState)
		if err != nil {
			return err
		}
		light.SavedLightState = setLightState
		//light.TargetLightState = setLightState
		light.Tracking = true
		light.Automatic = true
		log.Debugf("💡 Light %s was updated to %vK at %v%% brightness", light.Name, light.TargetLightState.ColorTemperature, light.TargetLightState.Brightness)
		return nil
	}

	// light in manual state
	if !light.Automatic {
		return nil
	}

	// did the user manually change the light state?
	if !light.SavedLightState.equals(light.CurrentLightState) {
		if log.GetLevel() == log.DebugLevel {
			log.Debugf("💡 Light %s was manually changed. Current state: %+v - Saved state: %+v", light.Name, light.CurrentLightState, light.SavedLightState)
		} else {
			log.Printf("💡 Light %s was changed manually. Disabling Kelvin.", light.Name)
		}
		light.Automatic = false
		return nil
	}

	// Update needed?
	if light.TargetLightState.equals(light.CurrentLightState) {
		return nil
	}
	log.Debugf("💡 Light %s - Target and Current state differ: %v, %v", light.Name, light.TargetLightState, light.CurrentLightState)

	// Light is reachable, turned on and in automatic state. Update to new state.
	setLightState, err := light.setLightState(light.TargetLightState)
	if err != nil {
		return err
	}

	// Debug: Compare values
	if !setLightState.equals(light.TargetLightState) {
		log.Debugf("💡 Light %s - Target and Set state differ: %v, %v", light.Name, light.TargetLightState, setLightState)
	}

	light.SavedLightState = setLightState
	//light.TargetLightState = setLightState
	log.Printf("💡 Light %s was updated to %vK at %v%% brightness", light.Name, light.TargetLightState.ColorTemperature, light.TargetLightState.Brightness)
	return nil
}

func (light *Light) setLightState(state LightState) (LightState, error) {
	// Don't send repeated "On" as this slows the bridge down (see https://developers.meethue.com/faq-page #Performance)
	var hueLightState hue.SetLightState
	colorTemperature, color, brightness := state.convertValuesToHue()
	if light.SupportsColorTemperature && state.ColorTemperature != 0 {
		hueLightState.Ct = strconv.Itoa(colorTemperature)
	} else if light.SupportsXYColor && state.Color[0] != 0 && state.Color[1] != 0 {
		hueLightState.Xy = []float32{color[0], color[1]}
	}
	if light.Dimmable && state.Brightness != 0 {
		hueLightState.Bri = strconv.Itoa(brightness)
	}
	hueLightState.TransitionTime = strconv.Itoa(lightTransitionIntervalInSeconds * 10) // conversion to 100ms-value

	results, err := light.HueLight.SetState(hueLightState)
	if err != nil {
		return LightState{0, []float32{0, 0}, 0}, err
	}

	// iterate over result to acquire set values
	for _, result := range results {
		for key, value := range result.Success {
			path := strings.Split(key, "/")

			switch path[len(path)-1] {
			case "xy":
				color = []float32{} // clear old color values
				for _, elem := range value.([]interface{}) {
					color = append(color, float32(elem.(float64)))
				}
			case "bri":
				brightness = int(value.(float64))
			case "ct":
				colorTemperature = int(value.(float64))
			}
		}
	}
	setLightState := lightStateFromHueValues(colorTemperature, color, brightness)

	// wait while the light is in transition before returning
	time.Sleep(lightTransitionIntervalInSeconds + 1*time.Second)
	return setLightState, nil
}

func (light *Light) updateConfiguration(configuration *Configuration) {
	light.Configuration = configuration
	light.updateSchedule()
	light.updateInterval()
	light.updateTargetLightState()
}

func (light *Light) updateSchedule() {
	newSchedule, err := light.Configuration.lightScheduleForDay(light.ID, time.Now())
	if err != nil {
		log.Printf("💡 Light %v is not associated to any schedule. Ignoring...", light.Name)
		light.Schedule = newSchedule // assign empty schedule
		return
	}
	light.Schedule = newSchedule
	log.Printf("💡 Light %s: Activating schedule for %v (sunrise: %v, sunset: %v)", light.Name, light.Schedule.endOfDay.Format("Jan 2 2006"), light.Schedule.sunrise.Time.Format("15:04"), light.Schedule.sunset.Time.Format("15:04"))
	light.updateInterval()
}

func (light *Light) updateInterval() {
	newInterval, err := light.Schedule.currentInterval(time.Now())
	if err != nil {
		log.Printf("💡 Light %v has no active interval. Ignoring...", light.Name)
		light.Interval = newInterval // assign empty interval
		return
	}
	if newInterval != light.Interval {
		light.Interval = newInterval
		log.Printf("💡 Light %s: Activating interval %v - %v", light.Name, light.Interval.Start.Time.Format("15:04"), light.Interval.End.Time.Format("15:04"))
	}
}

func (light *Light) updateTargetLightState() {
	newLightState, err := light.Interval.calculateLightStateInInterval(time.Now())
	if err != nil {
		// interval seems to be wrong. Ignore and try again in one minute.
		log.Debugln(err)
		return
	}
	//log.Debugf("💡 Light %s: Updating target lightstate from %+v to %+v)", light.Name, light.TargetLightState, newLightState)

	// First initialization of the TargetLightState
	if light.TargetLightState.ColorTemperature == 0 && len(light.TargetLightState.Color) == 0 && light.TargetLightState.Brightness == 0 {
		light.TargetLightState = newLightState
		log.Debugf("💡 Light %s: Initialized target state to %+v", light.Name, light.TargetLightState)
		return
	}

	if !light.TargetLightState.equals(newLightState) {
		light.TargetLightState = newLightState
		log.Debugf("💡 Light %s: Updated target state to %+v", light.Name, light.TargetLightState)
	}
}
<|endoftext|>


<commit_before>// MIT License
//
// Copyright (c) 2017 Stefan Wichmann
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package main

import (
	log "github.com/Sirupsen/logrus"
	"github.com/stefanwichmann/go.hue"
	"strconv"
	"strings"
	"time"
)

var lightsSupportingDimming = []string{"Dimmable Light", "Color Temperature Light", "Color Light", "Extended Color Light"}
var lightsSupportingColorTemperature = []string{"Color Temperature Light", "Extended Color Light"}
var lightsSupportingXYColor = []string{"Color Light", "Extended Color Light"}

// Light represents a single physical hue light in your system.
// It is used to read and write it's state.
type Light struct {
	ID                       int
	Name                     string
	HueLight                 hue.Light
	SavedLightState          LightState
	CurrentLightState        LightState
	TargetLightState         LightState
	Scheduled                bool
	On                       bool
	Reachable                bool
	Tracking                 bool
	Automatic                bool
	Dimmable                 bool
	SupportsColorTemperature bool
	SupportsXYColor          bool
	Configuration            *Configuration
	Schedule                 Schedule
	Interval                 Interval
}

const lightUpdateIntervalInSeconds = 1
const lightTransitionIntervalInSeconds = 1
const stateUpdateIntervalInSeconds = 60

func (light *Light) updateCyclic(configuration *Configuration) {
	light.Configuration = configuration

	// Filter devices we can't control
	if !light.Dimmable && !light.SupportsXYColor && !light.SupportsColorTemperature {
		log.Printf("💡 Device %v doesn't support any functionality Kelvin uses. Ignoring...", light.Name)
		return
	}

	light.updateSchedule()
	light.updateInterval()
	light.updateTargetLightState()

	// Start cyclic polling
	log.Debugf("💡 Light %s: Starting cyclic update...", light.Name)
	lightUpdateTick := time.Tick(lightUpdateIntervalInSeconds * time.Second)
	stateUpdateTick := time.Tick(stateUpdateIntervalInSeconds * time.Second)
	for {
		select {
		case <-time.After(time.Until(light.Schedule.endOfDay) + 1*time.Second):
			// Day has ended, calculate new schedule
			light.updateSchedule()
		case <-stateUpdateTick:
			// update interval and color every minute
			light.updateInterval()
			light.updateTargetLightState()
		case <-lightUpdateTick:
			light.update()
		}
	}
}

func (light *Light) initialize() error {
	attr, err := light.HueLight.GetLightAttributes()
	if err != nil {
		return err
	}

	// initialize non changing values
	light.Name = attr.Name
	light.Dimmable = containsString(lightsSupportingDimming, attr.Type)
	light.SupportsColorTemperature = containsString(lightsSupportingColorTemperature, attr.Type)
	light.SupportsXYColor = containsString(lightsSupportingXYColor, attr.Type)

	light.updateCurrentLightState()
	return nil
}

func (light *Light) updateCurrentLightState() error {
	attr, err := light.HueLight.GetLightAttributes()
	if err != nil {
		return err
	}
	light.Reachable = attr.State.Reachable
	if !light.Reachable {
		light.On = false
		light.CurrentLightState = LightState{0, []float32{0, 0}, 0}
	} else {
		light.On = attr.State.On
		light.CurrentLightState = lightStateFromHueValues(attr.State.Ct, attr.State.Xy, attr.State.Bri)
	}
	light.CurrentLightState.isValid()
	return nil
}

func (light *Light) update() error {
	// refresh current state
	light.updateCurrentLightState()

	// is the light associated to any schedule?
	if !light.Scheduled {
		return nil
	}

	// Light reachable or on?
	if !light.Reachable || !light.On {
		if light.Tracking {
			log.Printf("💡 Light %s is no longer reachable or turned on. Clearing state.", light.Name)
			light.Tracking = false
			light.Automatic = false
			return nil
		}

		// ignore light because we are not tracking it.
		return nil
	}

	// did the light just appear?
	if !light.Tracking {
		log.Printf("💡 Light %s just appeared. Initializing state to %vK at %v%% brightness.", light.Name, light.TargetLightState.ColorTemperature, light.TargetLightState.Brightness)

		// For initialization we set the state again and again for 10 seconds
		// because during startup the zigbee communication might be unstable
		for index := 0; index < 9; index++ {
			light.setLightState(light.TargetLightState)
		}
		// safe the state of the last iteration
		setLightState, err := light.setLightState(light.TargetLightState)
		if err != nil {
			return err
		}
		light.SavedLightState = setLightState
		//light.TargetLightState = setLightState
		light.Tracking = true
		light.Automatic = true
		log.Debugf("💡 Light %s was updated to %vK at %v%% brightness", light.Name, light.TargetLightState.ColorTemperature, light.TargetLightState.Brightness)
		return nil
	}

	// light in manual state
	if !light.Automatic {
		return nil
	}

	// did the user manually change the light state?
	if !light.SavedLightState.equals(light.CurrentLightState) {
		if log.GetLevel() == log.DebugLevel {
			log.Debugf("💡 Light %s was manually changed. Current state: %+v - Saved state: %+v", light.Name, light.CurrentLightState, light.SavedLightState)
		} else {
			log.Printf("💡 Light %s was changed manually. Disabling Kelvin.", light.Name)
		}
		light.Automatic = false
		return nil
	}

	// Update needed?
	if light.TargetLightState.equals(light.CurrentLightState) {
		return nil
	}
	log.Debugf("💡 Light %s - Target and Current state differ: %v, %v", light.Name, light.TargetLightState, light.CurrentLightState)

	// Light is reachable, turned on and in automatic state. Update to new state.
	setLightState, err := light.setLightState(light.TargetLightState)
	if err != nil {
		return err
	}

	// Debug: Compare values
	if !setLightState.equals(light.TargetLightState) {
		log.Debugf("💡 Light %s - Target and set state differ: %v, %v", light.Name, light.TargetLightState, setLightState)
	}

	light.SavedLightState = setLightState
	log.Printf("💡 Light %s was updated to %vK at %v%% brightness", light.Name, light.TargetLightState.ColorTemperature, light.TargetLightState.Brightness)
	return nil
}

func (light *Light) setLightState(state LightState) (LightState, error) {
	state.isValid()

	// Don't send repeated "On" as this slows the bridge down (see https://developers.meethue.com/faq-page #Performance)
	var hueLightState hue.SetLightState
	colorTemperature, color, brightness := state.convertValuesToHue()
	if light.SupportsXYColor && (state.Color[0] != 0 || state.Color[1] != 0) {
		hueLightState.Xy = []float32{color[0], color[1]}
	} else if light.SupportsColorTemperature && state.ColorTemperature != 0 {
		hueLightState.Ct = strconv.Itoa(colorTemperature)
	}
	if light.Dimmable && state.Brightness != 0 {
		hueLightState.Bri = strconv.Itoa(brightness)
	}
	hueLightState.TransitionTime = strconv.Itoa(lightTransitionIntervalInSeconds * 10) // conversion to 100ms-value

	results, err := light.HueLight.SetState(hueLightState)
	if err != nil {
		return LightState{0, []float32{0, 0}, 0}, err
	}

	// iterate over result to acquire set values
	for _, result := range results {
		for key, value := range result.Success {
			path := strings.Split(key, "/")

			switch path[len(path)-1] {
			case "xy":
				color = []float32{} // clear old color values
				for _, elem := range value.([]interface{}) {
					color = append(color, float32(elem.(float64)))
				}
			case "bri":
				brightness = int(value.(float64))
			case "ct":
				colorTemperature = int(value.(float64))
			}
		}
	}
	setLightState := lightStateFromHueValues(colorTemperature, color, brightness)
	setLightState.isValid()

	// wait while the light is in transition before returning
	time.Sleep(lightTransitionIntervalInSeconds + 1*time.Second)
	return setLightState, nil
}

func (light *Light) updateConfiguration(configuration *Configuration) {
	light.Configuration = configuration
	light.updateSchedule()
	light.updateInterval()
	light.updateTargetLightState()
}

func (light *Light) updateSchedule() {
	newSchedule, err := light.Configuration.lightScheduleForDay(light.ID, time.Now())
	if err != nil {
		log.Printf("💡 Light %v is not associated to any schedule. Ignoring...", light.Name)
		light.Schedule = newSchedule // assign empty schedule
		light.Scheduled = false
		return
	}
	light.Schedule = newSchedule
	light.Scheduled = true
	log.Printf("💡 Light %s: Activating schedule for %v (sunrise: %v, sunset: %v)", light.Name, light.Schedule.endOfDay.Format("Jan 2 2006"), light.Schedule.sunrise.Time.Format("15:04"), light.Schedule.sunset.Time.Format("15:04"))
	light.updateInterval()
}

func (light *Light) updateInterval() {
	if !light.Scheduled {
		log.Debugf("💡 Light %v is not associated to any schedule. No interval to update...", light.Name)
		return
	}

	newInterval, err := light.Schedule.currentInterval(time.Now())
	if err != nil {
		log.Printf("💡 Light %v has no active interval. Ignoring...", light.Name)
		light.Interval = newInterval // assign empty interval
		return
	}
	if newInterval != light.Interval {
		light.Interval = newInterval
		log.Printf("💡 Light %s: Activating interval %v - %v", light.Name, light.Interval.Start.Time.Format("15:04"), light.Interval.End.Time.Format("15:04"))
	}
}

func (light *Light) updateTargetLightState() {
	if !light.Scheduled {
		log.Debugf("💡 Light %v is not associated to any schedule. No target light state to update...", light.Name)
		return
	}

	newLightState := light.Interval.calculateLightStateInInterval(time.Now())
	newLightState.isValid()

	// First initialization of the TargetLightState
	if light.TargetLightState.ColorTemperature == 0 && len(light.TargetLightState.Color) == 0 && light.TargetLightState.Brightness == 0 {
		light.TargetLightState = newLightState
		log.Debugf("💡 Light %s: Initialized target state to %+v", light.Name, light.TargetLightState)
		return
	}

	if !light.TargetLightState.equals(newLightState) {
		light.TargetLightState = newLightState
		log.Debugf("💡 Light %s: Updated target state to %+v", light.Name, light.TargetLightState)
	}
}
<commit_msg>Print warning if lightstate validation fails<commit_after>// MIT License
//
// Copyright (c) 2017 Stefan Wichmann
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package main

import (
	log "github.com/Sirupsen/logrus"
	"github.com/stefanwichmann/go.hue"
	"strconv"
	"strings"
	"time"
)

var lightsSupportingDimming = []string{"Dimmable Light", "Color Temperature Light", "Color Light", "Extended Color Light"}
var lightsSupportingColorTemperature = []string{"Color Temperature Light", "Extended Color Light"}
var lightsSupportingXYColor = []string{"Color Light", "Extended Color Light"}

// Light represents a single physical hue light in your system.
// It is used to read and write it's state.
type Light struct {
	ID                       int
	Name                     string
	HueLight                 hue.Light
	SavedLightState          LightState
	CurrentLightState        LightState
	TargetLightState         LightState
	Scheduled                bool
	On                       bool
	Reachable                bool
	Tracking                 bool
	Automatic                bool
	Dimmable                 bool
	SupportsColorTemperature bool
	SupportsXYColor          bool
	Configuration            *Configuration
	Schedule                 Schedule
	Interval                 Interval
}

const lightUpdateIntervalInSeconds = 1
const lightTransitionIntervalInSeconds = 1
const stateUpdateIntervalInSeconds = 60

func (light *Light) updateCyclic(configuration *Configuration) {
	light.Configuration = configuration

	// Filter devices we can't control
	if !light.Dimmable && !light.SupportsXYColor && !light.SupportsColorTemperature {
		log.Printf("💡 Device %v doesn't support any functionality Kelvin uses. Ignoring...", light.Name)
		return
	}

	light.updateSchedule()
	light.updateInterval()
	light.updateTargetLightState()

	// Start cyclic polling
	log.Debugf("💡 Light %s: Starting cyclic update...", light.Name)
	lightUpdateTick := time.Tick(lightUpdateIntervalInSeconds * time.Second)
	stateUpdateTick := time.Tick(stateUpdateIntervalInSeconds * time.Second)
	for {
		select {
		case <-time.After(time.Until(light.Schedule.endOfDay) + 1*time.Second):
			// Day has ended, calculate new schedule
			light.updateSchedule()
		case <-stateUpdateTick:
			// update interval and color every minute
			light.updateInterval()
			light.updateTargetLightState()
		case <-lightUpdateTick:
			light.update()
		}
	}
}

func (light *Light) initialize() error {
	attr, err := light.HueLight.GetLightAttributes()
	if err != nil {
		return err
	}

	// initialize non changing values
	light.Name = attr.Name
	light.Dimmable = containsString(lightsSupportingDimming, attr.Type)
	light.SupportsColorTemperature = containsString(lightsSupportingColorTemperature, attr.Type)
	light.SupportsXYColor = containsString(lightsSupportingXYColor, attr.Type)

	light.updateCurrentLightState()
	return nil
}

func (light *Light) updateCurrentLightState() error {
	attr, err := light.HueLight.GetLightAttributes()
	if err != nil {
		return err
	}
	light.Reachable = attr.State.Reachable
	if !light.Reachable {
		light.On = false
		light.CurrentLightState = LightState{0, []float32{0, 0}, 0}
	} else {
		light.On = attr.State.On
		light.CurrentLightState = lightStateFromHueValues(attr.State.Ct, attr.State.Xy, attr.State.Bri)
	}

	// validate lightstate after updating
	if !light.CurrentLightState.isValid() {
		log.Warningf("Validation failed in updateCurrentLightState for light %s", light.Name)
	}
	return nil
}

func (light *Light) update() error {
	// refresh current state
	light.updateCurrentLightState()

	// is the light associated to any schedule?
	if !light.Scheduled {
		return nil
	}

	// Light reachable or on?
	if !light.Reachable || !light.On {
		if light.Tracking {
			log.Printf("💡 Light %s is no longer reachable or turned on. Clearing state.", light.Name)
			light.Tracking = false
			light.Automatic = false
			return nil
		}

		// ignore light because we are not tracking it.
		return nil
	}

	// did the light just appear?
	if !light.Tracking {
		log.Printf("💡 Light %s just appeared. Initializing state to %vK at %v%% brightness.", light.Name, light.TargetLightState.ColorTemperature, light.TargetLightState.Brightness)

		// For initialization we set the state again and again for 10 seconds
		// because during startup the zigbee communication might be unstable
		for index := 0; index < 9; index++ {
			light.setLightState(light.TargetLightState)
		}
		// safe the state of the last iteration
		setLightState, err := light.setLightState(light.TargetLightState)
		if err != nil {
			return err
		}
		light.SavedLightState = setLightState
		//light.TargetLightState = setLightState
		light.Tracking = true
		light.Automatic = true
		log.Debugf("💡 Light %s was updated to %vK at %v%% brightness", light.Name, light.TargetLightState.ColorTemperature, light.TargetLightState.Brightness)
		return nil
	}

	// light in manual state
	if !light.Automatic {
		return nil
	}

	// did the user manually change the light state?
	if !light.SavedLightState.equals(light.CurrentLightState) {
		if log.GetLevel() == log.DebugLevel {
			log.Debugf("💡 Light %s was manually changed. Current state: %+v - Saved state: %+v", light.Name, light.CurrentLightState, light.SavedLightState)
		} else {
			log.Printf("💡 Light %s was changed manually. Disabling Kelvin.", light.Name)
		}
		light.Automatic = false
		return nil
	}

	// Update needed?
	if light.TargetLightState.equals(light.CurrentLightState) {
		return nil
	}
	log.Debugf("💡 Light %s - Target and Current state differ: %v, %v", light.Name, light.TargetLightState, light.CurrentLightState)

	// Light is reachable, turned on and in automatic state. Update to new state.
	setLightState, err := light.setLightState(light.TargetLightState)
	if err != nil {
		return err
	}

	// Debug: Compare values
	if !setLightState.equals(light.TargetLightState) {
		log.Debugf("💡 Light %s - Target and set state differ: %v, %v", light.Name, light.TargetLightState, setLightState)
	}

	light.SavedLightState = setLightState
	log.Printf("💡 Light %s was updated to %vK at %v%% brightness", light.Name, light.TargetLightState.ColorTemperature, light.TargetLightState.Brightness)
	return nil
}

func (light *Light) setLightState(state LightState) (LightState, error) {
	if !state.isValid() {
		log.Warningf("Validation failed in setLightState for light %s", light.Name)
	}

	// Don't send repeated "On" as this slows the bridge down (see https://developers.meethue.com/faq-page #Performance)
	var hueLightState hue.SetLightState
	colorTemperature, color, brightness := state.convertValuesToHue()
	if light.SupportsXYColor && (state.Color[0] != 0 || state.Color[1] != 0) {
		hueLightState.Xy = []float32{color[0], color[1]}
	} else if light.SupportsColorTemperature && state.ColorTemperature != 0 {
		hueLightState.Ct = strconv.Itoa(colorTemperature)
	}
	if light.Dimmable && state.Brightness != 0 {
		hueLightState.Bri = strconv.Itoa(brightness)
	}
	hueLightState.TransitionTime = strconv.Itoa(lightTransitionIntervalInSeconds * 10) // conversion to 100ms-value

	results, err := light.HueLight.SetState(hueLightState)
	if err != nil {
		return LightState{0, []float32{0, 0}, 0}, err
	}

	// iterate over result to acquire set values
	for _, result := range results {
		for key, value := range result.Success {
			path := strings.Split(key, "/")

			switch path[len(path)-1] {
			case "xy":
				color = []float32{} // clear old color values
				for _, elem := range value.([]interface{}) {
					color = append(color, float32(elem.(float64)))
				}
			case "bri":
				brightness = int(value.(float64))
			case "ct":
				colorTemperature = int(value.(float64))
			}
		}
	}
	setLightState := lightStateFromHueValues(colorTemperature, color, brightness)
	setLightState.isValid()

	// wait while the light is in transition before returning
	time.Sleep(lightTransitionIntervalInSeconds + 1*time.Second)
	return setLightState, nil
}

func (light *Light) updateConfiguration(configuration *Configuration) {
	light.Configuration = configuration
	light.updateSchedule()
	light.updateInterval()
	light.updateTargetLightState()
}

func (light *Light) updateSchedule() {
	newSchedule, err := light.Configuration.lightScheduleForDay(light.ID, time.Now())
	if err != nil {
		log.Printf("💡 Light %v is not associated to any schedule. Ignoring...", light.Name)
		light.Schedule = newSchedule // assign empty schedule
		light.Scheduled = false
		return
	}
	light.Schedule = newSchedule
	light.Scheduled = true
	log.Printf("💡 Light %s: Activating schedule for %v (sunrise: %v, sunset: %v)", light.Name, light.Schedule.endOfDay.Format("Jan 2 2006"), light.Schedule.sunrise.Time.Format("15:04"), light.Schedule.sunset.Time.Format("15:04"))
	light.updateInterval()
}

func (light *Light) updateInterval() {
	if !light.Scheduled {
		log.Debugf("💡 Light %v is not associated to any schedule. No interval to update...", light.Name)
		return
	}

	newInterval, err := light.Schedule.currentInterval(time.Now())
	if err != nil {
		log.Printf("💡 Light %v has no active interval. Ignoring...", light.Name)
		light.Interval = newInterval // assign empty interval
		return
	}
	if newInterval != light.Interval {
		light.Interval = newInterval
		log.Printf("💡 Light %s: Activating interval %v - %v", light.Name, light.Interval.Start.Time.Format("15:04"), light.Interval.End.Time.Format("15:04"))
	}
}

func (light *Light) updateTargetLightState() {
	if !light.Scheduled {
		log.Debugf("💡 Light %v is not associated to any schedule. No target light state to update...", light.Name)
		return
	}

	newLightState := light.Interval.calculateLightStateInInterval(time.Now())
	if !newLightState.isValid() {
		log.Warningf("Validation failed in updateTargetLightState for light %s", light.Name)
	}

	// First initialization of the TargetLightState
	if light.TargetLightState.ColorTemperature == 0 && len(light.TargetLightState.Color) == 0 && light.TargetLightState.Brightness == 0 {
		light.TargetLightState = newLightState
		log.Debugf("💡 Light %s: Initialized target state to %+v", light.Name, light.TargetLightState)
		return
	}

	if !light.TargetLightState.equals(newLightState) {
		light.TargetLightState = newLightState
		log.Debugf("💡 Light %s: Updated target state to %+v", light.Name, light.TargetLightState)
	}
}
<|endoftext|>


<commit_before>package hypermedia

import (
	"encoding/json"
	"fmt"
)

// A hyperlink with a href/URL and a relationship
type Hyperlink struct {
	Rel  string
	Href string
}

// Create a hyperlink to a URL
func Link(rel string, url string) Hyperlink {
	return Hyperlink{rel, host + url}
}

// Create a hyperlink to a URL with a format string
func Linkf(rel string, format string, args ...interface{}) Hyperlink {
	return Link(rel, fmt.Sprintf(format, args...))
}

// Create a rel:self hyperlink to a url
func Self(url string) Hyperlink {
	return Link("self", url)
}

// Create a rel:self hyperlink with a format string
func Selff(format string, args ...interface{}) Hyperlink {
	return Self(fmt.Sprintf(format, args...))
}

// Create a set of hyperlinks
func Links(links ...Hyperlink) HyperlinkSet {
	return HyperlinkSet{links}
}

// Set of hyperlinks
type HyperlinkSet struct {
	links []Hyperlink
}

func (l HyperlinkSet) MarshalJSON() ([]byte, error) {
	out := make(map[string]map[string]string)

	if l.links != nil {
		for _, link := range l.links {
			out[link.Rel] = map[string]string{
				"href": link.Href,
			}
		}
	}

	return json.Marshal(out)
}

func (l *HyperlinkSet) UnmarshalJSON(d []byte) error {
	var out map[string]map[string]string

	if err := json.Unmarshal(d, &out); err != nil {
		return err
	}

	l.links = make([]Hyperlink, 0, len(out))

	for rel, link := range out {
		l.links = append(l.links, Hyperlink{rel, link["href"]})
	}

	return nil
}

// Find the href of a link by its relationship. Returns
// "" if a link doesn't exist.
func (l HyperlinkSet) Href(rel string) string {
	for _, link := range l.links {
		if link.Rel == rel {
			return link.Href
		}
	}

	return ""
}
<commit_msg>HyperlinkSet uses a map instead of a slice for internal storage<commit_after>package hypermedia

import (
	"encoding/json"
	"fmt"
)

// A hyperlink with a href/URL and a relationship
type Hyperlink struct {
	Rel  string
	Href string
}

// Create a hyperlink to a URL
func Link(rel string, url string) Hyperlink {
	return Hyperlink{rel, host + url}
}

// Create a hyperlink to a URL with a format string
func Linkf(rel string, format string, args ...interface{}) Hyperlink {
	return Link(rel, fmt.Sprintf(format, args...))
}

// Create a rel:self hyperlink to a url
func Self(url string) Hyperlink {
	return Link("self", url)
}

// Create a rel:self hyperlink with a format string
func Selff(format string, args ...interface{}) Hyperlink {
	return Self(fmt.Sprintf(format, args...))
}

// Create a set of hyperlinks
func Links(links ...Hyperlink) HyperlinkSet {
	linkmap := make(map[string]Hyperlink, len(links))
	for _, link := range links {
		linkmap[link.Rel] = link
	}

	return HyperlinkSet{linkmap}
}

// Set of hyperlinks
type HyperlinkSet struct {
	links map[string]Hyperlink
}

func (l HyperlinkSet) MarshalJSON() ([]byte, error) {
	out := make(map[string]map[string]string)

	if l.links != nil {
		for rel, link := range l.links {
			out[rel] = map[string]string{
				"href": link.Href,
			}
		}
	}

	return json.Marshal(out)
}

func (l *HyperlinkSet) UnmarshalJSON(d []byte) error {
	var out map[string]map[string]string

	if err := json.Unmarshal(d, &out); err != nil {
		return err
	}

	l.links = make(map[string]Hyperlink, len(out))

	for rel, link := range out {
		l.links[rel] = Hyperlink{rel, link["href"]}
	}

	return nil
}

// Find the href of a link by its relationship. Returns
// "" if a link doesn't exist.
func (l HyperlinkSet) Href(rel string) string {
	link, found := l.links[rel]

	if found {
		return link.Href
	}

	return ""
}
<|endoftext|>


<commit_before>package utils

import (
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"syscall"

	"github.com/fatih/color"
)

var (
	srv *http.Server
)

func StartServer() *http.Server {
	srv = &http.Server{Addr: ":8000"}
	http.Handle("/", http.FileServer(http.Dir("./package")))
	color.Green("\r\nHTTP file server listening at: 0.0.0.0:8000\r\n")

	go func() {
		if err := srv.ListenAndServe(); err != nil {
			// cannot panic, because this probably is an intentional close
			log.Printf("Httpserver: ListenAndServe() error: %s", err)
		}
	}()

	return srv
}

func RunSetup(script string, ch chan int, args ...string) {
	//cmd := exec.Command("bash", "-s", script)
	cmd := exec.Command(script, args...)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		log.Fatal(err)
	}

	defer stdout.Close()
	if err := cmd.Start(); err != nil {
		log.Fatal(err)
	}

	if strings.Contains(script, "master") {
		go saveLog(stdout, true)
	} else {
		go saveLog(stdout, false)
	}

	if err := cmd.Wait(); err != nil {
		if exiterr, ok := err.(*exec.ExitError); ok {
			// The program has exited with an exit code != 0
			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
				log.Printf("Exit Status: %d", status.ExitStatus())
			}
		} else {
			log.Fatalf("cmd.Wait: %v", err)
		}
		ch <- 1
	} else {
		ch <- 0
	}
}

func matchToken(buf []byte) {
	if strings.Contains(string(buf), "kubeadm join --token") {
		re := regexp.MustCompile("kubeadm join --token [0-9a-z.]*")
		result := re.Find(buf)

		//Get the token string
		token := strings.Split(string(result), " ")[3]
		ioutil.WriteFile("./.k8s-token", []byte(token), os.ModeAppend)
		color.Green("%sMaster token %s saved into .k8s.token file.", CheckSymbol, token)
	}
}

func outputProgress(buf []byte) {
	re := regexp.MustCompile("KUBEKIT_OUTPUT.*.")
	results := re.FindAll(buf, -1)

	if results != nil {
		for _, r := range results {
			//Get the token string
			output := strings.Replace(string(r), "KUBEKIT_OUTPUT ", "", 1)
			color.Blue(output)
		}
	}
}

func SaveMasterIP(masterIP string) {
	ioutil.WriteFile("./.master-ip", []byte(masterIP), os.ModeAppend)
}

func saveLog(stdout io.ReadCloser, saveToken bool) {

	fd, _ := os.OpenFile("install.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)

	defer fd.Close()

	for {
		var n int
		buf := make([]byte, 1024)
		n, err := stdout.Read(buf)

		if err != nil {
			//End of output
			break
		}

		//Output the install progress
		if strings.Contains(string(buf), "KUBEKIT_OUTPUT") {
			outputProgress(buf)
		}

		if saveToken {
			matchToken(buf)
		}

		fd.WriteString(string(buf[:n]))
	}
}

func SetupDocker() bool {
	color.Blue("Start to install docker engine...\r\n\r\n")
	ch := make(chan int)

	go RunSetup("./package/docker.sh", ch)
	if <-ch == 1 {
		color.Red("\r\n%sFailed to install docker engine...\r\n\r\n", CrossSymbol)
		return false
	}

	color.Green("\r\n%sDocker engine installed...\r\n\r\n", CheckSymbol)
	return true
}

func SetupMaster() bool {
	color.Blue("Start to initialize Kubernetes master node...\r\n\r\n")
	ch := make(chan int)

	go RunSetup("./package/master.sh", ch, "master")
	if <-ch == 1 {
		color.Red("\r\n%sFailed to initialize Kubernetes master node...\r\n\r\n", CrossSymbol)
		return false
	}

	color.Green("\r\n%sKubernetes master node initialized...\r\n\r\n", CheckSymbol)
	color.Blue("Remember to reload shell with: source ~/.bashrc before using kubectl!\r\n")
	return true
}
<commit_msg>optimize setup process<commit_after>package utils

import (
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"syscall"

	"github.com/fatih/color"
)

var (
	srv *http.Server
)

func StartServer() *http.Server {
	srv = &http.Server{Addr: ":8000"}
	http.Handle("/", http.FileServer(http.Dir("./package")))
	color.Green("%sHTTP file server listening at: 0.0.0.0:8000\r\n\r\n", CheckSymbol)

	go func() {
		if err := srv.ListenAndServe(); err != nil {
			// cannot panic, because this probably is an intentional close
			log.Printf("Httpserver: ListenAndServe() error: %s", err)
		}
	}()

	return srv
}

func RunSetup(script string, ch chan int, args ...string) {
	//cmd := exec.Command("bash", "-s", script)
	cmd := exec.Command(script, args...)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		log.Fatal(err)
	}

	defer stdout.Close()
	if err := cmd.Start(); err != nil {
		log.Fatal(err)
	}

	if strings.Contains(script, "master") {
		go saveLog(stdout, true)
	} else {
		go saveLog(stdout, false)
	}

	if err := cmd.Wait(); err != nil {
		if exiterr, ok := err.(*exec.ExitError); ok {
			// The program has exited with an exit code != 0
			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
				log.Printf("Exit Status: %d", status.ExitStatus())
			}
		} else {
			log.Fatalf("cmd.Wait: %v", err)
		}
		ch <- 1
	} else {
		ch <- 0
	}
}

func matchToken(buf []byte) {
	if strings.Contains(string(buf), "kubeadm join --token") {
		re := regexp.MustCompile("kubeadm join --token [0-9a-z.]*")
		result := re.Find(buf)

		//Get the token string
		token := strings.Split(string(result), " ")[3]
		ioutil.WriteFile("./.k8s-token", []byte(token), os.ModeAppend)
		color.Green("%sMaster token %s saved into .k8s.token file.", CheckSymbol, token)
	}
}

func outputProgress(buf []byte) {
	re := regexp.MustCompile("KUBEKIT_OUTPUT.*.")
	results := re.FindAll(buf, -1)

	if results != nil {
		for _, r := range results {
			//Get the token string
			output := strings.Replace(string(r), "KUBEKIT_OUTPUT ", "", 1)
			color.Blue(output)
		}
	}
}

func SaveMasterIP(masterIP string) {
	ioutil.WriteFile("./.master-ip", []byte(masterIP), os.ModeAppend)
}

func saveLog(stdout io.ReadCloser, saveToken bool) {

	fd, _ := os.OpenFile("install.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)

	defer fd.Close()

	for {
		var n int
		buf := make([]byte, 1024)
		n, err := stdout.Read(buf)

		if err != nil {
			//End of output
			break
		}

		//Output the install progress
		if strings.Contains(string(buf), "KUBEKIT_OUTPUT") {
			outputProgress(buf)
		}

		if saveToken {
			matchToken(buf)
		}

		fd.WriteString(string(buf[:n]))
	}
}

func SetupDocker() bool {
	color.Blue("Start to install docker engine...\r\n\r\n")
	ch := make(chan int)

	go RunSetup("./package/docker.sh", ch)
	if <-ch == 1 {
		color.Red("\r\n%sFailed to install docker engine...\r\n\r\n", CrossSymbol)
		return false
	}

	color.Green("\r\n%sDocker engine installed...\r\n\r\n", CheckSymbol)
	return true
}

func SetupMaster() bool {
	color.Blue("Start to initialize Kubernetes master node...\r\n\r\n")
	ch := make(chan int)

	go RunSetup("./package/master.sh", ch, "master")
	if <-ch == 1 {
		color.Red("\r\n%sFailed to initialize Kubernetes master node...\r\n\r\n", CrossSymbol)
		return false
	}

	color.Green("\r\n%sKubernetes master node initialized...\r\n\r\n", CheckSymbol)
	color.Blue("Remember to reload shell with: source ~/.bashrc before using kubectl!\r\n")
	return true
}
<|endoftext|>


<commit_before>package forecast

import (
	"encoding/json"
	"io/ioutil"
	"net/http"
	"strconv"
)

// URL example:  "https://api.forecast.io/forecast/APIKEY/LATITUDE,LONGITUDE,TIME?units=ca"
const (
	BASEURL = "https://api.forecast.io/forecast"
)

type Flags struct {
	DarkSkyUnavailable string   `json:"darksky-unavailable"`
	DarkSkyStations    []string `json:"darksky-stations"`
	DataPointStations  []string `json:"datapoint-stations"`
	ISDStations        []string `json:"isds-stations"`
	LAMPStations       []string `json:"lamp-stations"`
	METARStations      []string `json:"metars-stations"`
	METNOLicense       string   `json:"metnol-license"`
	Sources            []string `json:"sources"`
	Units              string   `json:"units"`
}

type DataPoint struct {
	Time                   float64 `json:"time"`
	Summary                string  `json:"summary"`
	Icon                   string  `json:"icon"`
	SunriseTime            float64 `json:"sunriseTime"`
	SunsetTime             float64 `json:"sunsetTime"`
	PrecipIntensity        float64 `json:"precipIntensity"`
	PrecipIntensityMax     float64 `json:"precipIntensityMax"`
	PrecipIntensityMaxTime float64 `json:"precipIntensityMaxTime"`
	PrecipProbability      float64 `json:"precipProbability"`
	PrecipType             string  `json:"precipType"`
	PrecipAccumulation     float64 `json:"precipAccumulation"`
	Temperature            float64 `json:"temperature"`
	TemperatureMin         float64 `json:"temperatureMin"`
	TemperatureMinTime     float64 `json:"temperatureMinTime"`
	TemperatureMax         float64 `json:"temperatureMax"`
	TemperatureMaxTime     float64 `json:"temperatureMaxTime"`
	ApparentTemperature    float64 `json:"apparentTemperature"`
	DewPoint               float64 `json:"dewPoint"`
	WindSpeed              float64 `json:"windSpeed"`
	WindBearing            float64 `json:"windBearing"`
	CloudCover             float64 `json:"cloudCover"`
	Humidity               float64 `json:"humidity"`
	Pressure               float64 `json:"pressure"`
	Visibility             float64 `json:"visibility"`
	Ozone                  float64 `json:"ozone"`
	MoonPhase              float64 `json:"moonPhase"`
}

type DataBlock struct {
	Summary string      `json:"summary"`
	Icon    string      `json:"icon"`
	Data    []DataPoint `json:"data"`
}

type alert struct {
	Title       string  `json:"title"`
	Expires     float64 `json:"expires"`
	URI         string  `json:"uri"`
}

type Forecast struct {
	Latitude  float64   `json:"latitude"`
	Longitude float64   `json:"longitude"`
	Timezone  string    `json:"timezone"`
	Offset    float64   `json:"offset"`
	Currently DataPoint `json:"currently"`
	Minutely  DataBlock `json:"minutely"`
	Hourly    DataBlock `json:"hourly"`
	Daily     DataBlock `json:"daily"`
	Alerts    []alert   `json:"alerts"`
	Flags     Flags     `json:"flags"`
	APICalls  int       `json:"apicalls"`
	Code      int       `json:"code"`
}

type Units string

const (
	CA   Units = "ca"
	SI   Units = "si"
	US   Units = "us"
	UK   Units = "uk"
	AUTO Units = "auto"
)

func Get(key string, lat string, long string, time string, units Units) (*Forecast, error) {
	res, err := GetResponse(key, lat, long, time, units)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}

	f, err := FromJSON(body)
	if err != nil {
		return nil, err
	}

	calls, _ := strconv.Atoi(res.Header.Get("X-Forecast-API-Calls"))
	f.APICalls = calls

	return f, nil
}

func FromJSON(json_blob []byte) (*Forecast, error) {
	var f Forecast
	err := json.Unmarshal(json_blob, &f)
	if err != nil {
		return nil, err
	}

	return &f, nil
}

func GetResponse(key string, lat string, long string, time string, units Units) (*http.Response, error) {
	coord := lat + "," + long

	var url string
	if time == "now" {
		url = BASEURL + "/" + key + "/" + coord + "?units=" + string(units)
	} else {
		url = BASEURL + "/" + key + "/" + coord + "," + time + "?units=" + string(units)
	}

	res, err := http.Get(url)
	if err != nil {
		return res, err
	}

	return res, nil
}
<commit_msg>Added fields to 'alert' type<commit_after>package forecast

import (
	"encoding/json"
	"io/ioutil"
	"net/http"
	"strconv"
)

// URL example:  "https://api.forecast.io/forecast/APIKEY/LATITUDE,LONGITUDE,TIME?units=ca"
const (
	BASEURL = "https://api.forecast.io/forecast"
)

type Flags struct {
	DarkSkyUnavailable string   `json:"darksky-unavailable"`
	DarkSkyStations    []string `json:"darksky-stations"`
	DataPointStations  []string `json:"datapoint-stations"`
	ISDStations        []string `json:"isds-stations"`
	LAMPStations       []string `json:"lamp-stations"`
	METARStations      []string `json:"metars-stations"`
	METNOLicense       string   `json:"metnol-license"`
	Sources            []string `json:"sources"`
	Units              string   `json:"units"`
}

type DataPoint struct {
	Time                   float64 `json:"time"`
	Summary                string  `json:"summary"`
	Icon                   string  `json:"icon"`
	SunriseTime            float64 `json:"sunriseTime"`
	SunsetTime             float64 `json:"sunsetTime"`
	PrecipIntensity        float64 `json:"precipIntensity"`
	PrecipIntensityMax     float64 `json:"precipIntensityMax"`
	PrecipIntensityMaxTime float64 `json:"precipIntensityMaxTime"`
	PrecipProbability      float64 `json:"precipProbability"`
	PrecipType             string  `json:"precipType"`
	PrecipAccumulation     float64 `json:"precipAccumulation"`
	Temperature            float64 `json:"temperature"`
	TemperatureMin         float64 `json:"temperatureMin"`
	TemperatureMinTime     float64 `json:"temperatureMinTime"`
	TemperatureMax         float64 `json:"temperatureMax"`
	TemperatureMaxTime     float64 `json:"temperatureMaxTime"`
	ApparentTemperature    float64 `json:"apparentTemperature"`
	DewPoint               float64 `json:"dewPoint"`
	WindSpeed              float64 `json:"windSpeed"`
	WindBearing            float64 `json:"windBearing"`
	CloudCover             float64 `json:"cloudCover"`
	Humidity               float64 `json:"humidity"`
	Pressure               float64 `json:"pressure"`
	Visibility             float64 `json:"visibility"`
	Ozone                  float64 `json:"ozone"`
	MoonPhase              float64 `json:"moonPhase"`
}

type DataBlock struct {
	Summary string      `json:"summary"`
	Icon    string      `json:"icon"`
	Data    []DataPoint `json:"data"`
}

type alert struct {
	Title       string  `json:"title"`
	Description string  `json:"description"`
	Time        float64 `json:"time"`
	Expires     float64 `json:"expires"`
	URI         string  `json:"uri"`
}

type Forecast struct {
	Latitude  float64   `json:"latitude"`
	Longitude float64   `json:"longitude"`
	Timezone  string    `json:"timezone"`
	Offset    float64   `json:"offset"`
	Currently DataPoint `json:"currently"`
	Minutely  DataBlock `json:"minutely"`
	Hourly    DataBlock `json:"hourly"`
	Daily     DataBlock `json:"daily"`
	Alerts    []alert   `json:"alerts"`
	Flags     Flags     `json:"flags"`
	APICalls  int       `json:"apicalls"`
	Code      int       `json:"code"`
}

type Units string

const (
	CA   Units = "ca"
	SI   Units = "si"
	US   Units = "us"
	UK   Units = "uk"
	AUTO Units = "auto"
)

func Get(key string, lat string, long string, time string, units Units) (*Forecast, error) {
	res, err := GetResponse(key, lat, long, time, units)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}

	f, err := FromJSON(body)
	if err != nil {
		return nil, err
	}

	calls, _ := strconv.Atoi(res.Header.Get("X-Forecast-API-Calls"))
	f.APICalls = calls

	return f, nil
}

func FromJSON(json_blob []byte) (*Forecast, error) {
	var f Forecast
	err := json.Unmarshal(json_blob, &f)
	if err != nil {
		return nil, err
	}

	return &f, nil
}

func GetResponse(key string, lat string, long string, time string, units Units) (*http.Response, error) {
	coord := lat + "," + long

	var url string
	if time == "now" {
		url = BASEURL + "/" + key + "/" + coord + "?units=" + string(units)
	} else {
		url = BASEURL + "/" + key + "/" + coord + "," + time + "?units=" + string(units)
	}

	res, err := http.Get(url)
	if err != nil {
		return res, err
	}

	return res, nil
}
<|endoftext|>


<commit_before>package util

import "github.com/zmap/zcrypto/x509"

// HasEKU tests whether an EKU is present in a certificate.
func HasEKU(cert *x509.Certificate, eku x509.ExtKeyUsage) bool {
	for _, currentEku := range cert.ExtKeyUsage {
		if currentEku == eku {
			return true
		}
	}

	return false
}
<commit_msg>Improve readability of "EKU" abbreviation (#489)<commit_after>package util

import "github.com/zmap/zcrypto/x509"

// HasEKU tests whether an Extended Key Usage (EKU) is present in a certificate.
func HasEKU(cert *x509.Certificate, eku x509.ExtKeyUsage) bool {
	for _, currentEku := range cert.ExtKeyUsage {
		if currentEku == eku {
			return true
		}
	}

	return false
}
<|endoftext|>


<commit_before>package vanity_test

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"

	"kkn.fi/vanity"
)

var addr = "https://kkn.fi"

func TestHTTPMethodsSupport(t *testing.T) {
	tests := []struct {
		method string
		status int
	}{
		{
			method: http.MethodGet,
			status: http.StatusOK,
		},
		{
			method: http.MethodHead,
			status: http.StatusMethodNotAllowed,
		},
		{
			method: http.MethodPost,
			status: http.StatusMethodNotAllowed,
		},
		{
			method: http.MethodPut,
			status: http.StatusMethodNotAllowed,
		},
		{
			method: http.MethodDelete,
			status: http.StatusMethodNotAllowed,
		},
		{
			method: http.MethodTrace,
			status: http.StatusMethodNotAllowed,
		},
		{
			method: http.MethodOptions,
			status: http.StatusMethodNotAllowed,
		},
	}
	for _, test := range tests {
		req := httptest.NewRequest(test.method, addr+"/gist?go-get=1", nil)
		rec := httptest.NewRecorder()
		srv := vanity.Handler(
			vanity.Log(log.New(ioutil.Discard, "", 0)),
		)
		srv.ServeHTTP(rec, req)
		res := rec.Result()
		if res.StatusCode != test.status {
			t.Errorf("Expecting status code %v for method '%v', but got %v", test.status, test.method, res.StatusCode)
		}
	}
}

func TestIndexPageNotFound(t *testing.T) {
	tests := []struct {
		name string
		url  string
	}{
		{
			name: "with trailing slash",
			url:  "https://kkn.fi/",
		},
		{
			name: "without trailing slash",
			url:  "https://kkn.fi",
		},
	}
	for _, test := range tests {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, test.url, nil)
		srv := vanity.Handler(
			vanity.VCSURL("https://github.com/kare"),
			vanity.Log(log.New(ioutil.Discard, "", 0)),
			vanity.StaticDir("/not-found", "/.static/"),
		)
		srv.ServeHTTP(rec, req)
		res := rec.Result()
		if res.StatusCode != http.StatusNotFound {
			t.Errorf("%v: expected response status 404, but got %v\n", test.name, res.StatusCode)
		}
	}
}

func TestBrowserGoDoc(t *testing.T) {
	tests := []struct {
		path         string
		moduleServer string
		result       string
	}{
		{
			path:         "/gist",
			moduleServer: "https://pkg.go.dev/",
			result:       "https://pkg.go.dev/kkn.fi/gist",
		},
		{
			path:         "/gist/",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/gist",
		},
		{
			path:         "/set",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/set",
		},
		{
			path:         "/cmd/kkn.fi-srv",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/cmd/kkn.fi-srv",
		},
		{
			path:         "/cmd/tcpproxy/",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/cmd/tcpproxy",
		},
		{
			path:         "/pkgabc/sub/foo",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/pkgabc/sub",
		},
		{
			path:         "/pkgabc/sub/foo",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/pkgabc/sub",
		},
		{
			path:         "/vanity",
			moduleServer: "",
			result:       "https://pkg.go.dev/kkn.fi/vanity",
		},
		{
			path:         "/cmd/healthcheck",
			moduleServer: "https://github.com/kare",
			result:       "https://github.com/kare/healthcheck",
		},
		{
			path:         "/vanity",
			moduleServer: "https://github.com/kare/",
			result:       "https://github.com/kare/vanity",
		},
	}
	for _, test := range tests {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, addr+test.path, nil)
		srv := vanity.Handler(
			vanity.ModuleServerURL(test.moduleServer),
			vanity.Log(log.New(ioutil.Discard, "", 0)),
		)
		srv.ServeHTTP(rec, req)
		res := rec.Result()
		if res.StatusCode != http.StatusTemporaryRedirect {
			t.Errorf("expected response status %v, but got %v", http.StatusTemporaryRedirect, res.StatusCode)
		}
		body, _ := ioutil.ReadAll(res.Body)
		if !strings.Contains(string(body), test.result) {
			t.Errorf("expecting\n%v be contained in\n%v", test.result, string(body))
		}
	}
}

func TestGoTool(t *testing.T) {
	tests := []struct {
		path   string
		vcs    string
		vcsURL string
		result string
	}{
		{
			path:   "/gist?go-get=1",
			vcs:    "hg",
			vcsURL: "https://bitbucket.org/kare/",
			result: "kkn.fi/gist hg https://bitbucket.org/kare/gist",
		},
		{
			path:   "/set/?go-get=1",
			vcs:    "hg",
			vcsURL: "https://bitbucket.org/kare",
			result: "kkn.fi/set hg https://bitbucket.org/kare/set",
		},
		{
			path:   "/cmd/kkn.fi-srv?go-get=1",
			vcs:    "git",
			vcsURL: "https://github.com/kare/",
			result: "kkn.fi/cmd/kkn.fi-srv git https://github.com/kare/kkn.fi-srv",
		},
		{
			path:   "/cmd/kkn.fi-srv/?go-get=1",
			vcs:    "git",
			vcsURL: "https://github.com/kare",
			result: "kkn.fi/cmd/kkn.fi-srv git https://github.com/kare/kkn.fi-srv",
		},
		{
			path:   "/pkgabc/sub/foo?go-get=1",
			vcs:    "git",
			vcsURL: "https://github.com/kare",
			result: "kkn.fi/pkgabc/sub/foo git https://github.com/kare/pkgabc",
		},
		{
			path:   "/pkgabc/sub/foo/?go-get=1",
			vcs:    "git",
			vcsURL: "https://github.com/kare",
			result: "kkn.fi/pkgabc/sub/foo git https://github.com/kare/pkgabc",
		},
	}
	for _, test := range tests {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, addr+test.path, nil)
		srv := vanity.Handler(
			vanity.VCS(test.vcs),
			vanity.VCSURL(test.vcsURL),
			vanity.Log(log.New(ioutil.Discard, "", 0)),
		)
		srv.ServeHTTP(rec, req)

		res := rec.Result()
		body, _ := ioutil.ReadAll(res.Body)
		expected := fmt.Sprintf(`<meta name="go-import" content="%v">`, test.result)
		if !strings.Contains(string(body), expected) {
			t.Errorf("expecting url '%v' body to contain html meta tag:\n%v, but got:\n%v", test.path, expected, string(body))
		}

		if res.StatusCode != http.StatusOK {
			t.Errorf("expected response status 200, but got %v", res.StatusCode)
		}
	}
}

func TestStaticDir(t *testing.T) {
	tests := []struct {
		name string
		url  string
	}{
		{
			name: "with trailing slash",
			url:  "https://kkn.fi/",
		},
		{
			name: "without trailing slash",
			url:  "https://kkn.fi",
		},
	}
	for _, test := range tests {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, test.url, nil)
		srv := vanity.Handler(
			vanity.StaticDir("testdata", "dir"),
			vanity.Log(log.New(ioutil.Discard, "", 0)),
		)
		srv.ServeHTTP(rec, req)
		res := rec.Result()
		if res.StatusCode != http.StatusOK {
			t.Errorf("%v: expected response status 200, but got %v", test.name, res.StatusCode)
		}

		body, _ := ioutil.ReadAll(res.Body)
		expected := "homepage\n"
		if string(body) != expected {
			t.Errorf("%v: expecting body to match:\n'%v', but got:\n'%s'", test.name, expected, body)
		}
	}
}

func TestRobotsTxt(t *testing.T) {
	tests := []struct {
		name string
		url  string
	}{
		{
			name: "GET /robots.txt",
			url:  "https://kkn.fi/robots.txt",
		},
	}
	for _, test := range tests {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, test.url, nil)
		srv := vanity.Handler(
			vanity.RobotsTxt(""),
			//vanity.Log(log.New(ioutil.Discard, "", 0)),
		)
		srv.ServeHTTP(rec, req)
		res := rec.Result()
		if res.StatusCode != http.StatusOK {
			t.Errorf("%v: expected response status 200, but got %v", test.name, res.StatusCode)
		}

		body, _ := ioutil.ReadAll(res.Body)
		expected := vanity.DefaultRobotsTxt
		if string(body) != expected {
			t.Errorf("%v: expecting body to match:\n'%v', but got:\n'%s'", test.name, expected, body)
		}
	}
}

func ExampleHandler() {
	errorLog := log.New(os.Stderr, "vanity: ", log.Ldate|log.Ltime|log.LUTC)
	srv := vanity.Handler(
		vanity.ModuleServerURL("https://pkg.go.dev"),
		vanity.Log(errorLog),
		vanity.VCSURL("https://github.com/kare"),
		vanity.VCS("git"),
		vanity.StaticDir("testdata", "/.static/"),
		vanity.IndexPageHandler(vanity.DefaultIndexPageHandler("testdata/index.html")),
	)
	http.Handle("/", srv)
	// Output:
}
<commit_msg>test: remove duplicate test case<commit_after>package vanity_test

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"

	"kkn.fi/vanity"
)

var addr = "https://kkn.fi"

func TestHTTPMethodsSupport(t *testing.T) {
	tests := []struct {
		method string
		status int
	}{
		{
			method: http.MethodGet,
			status: http.StatusOK,
		},
		{
			method: http.MethodHead,
			status: http.StatusMethodNotAllowed,
		},
		{
			method: http.MethodPost,
			status: http.StatusMethodNotAllowed,
		},
		{
			method: http.MethodPut,
			status: http.StatusMethodNotAllowed,
		},
		{
			method: http.MethodDelete,
			status: http.StatusMethodNotAllowed,
		},
		{
			method: http.MethodTrace,
			status: http.StatusMethodNotAllowed,
		},
		{
			method: http.MethodOptions,
			status: http.StatusMethodNotAllowed,
		},
	}
	for _, test := range tests {
		req := httptest.NewRequest(test.method, addr+"/gist?go-get=1", nil)
		rec := httptest.NewRecorder()
		srv := vanity.Handler(
			vanity.Log(log.New(ioutil.Discard, "", 0)),
		)
		srv.ServeHTTP(rec, req)
		res := rec.Result()
		if res.StatusCode != test.status {
			t.Errorf("Expecting status code %v for method '%v', but got %v", test.status, test.method, res.StatusCode)
		}
	}
}

func TestIndexPageNotFound(t *testing.T) {
	tests := []struct {
		name string
		url  string
	}{
		{
			name: "with trailing slash",
			url:  "https://kkn.fi/",
		},
		{
			name: "without trailing slash",
			url:  "https://kkn.fi",
		},
	}
	for _, test := range tests {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, test.url, nil)
		srv := vanity.Handler(
			vanity.VCSURL("https://github.com/kare"),
			vanity.Log(log.New(ioutil.Discard, "", 0)),
			vanity.StaticDir("/not-found", "/.static/"),
		)
		srv.ServeHTTP(rec, req)
		res := rec.Result()
		if res.StatusCode != http.StatusNotFound {
			t.Errorf("%v: expected response status 404, but got %v\n", test.name, res.StatusCode)
		}
	}
}

func TestBrowserGoDoc(t *testing.T) {
	tests := []struct {
		path         string
		moduleServer string
		result       string
	}{
		{
			path:         "/gist",
			moduleServer: "https://pkg.go.dev/",
			result:       "https://pkg.go.dev/kkn.fi/gist",
		},
		{
			path:         "/gist/",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/gist",
		},
		{
			path:         "/set",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/set",
		},
		{
			path:         "/cmd/kkn.fi-srv",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/cmd/kkn.fi-srv",
		},
		{
			path:         "/cmd/tcpproxy/",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/cmd/tcpproxy",
		},
		{
			path:         "/pkgabc/sub/foo",
			moduleServer: "https://pkg.go.dev",
			result:       "https://pkg.go.dev/kkn.fi/pkgabc/sub",
		},
		{
			path:         "/vanity",
			moduleServer: "",
			result:       "https://pkg.go.dev/kkn.fi/vanity",
		},
		{
			path:         "/cmd/healthcheck",
			moduleServer: "https://github.com/kare",
			result:       "https://github.com/kare/healthcheck",
		},
		{
			path:         "/vanity",
			moduleServer: "https://github.com/kare/",
			result:       "https://github.com/kare/vanity",
		},
	}
	for _, test := range tests {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, addr+test.path, nil)
		srv := vanity.Handler(
			vanity.ModuleServerURL(test.moduleServer),
			vanity.Log(log.New(ioutil.Discard, "", 0)),
		)
		srv.ServeHTTP(rec, req)
		res := rec.Result()
		if res.StatusCode != http.StatusTemporaryRedirect {
			t.Errorf("expected response status %v, but got %v", http.StatusTemporaryRedirect, res.StatusCode)
		}
		body, _ := ioutil.ReadAll(res.Body)
		if !strings.Contains(string(body), test.result) {
			t.Errorf("expecting\n%v be contained in\n%v", test.result, string(body))
		}
	}
}

func TestGoTool(t *testing.T) {
	tests := []struct {
		path   string
		vcs    string
		vcsURL string
		result string
	}{
		{
			path:   "/gist?go-get=1",
			vcs:    "hg",
			vcsURL: "https://bitbucket.org/kare/",
			result: "kkn.fi/gist hg https://bitbucket.org/kare/gist",
		},
		{
			path:   "/set/?go-get=1",
			vcs:    "hg",
			vcsURL: "https://bitbucket.org/kare",
			result: "kkn.fi/set hg https://bitbucket.org/kare/set",
		},
		{
			path:   "/cmd/kkn.fi-srv?go-get=1",
			vcs:    "git",
			vcsURL: "https://github.com/kare/",
			result: "kkn.fi/cmd/kkn.fi-srv git https://github.com/kare/kkn.fi-srv",
		},
		{
			path:   "/cmd/kkn.fi-srv/?go-get=1",
			vcs:    "git",
			vcsURL: "https://github.com/kare",
			result: "kkn.fi/cmd/kkn.fi-srv git https://github.com/kare/kkn.fi-srv",
		},
		{
			path:   "/pkgabc/sub/foo?go-get=1",
			vcs:    "git",
			vcsURL: "https://github.com/kare",
			result: "kkn.fi/pkgabc/sub/foo git https://github.com/kare/pkgabc",
		},
		{
			path:   "/pkgabc/sub/foo/?go-get=1",
			vcs:    "git",
			vcsURL: "https://github.com/kare",
			result: "kkn.fi/pkgabc/sub/foo git https://github.com/kare/pkgabc",
		},
	}
	for _, test := range tests {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, addr+test.path, nil)
		srv := vanity.Handler(
			vanity.VCS(test.vcs),
			vanity.VCSURL(test.vcsURL),
			vanity.Log(log.New(ioutil.Discard, "", 0)),
		)
		srv.ServeHTTP(rec, req)

		res := rec.Result()
		body, _ := ioutil.ReadAll(res.Body)
		expected := fmt.Sprintf(`<meta name="go-import" content="%v">`, test.result)
		if !strings.Contains(string(body), expected) {
			t.Errorf("expecting url '%v' body to contain html meta tag:\n%v, but got:\n%v", test.path, expected, string(body))
		}

		if res.StatusCode != http.StatusOK {
			t.Errorf("expected response status 200, but got %v", res.StatusCode)
		}
	}
}

func TestStaticDir(t *testing.T) {
	tests := []struct {
		name string
		url  string
	}{
		{
			name: "with trailing slash",
			url:  "https://kkn.fi/",
		},
		{
			name: "without trailing slash",
			url:  "https://kkn.fi",
		},
	}
	for _, test := range tests {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, test.url, nil)
		srv := vanity.Handler(
			vanity.StaticDir("testdata", "dir"),
			vanity.Log(log.New(ioutil.Discard, "", 0)),
		)
		srv.ServeHTTP(rec, req)
		res := rec.Result()
		if res.StatusCode != http.StatusOK {
			t.Errorf("%v: expected response status 200, but got %v", test.name, res.StatusCode)
		}

		body, _ := ioutil.ReadAll(res.Body)
		expected := "homepage\n"
		if string(body) != expected {
			t.Errorf("%v: expecting body to match:\n'%v', but got:\n'%s'", test.name, expected, body)
		}
	}
}

func TestRobotsTxt(t *testing.T) {
	tests := []struct {
		name string
		url  string
	}{
		{
			name: "GET /robots.txt",
			url:  "https://kkn.fi/robots.txt",
		},
	}
	for _, test := range tests {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, test.url, nil)
		srv := vanity.Handler(
			vanity.RobotsTxt(""),
			//vanity.Log(log.New(ioutil.Discard, "", 0)),
		)
		srv.ServeHTTP(rec, req)
		res := rec.Result()
		if res.StatusCode != http.StatusOK {
			t.Errorf("%v: expected response status 200, but got %v", test.name, res.StatusCode)
		}

		body, _ := ioutil.ReadAll(res.Body)
		expected := vanity.DefaultRobotsTxt
		if string(body) != expected {
			t.Errorf("%v: expecting body to match:\n'%v', but got:\n'%s'", test.name, expected, body)
		}
	}
}

func ExampleHandler() {
	errorLog := log.New(os.Stderr, "vanity: ", log.Ldate|log.Ltime|log.LUTC)
	srv := vanity.Handler(
		vanity.ModuleServerURL("https://pkg.go.dev"),
		vanity.Log(errorLog),
		vanity.VCSURL("https://github.com/kare"),
		vanity.VCS("git"),
		vanity.StaticDir("testdata", "/.static/"),
		vanity.IndexPageHandler(vanity.DefaultIndexPageHandler("testdata/index.html")),
	)
	http.Handle("/", srv)
	// Output:
}
<|endoftext|>


<commit_before>package vault

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"strings"
	"sync"
	"time"

	"github.com/armon/go-metrics"
	"github.com/hashicorp/vault/audit"
	"github.com/hashicorp/vault/logical"
)

const (
	// coreAuditConfigPath is used to store the audit configuration.
	// Audit configuration is protected within the Vault itself, which means it
	// can only be viewed or modified after an unseal.
	coreAuditConfigPath = "core/audit"

	// auditBarrierPrefix is the prefix to the UUID used in the
	// barrier view for the audit backends.
	auditBarrierPrefix = "audit/"
)

var (
	// loadAuditFailed if loading audit tables encounters an error
	loadAuditFailed = errors.New("failed to setup audit table")
)

// enableAudit is used to enable a new audit backend
func (c *Core) enableAudit(entry *MountEntry) error {
	c.audit.Lock()
	defer c.audit.Unlock()

	// Ensure we end the path in a slash
	if !strings.HasSuffix(entry.Path, "/") {
		entry.Path += "/"
	}

	// Ensure there is a name
	if entry.Path == "/" {
		return fmt.Errorf("backend path must be specified")
	}

	// Look for matching name
	for _, ent := range c.audit.Entries {
		switch {
		// Existing is sql/mysql/ new is sql/ or
		// existing is sql/ and new is sql/mysql/
		case strings.HasPrefix(ent.Path, entry.Path):
			fallthrough
		case strings.HasPrefix(entry.Path, ent.Path):
			return fmt.Errorf("path already in use")
		}
	}

	// Lookup the new backend
	backend, err := c.newAuditBackend(entry.Type, entry.Options)
	if err != nil {
		return err
	}

	// Generate a new UUID and view
	entry.UUID = generateUUID()
	view := NewBarrierView(c.barrier, auditBarrierPrefix+entry.UUID+"/")

	// Update the audit table
	newTable := c.audit.Clone()
	newTable.Entries = append(newTable.Entries, entry)
	if err := c.persistAudit(newTable); err != nil {
		return errors.New("failed to update audit table")
	}
	c.audit = newTable

	// Register the backend
	c.auditBroker.Register(entry.Path, backend, view)
	c.logger.Printf("[INFO] core: enabled audit backend '%s' type: %s",
		entry.Path, entry.Type)
	return nil
}

// disableAudit is used to disable an existing audit backend
func (c *Core) disableAudit(path string) error {
	c.audit.Lock()
	defer c.audit.Unlock()

	// Ensure we end the path in a slash
	if !strings.HasSuffix(path, "/") {
		path += "/"
	}

	// Remove the entry from the mount table
	newTable := c.audit.Clone()
	found := newTable.Remove(path)

	// Ensure there was a match
	if !found {
		return fmt.Errorf("no matching backend")
	}

	// Update the audit table
	if err := c.persistAudit(newTable); err != nil {
		return errors.New("failed to update audit table")
	}
	c.audit = newTable

	// Unmount the backend
	c.auditBroker.Deregister(path)
	c.logger.Printf("[INFO] core: disabled audit backend '%s'", path)
	return nil
}

// loadAudits is invoked as part of postUnseal to load the audit table
func (c *Core) loadAudits() error {
	// Load the existing audit table
	raw, err := c.barrier.Get(coreAuditConfigPath)
	if err != nil {
		c.logger.Printf("[ERR] core: failed to read audit table: %v", err)
		return loadAuditFailed
	}
	if raw != nil {
		c.audit = &MountTable{}
		if err := json.Unmarshal(raw.Value, c.audit); err != nil {
			c.logger.Printf("[ERR] core: failed to decode audit table: %v", err)
			return loadAuditFailed
		}
	}

	// Done if we have restored the audit table
	if c.audit != nil {
		return nil
	}

	// Create and persist the default audit table
	c.audit = defaultAuditTable()
	if err := c.persistAudit(c.audit); err != nil {
		return loadAuditFailed
	}
	return nil
}

// persistAudit is used to persist the audit table after modification
func (c *Core) persistAudit(table *MountTable) error {
	// Marshal the table
	raw, err := json.Marshal(table)
	if err != nil {
		c.logger.Printf("[ERR] core: failed to encode audit table: %v", err)
		return err
	}

	// Create an entry
	entry := &Entry{
		Key:   coreAuditConfigPath,
		Value: raw,
	}

	// Write to the physical backend
	if err := c.barrier.Put(entry); err != nil {
		c.logger.Printf("[ERR] core: failed to persist audit table: %v", err)
		return err
	}
	return nil
}

// setupAudit is invoked after we've loaded the audit able to
// initialize the audit backends
func (c *Core) setupAudits() error {
	broker := NewAuditBroker(c.logger)
	for _, entry := range c.audit.Entries {
		// Initialize the backend
		audit, err := c.newAuditBackend(entry.Type, entry.Options)
		if err != nil {
			c.logger.Printf(
				"[ERR] core: failed to create audit entry %#v: %v",
				entry, err)
			return loadAuditFailed
		}

		// Create a barrier view using the UUID
		view := NewBarrierView(c.barrier, auditBarrierPrefix+entry.UUID+"/")

		// Mount the backend
		broker.Register(entry.Path, audit, view)
	}
	c.auditBroker = broker
	return nil
}

// teardownAudit is used before we seal the vault to reset the audit
// backends to their unloaded state. This is reversed by loadAudits.
func (c *Core) teardownAudits() error {
	c.audit = nil
	c.auditBroker = nil
	return nil
}

// newAuditBackend is used to create and configure a new audit backend by name
func (c *Core) newAuditBackend(t string, conf map[string]string) (audit.Backend, error) {
	f, ok := c.auditBackends[t]
	if !ok {
		return nil, fmt.Errorf("unknown backend type: %s", t)
	}
	return f(conf)
}

// defaultAuditTable creates a default audit table
func defaultAuditTable() *MountTable {
	table := &MountTable{}
	return table
}

type backendEntry struct {
	backend audit.Backend
	view    *BarrierView
}

// AuditBroker is used to provide a single ingest interface to auditable
// events given that multiple backends may be configured.
type AuditBroker struct {
	l        sync.RWMutex
	backends map[string]backendEntry
	logger   *log.Logger
}

// NewAuditBroker creates a new audit broker
func NewAuditBroker(log *log.Logger) *AuditBroker {
	b := &AuditBroker{
		backends: make(map[string]backendEntry),
		logger:   log,
	}
	return b
}

// Register is used to add new audit backend to the broker
func (a *AuditBroker) Register(name string, b audit.Backend, v *BarrierView) {
	a.l.Lock()
	defer a.l.Unlock()
	a.backends[name] = backendEntry{
		backend: b,
		view:    v,
	}
}

// Deregister is used to remove an audit backend from the broker
func (a *AuditBroker) Deregister(name string) {
	a.l.Lock()
	defer a.l.Unlock()
	delete(a.backends, name)
}

// IsRegistered is used to check if a given audit backend is registered
func (a *AuditBroker) IsRegistered(name string) bool {
	a.l.RLock()
	defer a.l.RUnlock()
	_, ok := a.backends[name]
	return ok
}

// LogRequest is used to ensure all the audit backends have an opportunity to
// log the given request and that *at least one* succeeds.
func (a *AuditBroker) LogRequest(auth *logical.Auth, req *logical.Request) error {
	defer metrics.MeasureSince([]string{"audit", "log_request"}, time.Now())
	a.l.RLock()
	defer a.l.RUnlock()

	// Ensure at least one backend logs
	anyLogged := false
	for name, be := range a.backends {
		start := time.Now()
		err := be.backend.LogRequest(auth, req)
		metrics.MeasureSince([]string{"audit", name, "log_request"}, start)
		if err != nil {
			a.logger.Printf("[ERR] audit: backend '%s' failed to log request: %v", name, err)
		} else {
			anyLogged = true
		}
	}
	if !anyLogged && len(a.backends) > 0 {
		return fmt.Errorf("no audit backend succeeded in logging the request")
	}
	return nil
}

// LogResponse is used to ensure all the audit backends have an opportunity to
// log the given response and that *at least one* succeeds.
func (a *AuditBroker) LogResponse(auth *logical.Auth, req *logical.Request,
	resp *logical.Response, err error) error {
	defer metrics.MeasureSince([]string{"audit", "log_response"}, time.Now())
	a.l.RLock()
	defer a.l.RUnlock()

	// Ensure at least one backend logs
	anyLogged := false
	for name, be := range a.backends {
		start := time.Now()
		err := be.backend.LogResponse(auth, req, resp, err)
		metrics.MeasureSince([]string{"audit", name, "log_response"}, start)
		if err != nil {
			a.logger.Printf("[ERR] audit: backend '%s' failed to log response: %v", name, err)
		} else {
			anyLogged = true
		}
	}
	if !anyLogged && len(a.backends) > 0 {
		return fmt.Errorf("no audit backend succeeded in logging the response")
	}
	return nil
}
<commit_msg>vault: handle a panic while generating audit output<commit_after>package vault

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"strings"
	"sync"
	"time"

	"github.com/armon/go-metrics"
	"github.com/hashicorp/vault/audit"
	"github.com/hashicorp/vault/logical"
)

const (
	// coreAuditConfigPath is used to store the audit configuration.
	// Audit configuration is protected within the Vault itself, which means it
	// can only be viewed or modified after an unseal.
	coreAuditConfigPath = "core/audit"

	// auditBarrierPrefix is the prefix to the UUID used in the
	// barrier view for the audit backends.
	auditBarrierPrefix = "audit/"
)

var (
	// loadAuditFailed if loading audit tables encounters an error
	loadAuditFailed = errors.New("failed to setup audit table")
)

// enableAudit is used to enable a new audit backend
func (c *Core) enableAudit(entry *MountEntry) error {
	c.audit.Lock()
	defer c.audit.Unlock()

	// Ensure we end the path in a slash
	if !strings.HasSuffix(entry.Path, "/") {
		entry.Path += "/"
	}

	// Ensure there is a name
	if entry.Path == "/" {
		return fmt.Errorf("backend path must be specified")
	}

	// Look for matching name
	for _, ent := range c.audit.Entries {
		switch {
		// Existing is sql/mysql/ new is sql/ or
		// existing is sql/ and new is sql/mysql/
		case strings.HasPrefix(ent.Path, entry.Path):
			fallthrough
		case strings.HasPrefix(entry.Path, ent.Path):
			return fmt.Errorf("path already in use")
		}
	}

	// Lookup the new backend
	backend, err := c.newAuditBackend(entry.Type, entry.Options)
	if err != nil {
		return err
	}

	// Generate a new UUID and view
	entry.UUID = generateUUID()
	view := NewBarrierView(c.barrier, auditBarrierPrefix+entry.UUID+"/")

	// Update the audit table
	newTable := c.audit.Clone()
	newTable.Entries = append(newTable.Entries, entry)
	if err := c.persistAudit(newTable); err != nil {
		return errors.New("failed to update audit table")
	}
	c.audit = newTable

	// Register the backend
	c.auditBroker.Register(entry.Path, backend, view)
	c.logger.Printf("[INFO] core: enabled audit backend '%s' type: %s",
		entry.Path, entry.Type)
	return nil
}

// disableAudit is used to disable an existing audit backend
func (c *Core) disableAudit(path string) error {
	c.audit.Lock()
	defer c.audit.Unlock()

	// Ensure we end the path in a slash
	if !strings.HasSuffix(path, "/") {
		path += "/"
	}

	// Remove the entry from the mount table
	newTable := c.audit.Clone()
	found := newTable.Remove(path)

	// Ensure there was a match
	if !found {
		return fmt.Errorf("no matching backend")
	}

	// Update the audit table
	if err := c.persistAudit(newTable); err != nil {
		return errors.New("failed to update audit table")
	}
	c.audit = newTable

	// Unmount the backend
	c.auditBroker.Deregister(path)
	c.logger.Printf("[INFO] core: disabled audit backend '%s'", path)
	return nil
}

// loadAudits is invoked as part of postUnseal to load the audit table
func (c *Core) loadAudits() error {
	// Load the existing audit table
	raw, err := c.barrier.Get(coreAuditConfigPath)
	if err != nil {
		c.logger.Printf("[ERR] core: failed to read audit table: %v", err)
		return loadAuditFailed
	}
	if raw != nil {
		c.audit = &MountTable{}
		if err := json.Unmarshal(raw.Value, c.audit); err != nil {
			c.logger.Printf("[ERR] core: failed to decode audit table: %v", err)
			return loadAuditFailed
		}
	}

	// Done if we have restored the audit table
	if c.audit != nil {
		return nil
	}

	// Create and persist the default audit table
	c.audit = defaultAuditTable()
	if err := c.persistAudit(c.audit); err != nil {
		return loadAuditFailed
	}
	return nil
}

// persistAudit is used to persist the audit table after modification
func (c *Core) persistAudit(table *MountTable) error {
	// Marshal the table
	raw, err := json.Marshal(table)
	if err != nil {
		c.logger.Printf("[ERR] core: failed to encode audit table: %v", err)
		return err
	}

	// Create an entry
	entry := &Entry{
		Key:   coreAuditConfigPath,
		Value: raw,
	}

	// Write to the physical backend
	if err := c.barrier.Put(entry); err != nil {
		c.logger.Printf("[ERR] core: failed to persist audit table: %v", err)
		return err
	}
	return nil
}

// setupAudit is invoked after we've loaded the audit able to
// initialize the audit backends
func (c *Core) setupAudits() error {
	broker := NewAuditBroker(c.logger)
	for _, entry := range c.audit.Entries {
		// Initialize the backend
		audit, err := c.newAuditBackend(entry.Type, entry.Options)
		if err != nil {
			c.logger.Printf(
				"[ERR] core: failed to create audit entry %#v: %v",
				entry, err)
			return loadAuditFailed
		}

		// Create a barrier view using the UUID
		view := NewBarrierView(c.barrier, auditBarrierPrefix+entry.UUID+"/")

		// Mount the backend
		broker.Register(entry.Path, audit, view)
	}
	c.auditBroker = broker
	return nil
}

// teardownAudit is used before we seal the vault to reset the audit
// backends to their unloaded state. This is reversed by loadAudits.
func (c *Core) teardownAudits() error {
	c.audit = nil
	c.auditBroker = nil
	return nil
}

// newAuditBackend is used to create and configure a new audit backend by name
func (c *Core) newAuditBackend(t string, conf map[string]string) (audit.Backend, error) {
	f, ok := c.auditBackends[t]
	if !ok {
		return nil, fmt.Errorf("unknown backend type: %s", t)
	}
	return f(conf)
}

// defaultAuditTable creates a default audit table
func defaultAuditTable() *MountTable {
	table := &MountTable{}
	return table
}

type backendEntry struct {
	backend audit.Backend
	view    *BarrierView
}

// AuditBroker is used to provide a single ingest interface to auditable
// events given that multiple backends may be configured.
type AuditBroker struct {
	l        sync.RWMutex
	backends map[string]backendEntry
	logger   *log.Logger
}

// NewAuditBroker creates a new audit broker
func NewAuditBroker(log *log.Logger) *AuditBroker {
	b := &AuditBroker{
		backends: make(map[string]backendEntry),
		logger:   log,
	}
	return b
}

// Register is used to add new audit backend to the broker
func (a *AuditBroker) Register(name string, b audit.Backend, v *BarrierView) {
	a.l.Lock()
	defer a.l.Unlock()
	a.backends[name] = backendEntry{
		backend: b,
		view:    v,
	}
}

// Deregister is used to remove an audit backend from the broker
func (a *AuditBroker) Deregister(name string) {
	a.l.Lock()
	defer a.l.Unlock()
	delete(a.backends, name)
}

// IsRegistered is used to check if a given audit backend is registered
func (a *AuditBroker) IsRegistered(name string) bool {
	a.l.RLock()
	defer a.l.RUnlock()
	_, ok := a.backends[name]
	return ok
}

// LogRequest is used to ensure all the audit backends have an opportunity to
// log the given request and that *at least one* succeeds.
func (a *AuditBroker) LogRequest(auth *logical.Auth, req *logical.Request) (reterr error) {
	defer metrics.MeasureSince([]string{"audit", "log_request"}, time.Now())
	a.l.RLock()
	defer a.l.RUnlock()
	defer func() {
		if r := recover(); r != nil {
			a.logger.Printf("[ERR] audit: panic logging: auth: %#v, req: %#v: %v", auth, req, r)
			reterr = fmt.Errorf("panic generating audit log")
		}
	}()

	// Ensure at least one backend logs
	anyLogged := false
	for name, be := range a.backends {
		start := time.Now()
		err := be.backend.LogRequest(auth, req)
		metrics.MeasureSince([]string{"audit", name, "log_request"}, start)
		if err != nil {
			a.logger.Printf("[ERR] audit: backend '%s' failed to log request: %v", name, err)
		} else {
			anyLogged = true
		}
	}
	if !anyLogged && len(a.backends) > 0 {
		return fmt.Errorf("no audit backend succeeded in logging the request")
	}
	return nil
}

// LogResponse is used to ensure all the audit backends have an opportunity to
// log the given response and that *at least one* succeeds.
func (a *AuditBroker) LogResponse(auth *logical.Auth, req *logical.Request,
	resp *logical.Response, err error) (reterr error) {
	defer metrics.MeasureSince([]string{"audit", "log_response"}, time.Now())
	a.l.RLock()
	defer a.l.RUnlock()
	defer func() {
		if r := recover(); r != nil {
			a.logger.Printf("[ERR] audit: panic logging: auth: %#v, req: %#v, resp: %#v: %v", auth, req, resp, r)
			reterr = fmt.Errorf("panic generating audit log")
		}
	}()

	// Ensure at least one backend logs
	anyLogged := false
	for name, be := range a.backends {
		start := time.Now()
		err := be.backend.LogResponse(auth, req, resp, err)
		metrics.MeasureSince([]string{"audit", name, "log_response"}, start)
		if err != nil {
			a.logger.Printf("[ERR] audit: backend '%s' failed to log response: %v", name, err)
		} else {
			anyLogged = true
		}
	}
	if !anyLogged && len(a.backends) > 0 {
		return fmt.Errorf("no audit backend succeeded in logging the response")
	}
	return nil
}
<|endoftext|>