Skip to content

Commit

Permalink
Feature/password files (#24)
Browse files Browse the repository at this point in the history
* read default repository passwords from RESTIC_PASSWORD_FILE
* allow reading passwords in location-dialog from a file
  • Loading branch information
emuell authored Aug 22, 2022
1 parent b251c2f commit b2f701b
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 63 deletions.
66 changes: 65 additions & 1 deletion app.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bytes"
"context"
"fmt"
"io/ioutil"
Expand All @@ -10,7 +11,9 @@ import (
"restic-browser/backend/open"
"restic-browser/backend/restic"

"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/text/encoding/unicode"
)

type ResticBrowserApp struct {
Expand Down Expand Up @@ -104,9 +107,45 @@ func (r *ResticBrowserApp) OpenFileOrUrl(path string) error {
return open.OpenFileOrURL(path)
}

// read a text file, skipping BOM headers if present
func readTextFile(filename string) ([]byte, error) {
var (
bomUTF8 = []byte{0xef, 0xbb, 0xbf}
bomUTF16BigEndian = []byte{0xfe, 0xff}
bomUTF16LittleEndian = []byte{0xff, 0xfe}
)
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
if bytes.HasPrefix(data, bomUTF8) {
return data[len(bomUTF8):], nil
}
if !bytes.HasPrefix(data, bomUTF16BigEndian) && !bytes.HasPrefix(data, bomUTF16LittleEndian) {
return data, nil
}
// UseBom means automatic endianness selection
e := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
return e.NewDecoder().Bytes(data)
}

// defaultRepoPassword determines the default restic repository password from the environment.
func defaultRepoPassword() (string, error) {
passwordFile := os.Getenv("RESTIC_PASSWORD_FILE")
if passwordFile != "" {
s, err := readTextFile(passwordFile)
if errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf("%s does not exist", passwordFile)
}
return strings.TrimSpace(strings.Replace(string(s), "\n", "", -1)), err
}
password := os.Getenv("RESTIC_PASSWORD")
return password, nil
}

func (r *ResticBrowserApp) DefaultRepoLocation() restic.Location {
location := restic.Location{}
location.Password = os.Getenv("RESTIC_PASSWORD")
location.Password, _ = defaultRepoPassword()
repo := os.Getenv("RESTIC_REPOSITORY")
if len(repo) == 0 {
return location
Expand Down Expand Up @@ -140,6 +179,31 @@ func (r *ResticBrowserApp) DefaultRepoLocation() restic.Location {
return location
}

func (r *ResticBrowserApp) ReadPasswordFromFile() (string, error) {
options := runtime.OpenDialogOptions{
DefaultDirectory: "",
DefaultFilename: "",
Title: "Please select a password file",
Filters: []runtime.FileFilter{},
ShowHiddenFiles: true,
CanCreateDirectories: false,
ResolvesAliases: true,
TreatPackagesAsDirectories: true,
}
filename, err := runtime.OpenFileDialog(*r.context, options)
if err == nil && filename == "" {
return "", nil
}
if err != nil {
return "", fmt.Errorf("failed to show dialog: %s", err.Error())
}
bytes, err := readTextFile(filename)
if err != nil {
return "", fmt.Errorf("failed to read file: %s", err.Error())
}
return strings.TrimSpace(strings.Replace(string(bytes), "\n", "", -1)), nil
}

func (r *ResticBrowserApp) SelectLocalRepo() (string, error) {
options := runtime.OpenDialogOptions{
DefaultDirectory: "",
Expand Down
41 changes: 31 additions & 10 deletions frontend/src/components/location-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as mobx from 'mobx';
import { appState } from '../states/app-state';
import { locationInfos, RepositoryType, Location } from '../states/location';

import { SelectLocalRepo } from '../../wailsjs/go/main/ResticBrowserApp';
import { SelectLocalRepo, ReadPasswordFromFile } from '../../wailsjs/go/main/ResticBrowserApp';

import { Notification } from '@vaadin/notification';

Expand Down Expand Up @@ -44,6 +44,7 @@ export class ResticBrowserLocationDialog extends MobxLitElement {
this._location.setFromOtherLocation(appState.repoLocation);
// bind this to callbacks
this._browseLocalRepositoryPath = this._browseLocalRepositoryPath.bind(this);
this._readRepositoryPasswordFile = this._readRepositoryPasswordFile.bind(this);
}

private _browseLocalRepositoryPath() {
Expand All @@ -61,6 +62,19 @@ export class ResticBrowserLocationDialog extends MobxLitElement {
});
}

private _readRepositoryPasswordFile() {
ReadPasswordFromFile()
.then(mobx.action((password) => {
this._location.password = password;
}))
.catch((err) => {
Notification.show(`Invalid password file: '${err.message || err}'`, {
position: 'bottom-center',
theme: "error"
});
});
}

private _handleClose() {
appState.repoLocation.setFromOtherLocation(this._location);
this._handledClose = true;
Expand All @@ -85,7 +99,7 @@ export class ResticBrowserLocationDialog extends MobxLitElement {
})}
></vaadin-select>
<vaadin-horizontal-layout style="width: 24rem">
<vaadin-text-field style="width: 100%; margin-right: 4px"
<vaadin-text-field style="width: 100%; margin-right: 4px;"
label=${this._location.type === "local"
? "Path" : (["sftp", "rest"].includes(this._location.type)) ? "URL" : "Bucket"}
required
Expand Down Expand Up @@ -113,14 +127,21 @@ export class ResticBrowserLocationDialog extends MobxLitElement {
})}>
</vaadin-password-field>
`)}
<vaadin-password-field
label="Repository Password"
required
value=${this._location.password}
@change=${mobx.action((event: CustomEvent) => {
this._location.password = (event.target as HTMLInputElement).value;
})}
></vaadin-password-field>
<vaadin-horizontal-layout style="width: 24rem">
<vaadin-password-field
style="width: 100%; margin-right: 4px;"
label="Repository Password"
required
value=${this._location.password}
@change=${mobx.action((event: CustomEvent) => {
this._location.password = (event.target as HTMLInputElement).value;
})}
>
</vaadin-password-field>
<vaadin-button theme="primary" style="width: 4rem; margin-top: 35.5px;"
@click=${this._readRepositoryPasswordFile}>Read</vaadin-button>
</vaadin-horizontal-layout>
</vaadin-vertical-layout>
`;

Expand Down
2 changes: 2 additions & 0 deletions frontend/wailsjs/go/main/ResticBrowserApp.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export function OpenFileOrUrl(arg1:string):Promise<Error>;

export function OpenRepo(arg1:restic.Location):Promise<Array<restic.Snapshot>>;

export function ReadPasswordFromFile():Promise<string>;

export function RestoreFile(arg1:string,arg2:restic.File):Promise<string>;

export function SelectLocalRepo():Promise<string>;
4 changes: 4 additions & 0 deletions frontend/wailsjs/go/main/ResticBrowserApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export function OpenRepo(arg1) {
return window['go']['main']['ResticBrowserApp']['OpenRepo'](arg1);
}

export function ReadPasswordFromFile() {
return window['go']['main']['ResticBrowserApp']['ReadPasswordFromFile']();
}

export function RestoreFile(arg1, arg2) {
return window['go']['main']['ResticBrowserApp']['RestoreFile'](arg1, arg2);
}
Expand Down
100 changes: 50 additions & 50 deletions frontend/wailsjs/go/models.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,5 @@
export namespace restic {

export class EnvValue {
name: string;
value: string;

static createFrom(source: any = {}) {
return new EnvValue(source);
}

constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.value = source["value"];
}
}
export class Location {
prefix: string;
path: string;
credentials: EnvValue[];
password: string;

static createFrom(source: any = {}) {
return new Location(source);
}

constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.prefix = source["prefix"];
this.path = source["path"];
this.credentials = this.convertValues(source["credentials"], EnvValue);
this.password = source["password"];
}

convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class File {
name: string;
type: string;
Expand Down Expand Up @@ -104,6 +54,56 @@ export namespace restic {
this.username = source["username"];
}
}
export class EnvValue {
name: string;
value: string;

static createFrom(source: any = {}) {
return new EnvValue(source);
}

constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.value = source["value"];
}
}
export class Location {
prefix: string;
path: string;
credentials: EnvValue[];
password: string;

static createFrom(source: any = {}) {
return new Location(source);
}

constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.prefix = source["prefix"];
this.path = source["path"];
this.credentials = this.convertValues(source["credentials"], EnvValue);
this.password = source["password"];
}

convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}

}

4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ go 1.17

require (
github.com/leaanthony/sail v0.3.0
github.com/pkg/errors v0.9.1
github.com/wailsapp/wails/v2 v2.0.0-beta.42
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f
golang.org/x/text v0.3.7
)

require (
Expand All @@ -22,13 +24,11 @@ require (
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/tkrajina/go-reflector v0.5.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/net v0.0.0-20220725212005-46097bf591d3 // indirect
golang.org/x/text v0.3.7 // indirect
)

0 comments on commit b2f701b

Please sign in to comment.