Commit edd6dce5 authored by Loïck Bonniot's avatar Loïck Bonniot

Merge branch '196_gui_kernel' into 'master'

196 gui kernel

This merge requests is the beginning of the gui component of the DFSS client.

- It adds several makefile instructions to build the gui with docker on linux, but these instructions and the gui code are ignored by the CI
- It adds two screens for the GUI: registration and authentication
- GUI configuration is stored in $HOME/.dfss directory

For testing this MR, please try to:

- Compile the GUI on your computer using the provided docker commands:
  - `make prepare_gui`
  - `make gui`
- Test it
- Open .ui files with QtCreator

See merge request !44
parents 71599f9e e0054132
Pipeline #412 passed with stages
build/embed/VERSION
release/
bin/
*.qrc.go
......@@ -22,7 +22,7 @@ Unit tests:
script:
- "ln -s $(pwd) $GOPATH/src/dfss"
- "./build/deps.sh"
- "cd $GOPATH/src/dfss && go install ./..."
- "cd $GOPATH/src/dfss && rm -rf gui && go install ./..."
- "go test -coverprofile auth.part -v dfss/auth"
- "go test -coverprofile mgdb.part -v dfss/mgdb"
- "go test -coverprofile mails.part -v dfss/mails"
......@@ -53,7 +53,7 @@ Integration tests:
script:
- "ln -s -f $(pwd) $GOPATH/src/dfss"
- "./build/deps.sh"
- "cd $GOPATH/src/dfss && go install ./..."
- "cd $GOPATH/src/dfss && rm -rf gui && go install ./..."
- "go test -v dfss/tests"
Code lint:
......@@ -68,7 +68,7 @@ Code lint:
- "ln -s $(pwd) $GOPATH/src/dfss"
- "go get github.com/alecthomas/gometalinter"
- "./build/deps.sh"
- "cd $GOPATH/src/dfss && go install ./..."
- "cd $GOPATH/src/dfss && rm -rf gui && go install ./..."
- "gometalinter --install"
- "gometalinter -t --deadline=600s -j1 --skip=api --skip=fixtures --disable=aligncheck ./..."
......
......@@ -6,11 +6,37 @@ endif
.PHONY:
install: nocache
go install .
go install ./dfssc
go install ./dfssd
go install ./dfssp
go install ./dfsst
release: clean build_all package
clean:
rm -rf release
# GUI Build (Docker required)
# prepare_gui builds a new container from the goqt image, adding DFSS dependencies for faster builds.
# call it once or after dependency addition.
prepare_gui: nocache
docker run --name dfss-builder -v ${PWD}:/go/src/dfss -w /go/src/dfss lesterpig/goqt /bin/bash -c \
"cp -r ../github.com/visualfc/goqt/bin . ; ./build/deps.sh"
docker commit dfss-builder dfss:builder
docker rm dfss-builder
# gui builds the gui component of the dfss project into a docker container, outputing the result in bin/ directory.
gui: nocache
docker run --rm -v ${PWD}:/go/src/dfss -w /go/src/dfss/gui dfss:builder \
../bin/goqt_rcc -go main -o application.qrc.go application.qrc
docker run --rm -v ${PWD}:/go/src/dfss -w /go/src/dfss/gui dfss:builder \
go build -ldflags "-r ." -o ../bin/gui
# Release internals
build_all:
go get github.com/mitchellh/gox
gox -os "linux darwin windows" -parallel 1 -output "release/dfss_${VERSION}_{{.OS}}_{{.Arch}}/{{.Dir}}" dfss/dfssc dfss/dfssd dfss/dfssp dfss/dfsst
......@@ -30,3 +56,5 @@ protobuf:
protoc --go_out=plugins=grpc:. dfss/dfssd/api/demonstrator.proto && \
protoc --go_out=plugins=grpc:. dfss/dfssp/api/platform.proto && \
protoc --go_out=plugins=grpc:. dfss/dfsst/api/resolution.proto
nocache:
......@@ -12,10 +12,36 @@ Configure workspace
2. Navigate under `$GOPATH/src` and clone this repository
3. At this point, you will be able to install the DFSS project with a simple command, anywhere from your computer:
3. Install build dependencies in `dfss/` directory
```bash
go install dfss/...
dfss/build/deps.sh
```
4. At this point, you will be able to install the DFSS project with some simple commands
- To install CLI applications:
```bash
go install dfss/dfssc # Client
go install dfss/dfssp # Platform
go install dfss/dfsst # TTP
# or
make install
```
- To build GUI for client into `bin/` directory (using docker image)
```
# You may have to run these commands as root due to docker (sudo won't work)
# Prepare docker image, one time only
make prepare_gui
# Build
make gui
```
Run dfss modules
......@@ -23,6 +49,6 @@ Run dfss modules
```bash
dfssc help # Client
dfssp help # Plaform
dfssd help # Demonstrator
dfssp help # Platform
dfsst help # TTP
```
......@@ -21,5 +21,5 @@ func EvaluateErrorCodeResponse(code *api.ErrorCode) error {
if len(code.Message) == 0 {
return errors.New("Received error code " + (code.Code).String())
}
return errors.New("Received error code " + (code.Code).String() + ": " + code.Message)
return errors.New(code.Message)
}
......@@ -46,5 +46,5 @@ func TestEvaluateErrorCodeResponse(t *testing.T) {
}
err = EvaluateErrorCodeResponse(otherWithMessage)
assert.Equal(t, "Received error code INVARG: Invalid mail", err.Error())
assert.Equal(t, "Invalid mail", err.Error())
}
package user
import (
"dfss/dfssc/common"
"dfss/dfssc/security"
pb "dfss/dfssp/api"
"errors"
"io/ioutil"
"regexp"
"time"
"dfss/dfssc/common"
"dfss/dfssc/security"
pb "dfss/dfssp/api"
"errors"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// AuthManager handles the authentication of a user
......@@ -102,11 +103,11 @@ func (m *AuthManager) sendRequest() (*pb.RegisteredUser, error) {
}
// Stop the context if it takes too long for the platform to answer
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
response, err := client.Auth(ctx, request)
if err != nil {
return nil, err
return nil, errors.New(grpc.ErrorDesc(err))
}
return response, nil
......
......@@ -9,6 +9,7 @@ import (
"dfss/dfssc/security"
pb "dfss/dfssp/api"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// RegisterManager handles the registration of a user
......@@ -153,7 +154,7 @@ func (m *RegisterManager) sendRequest(certRequest string) (*pb.ErrorCode, error)
defer cancel()
response, err := client.Register(ctx, request)
if err != nil {
return nil, err
return nil, errors.New(grpc.ErrorDesc(err))
}
return response, nil
......
<RCC>
<qresource prefix="/">
<file>userform/userform.ui</file>
<file>authform/authform.ui</file>
</qresource>
</RCC>
package authform
import (
"dfss/dfssc/user"
"dfss/gui/config"
"github.com/visualfc/goqt/ui"
)
type Widget struct {
*ui.QWidget
}
func NewWidget(conf *config.Config, onAuth func()) *Widget {
file := ui.NewFileWithName(":/authform/authform.ui")
loader := ui.NewUiLoader()
form := loader.Load(file)
tokenField := ui.NewLineEditFromDriver(form.FindChild("tokenField"))
feedbackLabel := ui.NewLabelFromDriver(form.FindChild("feedbackLabel"))
authButton := ui.NewPushButtonFromDriver(form.FindChild("authButton"))
home := config.GetHomeDir()
authButton.OnClicked(func() {
form.SetDisabled(true)
err := user.Authenticate(
home+config.CAFile,
home+config.CertFile,
conf.Platform,
conf.Email,
tokenField.Text(),
)
form.SetDisabled(false)
if err != nil {
feedbackLabel.SetText(err.Error())
tokenField.SetFocus()
tokenField.SelectAll()
} else {
onAuth()
}
})
return &Widget{QWidget: form}
}
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalculatorForm</class>
<widget class="QWidget" name="CalculatorForm">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>592</width>
<height>340</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Calculator Builder</string>
</property>
<layout class="QGridLayout">
<property name="margin">
<number>9</number>
</property>
<property name="spacing">
<number>6</number>
</property>
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="authLabel">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:20pt;&quot;&gt;Authentication&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Please check your mails and copy/paste the authentication token to continue.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<property name="formAlignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="horizontalSpacing">
<number>30</number>
</property>
<property name="verticalSpacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>50</number>
</property>
<property name="rightMargin">
<number>50</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="tokenLabel">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Token&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="tokenField">
<property name="autoFillBackground">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="authButton">
<property name="text">
<string>Authenticate</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="default">
<bool>true</bool>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="feedbackLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
// Package config handles basic configuration store for the GUI only.
// DFSS configuration is stored in the HOME/.dfss/config.json file
package config
import (
"encoding/json"
"io/ioutil"
"os"
"os/user"
"path/filepath"
)
// CAFile is the filename for the root certificate
const CAFile = "ca.pem"
// CertFile is the filename for the user certificate
const CertFile = "cert.pem"
// KeyFile is the filename for the private user key
const KeyFile = "key.pem"
// ConfigFile is the filename for the DFSS configuration file
const ConfigFile = "config.json"
// Config is the structure that will be persisted in the configuration file
type Config struct {
Email string
Platform string
// Virtual-only fields
Registered bool `json:"-"`
Authenticated bool `json:"-"`
}
// Load loads the configuration file into memory.
// If the file does not exist, the configuration will holds default values.
func Load() (conf Config) {
data, err := ioutil.ReadFile(getConfigFilename())
if err != nil {
return
}
_ = json.Unmarshal(data, &conf)
// Fill virtual-only fields
path := GetHomeDir()
conf.Registered = isFileValid(filepath.Join(path, KeyFile))
conf.Authenticated = isFileValid(filepath.Join(path, CertFile))
return
}
// Save stores the current configuration object from memory.
func Save(c Config) {
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return
}
_ = ioutil.WriteFile(getConfigFilename(), data, 0600)
}
// GetHomeDir is a helper to get the .dfss store directory
func GetHomeDir() string {
u, err := user.Current()
if err != nil {
return ""
}
dfssPath := filepath.Join(u.HomeDir, ".dfss")
if err := os.MkdirAll(dfssPath, os.ModeDir|0700); err != nil {
return ""
}
return dfssPath + string(filepath.Separator)
}
func getConfigFilename() string {
return filepath.Join(GetHomeDir(), ConfigFile)
}
func isFileValid(file string) bool {
_, err := os.Stat(file)
return err == nil
}
set GOARCH=386
set CGO_ENABLED=1
..\..\github.com\visualfc\goqt\bin\goqt_rcc.exe -go main -o application.qrc.go application.qrc
go build -v -ldflags "-H windowsgui" -o ..\bin\gui.exe
\ No newline at end of file
package main
import (
"dfss"
"dfss/gui/authform"
"dfss/gui/config"
"dfss/gui/userform"
"github.com/visualfc/goqt/ui"
)
const WIDTH = 650
const HEIGHT = 350
func main() {
// Load configuration
conf := config.Load()
// Start first window
ui.Run(func() {
layout := ui.NewVBoxLayout()
var newuser *userform.Widget
var newauth *authform.Widget
newauth = authform.NewWidget(&conf, func() {
layout.RemoveWidget(newauth)
newauth.Hide()
})
newuser = userform.NewWidget(&conf, func(pwd string) {
layout.RemoveWidget(newuser)
newuser.Hide()
layout.AddWidget(newauth)
})
if conf.Authenticated {
// TODO
} else if conf.Registered {
layout.AddWidget(newauth)
} else {
layout.AddWidget(newuser)
}
w := ui.NewWidget()
w.SetLayout(layout)
w.SetWindowTitle("DFSS Client v" + dfss.Version)
w.SetFixedSizeWithWidthHeight(WIDTH, HEIGHT)
w.Show()
})
}
package userform
import (
"io/ioutil"
"dfss/dfssc/user"
"dfss/gui/config"
"github.com/visualfc/goqt/ui"
)
type Widget struct {
*ui.QWidget
}
func NewWidget(conf *config.Config, onRegistered func(pw string)) *Widget {
file := ui.NewFileWithName(":/userform/userform.ui")
loader := ui.NewUiLoader()
form := loader.Load(file)
emailField := ui.NewLineEditFromDriver(form.FindChild("emailField"))
hostField := ui.NewLineEditFromDriver(form.FindChild("hostField"))
passwordField := ui.NewLineEditFromDriver(form.FindChild("passwordField"))
passwordField.SetEchoMode(ui.QLineEdit_Password)
feedbackLabel := ui.NewLabelFromDriver(form.FindChild("feedbackLabel"))
registerButton := ui.NewPushButtonFromDriver(form.FindChild("registerButton"))
home := config.GetHomeDir()
fileDialog := ui.NewFileDialogWithParentCaptionDirectoryFilter(nil, "Select the CA file for the platform", home, "Root Certificates (*.pem);;Any (*.*)")
// Events
registerButton.OnClicked(func() {
form.SetDisabled(true)
feedbackLabel.SetText("Registration in progress...")
fileDialog.Open()
})
fileDialog.OnFileSelected(func(ca string) {
fileDialog.Hide()
caDest := home + config.CAFile
_ = copyCA(ca, caDest)
err := user.Register(
caDest,
home+config.CertFile,
home+config.KeyFile,
hostField.Text(),
passwordField.Text(),
"", "", "", emailField.Text(), 2048,
)
if err != nil {
feedbackLabel.SetText(err.Error())
} else {
conf.Email = emailField.Text()
conf.Platform = hostField.Text()
onRegistered(passwordField.Text())
config.Save(*conf)
}
form.SetDisabled(false)
})
fileDialog.OnRejected(func() {
form.SetDisabled(false)
feedbackLabel.SetText("Registration aborted.")
})
return &Widget{QWidget: form}
}
func copyCA(from string, to string) error {
if from == to {
return nil
}
file, err := ioutil.ReadFile(from)
if err != nil {
return err
}
return ioutil.WriteFile(to, file, 0600)
}
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalculatorForm</class>
<widget class="QWidget" name="CalculatorForm">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>408</width>
<height>316</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Calculator Builder</string>
</property>
<layout class="QGridLayout">
<property name="margin">
<number>9</number>
</property>
<property name="spacing">
<number>6</number>
</property>
<item row="0" column="0">