mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-11-05 18:34:55 +00:00
Added a fallback keyring driver; fixed minor bugs
This commit is contained in:
parent
f2f59b3c2b
commit
394535faff
42
internal/keyring/driver/driver.go
Normal file
42
internal/keyring/driver/driver.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package driver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
Get(service string, v interface{}) error
|
||||
Set(service string, v interface{}) error
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
providers []Provider
|
||||
}
|
||||
|
||||
func NewStore(providers ...Provider) Store {
|
||||
return Store{providers}
|
||||
}
|
||||
|
||||
func (s Store) Get(service string, v interface{}) error {
|
||||
for _, provider := range s.providers {
|
||||
if err := provider.Get(service, v); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
log.Info(err)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("service %s not found in keyring services.", service)
|
||||
}
|
||||
|
||||
func (s Store) Set(service string, v interface{}) error {
|
||||
for _, provider := range s.providers {
|
||||
if err := provider.Set(service, v); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
log.Info(err)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to set keyring for service %s", service)
|
||||
}
|
||||
109
internal/keyring/driver/json/json.go
Normal file
109
internal/keyring/driver/json/json.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrUnsafePermission = errors.New("secrets.json file has unsafe permission")
|
||||
|
||||
type Provider struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
func NewProvider(dir string) Provider {
|
||||
return Provider{dir}
|
||||
}
|
||||
|
||||
func (p Provider) open(service string, write bool) (*os.File, error) {
|
||||
var flags int
|
||||
if write {
|
||||
// If file does not exist, then create a new one. Else, erase the file.
|
||||
// The file can only be written onto.
|
||||
flags = os.O_CREATE | os.O_WRONLY | os.O_TRUNC
|
||||
} else {
|
||||
// If file does not exist, then error out. The file can only be read
|
||||
// from.
|
||||
flags = os.O_RDONLY
|
||||
}
|
||||
|
||||
// Make a filename using the given service.
|
||||
var filename = fmt.Sprintf("%s_secret.json", SanitizeName(service))
|
||||
|
||||
f, err := os.OpenFile(filepath.Join(p.dir, filename), flags, 0600)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to open file")
|
||||
}
|
||||
|
||||
// Stat the file and verify that the permissions are safe.
|
||||
s, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to stat file")
|
||||
}
|
||||
if m := s.Mode(); m != 0600 {
|
||||
return nil, fmt.Errorf("secrets.json file has unsafe permission %06o", m)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Get unmarshals the service from the JSON secret file. It errors out if the
|
||||
// secret file does not exist.
|
||||
func (p Provider) Get(service string, v interface{}) error {
|
||||
f, err := p.open(service, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := json.NewDecoder(f).Decode(v); err != nil {
|
||||
return errors.Wrap(err, "Failed to decode JSON")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set writes the service's data into a JSON secret file.
|
||||
func (p Provider) Set(service string, v interface{}) error {
|
||||
f, err := p.open(service, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Create a new formatted encoder.
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", "\n")
|
||||
|
||||
// Encode using created encoder.
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return errors.Wrap(err, "Failed to encode JSON")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitizeName sanitizes the name so that it's safe to use as a filename.
|
||||
func SanitizeName(name string) string {
|
||||
// Escape all weird characters in a filename.
|
||||
name = strings.Map(underscoreNonAlphanum, name)
|
||||
|
||||
// Lower-case everything.
|
||||
name = strings.ToLower(name)
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func underscoreNonAlphanum(r rune) rune {
|
||||
// especially does not cover slashes
|
||||
if unicode.IsLetter(r) || unicode.IsNumber(r) {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}
|
||||
36
internal/keyring/driver/keyring/keyring.go
Normal file
36
internal/keyring/driver/keyring/keyring.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package keyring
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/keyring/driver"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
type Provider struct{}
|
||||
|
||||
var _ driver.Provider = (*Provider)(nil)
|
||||
|
||||
func NewProvider() driver.Provider {
|
||||
return Provider{}
|
||||
}
|
||||
|
||||
func (Provider) Get(service string, v interface{}) error {
|
||||
s, err := keyring.Get("cchat-gtk", service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gob.NewDecoder(strings.NewReader(s)).Decode(v)
|
||||
}
|
||||
|
||||
func (Provider) Set(service string, v interface{}) error {
|
||||
var b bytes.Buffer
|
||||
if err := gob.NewEncoder(&b).Encode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return keyring.Set("cchat-gtk", service, b.String())
|
||||
}
|
||||
|
|
@ -1,37 +1,21 @@
|
|||
package keyring
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/keyring/driver"
|
||||
"github.com/diamondburned/cchat-gtk/internal/keyring/driver/json"
|
||||
"github.com/diamondburned/cchat-gtk/internal/keyring/driver/keyring"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
func get(service string, v interface{}) error {
|
||||
s, err := keyring.Get("cchat-gtk", service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Deleting immediately does not work on a successful start-up.
|
||||
// keyring.Delete("cchat-gtk", service)
|
||||
|
||||
return gob.NewDecoder(strings.NewReader(s)).Decode(v)
|
||||
}
|
||||
|
||||
func set(service string, v interface{}) error {
|
||||
var b bytes.Buffer
|
||||
if err := gob.NewEncoder(&b).Encode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return keyring.Set("cchat-gtk", service, b.String())
|
||||
}
|
||||
// Declare a keyring store with fallbacks.
|
||||
var store = driver.NewStore(
|
||||
keyring.NewProvider(),
|
||||
json.NewProvider(config.DirPath()), // fallback
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
ID string
|
||||
|
|
@ -65,7 +49,7 @@ func ConvertSession(ses cchat.Session, name string) *Session {
|
|||
}
|
||||
|
||||
func SaveSessions(serviceName text.Rich, sessions []Session) {
|
||||
if err := set(serviceName.Content, sessions); err != nil {
|
||||
if err := store.Set(serviceName.Content, sessions); err != nil {
|
||||
log.Warn(errors.Wrap(err, "Error saving session"))
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +58,7 @@ func SaveSessions(serviceName text.Rich, sessions []Session) {
|
|||
// calls the auth callback inside the Gtk main thread.
|
||||
func RestoreSessions(serviceName text.Rich) (sessions []Session) {
|
||||
// Ignore the error, it's not important.
|
||||
if err := get(serviceName.Content, &sessions); err != nil {
|
||||
if err := store.Get(serviceName.Content, &sessions); err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
return
|
||||
|
|
|
|||
|
|
@ -50,8 +50,13 @@ func Error(err error) {
|
|||
Write("Error: " + err.Error())
|
||||
}
|
||||
|
||||
// Warn calls Info().
|
||||
func Warn(err error) {
|
||||
Write("Warn: " + err.Error())
|
||||
Info(err)
|
||||
}
|
||||
|
||||
func Info(err error) {
|
||||
Write("Info: " + err.Error())
|
||||
}
|
||||
|
||||
func Write(msg string) {
|
||||
|
|
|
|||
|
|
@ -84,10 +84,13 @@ func Sections() (sects [sectionLen][]Entry) {
|
|||
return
|
||||
}
|
||||
|
||||
// Save the global config.
|
||||
func Save() error {
|
||||
return MarshalToFile(ConfigFile, sections)
|
||||
}
|
||||
|
||||
// Restore the global config. IsNotExist is not an error and will not be
|
||||
// returned.
|
||||
func Restore() error {
|
||||
return UnmarshalFromFile(ConfigFile, §ions)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,19 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var DirPath string
|
||||
// dirPath indicates the path to the config. This variable is created when
|
||||
// __init is called.
|
||||
var dirPath string
|
||||
|
||||
func init() {
|
||||
// Singleton to initialize the config directories once.
|
||||
var __initonce sync.Once
|
||||
|
||||
func __init() {
|
||||
// Load the config dir:
|
||||
d, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
|
|
@ -19,16 +25,26 @@ func init() {
|
|||
}
|
||||
|
||||
// Fill Path:
|
||||
DirPath = filepath.Join(d, "cchat-gtk")
|
||||
dirPath = filepath.Join(d, "cchat-gtk")
|
||||
|
||||
// Ensure it exists:
|
||||
if err := os.Mkdir(DirPath, 0755|os.ModeDir); err != nil && !os.IsExist(err) {
|
||||
if err := os.Mkdir(dirPath, 0755|os.ModeDir); err != nil && !os.IsExist(err) {
|
||||
log.Fatalln("Failed to make config dir:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DirPath returns the config directory.
|
||||
func DirPath() string {
|
||||
// Ensure that files and folders are initialized.
|
||||
__initonce.Do(__init)
|
||||
|
||||
return dirPath
|
||||
}
|
||||
|
||||
// MarshalToFile marshals the given interface into the given filename. The
|
||||
// filename will be prepended with the config directory.
|
||||
func MarshalToFile(file string, from interface{}) error {
|
||||
file = filepath.Join(DirPath, file)
|
||||
file = filepath.Join(DirPath(), file)
|
||||
|
||||
f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_SYNC|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
|
|
@ -46,8 +62,11 @@ func MarshalToFile(file string, from interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalFromFile unmarshals the given filename to the given interface. The
|
||||
// filename will be prepended with the config directory. IsNotExist errors are
|
||||
// ignored.
|
||||
func UnmarshalFromFile(file string, to interface{}) error {
|
||||
file = filepath.Join(DirPath, file)
|
||||
file = filepath.Join(DirPath(), file)
|
||||
|
||||
f, err := os.OpenFile(file, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ func NewEmptyContainer() *GenericContainer {
|
|||
content.SetSelectable(true)
|
||||
content.Show()
|
||||
|
||||
// Causes bugs with selections.
|
||||
|
||||
// content.Connect("grab-notify", func(l *gtk.Label, grabbed bool) {
|
||||
// if grabbed {
|
||||
// // Hack to stop the label from selecting everything after being
|
||||
|
|
|
|||
Loading…
Reference in a new issue