<commit_before>package sessions

import (
	"encoding/json"
	"errors"
	"net/http"
	"time"

	"github.com/cozy/cozy-stack/pkg/config"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/crypto"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/pkg/utils"
	"github.com/labstack/echo"
)

// SessionCookieName : name of the cookie created by cozy
const SessionCookieName = "cozysessid"

const (
	// SessionMaxAge is the maximum duration of the session in seconds
	SessionMaxAge = 7 * 24 * 60 * 60

	// AppCookieMaxAge is the maximum duration of the application cookie is
	// seconds
	AppCookieMaxAge = 24 * 60 * 60
)

var (
	// ErrNoCookie is returned by GetSession if there is no cookie
	ErrNoCookie = errors.New("No session cookie")
	// ErrExpired is returned when the session has expired
	ErrExpired = errors.New("Session expired")
	// ErrInvalidID is returned by GetSession if the cookie contains wrong ID
	ErrInvalidID = errors.New("Session cookie has wrong ID")
)

// A Session is an instance opened in a browser
type Session struct {
	Instance *instance.Instance `json:"-"`
	DocID    string             `json:"_id,omitempty"`
	DocRev   string             `json:"_rev,omitempty"`
	LastSeen time.Time          `json:"last_seen,omitempty"`
}

// DocType implements couchdb.Doc
func (s *Session) DocType() string { return consts.Sessions }

// ID implements couchdb.Doc
func (s *Session) ID() string { return s.DocID }

// SetID implements couchdb.Doc
func (s *Session) SetID(v string) { s.DocID = v }

// Rev implements couchdb.Doc
func (s *Session) Rev() string { return s.DocRev }

// SetRev implements couchdb.Doc
func (s *Session) SetRev(v string) { s.DocRev = v }

// Clone implements couchdb.Doc
func (s *Session) Clone() couchdb.Doc {
	cloned := *s
	if cloned.Instance != nil {
		tmp := *s.Instance
		cloned.Instance = &tmp
	}
	return &cloned
}

// ensure Session implements couchdb.Doc
var _ couchdb.Doc = (*Session)(nil)

// OlderThan check if a session last seen is older than t from now
func (s *Session) OlderThan(t time.Duration) bool {
	return time.Now().After(s.LastSeen.Add(t))
}

// New creates a session in couchdb for the given instance
func New(i *instance.Instance) (*Session, error) {
	var s = &Session{
		Instance: i,
		LastSeen: time.Now(),
	}
	if err := couchdb.CreateDoc(i, s); err != nil {
		return nil, err
	}
	getCache().Set(i.Domain, s.DocID, s)
	return s, nil
}

// Get fetch the session
func Get(i *instance.Instance, sessionID string) (*Session, error) {
	updateCache := false

	s := getCache().Get(i.Domain, sessionID)
	if s == nil {
		s = &Session{}
		err := couchdb.GetDoc(i, consts.Sessions, sessionID, s)
		if couchdb.IsNotFoundError(err) {
			return nil, ErrInvalidID
		}
		if err != nil {
			return nil, err
		}
		updateCache = true
	}
	s.Instance = i

	// if the session is older than the session max age, it has expired and
	// should be deleted.
	if s.OlderThan(SessionMaxAge * time.Second) {
		err := couchdb.DeleteDoc(i, s)
		if err != nil {
			i.Logger().Warn("[session] Failed to delte expired session:", err)
		}
		if updateCache {
			getCache().Revoke(i.Domain, s.DocID)
		}
		return nil, ErrExpired
	}

	// if the session is older than half its half-life, update the LastSeen date.
	if s.OlderThan((SessionMaxAge * time.Second) / 2) {
		lastSeen := s.LastSeen
		s.LastSeen = time.Now()
		err := couchdb.UpdateDoc(i, s)
		if err != nil {
			i.Logger().Warn("[session] Failed to update session last seen:", err)
		} else {
			s.LastSeen = lastSeen
			updateCache = true
		}
	}

	if updateCache {
		getCache().Set(i.Domain, s.DocID, s)
	}

	return s, nil
}

// FromCookie retrieves the session from a echo.Context cookies.
func FromCookie(c echo.Context, i *instance.Instance) (*Session, error) {
	cookie, err := c.Cookie(SessionCookieName)
	if err != nil || cookie.Value == "" {
		return nil, ErrNoCookie
	}

	sessionID, err := crypto.DecodeAuthMessage(cookieMACConfig(i, SessionMaxAge),
		[]byte(cookie.Value), nil)
	if err != nil {
		return nil, err
	}

	return Get(i, string(sessionID))
}

// FromAppCookie retrives the session from a application submain's cookie.
func FromAppCookie(c echo.Context, i *instance.Instance, slug string) (*Session, error) {
	if config.GetConfig().Subdomains == config.FlatSubdomains {
		cookie, err := c.Cookie(SessionCookieName)
		if err != nil || cookie.Value == "" {
			return nil, ErrNoCookie
		}

		sessionID, err := crypto.DecodeAuthMessage(cookieMACConfig(i, AppCookieMaxAge),
			[]byte(cookie.Value), []byte(slug))
		if err != nil {
			return nil, err
		}

		return Get(i, string(sessionID))
	}
	return FromCookie(c, i)
}

// GetAll return all active sessions
func GetAll(inst *instance.Instance) ([]*Session, error) {
	var sessions []*Session
	if err := couchdb.GetAllDocs(inst, consts.Sessions, nil, &sessions); err != nil {
		return nil, err
	}
	return sessions, nil
}

// Delete is a function to delete the session in couchdb,
// and returns a cookie with a negative MaxAge to clear it
func (s *Session) Delete(i *instance.Instance) *http.Cookie {
	getCache().Revoke(i.Domain, s.DocID)
	err := couchdb.DeleteDoc(i, s)
	if err != nil {
		i.Logger().Error("[session] Failed to delete session:", err)
	}
	return &http.Cookie{
		Name:   SessionCookieName,
		Value:  "",
		MaxAge: -1,
		Path:   "/",
		Domain: utils.StripPort("." + i.Domain),
	}
}

// ToCookie returns an http.Cookie for this Session
func (s *Session) ToCookie() (*http.Cookie, error) {
	encoded, err := crypto.EncodeAuthMessage(cookieMACConfig(s.Instance, SessionMaxAge), []byte(s.ID()), nil)
	if err != nil {
		return nil, err
	}

	return &http.Cookie{
		Name:     SessionCookieName,
		Value:    string(encoded),
		MaxAge:   SessionMaxAge,
		Path:     "/",
		Domain:   utils.StripPort("." + s.Instance.Domain),
		Secure:   !s.Instance.Dev,
		HttpOnly: true,
	}, nil
}

// ToAppCookie returns an http.Cookie for this Session on an app subdomain
func (s *Session) ToAppCookie(domain, slug string) (*http.Cookie, error) {
	encoded, err := crypto.EncodeAuthMessage(cookieMACConfig(s.Instance, AppCookieMaxAge), []byte(s.ID()), []byte(slug))
	if err != nil {
		return nil, err
	}

	return &http.Cookie{
		Name:     SessionCookieName,
		Value:    string(encoded),
		MaxAge:   AppCookieMaxAge,
		Path:     "/",
		Domain:   utils.StripPort(domain),
		Secure:   !s.Instance.Dev,
		HttpOnly: true,
	}, nil
}

// DeleteOthers will remove all sessions except the one given in parameter.
func DeleteOthers(i *instance.Instance, selfSessionID string) error {
	var sessions []*Session
	err := couchdb.ForeachDocs(i, consts.Sessions, func(data []byte) error {
		var s Session
		if err := json.Unmarshal(data, &s); err != nil {
			return err
		}
		sessions = append(sessions, &s)
		return nil
	})
	if err != nil {
		return err
	}
	for _, s := range sessions {
		if s.ID() != selfSessionID {
			s.Delete(i)
		}
	}
	return nil
}

// cookieMACConfig returns the options to authenticate the session cookie.
//
// We rely on a MACed cookie value, without additional encryption of the
// message since it should not contain critical private informations and is
// protected by HTTPs (secure cookie).
//
// About MaxLength, for a session of size 100 bytes
//
//       8 bytes time
//   +  32 bytes HMAC-SHA256
//   + 100 bytes session
//   + base64 encoding (4*n/3)
//   < 200 bytes
//
// 256 bytes should be sufficient enough to support any type of session.
//
func cookieMACConfig(i *instance.Instance, maxAge int64) *crypto.MACConfig {
	return &crypto.MACConfig{
		Name:   SessionCookieName,
		Key:    i.SessionSecret,
		MaxAge: maxAge,
		MaxLen: 256,
	}
}
<commit_msg>Fix some typos in comments<commit_after>package sessions

import (
	"encoding/json"
	"errors"
	"net/http"
	"time"

	"github.com/cozy/cozy-stack/pkg/config"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/crypto"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/pkg/utils"
	"github.com/labstack/echo"
)

// SessionCookieName: name of the cookie created by cozy
const SessionCookieName = "cozysessid"

const (
	// SessionMaxAge is the maximum duration of the session in seconds
	SessionMaxAge = 7 * 24 * 60 * 60

	// AppCookieMaxAge is the maximum duration of the application cookie in
	// seconds
	AppCookieMaxAge = 24 * 60 * 60
)

var (
	// ErrNoCookie is returned by GetSession if there is no cookie
	ErrNoCookie = errors.New("No session cookie")
	// ErrExpired is returned when the session has expired
	ErrExpired = errors.New("Session expired")
	// ErrInvalidID is returned by GetSession if the cookie contains wrong ID
	ErrInvalidID = errors.New("Session cookie has wrong ID")
)

// A Session is an instance opened in a browser
type Session struct {
	Instance *instance.Instance `json:"-"`
	DocID    string             `json:"_id,omitempty"`
	DocRev   string             `json:"_rev,omitempty"`
	LastSeen time.Time          `json:"last_seen,omitempty"`
}

// DocType implements couchdb.Doc
func (s *Session) DocType() string { return consts.Sessions }

// ID implements couchdb.Doc
func (s *Session) ID() string { return s.DocID }

// SetID implements couchdb.Doc
func (s *Session) SetID(v string) { s.DocID = v }

// Rev implements couchdb.Doc
func (s *Session) Rev() string { return s.DocRev }

// SetRev implements couchdb.Doc
func (s *Session) SetRev(v string) { s.DocRev = v }

// Clone implements couchdb.Doc
func (s *Session) Clone() couchdb.Doc {
	cloned := *s
	if cloned.Instance != nil {
		tmp := *s.Instance
		cloned.Instance = &tmp
	}
	return &cloned
}

// ensure Session implements couchdb.Doc
var _ couchdb.Doc = (*Session)(nil)

// OlderThan checks if a session last seen is older than t from now
func (s *Session) OlderThan(t time.Duration) bool {
	return time.Now().After(s.LastSeen.Add(t))
}

// New creates a session in couchdb for the given instance
func New(i *instance.Instance) (*Session, error) {
	var s = &Session{
		Instance: i,
		LastSeen: time.Now(),
	}
	if err := couchdb.CreateDoc(i, s); err != nil {
		return nil, err
	}
	getCache().Set(i.Domain, s.DocID, s)
	return s, nil
}

// Get fetches the session
func Get(i *instance.Instance, sessionID string) (*Session, error) {
	updateCache := false

	s := getCache().Get(i.Domain, sessionID)
	if s == nil {
		s = &Session{}
		err := couchdb.GetDoc(i, consts.Sessions, sessionID, s)
		if couchdb.IsNotFoundError(err) {
			return nil, ErrInvalidID
		}
		if err != nil {
			return nil, err
		}
		updateCache = true
	}
	s.Instance = i

	// If the session is older than the session max age, it has expired and
	// should be deleted.
	if s.OlderThan(SessionMaxAge * time.Second) {
		err := couchdb.DeleteDoc(i, s)
		if err != nil {
			i.Logger().Warn("[session] Failed to delte expired session:", err)
		}
		if updateCache {
			getCache().Revoke(i.Domain, s.DocID)
		}
		return nil, ErrExpired
	}

	// If the session is older than half its half-life, update the LastSeen date.
	if s.OlderThan((SessionMaxAge * time.Second) / 2) {
		lastSeen := s.LastSeen
		s.LastSeen = time.Now()
		err := couchdb.UpdateDoc(i, s)
		if err != nil {
			i.Logger().Warn("[session] Failed to update session last seen:", err)
		} else {
			s.LastSeen = lastSeen
			updateCache = true
		}
	}

	if updateCache {
		getCache().Set(i.Domain, s.DocID, s)
	}

	return s, nil
}

// FromCookie retrieves the session from a echo.Context cookies.
func FromCookie(c echo.Context, i *instance.Instance) (*Session, error) {
	cookie, err := c.Cookie(SessionCookieName)
	if err != nil || cookie.Value == "" {
		return nil, ErrNoCookie
	}

	sessionID, err := crypto.DecodeAuthMessage(cookieMACConfig(i, SessionMaxAge),
		[]byte(cookie.Value), nil)
	if err != nil {
		return nil, err
	}

	return Get(i, string(sessionID))
}

// FromAppCookie retrives the session from an application submain cookie.
func FromAppCookie(c echo.Context, i *instance.Instance, slug string) (*Session, error) {
	if config.GetConfig().Subdomains == config.FlatSubdomains {
		cookie, err := c.Cookie(SessionCookieName)
		if err != nil || cookie.Value == "" {
			return nil, ErrNoCookie
		}

		sessionID, err := crypto.DecodeAuthMessage(cookieMACConfig(i, AppCookieMaxAge),
			[]byte(cookie.Value), []byte(slug))
		if err != nil {
			return nil, err
		}

		return Get(i, string(sessionID))
	}
	return FromCookie(c, i)
}

// GetAll returns all the active sessions
func GetAll(inst *instance.Instance) ([]*Session, error) {
	var sessions []*Session
	if err := couchdb.GetAllDocs(inst, consts.Sessions, nil, &sessions); err != nil {
		return nil, err
	}
	return sessions, nil
}

// Delete is a function to delete the session in couchdb,
// and returns a cookie with a negative MaxAge to clear it
func (s *Session) Delete(i *instance.Instance) *http.Cookie {
	getCache().Revoke(i.Domain, s.DocID)
	err := couchdb.DeleteDoc(i, s)
	if err != nil {
		i.Logger().Error("[session] Failed to delete session:", err)
	}
	return &http.Cookie{
		Name:   SessionCookieName,
		Value:  "",
		MaxAge: -1,
		Path:   "/",
		Domain: utils.StripPort("." + i.Domain),
	}
}

// ToCookie returns an http.Cookie for this Session
func (s *Session) ToCookie() (*http.Cookie, error) {
	encoded, err := crypto.EncodeAuthMessage(cookieMACConfig(s.Instance, SessionMaxAge), []byte(s.ID()), nil)
	if err != nil {
		return nil, err
	}

	return &http.Cookie{
		Name:     SessionCookieName,
		Value:    string(encoded),
		MaxAge:   SessionMaxAge,
		Path:     "/",
		Domain:   utils.StripPort("." + s.Instance.Domain),
		Secure:   !s.Instance.Dev,
		HttpOnly: true,
	}, nil
}

// ToAppCookie returns an http.Cookie for this Session on an app subdomain
func (s *Session) ToAppCookie(domain, slug string) (*http.Cookie, error) {
	encoded, err := crypto.EncodeAuthMessage(cookieMACConfig(s.Instance, AppCookieMaxAge), []byte(s.ID()), []byte(slug))
	if err != nil {
		return nil, err
	}

	return &http.Cookie{
		Name:     SessionCookieName,
		Value:    string(encoded),
		MaxAge:   AppCookieMaxAge,
		Path:     "/",
		Domain:   utils.StripPort(domain),
		Secure:   !s.Instance.Dev,
		HttpOnly: true,
	}, nil
}

// DeleteOthers will remove all sessions except the one given in parameter.
func DeleteOthers(i *instance.Instance, selfSessionID string) error {
	var sessions []*Session
	err := couchdb.ForeachDocs(i, consts.Sessions, func(data []byte) error {
		var s Session
		if err := json.Unmarshal(data, &s); err != nil {
			return err
		}
		sessions = append(sessions, &s)
		return nil
	})
	if err != nil {
		return err
	}
	for _, s := range sessions {
		if s.ID() != selfSessionID {
			s.Delete(i)
		}
	}
	return nil
}

// cookieMACConfig returns the options to authenticate the session cookie.
//
// We rely on a MACed cookie value, without additional encryption of the
// message since it should not contain critical private informations and is
// protected by HTTPs (secure cookie).
//
// About MaxLength, for a session of size 100 bytes
//
//       8 bytes time
//   +  32 bytes HMAC-SHA256
//   + 100 bytes session
//   + base64 encoding (4*n/3)
//   < 200 bytes
//
// 256 bytes should be sufficient enough to support any type of session.
//
func cookieMACConfig(i *instance.Instance, maxAge int64) *crypto.MACConfig {
	return &crypto.MACConfig{
		Name:   SessionCookieName,
		Key:    i.SessionSecret,
		MaxAge: maxAge,
		MaxLen: 256,
	}
}
<|endoftext|>


<commit_before>package sessions

import (
	"encoding/json"
	"errors"
	"net/http"
	"time"

	"github.com/cozy/cozy-stack/pkg/config"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/crypto"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/pkg/utils"
	"github.com/labstack/echo"
)

// SessionCookieName is name of the cookie created by cozy
const SessionCookieName = "cozysessid"

const (
	// SessionMaxAge is the maximum duration of the session in seconds
	SessionMaxAge = 7 * 24 * 60 * 60

	// AppCookieMaxAge is the maximum duration of the application cookie in
	// seconds
	AppCookieMaxAge = 24 * 60 * 60
)

var (
	// ErrNoCookie is returned by GetSession if there is no cookie
	ErrNoCookie = errors.New("No session cookie")
	// ErrExpired is returned when the session has expired
	ErrExpired = errors.New("Session expired")
	// ErrInvalidID is returned by GetSession if the cookie contains wrong ID
	ErrInvalidID = errors.New("Session cookie has wrong ID")
)

// A Session is an instance opened in a browser
type Session struct {
	Instance  *instance.Instance `json:"-"`
	DocID     string             `json:"_id,omitempty"`
	DocRev    string             `json:"_rev,omitempty"`
	CreatedAt time.Time          `json:"created_at"`
	LastSeen  time.Time          `json:"last_seen"`
}

// DocType implements couchdb.Doc
func (s *Session) DocType() string { return consts.Sessions }

// ID implements couchdb.Doc
func (s *Session) ID() string { return s.DocID }

// SetID implements couchdb.Doc
func (s *Session) SetID(v string) { s.DocID = v }

// Rev implements couchdb.Doc
func (s *Session) Rev() string { return s.DocRev }

// SetRev implements couchdb.Doc
func (s *Session) SetRev(v string) { s.DocRev = v }

// Clone implements couchdb.Doc
func (s *Session) Clone() couchdb.Doc {
	cloned := *s
	if cloned.Instance != nil {
		tmp := *s.Instance
		cloned.Instance = &tmp
	}
	return &cloned
}

// ensure Session implements couchdb.Doc
var _ couchdb.Doc = (*Session)(nil)

// OlderThan checks if a session last seen is older than t from now
func (s *Session) OlderThan(t time.Duration) bool {
	return time.Now().After(s.LastSeen.Add(t))
}

// New creates a session in couchdb for the given instance
func New(i *instance.Instance) (*Session, error) {
	now := time.Now()
	s := &Session{
		Instance:  i,
		LastSeen:  now,
		CreatedAt: now,
	}
	if err := couchdb.CreateDoc(i, s); err != nil {
		return nil, err
	}
	getCache().Set(i.Domain, s.DocID, s)
	return s, nil
}

// Get fetches the session
func Get(i *instance.Instance, sessionID string) (*Session, error) {
	updateCache := false

	s := getCache().Get(i.Domain, sessionID)
	if s == nil {
		s = &Session{}
		err := couchdb.GetDoc(i, consts.Sessions, sessionID, s)
		if couchdb.IsNotFoundError(err) {
			return nil, ErrInvalidID
		}
		if err != nil {
			return nil, err
		}
		updateCache = true
	}
	s.Instance = i

	// If the session is older than the session max age, it has expired and
	// should be deleted.
	if s.OlderThan(SessionMaxAge * time.Second) {
		err := couchdb.DeleteDoc(i, s)
		if err != nil {
			i.Logger().Warn("[session] Failed to delete expired session:", err)
		}
		getCache().Revoke(i.Domain, s.DocID)
		return nil, ErrExpired
	}

	// In order to avoid too many updates of the session document, we have an
	// update period of one day for the `last_seen` date, which is a good enough
	// granularity.
	if s.OlderThan(24 * time.Hour) {
		lastSeen := s.LastSeen
		s.LastSeen = time.Now()
		err := couchdb.UpdateDoc(i, s)
		if err != nil {
			i.Logger().Warn("[session] Failed to update session last seen:", err)
			s.LastSeen = lastSeen
			updateCache = false
		} else {
			updateCache = true
		}
	}

	if updateCache {
		getCache().Set(i.Domain, s.DocID, s)
	}

	return s, nil
}

// FromCookie retrieves the session from a echo.Context cookies.
func FromCookie(c echo.Context, i *instance.Instance) (*Session, error) {
	cookie, err := c.Cookie(SessionCookieName)
	if err != nil || cookie.Value == "" {
		return nil, ErrNoCookie
	}

	sessionID, err := crypto.DecodeAuthMessage(cookieMACConfig(i, SessionMaxAge),
		[]byte(cookie.Value), nil)
	if err != nil {
		return nil, err
	}

	return Get(i, string(sessionID))
}

// FromAppCookie retrives the session from an application submain cookie.
func FromAppCookie(c echo.Context, i *instance.Instance, slug string) (*Session, error) {
	if config.GetConfig().Subdomains == config.FlatSubdomains {
		cookie, err := c.Cookie(SessionCookieName)
		if err != nil || cookie.Value == "" {
			return nil, ErrNoCookie
		}

		sessionID, err := crypto.DecodeAuthMessage(cookieMACConfig(i, AppCookieMaxAge),
			[]byte(cookie.Value), []byte(slug))
		if err != nil {
			return nil, err
		}

		return Get(i, string(sessionID))
	}
	return FromCookie(c, i)
}

// GetAll returns all the active sessions
func GetAll(inst *instance.Instance) ([]*Session, error) {
	var sessions []*Session
	if err := couchdb.GetAllDocs(inst, consts.Sessions, nil, &sessions); err != nil {
		return nil, err
	}
	return sessions, nil
}

// Delete is a function to delete the session in couchdb,
// and returns a cookie with a negative MaxAge to clear it
func (s *Session) Delete(i *instance.Instance) *http.Cookie {
	err := couchdb.DeleteDoc(i, s)
	getCache().Revoke(i.Domain, s.DocID)
	if err != nil {
		i.Logger().Error("[session] Failed to delete session:", err)
	}
	return &http.Cookie{
		Name:   SessionCookieName,
		Value:  "",
		MaxAge: -1,
		Path:   "/",
		Domain: utils.StripPort("." + i.Domain),
	}
}

// ToCookie returns an http.Cookie for this Session
func (s *Session) ToCookie() (*http.Cookie, error) {
	encoded, err := crypto.EncodeAuthMessage(cookieMACConfig(s.Instance, SessionMaxAge), []byte(s.ID()), nil)
	if err != nil {
		return nil, err
	}

	return &http.Cookie{
		Name:     SessionCookieName,
		Value:    string(encoded),
		MaxAge:   0, // session cookie without maxage
		Path:     "/",
		Domain:   utils.StripPort("." + s.Instance.Domain),
		Secure:   !s.Instance.Dev,
		HttpOnly: true,
	}, nil
}

// ToAppCookie returns an http.Cookie for this Session on an app subdomain
func (s *Session) ToAppCookie(domain, slug string) (*http.Cookie, error) {
	encoded, err := crypto.EncodeAuthMessage(cookieMACConfig(s.Instance, AppCookieMaxAge), []byte(s.ID()), []byte(slug))
	if err != nil {
		return nil, err
	}

	return &http.Cookie{
		Name:     SessionCookieName,
		Value:    string(encoded),
		MaxAge:   AppCookieMaxAge,
		Path:     "/",
		Domain:   utils.StripPort(domain),
		Secure:   !s.Instance.Dev,
		HttpOnly: true,
	}, nil
}

// DeleteOthers will remove all sessions except the one given in parameter.
func DeleteOthers(i *instance.Instance, selfSessionID string) error {
	var sessions []*Session
	err := couchdb.ForeachDocs(i, consts.Sessions, func(data []byte) error {
		var s Session
		if err := json.Unmarshal(data, &s); err != nil {
			return err
		}
		sessions = append(sessions, &s)
		return nil
	})
	if err != nil {
		return err
	}
	for _, s := range sessions {
		if s.ID() != selfSessionID {
			s.Delete(i)
		}
	}
	return nil
}

// cookieMACConfig returns the options to authenticate the session cookie.
//
// We rely on a MACed cookie value, without additional encryption of the
// message since it should not contain critical private informations and is
// protected by HTTPs (secure cookie).
//
// About MaxLength, for a session of size 100 bytes
//
//       8 bytes time
//   +  32 bytes HMAC-SHA256
//   + 100 bytes session
//   + base64 encoding (4*n/3)
//   < 200 bytes
//
// 256 bytes should be sufficient enough to support any type of session.
//
func cookieMACConfig(i *instance.Instance, maxAge int64) *crypto.MACConfig {
	return &crypto.MACConfig{
		Name:   SessionCookieName,
		Key:    i.SessionSecret,
		MaxAge: maxAge,
		MaxLen: 256,
	}
}
<commit_msg>Set cookie expiration to 10 years<commit_after>package sessions

import (
	"encoding/json"
	"errors"
	"net/http"
	"time"

	"github.com/cozy/cozy-stack/pkg/config"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/crypto"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/pkg/utils"
	"github.com/labstack/echo"
)

// SessionCookieName is name of the cookie created by cozy
const SessionCookieName = "cozysessid"

const (
	// SessionMaxAge is the maximum duration of the session in seconds
	SessionMaxAge = 7 * 24 * 60 * 60

	// AppCookieMaxAge is the maximum duration of the application cookie in
	// seconds
	AppCookieMaxAge = 24 * 60 * 60
)

var (
	// ErrNoCookie is returned by GetSession if there is no cookie
	ErrNoCookie = errors.New("No session cookie")
	// ErrExpired is returned when the session has expired
	ErrExpired = errors.New("Session expired")
	// ErrInvalidID is returned by GetSession if the cookie contains wrong ID
	ErrInvalidID = errors.New("Session cookie has wrong ID")
)

// A Session is an instance opened in a browser
type Session struct {
	Instance  *instance.Instance `json:"-"`
	DocID     string             `json:"_id,omitempty"`
	DocRev    string             `json:"_rev,omitempty"`
	CreatedAt time.Time          `json:"created_at"`
	LastSeen  time.Time          `json:"last_seen"`
}

// DocType implements couchdb.Doc
func (s *Session) DocType() string { return consts.Sessions }

// ID implements couchdb.Doc
func (s *Session) ID() string { return s.DocID }

// SetID implements couchdb.Doc
func (s *Session) SetID(v string) { s.DocID = v }

// Rev implements couchdb.Doc
func (s *Session) Rev() string { return s.DocRev }

// SetRev implements couchdb.Doc
func (s *Session) SetRev(v string) { s.DocRev = v }

// Clone implements couchdb.Doc
func (s *Session) Clone() couchdb.Doc {
	cloned := *s
	if cloned.Instance != nil {
		tmp := *s.Instance
		cloned.Instance = &tmp
	}
	return &cloned
}

// ensure Session implements couchdb.Doc
var _ couchdb.Doc = (*Session)(nil)

// OlderThan checks if a session last seen is older than t from now
func (s *Session) OlderThan(t time.Duration) bool {
	return time.Now().After(s.LastSeen.Add(t))
}

// New creates a session in couchdb for the given instance
func New(i *instance.Instance) (*Session, error) {
	now := time.Now()
	s := &Session{
		Instance:  i,
		LastSeen:  now,
		CreatedAt: now,
	}
	if err := couchdb.CreateDoc(i, s); err != nil {
		return nil, err
	}
	getCache().Set(i.Domain, s.DocID, s)
	return s, nil
}

// Get fetches the session
func Get(i *instance.Instance, sessionID string) (*Session, error) {
	updateCache := false

	s := getCache().Get(i.Domain, sessionID)
	if s == nil {
		s = &Session{}
		err := couchdb.GetDoc(i, consts.Sessions, sessionID, s)
		if couchdb.IsNotFoundError(err) {
			return nil, ErrInvalidID
		}
		if err != nil {
			return nil, err
		}
		updateCache = true
	}
	s.Instance = i

	// If the session is older than the session max age, it has expired and
	// should be deleted.
	if s.OlderThan(SessionMaxAge * time.Second) {
		err := couchdb.DeleteDoc(i, s)
		if err != nil {
			i.Logger().Warn("[session] Failed to delete expired session:", err)
		}
		getCache().Revoke(i.Domain, s.DocID)
		return nil, ErrExpired
	}

	// In order to avoid too many updates of the session document, we have an
	// update period of one day for the `last_seen` date, which is a good enough
	// granularity.
	if s.OlderThan(24 * time.Hour) {
		lastSeen := s.LastSeen
		s.LastSeen = time.Now()
		err := couchdb.UpdateDoc(i, s)
		if err != nil {
			i.Logger().Warn("[session] Failed to update session last seen:", err)
			s.LastSeen = lastSeen
			updateCache = false
		} else {
			updateCache = true
		}
	}

	if updateCache {
		getCache().Set(i.Domain, s.DocID, s)
	}

	return s, nil
}

// FromCookie retrieves the session from a echo.Context cookies.
func FromCookie(c echo.Context, i *instance.Instance) (*Session, error) {
	cookie, err := c.Cookie(SessionCookieName)
	if err != nil || cookie.Value == "" {
		return nil, ErrNoCookie
	}

	sessionID, err := crypto.DecodeAuthMessage(cookieMACConfig(i, SessionMaxAge),
		[]byte(cookie.Value), nil)
	if err != nil {
		return nil, err
	}

	return Get(i, string(sessionID))
}

// FromAppCookie retrives the session from an application submain cookie.
func FromAppCookie(c echo.Context, i *instance.Instance, slug string) (*Session, error) {
	if config.GetConfig().Subdomains == config.FlatSubdomains {
		cookie, err := c.Cookie(SessionCookieName)
		if err != nil || cookie.Value == "" {
			return nil, ErrNoCookie
		}

		sessionID, err := crypto.DecodeAuthMessage(cookieMACConfig(i, AppCookieMaxAge),
			[]byte(cookie.Value), []byte(slug))
		if err != nil {
			return nil, err
		}

		return Get(i, string(sessionID))
	}
	return FromCookie(c, i)
}

// GetAll returns all the active sessions
func GetAll(inst *instance.Instance) ([]*Session, error) {
	var sessions []*Session
	if err := couchdb.GetAllDocs(inst, consts.Sessions, nil, &sessions); err != nil {
		return nil, err
	}
	return sessions, nil
}

// Delete is a function to delete the session in couchdb,
// and returns a cookie with a negative MaxAge to clear it
func (s *Session) Delete(i *instance.Instance) *http.Cookie {
	err := couchdb.DeleteDoc(i, s)
	getCache().Revoke(i.Domain, s.DocID)
	if err != nil {
		i.Logger().Error("[session] Failed to delete session:", err)
	}
	return &http.Cookie{
		Name:   SessionCookieName,
		Value:  "",
		MaxAge: -1,
		Path:   "/",
		Domain: utils.StripPort("." + i.Domain),
	}
}

// ToCookie returns an http.Cookie for this Session
func (s *Session) ToCookie() (*http.Cookie, error) {
	encoded, err := crypto.EncodeAuthMessage(cookieMACConfig(s.Instance, SessionMaxAge), []byte(s.ID()), nil)
	if err != nil {
		return nil, err
	}

	return &http.Cookie{
		Name:     SessionCookieName,
		Value:    string(encoded),
		MaxAge:   10 * 365 * 24 * 3600, // 10 years
		Path:     "/",
		Domain:   utils.StripPort("." + s.Instance.Domain),
		Secure:   !s.Instance.Dev,
		HttpOnly: true,
	}, nil
}

// ToAppCookie returns an http.Cookie for this Session on an app subdomain
func (s *Session) ToAppCookie(domain, slug string) (*http.Cookie, error) {
	encoded, err := crypto.EncodeAuthMessage(cookieMACConfig(s.Instance, AppCookieMaxAge), []byte(s.ID()), []byte(slug))
	if err != nil {
		return nil, err
	}

	return &http.Cookie{
		Name:     SessionCookieName,
		Value:    string(encoded),
		MaxAge:   AppCookieMaxAge,
		Path:     "/",
		Domain:   utils.StripPort(domain),
		Secure:   !s.Instance.Dev,
		HttpOnly: true,
	}, nil
}

// DeleteOthers will remove all sessions except the one given in parameter.
func DeleteOthers(i *instance.Instance, selfSessionID string) error {
	var sessions []*Session
	err := couchdb.ForeachDocs(i, consts.Sessions, func(data []byte) error {
		var s Session
		if err := json.Unmarshal(data, &s); err != nil {
			return err
		}
		sessions = append(sessions, &s)
		return nil
	})
	if err != nil {
		return err
	}
	for _, s := range sessions {
		if s.ID() != selfSessionID {
			s.Delete(i)
		}
	}
	return nil
}

// cookieMACConfig returns the options to authenticate the session cookie.
//
// We rely on a MACed cookie value, without additional encryption of the
// message since it should not contain critical private informations and is
// protected by HTTPs (secure cookie).
//
// About MaxLength, for a session of size 100 bytes
//
//       8 bytes time
//   +  32 bytes HMAC-SHA256
//   + 100 bytes session
//   + base64 encoding (4*n/3)
//   < 200 bytes
//
// 256 bytes should be sufficient enough to support any type of session.
//
func cookieMACConfig(i *instance.Instance, maxAge int64) *crypto.MACConfig {
	return &crypto.MACConfig{
		Name:   SessionCookieName,
		Key:    i.SessionSecret,
		MaxAge: maxAge,
		MaxLen: 256,
	}
}
<|endoftext|>


<commit_before>// Copyright 2016 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
//

package sql

import (
	"bytes"
	"fmt"
	"math"

	"github.com/cockroachdb/cockroach/pkg/sql/parser"
)

// formatColumns converts a column signature for a data source /
// planNode to a string. The column types are printed iff the 2nd
// argument specifies so.
func formatColumns(cols ResultColumns, printTypes bool) string {
	var buf bytes.Buffer
	buf.WriteByte('(')
	for i, rCol := range cols {
		if i > 0 {
			buf.WriteString(", ")
		}
		parser.Name(rCol.Name).Format(&buf, parser.FmtSimple)
		// Output extra properties like [hidden,omitted].
		hasProps := false
		outputProp := func(prop string) {
			if hasProps {
				buf.WriteByte(',')
			} else {
				buf.WriteByte('[')
			}
			hasProps = true
			buf.WriteString(prop)
		}
		if rCol.hidden {
			outputProp("hidden")
		}
		if rCol.omitted {
			outputProp("omitted")
		}
		if hasProps {
			buf.WriteByte(']')
		}

		if printTypes {
			buf.WriteByte(' ')
			buf.WriteString(rCol.Typ.String())
		}
	}
	buf.WriteByte(')')
	return buf.String()
}

// newExplainPlanNode instantiates a planNode that runs an EXPLAIN query.
func (p *planner) makeExplainPlanNode(explainer explainer, expanded bool, plan planNode) planNode {
	columns := ResultColumns{
		// Level is the depth of the node in the tree.
		{Name: "Level", Typ: parser.TypeInt},
		// Type is the node type.
		{Name: "Type", Typ: parser.TypeString},
		// Field is the part of the node that a row of output pertains to.
		// For example a select node may have separate "render" and
		// "filter" fields.
		{Name: "Field", Typ: parser.TypeString},
		// Description contains details about the field.
		{Name: "Description", Typ: parser.TypeString},
	}
	if explainer.showMetadata {
		// Columns is the type signature of the data source.
		columns = append(columns, ResultColumn{Name: "Columns", Typ: parser.TypeString})
		// Ordering indicates the known ordering of the data from this source.
		columns = append(columns, ResultColumn{Name: "Ordering", Typ: parser.TypeString})
	}

	explainer.fmtFlags = parser.FmtExpr(
		explainer.showTypes, explainer.symbolicVars, explainer.qualifyNames)

	node := &explainPlanNode{
		explainer: explainer,
		expanded:  expanded,
		plan:      plan,
		results:   p.newContainerValuesNode(columns, 0),
	}
	return node
}

// explainer represents the run-time state of the EXPLAIN logic.
type explainer struct {
	// showMetadata indicates whether the output has separate columns for the
	// schema signature and ordering information of the intermediate
	// nodes.
	showMetadata bool

	// showExprs indicates whether the plan prints expressions
	// embedded inside the node.
	showExprs bool

	// qualifyNames determines whether column names in expressions
	// should be fully qualified during pretty-printing.
	qualifyNames bool

	// symbolicVars determines whether ordinal column references
	// should be printed numerically.
	symbolicVars bool

	// fmtFlags is the formatter to use for pretty-printing expressions.
	fmtFlags parser.FmtFlags

	// showTypes indicates whether to print the type of embedded
	// expressions and result columns.
	showTypes bool

	// showSelectTop indicates whether intermediate selectTopNodes
	// are rendered.
	showSelectTop bool

	// level is the current depth in the tree of planNodes.
	level int

	// prefix is the whitespace inserted in front of the description to
	// clarify the hierarchical structure of the plans when doIndent is
	// true.
	prefix string

	// doIndent indicates whether the output should be clarified
	// with leading white spaces.
	doIndent bool

	// makeRow produces one row of EXPLAIN output.
	makeRow func(level int, typ, field, desc string, plan planNode)

	// err remembers whether any error was encountered by makeRow.
	err error
}

var emptyString = parser.NewDString("")

// populateExplain invokes explain() with a makeRow method
// which populates a valuesNode.
func (e *explainer) populateExplain(v *valuesNode, plan planNode) error {
	e.makeRow = func(level int, name, field, description string, plan planNode) {
		if e.err != nil {
			return
		}

		row := parser.DTuple{
			parser.NewDInt(parser.DInt(level)),
			parser.NewDString(name),
			parser.NewDString(field),
			parser.NewDString(description),
		}
		if e.showMetadata {
			if plan != nil {
				row = append(row, parser.NewDString(formatColumns(plan.Columns(), e.showTypes)))
				row = append(row, parser.NewDString(plan.Ordering().AsString(plan.Columns())))
			} else {
				row = append(row, emptyString, emptyString)
			}
		}
		if _, err := v.rows.AddRow(row); err != nil {
			e.err = err
		}
	}

	e.err = nil
	e.explain(plan)
	return e.err
}

// planToString uses explain() to build a string representation of the planNode.
func planToString(plan planNode) string {
	var buf bytes.Buffer
	e := explainer{
		showMetadata:  true,
		showTypes:     true,
		showExprs:     true,
		showSelectTop: true,
		makeRow: func(level int, name, field, description string, plan planNode) {
			if field != "" {
				field = "." + field
			}
			fmt.Fprintf(&buf, "%d %s%s %s %s %s\n", level, name, field, description,
				formatColumns(plan.Columns(), true),
				plan.Ordering().AsString(plan.Columns()),
			)
		},
	}
	e.explain(plan)
	return buf.String()
}

// explain extract information from planNodes and produces the EXPLAIN
// output via the makeRow callback in the explainer.
func (e *explainer) explain(plan planNode) {
	if e.err != nil {
		return
	}

	name, description, children := plan.ExplainPlan(e.showMetadata)

	if name == "select" && !e.showSelectTop {
		e.explain(children[len(children)-1])
		return
	}

	e.node(name, description, plan)

	if e.showExprs {
		plan.explainExprs(func(elt string, expr parser.Expr) {
			if e.err != nil {
				return
			}
			if expr != nil {
				e.attr(name, elt, parser.AsStringWithFlags(expr, e.fmtFlags))
			}
		})
	}

	for _, child := range children {
		e.subnode(child)
	}
}

func (e *explainer) node(name, desc string, plan planNode) {
	if e.doIndent {
		desc = fmt.Sprintf("%s-> %s %s", e.prefix, name, desc)
		e.prefix += "   "
	}
	e.makeRow(e.level, name, "", desc, plan)
}

func (e *explainer) attr(name, t, v string) {
	if e.doIndent {
		v = fmt.Sprintf("%s%s", e.prefix, v)
	}
	e.makeRow(e.level, name, t, v, nil)
}

func (e *explainer) subnode(plan planNode) {
	curPrefix := e.prefix
	e.level++
	e.explain(plan)
	e.level--
	e.prefix = curPrefix
}

// explainPlanNode implements the logic for EXPLAIN.
type explainPlanNode struct {
	explainer explainer
	expanded  bool
	plan      planNode
	results   *valuesNode
}

func (e *explainPlanNode) explainExprs(fn func(string, parser.Expr)) {}
func (e *explainPlanNode) Next() (bool, error)                       { return e.results.Next() }
func (e *explainPlanNode) Columns() ResultColumns                    { return e.results.Columns() }
func (e *explainPlanNode) Ordering() orderingInfo                    { return e.results.Ordering() }
func (e *explainPlanNode) Values() parser.DTuple                     { return e.results.Values() }
func (e *explainPlanNode) DebugValues() debugValues                  { return debugValues{} }
func (e *explainPlanNode) SetLimitHint(n int64, s bool)              { e.results.SetLimitHint(n, s) }
func (e *explainPlanNode) setNeededColumns(_ []bool)                 {}
func (e *explainPlanNode) MarkDebug(mode explainMode)                {}
func (e *explainPlanNode) expandPlan() error {
	if e.expanded {
		if err := e.plan.expandPlan(); err != nil {
			return err
		}
		// Trigger limit hint propagation, which would otherwise only occur
		// during the plan's Start() phase. This may trigger additional
		// optimizations (eg. in sortNode) which the user of EXPLAIN will be
		// interested in.
		e.plan.SetLimitHint(math.MaxInt64, true)
	}
	return nil
}
func (e *explainPlanNode) ExplainPlan(v bool) (string, string, []planNode) {
	return "explain", "plan", []planNode{e.plan}
}

func (e *explainPlanNode) Start() error {
	return e.explainer.populateExplain(e.results, e.plan)
}

func (e *explainPlanNode) Close() {
	e.plan.Close()
	e.results.Close()
}
<commit_msg>sql: reorganize explain_plan.go for clarity.<commit_after>// Copyright 2016 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
//

package sql

import (
	"bytes"
	"fmt"
	"math"

	"github.com/cockroachdb/cockroach/pkg/sql/parser"
)

// explainer represents the run-time state of the EXPLAIN logic.
type explainer struct {
	// showMetadata indicates whether the output has separate columns for the
	// schema signature and ordering information of the intermediate
	// nodes.
	showMetadata bool

	// showExprs indicates whether the plan prints expressions
	// embedded inside the node.
	showExprs bool

	// qualifyNames determines whether column names in expressions
	// should be fully qualified during pretty-printing.
	qualifyNames bool

	// symbolicVars determines whether ordinal column references
	// should be printed numerically.
	symbolicVars bool

	// fmtFlags is the formatter to use for pretty-printing expressions.
	fmtFlags parser.FmtFlags

	// showTypes indicates whether to print the type of embedded
	// expressions and result columns.
	showTypes bool

	// showSelectTop indicates whether intermediate selectTopNodes
	// are rendered.
	showSelectTop bool

	// level is the current depth in the tree of planNodes.
	level int

	// prefix is the whitespace inserted in front of the description to
	// clarify the hierarchical structure of the plans when doIndent is
	// true.
	prefix string

	// doIndent indicates whether the output should be clarified
	// with leading white spaces.
	doIndent bool

	// makeRow produces one row of EXPLAIN output.
	makeRow func(level int, typ, field, desc string, plan planNode)

	// err remembers whether any error was encountered by makeRow.
	err error
}

// newExplainPlanNode instantiates a planNode that runs an EXPLAIN query.
func (p *planner) makeExplainPlanNode(explainer explainer, expanded bool, plan planNode) planNode {
	columns := ResultColumns{
		// Level is the depth of the node in the tree.
		{Name: "Level", Typ: parser.TypeInt},
		// Type is the node type.
		{Name: "Type", Typ: parser.TypeString},
		// Field is the part of the node that a row of output pertains to.
		// For example a select node may have separate "render" and
		// "filter" fields.
		{Name: "Field", Typ: parser.TypeString},
		// Description contains details about the field.
		{Name: "Description", Typ: parser.TypeString},
	}
	if explainer.showMetadata {
		// Columns is the type signature of the data source.
		columns = append(columns, ResultColumn{Name: "Columns", Typ: parser.TypeString})
		// Ordering indicates the known ordering of the data from this source.
		columns = append(columns, ResultColumn{Name: "Ordering", Typ: parser.TypeString})
	}

	explainer.fmtFlags = parser.FmtExpr(
		explainer.showTypes, explainer.symbolicVars, explainer.qualifyNames)

	node := &explainPlanNode{
		explainer: explainer,
		expanded:  expanded,
		plan:      plan,
		results:   p.newContainerValuesNode(columns, 0),
	}
	return node
}

var emptyString = parser.NewDString("")

// populateExplain invokes explain() with a makeRow method
// which populates a valuesNode.
func (e *explainer) populateExplain(v *valuesNode, plan planNode) error {
	e.makeRow = func(level int, name, field, description string, plan planNode) {
		if e.err != nil {
			return
		}

		row := parser.DTuple{
			parser.NewDInt(parser.DInt(level)),
			parser.NewDString(name),
			parser.NewDString(field),
			parser.NewDString(description),
		}
		if e.showMetadata {
			if plan != nil {
				row = append(row, parser.NewDString(formatColumns(plan.Columns(), e.showTypes)))
				row = append(row, parser.NewDString(plan.Ordering().AsString(plan.Columns())))
			} else {
				row = append(row, emptyString, emptyString)
			}
		}
		if _, err := v.rows.AddRow(row); err != nil {
			e.err = err
		}
	}

	e.err = nil
	e.explain(plan)
	return e.err
}

// planToString uses explain() to build a string representation of the planNode.
func planToString(plan planNode) string {
	var buf bytes.Buffer
	e := explainer{
		showMetadata:  true,
		showTypes:     true,
		showExprs:     true,
		showSelectTop: true,
		makeRow: func(level int, name, field, description string, plan planNode) {
			if field != "" {
				field = "." + field
			}
			fmt.Fprintf(&buf, "%d %s%s %s %s %s\n", level, name, field, description,
				formatColumns(plan.Columns(), true),
				plan.Ordering().AsString(plan.Columns()),
			)
		},
	}
	e.explain(plan)
	return buf.String()
}

// explain extract information from planNodes and produces the EXPLAIN
// output via the makeRow callback in the explainer.
func (e *explainer) explain(plan planNode) {
	if e.err != nil {
		return
	}

	name, description, children := plan.ExplainPlan(e.showMetadata)

	if name == "select" && !e.showSelectTop {
		e.explain(children[len(children)-1])
		return
	}

	e.node(name, description, plan)

	if e.showExprs {
		plan.explainExprs(func(elt string, expr parser.Expr) {
			if e.err != nil {
				return
			}
			if expr != nil {
				e.attr(name, elt, parser.AsStringWithFlags(expr, e.fmtFlags))
			}
		})
	}

	for _, child := range children {
		e.subnode(child)
	}
}

func (e *explainer) node(name, desc string, plan planNode) {
	if e.doIndent {
		desc = fmt.Sprintf("%s-> %s %s", e.prefix, name, desc)
		e.prefix += "   "
	}
	e.makeRow(e.level, name, "", desc, plan)
}

func (e *explainer) attr(name, t, v string) {
	if e.doIndent {
		v = fmt.Sprintf("%s%s", e.prefix, v)
	}
	e.makeRow(e.level, name, t, v, nil)
}

func (e *explainer) subnode(plan planNode) {
	curPrefix := e.prefix
	e.level++
	e.explain(plan)
	e.level--
	e.prefix = curPrefix
}

// formatColumns converts a column signature for a data source /
// planNode to a string. The column types are printed iff the 2nd
// argument specifies so.
func formatColumns(cols ResultColumns, printTypes bool) string {
	var buf bytes.Buffer
	buf.WriteByte('(')
	for i, rCol := range cols {
		if i > 0 {
			buf.WriteString(", ")
		}
		parser.Name(rCol.Name).Format(&buf, parser.FmtSimple)
		// Output extra properties like [hidden,omitted].
		hasProps := false
		outputProp := func(prop string) {
			if hasProps {
				buf.WriteByte(',')
			} else {
				buf.WriteByte('[')
			}
			hasProps = true
			buf.WriteString(prop)
		}
		if rCol.hidden {
			outputProp("hidden")
		}
		if rCol.omitted {
			outputProp("omitted")
		}
		if hasProps {
			buf.WriteByte(']')
		}

		if printTypes {
			buf.WriteByte(' ')
			buf.WriteString(rCol.Typ.String())
		}
	}
	buf.WriteByte(')')
	return buf.String()
}

// explainPlanNode wraps the logic for EXPLAIN as a planNode.
type explainPlanNode struct {
	explainer explainer
	expanded  bool
	plan      planNode
	results   *valuesNode
}

func (e *explainPlanNode) explainExprs(fn func(string, parser.Expr)) {}
func (e *explainPlanNode) Next() (bool, error)                       { return e.results.Next() }
func (e *explainPlanNode) Columns() ResultColumns                    { return e.results.Columns() }
func (e *explainPlanNode) Ordering() orderingInfo                    { return e.results.Ordering() }
func (e *explainPlanNode) Values() parser.DTuple                     { return e.results.Values() }
func (e *explainPlanNode) DebugValues() debugValues                  { return debugValues{} }
func (e *explainPlanNode) SetLimitHint(n int64, s bool)              { e.results.SetLimitHint(n, s) }
func (e *explainPlanNode) setNeededColumns(_ []bool)                 {}
func (e *explainPlanNode) MarkDebug(mode explainMode)                {}
func (e *explainPlanNode) expandPlan() error {
	if e.expanded {
		if err := e.plan.expandPlan(); err != nil {
			return err
		}
		// Trigger limit hint propagation, which would otherwise only occur
		// during the plan's Start() phase. This may trigger additional
		// optimizations (eg. in sortNode) which the user of EXPLAIN will be
		// interested in.
		e.plan.SetLimitHint(math.MaxInt64, true)
	}
	return nil
}
func (e *explainPlanNode) ExplainPlan(v bool) (string, string, []planNode) {
	return "explain", "plan", []planNode{e.plan}
}

func (e *explainPlanNode) Start() error {
	return e.explainer.populateExplain(e.results, e.plan)
}

func (e *explainPlanNode) Close() {
	e.plan.Close()
	e.results.Close()
}
<|endoftext|>


<commit_before>package testutil

import (
	"strings"
	"unicode"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ErrorContains checks that the error is not nil, and contains the expected
// substring.
func ErrorContains(t require.TestingT, err error, expectedError string) {
	require.Error(t, err)
	assert.Contains(t, err.Error(), expectedError)
}

// EqualNormalizedString compare the actual value to the expected value after applying the specified
// transform function. It fails the test if these two transformed string are not equal.
// For example `EqualNormalizedString(t, RemoveSpace, "foo\n", "foo")` wouldn't fail the test as
// spaces (and thus '\n') are removed before comparing the string.
func EqualNormalizedString(t require.TestingT, transformFun func(rune) rune, actual, expected string) {
	require.Equal(t, strings.Map(transformFun, expected), strings.Map(transformFun, actual))
}

// RemoveSpace returns -1 if the specified runes is considered as a space (unicode)
// and the rune itself otherwise.
func RemoveSpace(r rune) rune {
	if unicode.IsSpace(r) {
		return -1
	}
	return r
}
<commit_msg>Remove unused helpers.<commit_after>package testutil

import (
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ErrorContains checks that the error is not nil, and contains the expected
// substring.
func ErrorContains(t require.TestingT, err error, expectedError string) {
	require.Error(t, err)
	assert.Contains(t, err.Error(), expectedError)
}
<|endoftext|>


<commit_before>package mysql

import (
	"context"
	"database/sql"
	"fmt"
	"strconv"
	"sync"

	"time"

	"github.com/go-sql-driver/mysql"
	"github.com/go-xorm/core"
	"github.com/go-xorm/xorm"
	"github.com/grafana/grafana/pkg/components/null"
	"github.com/grafana/grafana/pkg/components/simplejson"
	"github.com/grafana/grafana/pkg/log"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/tsdb"
)

type MysqlExecutor struct {
	datasource *models.DataSource
	engine     *xorm.Engine
	log        log.Logger
}

type engineCacheType struct {
	cache    map[int64]*xorm.Engine
	versions map[int64]int
	sync.Mutex
}

var engineCache = engineCacheType{
	cache:    make(map[int64]*xorm.Engine),
	versions: make(map[int64]int),
}

func init() {
	tsdb.RegisterExecutor("mysql", NewMysqlExecutor)
}

func NewMysqlExecutor(datasource *models.DataSource) (tsdb.Executor, error) {
	executor := &MysqlExecutor{
		datasource: datasource,
		log:        log.New("tsdb.mysql"),
	}

	err := executor.initEngine()
	if err != nil {
		return nil, err
	}

	return executor, nil
}

func (e *MysqlExecutor) initEngine() error {
	engineCache.Lock()
	defer engineCache.Unlock()

	if engine, present := engineCache.cache[e.datasource.Id]; present {
		if version, _ := engineCache.versions[e.datasource.Id]; version == e.datasource.Version {
			e.engine = engine
			return nil
		}
	}

	cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8mb4&parseTime=true&loc=UTC", e.datasource.User, e.datasource.Password, "tcp", e.datasource.Url, e.datasource.Database)
	e.log.Debug("getEngine", "connection", cnnstr)

	engine, err := xorm.NewEngine("mysql", cnnstr)
	engine.SetMaxOpenConns(10)
	engine.SetMaxIdleConns(10)
	if err != nil {
		return err
	}

	engineCache.cache[e.datasource.Id] = engine
	e.engine = engine
	return nil
}

func (e *MysqlExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
	result := &tsdb.BatchResult{
		QueryResults: make(map[string]*tsdb.QueryResult),
	}

	macroEngine := NewMysqlMacroEngine(context.TimeRange)
	session := e.engine.NewSession()
	defer session.Close()
	db := session.DB()

	for _, query := range queries {
		rawSql := query.Model.Get("rawSql").MustString()
		if rawSql == "" {
			continue
		}

		queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: query.RefId}
		result.QueryResults[query.RefId] = queryResult

		rawSql, err := macroEngine.Interpolate(rawSql)
		if err != nil {
			queryResult.Error = err
			continue
		}

		queryResult.Meta.Set("sql", rawSql)

		rows, err := db.Query(rawSql)
		if err != nil {
			queryResult.Error = err
			continue
		}

		defer rows.Close()

		format := query.Model.Get("format").MustString("time_series")

		switch format {
		case "time_series":
			err := e.TransformToTimeSeries(query, rows, queryResult)
			if err != nil {
				queryResult.Error = err
				continue
			}
		case "table":
			err := e.TransformToTable(query, rows, queryResult)
			if err != nil {
				queryResult.Error = err
				continue
			}
		}
	}

	return result
}

func (e MysqlExecutor) TransformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
	columnNames, err := rows.Columns()
	columnCount := len(columnNames)

	if err != nil {
		return err
	}

	table := &tsdb.Table{
		Columns: make([]tsdb.TableColumn, columnCount),
		Rows:    make([]tsdb.RowValues, 0),
	}

	for i, name := range columnNames {
		table.Columns[i].Text = name
	}

	columnTypes, err := rows.ColumnTypes()
	if err != nil {
		return err
	}

	rowLimit := 1000000
	rowCount := 0

	for ; rows.Next(); rowCount += 1 {
		if rowCount > rowLimit {
			return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
		}

		values, err := e.getTypedRowData(columnTypes, rows)
		if err != nil {
			return err
		}

		table.Rows = append(table.Rows, values)
	}

	result.Tables = append(result.Tables, table)
	result.Meta.Set("rowCount", rowCount)
	return nil
}

func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
	values := make([]interface{}, len(types))

	for i, stype := range types {
		switch stype.DatabaseTypeName() {
		case mysql.FieldTypeNameTiny:
			values[i] = new(int8)
		case mysql.FieldTypeNameInt24:
			values[i] = new(int32)
		case mysql.FieldTypeNameShort:
			values[i] = new(int16)
		case mysql.FieldTypeNameVarString:
			values[i] = new(string)
		case mysql.FieldTypeNameVarChar:
			values[i] = new(string)
		case mysql.FieldTypeNameLong:
			values[i] = new(int)
		case mysql.FieldTypeNameLongLong:
			values[i] = new(int64)
		case mysql.FieldTypeNameDouble:
			values[i] = new(float64)
		case mysql.FieldTypeNameDecimal:
			values[i] = new(float32)
		case mysql.FieldTypeNameNewDecimal:
			values[i] = new(float64)
		case mysql.FieldTypeNameTimestamp:
			values[i] = new(time.Time)
		case mysql.FieldTypeNameDateTime:
			values[i] = new(time.Time)
		case mysql.FieldTypeNameTime:
			values[i] = new(time.Duration)
		case mysql.FieldTypeNameYear:
			values[i] = new(int16)
		case mysql.FieldTypeNameNULL:
			values[i] = nil
		default:
			return nil, fmt.Errorf("Database type %s not supported", stype.DatabaseTypeName())
		}
	}

	if err := rows.Scan(values...); err != nil {
		return nil, err
	}

	return values, nil
}

func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
	pointsBySeries := make(map[string]*tsdb.TimeSeries)
	columnNames, err := rows.Columns()

	if err != nil {
		return err
	}

	rowData := NewStringStringScan(columnNames)
	rowLimit := 1000000
	rowCount := 0

	for ; rows.Next(); rowCount += 1 {
		if rowCount > rowLimit {
			return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
		}

		err := rowData.Update(rows.Rows)
		if err != nil {
			e.log.Error("MySQL response parsing", "error", err)
			return fmt.Errorf("MySQL response parsing error %v", err)
		}

		if rowData.metric == "" {
			rowData.metric = "Unknown"
		}

		//e.log.Debug("Rows", "metric", rowData.metric, "time", rowData.time, "value", rowData.value)

		if !rowData.time.Valid {
			return fmt.Errorf("Found row with no time value")
		}

		if series, exist := pointsBySeries[rowData.metric]; exist {
			series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time})
		} else {
			series := &tsdb.TimeSeries{Name: rowData.metric}
			series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time})
			pointsBySeries[rowData.metric] = series
		}
	}

	for _, value := range pointsBySeries {
		result.Series = append(result.Series, value)
	}

	result.Meta.Set("rowCount", rowCount)
	return nil
}

type stringStringScan struct {
	rowPtrs     []interface{}
	rowValues   []string
	columnNames []string
	columnCount int

	time   null.Float
	value  null.Float
	metric string
}

func NewStringStringScan(columnNames []string) *stringStringScan {
	s := &stringStringScan{
		columnCount: len(columnNames),
		columnNames: columnNames,
		rowPtrs:     make([]interface{}, len(columnNames)),
		rowValues:   make([]string, len(columnNames)),
	}

	for i := 0; i < s.columnCount; i++ {
		s.rowPtrs[i] = new(sql.RawBytes)
	}

	return s
}

func (s *stringStringScan) Update(rows *sql.Rows) error {
	if err := rows.Scan(s.rowPtrs...); err != nil {
		return err
	}

	s.time = null.FloatFromPtr(nil)
	s.value = null.FloatFromPtr(nil)

	for i := 0; i < s.columnCount; i++ {
		if rb, ok := s.rowPtrs[i].(*sql.RawBytes); ok {
			s.rowValues[i] = string(*rb)

			switch s.columnNames[i] {
			case "time_sec":
				if sec, err := strconv.ParseInt(s.rowValues[i], 10, 64); err == nil {
					s.time = null.FloatFrom(float64(sec * 1000))
				}
			case "value":
				if value, err := strconv.ParseFloat(s.rowValues[i], 64); err == nil {
					s.value = null.FloatFrom(value)
				}
			case "metric":
				s.metric = s.rowValues[i]
			}

			*rb = nil // reset pointer to discard current value to avoid a bug
		} else {
			return fmt.Errorf("Cannot convert index %d column %s to type *sql.RawBytes", i, s.columnNames[i])
		}
	}
	return nil
}
<commit_msg>mysql: fix for TIME columns, fixes #8534<commit_after>package mysql

import (
	"context"
	"database/sql"
	"fmt"
	"strconv"
	"sync"

	"time"

	"github.com/go-sql-driver/mysql"
	"github.com/go-xorm/core"
	"github.com/go-xorm/xorm"
	"github.com/grafana/grafana/pkg/components/null"
	"github.com/grafana/grafana/pkg/components/simplejson"
	"github.com/grafana/grafana/pkg/log"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/tsdb"
)

type MysqlExecutor struct {
	datasource *models.DataSource
	engine     *xorm.Engine
	log        log.Logger
}

type engineCacheType struct {
	cache    map[int64]*xorm.Engine
	versions map[int64]int
	sync.Mutex
}

var engineCache = engineCacheType{
	cache:    make(map[int64]*xorm.Engine),
	versions: make(map[int64]int),
}

func init() {
	tsdb.RegisterExecutor("mysql", NewMysqlExecutor)
}

func NewMysqlExecutor(datasource *models.DataSource) (tsdb.Executor, error) {
	executor := &MysqlExecutor{
		datasource: datasource,
		log:        log.New("tsdb.mysql"),
	}

	err := executor.initEngine()
	if err != nil {
		return nil, err
	}

	return executor, nil
}

func (e *MysqlExecutor) initEngine() error {
	engineCache.Lock()
	defer engineCache.Unlock()

	if engine, present := engineCache.cache[e.datasource.Id]; present {
		if version, _ := engineCache.versions[e.datasource.Id]; version == e.datasource.Version {
			e.engine = engine
			return nil
		}
	}

	cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8mb4&parseTime=true&loc=UTC", e.datasource.User, e.datasource.Password, "tcp", e.datasource.Url, e.datasource.Database)
	e.log.Debug("getEngine", "connection", cnnstr)

	engine, err := xorm.NewEngine("mysql", cnnstr)
	engine.SetMaxOpenConns(10)
	engine.SetMaxIdleConns(10)
	if err != nil {
		return err
	}

	engineCache.cache[e.datasource.Id] = engine
	e.engine = engine
	return nil
}

func (e *MysqlExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
	result := &tsdb.BatchResult{
		QueryResults: make(map[string]*tsdb.QueryResult),
	}

	macroEngine := NewMysqlMacroEngine(context.TimeRange)
	session := e.engine.NewSession()
	defer session.Close()
	db := session.DB()

	for _, query := range queries {
		rawSql := query.Model.Get("rawSql").MustString()
		if rawSql == "" {
			continue
		}

		queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: query.RefId}
		result.QueryResults[query.RefId] = queryResult

		rawSql, err := macroEngine.Interpolate(rawSql)
		if err != nil {
			queryResult.Error = err
			continue
		}

		queryResult.Meta.Set("sql", rawSql)

		rows, err := db.Query(rawSql)
		if err != nil {
			queryResult.Error = err
			continue
		}

		defer rows.Close()

		format := query.Model.Get("format").MustString("time_series")

		switch format {
		case "time_series":
			err := e.TransformToTimeSeries(query, rows, queryResult)
			if err != nil {
				queryResult.Error = err
				continue
			}
		case "table":
			err := e.TransformToTable(query, rows, queryResult)
			if err != nil {
				queryResult.Error = err
				continue
			}
		}
	}

	return result
}

func (e MysqlExecutor) TransformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
	columnNames, err := rows.Columns()
	columnCount := len(columnNames)

	if err != nil {
		return err
	}

	table := &tsdb.Table{
		Columns: make([]tsdb.TableColumn, columnCount),
		Rows:    make([]tsdb.RowValues, 0),
	}

	for i, name := range columnNames {
		table.Columns[i].Text = name
	}

	columnTypes, err := rows.ColumnTypes()
	if err != nil {
		return err
	}

	rowLimit := 1000000
	rowCount := 0

	for ; rows.Next(); rowCount += 1 {
		if rowCount > rowLimit {
			return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
		}

		values, err := e.getTypedRowData(columnTypes, rows)
		if err != nil {
			return err
		}

		table.Rows = append(table.Rows, values)
	}

	result.Tables = append(result.Tables, table)
	result.Meta.Set("rowCount", rowCount)
	return nil
}

func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
	values := make([]interface{}, len(types))

	for i, stype := range types {
		e.log.Info("type", "type", stype)
		switch stype.DatabaseTypeName() {
		case mysql.FieldTypeNameTiny:
			values[i] = new(int8)
		case mysql.FieldTypeNameInt24:
			values[i] = new(int32)
		case mysql.FieldTypeNameShort:
			values[i] = new(int16)
		case mysql.FieldTypeNameVarString:
			values[i] = new(string)
		case mysql.FieldTypeNameVarChar:
			values[i] = new(string)
		case mysql.FieldTypeNameLong:
			values[i] = new(int)
		case mysql.FieldTypeNameLongLong:
			values[i] = new(int64)
		case mysql.FieldTypeNameDouble:
			values[i] = new(float64)
		case mysql.FieldTypeNameDecimal:
			values[i] = new(float32)
		case mysql.FieldTypeNameNewDecimal:
			values[i] = new(float64)
		case mysql.FieldTypeNameTimestamp:
			values[i] = new(time.Time)
		case mysql.FieldTypeNameDateTime:
			values[i] = new(time.Time)
		case mysql.FieldTypeNameTime:
			values[i] = new(string)
		case mysql.FieldTypeNameYear:
			values[i] = new(int16)
		case mysql.FieldTypeNameNULL:
			values[i] = nil
		default:
			return nil, fmt.Errorf("Database type %s not supported", stype.DatabaseTypeName())
		}
	}

	if err := rows.Scan(values...); err != nil {
		return nil, err
	}

	return values, nil
}

func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
	pointsBySeries := make(map[string]*tsdb.TimeSeries)
	columnNames, err := rows.Columns()

	if err != nil {
		return err
	}

	rowData := NewStringStringScan(columnNames)
	rowLimit := 1000000
	rowCount := 0

	for ; rows.Next(); rowCount += 1 {
		if rowCount > rowLimit {
			return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
		}

		err := rowData.Update(rows.Rows)
		if err != nil {
			e.log.Error("MySQL response parsing", "error", err)
			return fmt.Errorf("MySQL response parsing error %v", err)
		}

		if rowData.metric == "" {
			rowData.metric = "Unknown"
		}

		//e.log.Debug("Rows", "metric", rowData.metric, "time", rowData.time, "value", rowData.value)

		if !rowData.time.Valid {
			return fmt.Errorf("Found row with no time value")
		}

		if series, exist := pointsBySeries[rowData.metric]; exist {
			series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time})
		} else {
			series := &tsdb.TimeSeries{Name: rowData.metric}
			series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time})
			pointsBySeries[rowData.metric] = series
		}
	}

	for _, value := range pointsBySeries {
		result.Series = append(result.Series, value)
	}

	result.Meta.Set("rowCount", rowCount)
	return nil
}

type stringStringScan struct {
	rowPtrs     []interface{}
	rowValues   []string
	columnNames []string
	columnCount int

	time   null.Float
	value  null.Float
	metric string
}

func NewStringStringScan(columnNames []string) *stringStringScan {
	s := &stringStringScan{
		columnCount: len(columnNames),
		columnNames: columnNames,
		rowPtrs:     make([]interface{}, len(columnNames)),
		rowValues:   make([]string, len(columnNames)),
	}

	for i := 0; i < s.columnCount; i++ {
		s.rowPtrs[i] = new(sql.RawBytes)
	}

	return s
}

func (s *stringStringScan) Update(rows *sql.Rows) error {
	if err := rows.Scan(s.rowPtrs...); err != nil {
		return err
	}

	s.time = null.FloatFromPtr(nil)
	s.value = null.FloatFromPtr(nil)

	for i := 0; i < s.columnCount; i++ {
		if rb, ok := s.rowPtrs[i].(*sql.RawBytes); ok {
			s.rowValues[i] = string(*rb)

			switch s.columnNames[i] {
			case "time_sec":
				if sec, err := strconv.ParseInt(s.rowValues[i], 10, 64); err == nil {
					s.time = null.FloatFrom(float64(sec * 1000))
				}
			case "value":
				if value, err := strconv.ParseFloat(s.rowValues[i], 64); err == nil {
					s.value = null.FloatFrom(value)
				}
			case "metric":
				s.metric = s.rowValues[i]
			}

			*rb = nil // reset pointer to discard current value to avoid a bug
		} else {
			return fmt.Errorf("Cannot convert index %d column %s to type *sql.RawBytes", i, s.columnNames[i])
		}
	}
	return nil
}
<|endoftext|>


<commit_before>// Copyright 2017-2020 The Usacloud Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package vdef

import (
	"encoding/json"
	"fmt"
	"reflect"

	"github.com/sacloud/libsacloud/v2/pkg/mapconv"
	"github.com/sacloud/libsacloud/v2/sacloud/ostype"
	"github.com/sacloud/libsacloud/v2/sacloud/types"
)

type definition struct {
	key   interface{}
	value interface{}
}

// definitions usacloudで使う名称(key)/値(value)のペア
var definitions = map[string][]*definition{
	"disk_plan": {
		{key: "ssd", value: types.DiskPlans.SSD},
		{key: "hdd", value: types.DiskPlans.HDD},
	},
	"disk_connection": {
		{key: types.DiskConnections.VirtIO.String(), value: types.DiskConnections.VirtIO.String()},
		{key: types.DiskConnections.IDE.String(), value: types.DiskConnections.IDE.String()},
	},
	"os_type": ostypeDefinition(),
}

func ostypeDefinition() []*definition {
	var def []*definition
	for _, name := range ostype.OSTypeShortNames {
		def = append(def, &definition{key: name, value: ostype.StrToOSType(name)})
	}
	return def
}

func init() {
	registerFunctions()
}

func registerFunctions() {
	// definitionsから各種定義を登録(Note: 同名のものがあった場合は上書き)
	registerConverterFilters()
	registerTemplateFuncMap()
	registerValidators()
	registerCLITagOptions()
}

func registerConverterFilters() {
	for name, defs := range definitions {
		ConverterFilters[name+"_to_value"] = convertFuncToValue(name, defs)
		ConverterFilters[name+"_to_key"] = convertFuncToKey(name, defs)
	}
}

func registerTemplateFuncMap() {
	for name, defs := range definitions {
		TemplateFuncMap[name+"_to_value"] = templateFuncToValue(defs)
		TemplateFuncMap[name+"_to_key"] = templateFuncToKey(defs)
	}
}

func registerValidators() {
	// definitionsの各値からキーを取り出し、"oneof=keyのスペース区切り"というルールを登録する
	for name, defs := range definitions {
		var allows []string
		for _, def := range defs {
			switch s := def.key.(type) {
			case string:
				allows = append(allows, s)
			case fmt.Stringer:
				allows = append(allows, s.String())
			}
		}
		validatorAliases[name] = fmt.Sprintf("oneof=%s", joinWithSpace(allows))
	}
}

func registerCLITagOptions() {
	// definitionsの各値からキーを取り出し、FlagOptionsMapに登録する
	for name, defs := range definitions {
		var allows []string
		for _, def := range defs {
			switch s := def.key.(type) {
			case string:
				allows = append(allows, s)
			case fmt.Stringer:
				allows = append(allows, s.String())
			}
		}
		FlagOptionsMap[name] = allows
	}
}

func convertFuncToValue(defName string, defs []*definition) mapconv.FilterFunc {
	return func(v interface{}) (interface{}, error) {
		var result interface{}
		for _, def := range defs {
			if reflect.DeepEqual(v, def.key) {
				result = def.value
				break
			}
		}
		if result == nil {
			return nil, fmt.Errorf("key %v not found in %s", v, defName)
		}
		return result, nil
	}
}

func convertFuncToKey(defName string, defs []*definition) mapconv.FilterFunc {
	return func(v interface{}) (interface{}, error) {
		var result interface{}
		for _, def := range defs {
			if reflect.DeepEqual(v, def.value) {
				result = def.key
				break
			}
		}
		if result == nil {
			return nil, fmt.Errorf("value %v not found in %s", v, defName)
		}
		return result, nil
	}
}

func templateFuncToValue(defs []*definition) func(interface{}) interface{} {
	return func(raw interface{}) interface{} {
		in := ""
		if v, ok := raw.(json.Number); ok {
			in = v.String()
		}
		var result interface{}
		for _, def := range defs {
			switch ky := def.key.(type) {
			case fmt.Stringer:
				if reflect.DeepEqual(in, ky.String()) {
					result = def.value
					break
				}
			default:
				if reflect.DeepEqual(in, def.key) {
					result = def.value
					break
				}
			}
		}
		return result
	}
}

func templateFuncToKey(defs []*definition) func(interface{}) interface{} {
	return func(raw interface{}) interface{} {
		in := ""
		if v, ok := raw.(json.Number); ok {
			in = v.String()
		}
		var result interface{}
		for _, def := range defs {
			switch val := def.value.(type) {
			case fmt.Stringer:
				if reflect.DeepEqual(in, val.String()) {
					result = def.key
					break
				}
			default:
				if reflect.DeepEqual(in, def.value) {
					result = def.key
					break
				}
			}
		}
		return result
	}
}
<commit_msg>Fixed a bug of value initialization & added scope definition<commit_after>// Copyright 2017-2020 The Usacloud Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package vdef

import (
	"encoding/json"
	"fmt"
	"reflect"

	"github.com/sacloud/libsacloud/v2/pkg/mapconv"
	"github.com/sacloud/libsacloud/v2/sacloud/ostype"
	"github.com/sacloud/libsacloud/v2/sacloud/types"
)

type definition struct {
	key   interface{}
	value interface{}
}

// definitions usacloudで使う名称(key)/値(value)のペア
var definitions = map[string][]*definition{
	"disk_plan": {
		{key: "ssd", value: types.DiskPlans.SSD},
		{key: "hdd", value: types.DiskPlans.HDD},
	},
	"disk_connection": {
		{key: types.DiskConnections.VirtIO.String(), value: types.DiskConnections.VirtIO.String()},
		{key: types.DiskConnections.IDE.String(), value: types.DiskConnections.IDE.String()},
	},
	"scope": {
		{key: types.Scopes.User.String(), value: types.Scopes.User},
		{key: types.Scopes.Shared.String(), value: types.Scopes.Shared},
	},
	"os_type": ostypeDefinition(),
}

func ostypeDefinition() []*definition {
	var def []*definition
	for _, name := range ostype.OSTypeShortNames {
		def = append(def, &definition{key: name, value: ostype.StrToOSType(name)})
	}
	return def
}

func init() {
	registerFunctions()
}

func registerFunctions() {
	// definitionsから各種定義を登録(Note: 同名のものがあった場合は上書き)
	registerConverterFilters()
	registerTemplateFuncMap()
	registerValidators()
	registerCLITagOptions()
}

func registerConverterFilters() {
	for name, defs := range definitions {
		ConverterFilters[name+"_to_value"] = convertFuncToValue(name, defs)
		ConverterFilters[name+"_to_key"] = convertFuncToKey(name, defs)
	}
}

func registerTemplateFuncMap() {
	for name, defs := range definitions {
		TemplateFuncMap[name+"_to_value"] = templateFuncToValue(defs)
		TemplateFuncMap[name+"_to_key"] = templateFuncToKey(defs)
	}
}

func registerValidators() {
	// definitionsの各値からキーを取り出し、"oneof=keyのスペース区切り"というルールを登録する
	for name, defs := range definitions {
		var allows []string
		for _, def := range defs {
			switch s := def.key.(type) {
			case string:
				allows = append(allows, s)
			case fmt.Stringer:
				allows = append(allows, s.String())
			}
		}
		validatorAliases[name] = fmt.Sprintf("oneof=%s", joinWithSpace(allows))
	}
}

func registerCLITagOptions() {
	// definitionsの各値からキーを取り出し、FlagOptionsMapに登録する
	for name, defs := range definitions {
		var allows []string
		for _, def := range defs {
			switch s := def.key.(type) {
			case string:
				allows = append(allows, s)
			case fmt.Stringer:
				allows = append(allows, s.String())
			}
		}
		FlagOptionsMap[name] = allows
	}
}

func convertFuncToValue(defName string, defs []*definition) mapconv.FilterFunc {
	return func(v interface{}) (interface{}, error) {
		var result interface{}
		for _, def := range defs {
			if reflect.DeepEqual(v, def.key) {
				result = def.value
				break
			}
		}
		if result == nil {
			return nil, fmt.Errorf("key %v not found in %s", v, defName)
		}
		return result, nil
	}
}

func convertFuncToKey(defName string, defs []*definition) mapconv.FilterFunc {
	return func(v interface{}) (interface{}, error) {
		var result interface{}
		for _, def := range defs {
			if reflect.DeepEqual(v, def.value) {
				result = def.key
				break
			}
		}
		if result == nil {
			return nil, fmt.Errorf("value %v not found in %s", v, defName)
		}
		return result, nil
	}
}

func templateFuncToValue(defs []*definition) func(interface{}) interface{} {
	return func(raw interface{}) interface{} {
		in := raw
		if v, ok := raw.(json.Number); ok {
			in = v.String()
		}
		var result interface{}
		for _, def := range defs {
			switch ky := def.key.(type) {
			case fmt.Stringer:
				if reflect.DeepEqual(in, ky.String()) {
					result = def.value
					break
				}
			default:
				if reflect.DeepEqual(in, def.key) {
					result = def.value
					break
				}
			}
		}
		return result
	}
}

func templateFuncToKey(defs []*definition) func(interface{}) interface{} {
	return func(raw interface{}) interface{} {
		in := raw
		if v, ok := raw.(json.Number); ok {
			in = v.String()
		}
		var result interface{}
		for _, def := range defs {
			switch val := def.value.(type) {
			case fmt.Stringer:
				if reflect.DeepEqual(in, val.String()) {
					result = def.key
					break
				}
			default:
				if reflect.DeepEqual(in, def.value) {
					result = def.key
					break
				}
			}
		}
		return result
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2015 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

import (
	"fmt"
	"os"
	"path"

	"github.com/golang/glog"
	"k8s.io/kubernetes/pkg/api/v1"
	metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
	storage "k8s.io/kubernetes/pkg/apis/storage/v1beta1"
	clientset "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
	"k8s.io/kubernetes/pkg/util/mount"
)

const readyFileName = "ready"

// IsReady checks for the existence of a regular file
// called 'ready' in the given directory and returns
// true if that file exists.
func IsReady(dir string) bool {
	readyFile := path.Join(dir, readyFileName)
	s, err := os.Stat(readyFile)
	if err != nil {
		return false
	}

	if !s.Mode().IsRegular() {
		glog.Errorf("ready-file is not a file: %s", readyFile)
		return false
	}

	return true
}

// SetReady creates a file called 'ready' in the given
// directory.  It logs an error if the file cannot be
// created.
func SetReady(dir string) {
	if err := os.MkdirAll(dir, 0750); err != nil && !os.IsExist(err) {
		glog.Errorf("Can't mkdir %s: %v", dir, err)
		return
	}

	readyFile := path.Join(dir, readyFileName)
	file, err := os.Create(readyFile)
	if err != nil {
		glog.Errorf("Can't touch %s: %v", readyFile, err)
		return
	}
	file.Close()
}

// UnmountPath is a common unmount routine that unmounts the given path and
// deletes the remaining directory if successful.
func UnmountPath(mountPath string, mounter mount.Interface) error {
	if pathExists, pathErr := PathExists(mountPath); pathErr != nil {
		return fmt.Errorf("Error checking if path exists: %v", pathErr)
	} else if !pathExists {
		glog.Warningf("Warning: Unmount skipped because path does not exist: %v", mountPath)
		return nil
	}

	notMnt, err := mounter.IsLikelyNotMountPoint(mountPath)
	if err != nil {
		return err
	}
	if notMnt {
		glog.Warningf("Warning: %q is not a mountpoint, deleting", mountPath)
		return os.Remove(mountPath)
	}

	// Unmount the mount path
	if err := mounter.Unmount(mountPath); err != nil {
		return err
	}
	notMnt, mntErr := mounter.IsLikelyNotMountPoint(mountPath)
	if mntErr != nil {
		return err
	}
	if notMnt {
		glog.V(4).Infof("%q is unmounted, deleting the directory", mountPath)
		return os.Remove(mountPath)
	}
	return nil
}

// PathExists returns true if the specified path exists.
func PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	} else if os.IsNotExist(err) {
		return false, nil
	} else {
		return false, err
	}
}

// GetSecretForPod locates secret by name in the pod's namespace and returns secret map
func GetSecretForPod(pod *v1.Pod, secretName string, kubeClient clientset.Interface) (map[string]string, error) {
	secret := make(map[string]string)
	if kubeClient == nil {
		return secret, fmt.Errorf("Cannot get kube client")
	}
	secrets, err := kubeClient.Core().Secrets(pod.Namespace).Get(secretName, metav1.GetOptions{})
	if err != nil {
		return secret, err
	}
	for name, data := range secrets.Data {
		secret[name] = string(data)
	}
	return secret, nil
}

// GetSecretForPV locates secret by name and namespace, verifies the secret type, and returns secret map
func GetSecretForPV(secretNamespace, secretName, volumePluginName string, kubeClient clientset.Interface) (map[string]string, error) {
	secret := make(map[string]string)
	if kubeClient == nil {
		return secret, fmt.Errorf("Cannot get kube client")
	}
	secrets, err := kubeClient.Core().Secrets(secretNamespace).Get(secretName, metav1.GetOptions{})
	if err != nil {
		return secret, err
	}
	if secrets.Type != v1.SecretType(volumePluginName) {
		return secret, fmt.Errorf("Cannot get secret of type %s", volumePluginName)
	}
	for name, data := range secrets.Data {
		secret[name] = string(data)
	}
	return secret, nil
}

func GetClassForVolume(kubeClient clientset.Interface, pv *v1.PersistentVolume) (*storage.StorageClass, error) {
	// TODO: replace with a real attribute after beta
	className, found := pv.Annotations["volume.beta.kubernetes.io/storage-class"]
	if !found {
		return nil, fmt.Errorf("Volume has no class annotation")
	}

	class, err := kubeClient.Storage().StorageClasses().Get(className, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}
	return class, nil
}
<commit_msg>Use common unmount util func for TearDownAt()<commit_after>/*
Copyright 2015 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

import (
	"fmt"
	"os"
	"path"

	"github.com/golang/glog"
	"k8s.io/kubernetes/pkg/api/v1"
	metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
	storage "k8s.io/kubernetes/pkg/apis/storage/v1beta1"
	clientset "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
	"k8s.io/kubernetes/pkg/util/mount"
)

const readyFileName = "ready"

// IsReady checks for the existence of a regular file
// called 'ready' in the given directory and returns
// true if that file exists.
func IsReady(dir string) bool {
	readyFile := path.Join(dir, readyFileName)
	s, err := os.Stat(readyFile)
	if err != nil {
		return false
	}

	if !s.Mode().IsRegular() {
		glog.Errorf("ready-file is not a file: %s", readyFile)
		return false
	}

	return true
}

// SetReady creates a file called 'ready' in the given
// directory.  It logs an error if the file cannot be
// created.
func SetReady(dir string) {
	if err := os.MkdirAll(dir, 0750); err != nil && !os.IsExist(err) {
		glog.Errorf("Can't mkdir %s: %v", dir, err)
		return
	}

	readyFile := path.Join(dir, readyFileName)
	file, err := os.Create(readyFile)
	if err != nil {
		glog.Errorf("Can't touch %s: %v", readyFile, err)
		return
	}
	file.Close()
}

// UnmountPath is a common unmount routine that unmounts the given path and
// deletes the remaining directory if successful.
func UnmountPath(mountPath string, mounter mount.Interface) error {
	if pathExists, pathErr := PathExists(mountPath); pathErr != nil {
		return fmt.Errorf("Error checking if path exists: %v", pathErr)
	} else if !pathExists {
		glog.Warningf("Warning: Unmount skipped because path does not exist: %v", mountPath)
		return nil
	}

	notMnt, err := mounter.IsLikelyNotMountPoint(mountPath)
	if err != nil {
		return err
	}
	if notMnt {
		glog.Warningf("Warning: %q is not a mountpoint, deleting", mountPath)
		return os.Remove(mountPath)
	}

	// Unmount the mount path
	if err := mounter.Unmount(mountPath); err != nil {
		return err
	}
	notMnt, mntErr := mounter.IsLikelyNotMountPoint(mountPath)
	if mntErr != nil {
		return err
	}
	if notMnt {
		glog.V(4).Infof("%q is unmounted, deleting the directory", mountPath)
		return os.Remove(mountPath)
	}
	return fmt.Errorf("Failed to unmount path %v", mountPath)
}

// PathExists returns true if the specified path exists.
func PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	} else if os.IsNotExist(err) {
		return false, nil
	} else {
		return false, err
	}
}

// GetSecretForPod locates secret by name in the pod's namespace and returns secret map
func GetSecretForPod(pod *v1.Pod, secretName string, kubeClient clientset.Interface) (map[string]string, error) {
	secret := make(map[string]string)
	if kubeClient == nil {
		return secret, fmt.Errorf("Cannot get kube client")
	}
	secrets, err := kubeClient.Core().Secrets(pod.Namespace).Get(secretName, metav1.GetOptions{})
	if err != nil {
		return secret, err
	}
	for name, data := range secrets.Data {
		secret[name] = string(data)
	}
	return secret, nil
}

// GetSecretForPV locates secret by name and namespace, verifies the secret type, and returns secret map
func GetSecretForPV(secretNamespace, secretName, volumePluginName string, kubeClient clientset.Interface) (map[string]string, error) {
	secret := make(map[string]string)
	if kubeClient == nil {
		return secret, fmt.Errorf("Cannot get kube client")
	}
	secrets, err := kubeClient.Core().Secrets(secretNamespace).Get(secretName, metav1.GetOptions{})
	if err != nil {
		return secret, err
	}
	if secrets.Type != v1.SecretType(volumePluginName) {
		return secret, fmt.Errorf("Cannot get secret of type %s", volumePluginName)
	}
	for name, data := range secrets.Data {
		secret[name] = string(data)
	}
	return secret, nil
}

func GetClassForVolume(kubeClient clientset.Interface, pv *v1.PersistentVolume) (*storage.StorageClass, error) {
	// TODO: replace with a real attribute after beta
	className, found := pv.Annotations["volume.beta.kubernetes.io/storage-class"]
	if !found {
		return nil, fmt.Errorf("Volume has no class annotation")
	}

	class, err := kubeClient.Storage().StorageClasses().Get(className, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}
	return class, nil
}
<|endoftext|>