login-app: Use a pure-Go tty interface instead of curses

This commit is contained in:
nemunaire 2021-02-16 21:18:56 +01:00
parent af53a37d33
commit 8ab758ac9a
8 changed files with 372 additions and 294 deletions

View File

@ -1,10 +1,11 @@
tuto1: token-validator/token-validator server.iso
login-app: pkg/login-app/build.yml pkg/login-app/Dockerfile pkg/login-app/cmd/login.go pkg/login-app/cmd/main.go pkg/login-app/cmd/windows.go
pkg/login-app: pkg/login-app/cmd/login.go pkg/login-app/cmd/dialog-checklogin.go pkg/login-app/cmd/cmd pkg/login-app/cmd/dialog-login.go pkg/login-app/cmd/login-app pkg/login-app/cmd/dialog-errmsg.go pkg/login-app/cmd/main.go pkg/login-app/cmd/dialog-reboot.go pkg/login-app/cmd/debug.log pkg/login-app/build.yml pkg/login-app/Dockerfile
linuxkit pkg build -org nemunaire pkg/login-app/
#linuxkit pkg push -org nemunaire --sign=false pkg/login-app/
touch pkg/login-app
login-initrd.img: login.yml login-app
login-initrd.img: login.yml pkg/login-app
linuxkit build -docker $<
token-validator/token-validator: token-validator/*.go

View File

@ -3,14 +3,14 @@ FROM golang:alpine as gobuild
ENV GOOS linux
ENV GOARCH amd64
RUN apk add --no-cache git pkgconfig ncurses-dev ncurses-static gcc libc-dev
RUN apk add --no-cache git gcc
WORKDIR /go/src/login-app
ADD cmd ./
RUN go get -d -v
RUN go build -v -tags netgo -ldflags '-w -extldflags "-static -lpanelw -lncursesw"'
RUN go build -v -tags netgo
FROM alpine
@ -19,7 +19,6 @@ MAINTAINER Pierre-Olivier Mercier <nemunaire@nemunai.re>
EXPOSE 8081
COPY --from=gobuild /go/src/login-app/login-app /bin/login-app
COPY --from=gobuild /etc/terminfo/l/linux /etc/terminfo/l/linux
COPY --from=gobuild /usr/share/udhcpc/default.script /usr/share/udhcpc/default.script
ENTRYPOINT ["/bin/login-app"]

View File

@ -0,0 +1,79 @@
package main
import (
"math"
"time"
ui "github.com/VladimirMarkelov/clui"
)
type CheckDialog struct {
View *ui.Window
result int
beforeClose func(ui.Event)
onClose func()
}
func CreateCheckDialog(title, username, password string) *CheckDialog {
dlg := new(CheckDialog)
sWidth, sHeight := ui.ScreenSize()
wWidth, wHeight := 40, 5
dlg.View = ui.AddWindow(sWidth/2-wWidth/2, sHeight/2-wHeight/2, wWidth, wHeight, title)
ui.WindowManager().BeginUpdate()
defer ui.WindowManager().EndUpdate()
dlg.View.SetModal(true)
dlg.View.SetSizable(false)
dlg.View.SetTitleButtons(ui.ButtonDefault)
dlg.View.SetPack(ui.Vertical)
textfrm := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, ui.Fixed)
textfrm.SetPaddings(1, 1)
textfrm.SetPack(ui.Vertical)
textfrm.SetGaps(2, 1)
ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, " Please wait", ui.Fixed)
progress := ui.CreateProgressBar(textfrm, ui.AutoSize, ui.AutoSize, 1)
progress.SetLimits(0, 100)
ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, "Connecting to login server...", ui.Fixed)
dlg.View.OnKeyDown(func(ev ui.Event, data interface{}) bool {
if ev.Key == 0 && ev.Msg == "login-complete" {
if dlg.beforeClose != nil {
dlg.beforeClose(ev)
}
ui.WindowManager().DestroyWindow(dlg.View)
ui.WindowManager().BeginUpdate()
closeFunc := dlg.onClose
ui.WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
return true
}
return false
}, nil)
go func() {
if ok, err := checkLogin(username, password); ok {
ui.PutEvent(ui.Event{Type: ui.EventKey, Msg: "login-complete"})
} else {
ui.PutEvent(ui.Event{Type: ui.EventKey, Msg: "login-complete", Err: err})
}
}()
go func() {
for i := 0; i < 422; i += 1 {
progress.SetValue(int(math.Floor(math.Log(float64(i)*8)*16 - 30)))
time.Sleep(64 * time.Millisecond)
ui.PutEvent(ui.Event{Type: ui.EventRedraw})
}
}()
return dlg
}

View File

@ -0,0 +1,85 @@
package main
import (
ui "github.com/VladimirMarkelov/clui"
term "github.com/nsf/termbox-go"
)
type ErrMsgDialog struct {
View *ui.Window
result int
beforeClose func()
onClose func()
}
func CreateErrMsgDialog(title string, err error) *ErrMsgDialog {
dlg := new(ErrMsgDialog)
sWidth, sHeight := ui.ScreenSize()
wWidth, wHeight := 60, 10
dlg.View = ui.AddWindow(sWidth/2-wWidth/2, sHeight/2-wHeight/2, wWidth, wHeight, title)
ui.WindowManager().BeginUpdate()
defer ui.WindowManager().EndUpdate()
dlg.View.SetModal(true)
dlg.View.SetSizable(false)
dlg.View.SetTitleButtons(ui.ButtonDefault)
dlg.View.SetPack(ui.Vertical)
textfrm := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, ui.Fixed)
textfrm.SetPaddings(1, 1)
textfrm.SetPack(ui.Vertical)
textfrm.SetGaps(2, 1)
lbl := ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, "An error occurs:", ui.Fixed)
lbl.SetTextColor(ui.ColorRedBold)
tv := ui.CreateTextView(textfrm, ui.AutoSize, 4, ui.Fixed)
tv.SetWordWrap(true)
tv.SetText([]string{err.Error()})
tv.SetTextColor(ui.ColorWhite)
tv.SetBackColor(ui.ColorBlack)
ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, "Please try again.", ui.Fixed)
btnOk := ui.CreateButton(textfrm, 20, 4, "Ok", 1)
btnOk.OnClick(func(ev ui.Event) {
if dlg.beforeClose != nil {
dlg.beforeClose()
}
ui.WindowManager().DestroyWindow(dlg.View)
ui.WindowManager().BeginUpdate()
closeFunc := dlg.onClose
ui.WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
})
dlg.View.OnKeyDown(func(ev ui.Event, data interface{}) bool {
if ev.Key == term.KeyEnter {
if dlg.beforeClose != nil {
dlg.beforeClose()
}
ui.WindowManager().DestroyWindow(dlg.View)
ui.WindowManager().BeginUpdate()
closeFunc := dlg.onClose
ui.WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
return true
}
return false
}, nil)
ui.ActivateControl(dlg.View, btnOk)
return dlg
}

View File

@ -0,0 +1,124 @@
package main
import (
ui "github.com/VladimirMarkelov/clui"
term "github.com/nsf/termbox-go"
)
const (
LoginOk = iota
LoginCanceled
LoginInvalid
)
type LoginDialog struct {
View *ui.Window
Username string
Password string
Action int
result int
beforeClose func()
onClose func()
onCheck func(string, string) bool
}
func CreateLoginDialog(title string) *LoginDialog {
dlg := new(LoginDialog)
sWidth, sHeight := ui.ScreenSize()
wWidth, wHeight := 40, 15
dlg.View = ui.AddWindow(sWidth/2-wWidth/2, sHeight/2-wHeight/2, wWidth, wHeight, title)
ui.WindowManager().BeginUpdate()
defer ui.WindowManager().EndUpdate()
dlg.View.SetModal(true)
dlg.View.SetSizable(false)
dlg.View.SetTitleButtons(ui.ButtonDefault)
dlg.View.SetPack(ui.Vertical)
userfrm := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, ui.Fixed)
userfrm.SetPaddings(1, 1)
userfrm.SetPack(ui.Horizontal)
userfrm.SetGaps(1, 0)
ui.CreateLabel(userfrm, ui.AutoSize, ui.AutoSize, " Login", ui.Fixed)
edUser := ui.CreateEditField(userfrm, 20, "", 1)
passfrm := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, 1)
passfrm.SetPaddings(1, 1)
passfrm.SetPack(ui.Horizontal)
passfrm.SetGaps(1, 0)
ui.CreateLabel(passfrm, ui.AutoSize, ui.AutoSize, "Password", ui.Fixed)
edPass := ui.CreateEditField(passfrm, 20, "", 1)
edPass.SetPasswordMode(true)
filler := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, 1)
filler.SetPack(ui.Horizontal)
lbRes := ui.CreateLabel(filler, ui.AutoSize, ui.AutoSize, "", 1)
blist := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, ui.Fixed)
blist.SetPack(ui.Horizontal)
blist.SetPaddings(1, 1)
btnOk := ui.CreateButton(blist, 20, 4, "Authenticate me", 1)
btnOk.OnClick(func(ev ui.Event) {
if dlg.onCheck != nil && !dlg.onCheck(edUser.Title(), edPass.Title()) {
lbRes.SetTitle("Invalid username or password")
dlg.Action = LoginInvalid
return
}
dlg.Action = LoginOk
if dlg.onCheck == nil {
dlg.Username = edUser.Title()
dlg.Password = edPass.Title()
}
if dlg.beforeClose != nil {
dlg.beforeClose()
}
ui.WindowManager().DestroyWindow(dlg.View)
ui.WindowManager().BeginUpdate()
closeFunc := dlg.onClose
ui.WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
})
dlg.View.OnKeyDown(func(ev ui.Event, data interface{}) bool {
if ev.Key == term.KeyEnter {
if edUser.Title() == "" {
ui.ActivateControl(dlg.View, edUser)
} else if edPass.Title() == "" {
ui.ActivateControl(dlg.View, edPass)
} else {
if dlg.beforeClose != nil {
dlg.beforeClose()
}
ui.WindowManager().DestroyWindow(dlg.View)
ui.WindowManager().BeginUpdate()
closeFunc := dlg.onClose
ui.WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
}
return true
}
return false
}, nil)
edUser.OnChange(func(ev ui.Event) {
lbRes.SetTitle("")
})
edPass.OnChange(func(ev ui.Event) {
lbRes.SetTitle("")
})
ui.ActivateControl(dlg.View, edUser)
return dlg
}

View File

@ -0,0 +1,50 @@
package main
import (
"time"
ui "github.com/VladimirMarkelov/clui"
)
type RebootDialog struct {
View *ui.Window
}
func CreateRebootDialog(title, login string) *RebootDialog {
dlg := new(RebootDialog)
sWidth, sHeight := ui.ScreenSize()
wWidth, wHeight := 50, 10
dlg.View = ui.AddWindow(sWidth/2-wWidth/2, sHeight/2-wHeight/2, wWidth, wHeight, title)
ui.WindowManager().BeginUpdate()
defer ui.WindowManager().EndUpdate()
dlg.View.SetModal(true)
dlg.View.SetSizable(false)
dlg.View.SetTitleButtons(ui.ButtonDefault)
dlg.View.SetPack(ui.Vertical)
textfrm := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, ui.Fixed)
textfrm.SetPaddings(1, 1)
textfrm.SetPack(ui.Vertical)
textfrm.SetGaps(2, 1)
ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, "You are now successfully logged in as:", ui.Fixed)
ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, login, ui.Fixed)
ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, "Your computer will automatically", ui.Fixed)
ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, "reboot in a few seconds...", ui.Fixed)
progress := ui.CreateProgressBar(textfrm, ui.AutoSize, ui.AutoSize, 1)
progress.SetLimits(0, 100)
go func() {
for i := 0; i < 100; i += 1 {
progress.SetValue(i)
ui.PutEvent(ui.Event{Type: ui.EventRedraw})
time.Sleep(64 * time.Millisecond)
}
ui.WindowManager().DestroyWindow(dlg.View)
}()
return dlg
}

View File

@ -1,58 +1,47 @@
package main
import (
gc "github.com/rthornton128/goncurses"
"os"
ui "github.com/VladimirMarkelov/clui"
)
const URLLogin = "https://auth.adlin.nemunai.re/login"
func goLogin(stdscr *gc.Window, in chan gc.Key) (string, string, bool) {
username, password := login(stdscr, in)
var logged = false
validator := make(chan bool)
validator_err := make(chan error)
go func(username, password string, progress chan bool, err chan error) {
st, errm := checkLogin(username, password)
progress <- st
err <- errm
}(username, password, validator, validator_err)
func askLogin() (lgd *LoginDialog) {
lgd = CreateLoginDialog(" SRS AdLin - Login ")
if connection(stdscr, in, validator, validator_err) {
e := <-validator_err
return username, e.Error(), true
} else {
return goLogin(stdscr, in)
lgd.beforeClose = func() {
// Display next dialoag
ckd := CreateCheckDialog(" SRS AdLin - Login ", lgd.Username, lgd.Password)
ckd.beforeClose = func(ev ui.Event) {
if ev.Err == nil {
logged = true
CreateRebootDialog(" SRS AdLin - Login", lgd.Username)
} else {
errd := CreateErrMsgDialog(" SRS AdLin - Login ", ev.Err)
errd.beforeClose = func() {
askLogin()
}
}
}
}
return
}
func main() {
stdscr, _ := gc.Init()
defer gc.End()
ui.InitLibrary()
defer ui.DeinitLibrary()
// Set main properties
gc.Cursor(0)
gc.StartColor()
gc.Raw(true)
gc.Echo(false)
stdscr.Keypad(true)
askLogin()
// Define colors
gc.InitPair(1, gc.C_WHITE, gc.C_BLUE)
gc.InitPair(2, gc.C_GREEN, gc.C_BLACK)
gc.InitPair(3, gc.C_CYAN, gc.C_BLACK)
gc.InitPair(4, gc.C_RED, gc.C_BLACK)
gc.InitPair(5, gc.C_BLACK, gc.C_WHITE)
ui.MainLoop()
// Register pressed key through channel
in := make(chan gc.Key)
go func(w *gc.Window, ch chan<- gc.Key) {
for {
ch <- w.GetChar()
}
}(stdscr, in)
// Run!
if username, ip, ok := goLogin(stdscr, in); ok {
okreboot(stdscr, username, ip)
if !logged {
os.Exit(1)
}
}

View File

@ -1,249 +0,0 @@
package main
import (
"strings"
"time"
gc "github.com/rthornton128/goncurses"
)
func login(stdscr *gc.Window, in chan gc.Key) (username, password string) {
gc.Cursor(1)
stdscr.Clear()
// Initialize the fields and Set field options
fields := make([]*gc.Field, 3)
fields[0], _ = gc.NewField(1, 28, 2, 12, 0, 0)
defer fields[0].Free()
fields[0].SetBackground(gc.ColorPair(5) | gc.A_UNDERLINE)
fields[0].SetOptionsOff(gc.FO_AUTOSKIP)
fields[1], _ = gc.NewField(1, 28, 4, 12, 0, 0)
defer fields[1].Free()
fields[1].SetBackground(gc.ColorPair(5) | gc.A_UNDERLINE)
fields[1].SetOptionsOff(gc.FO_PUBLIC)
fields[1].SetOptionsOff(gc.FO_AUTOSKIP)
fields[2], _ = gc.NewField(1, 11, 7, 18, 0, 0)
defer fields[2].Free()
fields[2].SetBuffer("< Connect >")
fields[2].SetBackground(gc.ColorPair(1))
fields[2].SetOptionsOff(gc.FO_EDIT)
fields[2].SetOptionsOff(gc.FO_AUTOSKIP)
// Create the form and post it
form, _ := gc.NewForm(fields)
defer form.Free()
// Create the window to be associated with the form
rows, cols := stdscr.MaxYX()
height, width := 14, 44
y, x := (rows-height)/2, (cols-width)/2
mainwin, _ := gc.NewWindow(height, width, y, x)
defer mainwin.Delete()
defer mainwin.Erase()
mainwin.Keypad(true)
// Set main window and sub window
form.SetWindow(mainwin)
form.SetSub(mainwin.Derived(8, 42, 3, 1))
// Print a border around the main window and print a title
mainwin.Box(0, 0)
y, x = mainwin.MaxYX()
title := "SRS ADLIN - Login"
mainwin.ColorOn(2)
mainwin.MovePrint(1, (x/2)-(len(title)/2), title)
mainwin.ColorOff(1)
mainwin.MoveAddChar(2, 0, gc.ACS_LTEE)
mainwin.HLine(2, 1, gc.ACS_HLINE, x-2)
mainwin.MoveAddChar(2, x-1, gc.ACS_RTEE)
stdscr.Refresh()
form.Post()
defer form.UnPost()
mainwin.MovePrint(5, 3, "Login:")
mainwin.MovePrint(7, 3, "Password:")
form.Driver(gc.REQ_FIRST_FIELD)
stdscr.Refresh()
mainwin.Refresh()
login:
for {
select {
case ch := <-in:
switch ch {
case gc.KEY_DOWN, gc.KEY_TAB:
form.Driver(gc.REQ_NEXT_FIELD)
form.Driver(gc.REQ_END_LINE)
case gc.KEY_UP:
form.Driver(gc.REQ_PREV_FIELD)
form.Driver(gc.REQ_END_LINE)
case gc.KEY_BACKSPACE:
form.Driver(gc.REQ_CLR_FIELD)
case gc.KEY_RETURN:
form.Driver(gc.REQ_NEXT_FIELD)
form.Driver(gc.REQ_END_LINE)
if len(strings.TrimSpace(fields[0].Buffer())) > 0 && len(strings.TrimSpace(fields[1].Buffer())) > 0 {
break login
}
default:
form.Driver(ch)
}
}
mainwin.Refresh()
}
username = strings.TrimSpace(fields[0].Buffer())
password = strings.TrimSpace(fields[1].Buffer())
return
}
func connection(stdscr *gc.Window, in chan gc.Key, validator chan bool, validator_err chan error) (canContinue bool) {
gc.Cursor(0)
stdscr.Clear()
// Create the window to be associated with the form
rows, cols := stdscr.MaxYX()
height, width := 10, 60
y, x := (rows-height)/2, (cols-width)/2
mainwin, _ := gc.NewWindow(height, width, y, x)
defer mainwin.Delete()
mainwin.Keypad(true)
// Print a border around the main window and print a title
mainwin.Box(0, 0)
y, x = mainwin.MaxYX()
title := "SRS ADLIN - Login"
mainwin.ColorOn(2)
mainwin.MovePrint(1, (x/2)-(len(title)/2), title)
mainwin.ColorOff(2)
mainwin.MoveAddChar(2, 0, gc.ACS_LTEE)
mainwin.HLine(2, 1, gc.ACS_HLINE, x-2)
mainwin.MoveAddChar(2, x-1, gc.ACS_RTEE)
stdscr.Refresh()
mainwin.MovePrint(4, 4, " Please wait...")
mainwin.ColorOn(3)
mainwin.MovePrint(6, 2, "Connecting to login server...")
mainwin.ColorOff(3)
stdscr.Refresh()
mainwin.Refresh()
ticker := time.NewTicker(time.Millisecond * 150)
rotPos := 0
loginloop:
for {
select {
case ch := <-in:
if gc.Char(ch) == gc.Char('r') {
break loginloop
}
case st := <-validator:
if st {
canContinue = true
break loginloop
} else {
e := <-validator_err
mainwin.ColorOn(4)
mainwin.MovePrint(4, 2, e.Error())
mainwin.ColorOff(4)
ticker.Stop()
mainwin.MovePrint(7, 2, " Press any key to retry ")
mainwin.Refresh()
<-in
break loginloop
}
case <-ticker.C:
switch rotPos += 1; rotPos {
case 0, 4:
mainwin.MovePrint(4, 4, "|")
case 1, 5:
mainwin.MovePrint(4, 4, "/")
case 2, 6:
mainwin.MovePrint(4, 4, "-")
case 3, 7:
mainwin.MovePrint(4, 4, "\\")
default:
mainwin.MovePrint(4, 4, "|")
rotPos = 0
}
}
mainwin.Refresh()
}
ticker.Stop()
return
}
func okreboot(stdscr *gc.Window, login string, ip string) {
gc.Cursor(0)
stdscr.Clear()
// Create the window to be associated with the form
rows, cols := stdscr.MaxYX()
height, width := 14, 42
y, x := (rows-height)/2, (cols-width)/2
mainwin, _ := gc.NewWindow(height, width, y, x)
defer mainwin.Delete()
mainwin.Keypad(true)
// Print a border around the main window and print a title
mainwin.Box(0, 0)
y, x = mainwin.MaxYX()
title := "SRS ADLIN"
mainwin.ColorOn(2)
mainwin.MovePrint(1, (x/2)-(len(title)/2), title)
mainwin.ColorOff(2)
mainwin.MoveAddChar(2, 0, gc.ACS_LTEE)
mainwin.HLine(2, 1, gc.ACS_HLINE, x-2)
mainwin.MoveAddChar(2, x-1, gc.ACS_RTEE)
stdscr.Refresh()
mainwin.ColorOn(2)
mainwin.MovePrint(4, 2, "You are now successfully logged in as: ")
mainwin.ColorOff(2)
mainwin.ColorOn(3)
mainwin.MovePrint(5, (x/2)-(len(login)/2), login)
mainwin.ColorOff(3)
mainwin.MovePrint(8, 2, "Your computer will automatically")
mainwin.MovePrint(9, 2, "reboot in a few seconds...")
mainwin.MovePrint(11, 2, "|------------------------------------|")
stdscr.Refresh()
mainwin.Refresh()
ticker := time.NewTicker(time.Millisecond * 98)
pos := 0
rebootloop:
for {
select {
case <-ticker.C:
mainwin.MovePrint(11, 3+pos, "*")
pos += 1
if pos > 36 {
break rebootloop
}
}
mainwin.Refresh()
}
ticker.Stop()
}