diff options
-rw-r--r-- | gui/backend.go | 302 | ||||
-rw-r--r-- | pkg/backend/api.go | 74 | ||||
-rw-r--r-- | pkg/backend/bitmask.go | 52 | ||||
-rw-r--r-- | pkg/backend/callbacks.go | 63 | ||||
-rw-r--r-- | pkg/backend/mocks.go | 32 | ||||
-rw-r--r-- | pkg/backend/status.go | 122 |
6 files changed, 357 insertions, 288 deletions
diff --git a/gui/backend.go b/gui/backend.go index cf7c0fb..9c65025 100644 --- a/gui/backend.go +++ b/gui/backend.go @@ -1,333 +1,59 @@ package main -/* a wrapper around bitmask that exposes status to a QtQml gui */ +/* a wrapper around bitmask that exposes status to a QtQml gui. + Have a look at the pkg/backend module for further enlightment. */ import ( - "bytes" - "encoding/json" - "fmt" - "log" - "net/http" - "os" - "reflect" - "sync" - //"time" + "C" "unsafe" - "0xacab.org/leap/bitmask-vpn/pkg/bitmask" - "0xacab.org/leap/bitmask-vpn/pkg/pickle" + "0xacab.org/leap/bitmask-vpn/pkg/backend" ) -// typedef void (*cb)(); -// inline void _do_callback(cb f) { -// f(); -// } -import "C" - -/* callbacks into C-land */ - -var mut sync.Mutex -var stmut sync.Mutex -var cbs = make(map[string](*[0]byte)) -var initOnce sync.Once - -// Events are just a enumeration of all the posible events that C functions can -// be interested in subscribing to. You cannot subscribe to an event that is -// not listed here. -type Events struct { - OnStatusChanged string -} - -const OnStatusChanged string = "OnStatusChanged" - -// subscribe registers a callback from C-land. -// This callback needs to be passed as a void* C function pointer. -func subscribe(event string, fp unsafe.Pointer) { - mut.Lock() - defer mut.Unlock() - e := &Events{} - v := reflect.Indirect(reflect.ValueOf(&e)) - hf := v.Elem().FieldByName(event) - if reflect.ValueOf(hf).IsZero() { - fmt.Println("ERROR: not a valid event:", event) - } else { - cbs[event] = (*[0]byte)(fp) - } -} - -// trigger fires a callback from C-land. -func trigger(event string) { - mut.Lock() - defer mut.Unlock() - cb := cbs[event] - if cb != nil { - C._do_callback(cb) - } else { - fmt.Println("ERROR: this event does not have subscribers:", event) - } -} - -/* connection status */ - -const ( - offStr = "off" - startingStr = "starting" - onStr = "on" - stoppingStr = "stopping" - failedStr = "failed" -) - -// status reflects the current VPN status. Go code is responsible for updating -// it; C-land just watches its changes and pulls its updates via the serialized -// context object. -type status int - -const ( - off status = iota - starting - on - stopping - failed - unknown -) - -func (s status) String() string { - return [...]string{offStr, startingStr, onStr, stoppingStr, failedStr}[s] -} - -func (s status) MarshalJSON() ([]byte, error) { - b := bytes.NewBufferString(`"`) - b.WriteString(s.String()) - b.WriteString(`"`) - return b.Bytes(), nil -} - -func (s status) fromString(st string) status { - switch st { - case offStr: - return off - case startingStr: - return starting - case onStr: - return on - case stoppingStr: - return stopping - case failedStr: - return failed - default: - return unknown - } -} - -// The connectionCtx keeps the global state that is passed around to C-land. It -// also serves as the primary way of passing requests from the frontend to the -// Go-core, by letting the UI write some of these variables and processing -// them. -type connectionCtx struct { - AppName string `json:"appName"` - Provider string `json:"provider"` - Donate bool `json:"donate"` - Status status `json:"status"` - bm bitmask.Bitmask -} - -func (c connectionCtx) toJson() ([]byte, error) { - stmut.Lock() - defer stmut.Unlock() - b, err := json.Marshal(c) - if err != nil { - log.Println(err) - return nil, err - } - return b, nil -} - -func (c connectionCtx) updateStatus() { - if stStr, err := c.bm.GetStatus(); err != nil { - log.Printf("Error getting status: %v", err) - } else { - setStatusFromStr(stStr) - } - - statusCh := c.bm.GetStatusCh() - for { - select { - case stStr := <-statusCh: - setStatusFromStr(stStr) - } - } -} - -var ctx *connectionCtx - -func setStatus(st status) { - stmut.Lock() - defer stmut.Unlock() - ctx.Status = st - go trigger(OnStatusChanged) -} - -func toggleDonate() { - stmut.Lock() - defer stmut.Unlock() - ctx.Donate = !ctx.Donate - go trigger(OnStatusChanged) -} - -func setStatusFromStr(stStr string) { - setStatus(unknown.fromString(stStr)) -} - -// initializeBitmask instantiates a bitmask connection -func initializeBitmask() { - if ctx == nil { - log.Println("error: cannot initialize bitmask, ctx is nil") - os.Exit(1) - } - bitmask.InitializeLogger() - - b, err := bitmask.InitializeBitmask() - if err != nil { - log.Println("error: cannot initialize bitmask") - } - ctx.bm = b -} - -func startVPN() { - err := ctx.bm.StartVPN(ctx.Provider) - if err != nil { - log.Println(err) - os.Exit(1) - } -} - -func stopVPN() { - err := ctx.bm.StopVPN() - if err != nil { - log.Println(err) - } -} - -// initializeContext initializes an empty connStatus and assigns it to the -// global ctx holder. This is expected to be called only once, so the public -// api uses the sync.Once primitive to call this. -func initializeContext(provider, appName string) { - var st status = off - ctx = &connectionCtx{ - AppName: appName, - Provider: provider, - Donate: false, - Status: st, - } - go trigger(OnStatusChanged) - initializeBitmask() -} - -/* mock http server: easy way to mocking vpn behavior on ui interaction. This -* should also show a good way of writing functionality tests just for the Qml -* layer */ - -func mockUIOn(w http.ResponseWriter, r *http.Request) { - log.Println("changing status: on") - setStatus(on) -} - -func mockUIOff(w http.ResponseWriter, r *http.Request) { - log.Println("changing status: off") - setStatus(off) -} - -func mockUIFailed(w http.ResponseWriter, r *http.Request) { - log.Println("changing status: failed") - setStatus(failed) -} - -func mockUI() { - http.HandleFunc("/on", mockUIOn) - http.HandleFunc("/off", mockUIOff) - http.HandleFunc("/failed", mockUIFailed) - http.ListenAndServe(":8080", nil) -} - -/* - - exported C api - -*/ - //export SwitchOn func SwitchOn() { - go setStatus(starting) - go startVPN() + backend.SwitchOn() } //export SwitchOff func SwitchOff() { - go setStatus(stopping) - go stopVPN() + backend.SwitchOff() } //export Unblock func Unblock() { - fmt.Println("unblock... [not implemented]") + backend.Unblock() } //export Quit func Quit() { - if ctx.Status != off { - go setStatus(stopping) - stopVPN() - } + backend.Quit() + } //export ToggleDonate func ToggleDonate() { - toggleDonate() + backend.ToggleDonate() } //export SubscribeToEvent func SubscribeToEvent(event string, f unsafe.Pointer) { - subscribe(event, f) + backend.SubscribeToEvent(event, f) } //export InitializeBitmaskContext func InitializeBitmaskContext() { - pi := bitmask.GetConfiguredProvider() - - initOnce.Do(func() { - initializeContext(pi.Provider, pi.AppName) - }) - go ctx.updateStatus() - - /* DEBUG - timer := time.NewTimer(time.Second * 3) - go func() { - <-timer.C - fmt.Println("donate timer fired") - toggleDonate() - }() - */ + backend.InitializeBitmaskContext() } //export RefreshContext func RefreshContext() *C.char { - c, _ := ctx.toJson() - return C.CString(string(c)) + return (*C.char)(backend.RefreshContext()) } //export InstallHelpers func InstallHelpers() { - pickle.InstallHelpers() -} - -/* end of the exposed api */ - -/* we could enable this one optionally for the qt tests */ - -/* uncomment: export MockUIInteraction */ -func MockUIInteraction() { - log.Println("mocking ui interaction on port 8080. \nTry 'curl localhost:8080/{on|off|failed}' to toggle status.") - go mockUI() + backend.InstallHelpers() } func main() {} diff --git a/pkg/backend/api.go b/pkg/backend/api.go new file mode 100644 index 0000000..f924cbd --- /dev/null +++ b/pkg/backend/api.go @@ -0,0 +1,74 @@ +/* All the exported functions live here */ + +package backend + +import ( + "C" + "fmt" + "log" + "unsafe" + + "0xacab.org/leap/bitmask-vpn/pkg/bitmask" + "0xacab.org/leap/bitmask-vpn/pkg/pickle" +) + +func SwitchOn() { + go setStatus(starting) + go startVPN() +} + +func SwitchOff() { + go setStatus(stopping) + go stopVPN() +} + +func Unblock() { + fmt.Println("unblock... [not implemented]") +} + +func Quit() { + if ctx.Status != off { + go setStatus(stopping) + stopVPN() + } +} + +func ToggleDonate() { + toggleDonate() +} + +func SubscribeToEvent(event string, f unsafe.Pointer) { + subscribe(event, f) +} + +func InitializeBitmaskContext() { + pi := bitmask.GetConfiguredProvider() + + initOnce.Do(func() { + initializeContext(pi.Provider, pi.AppName) + }) + go ctx.updateStatus() + + /* DEBUG + timer := time.NewTimer(time.Second * 3) + go func() { + <-timer.C + fmt.Println("donate timer fired") + toggleDonate() + }() + */ +} + +func RefreshContext() *C.char { + c, _ := ctx.toJson() + return C.CString(string(c)) +} + +func InstallHelpers() { + pickle.InstallHelpers() +} + +func MockUIInteraction() { + log.Println("mocking ui interaction on port 8080. \nTry 'curl localhost:8080/{on|off|failed}' to toggle status.") + go mockUI() +} diff --git a/pkg/backend/bitmask.go b/pkg/backend/bitmask.go new file mode 100644 index 0000000..07d27ea --- /dev/null +++ b/pkg/backend/bitmask.go @@ -0,0 +1,52 @@ +package backend + +import ( + "log" + "os" + + "0xacab.org/leap/bitmask-vpn/pkg/bitmask" +) + +func initializeBitmask() { + if ctx == nil { + log.Println("error: cannot initialize bitmask, ctx is nil") + os.Exit(1) + } + bitmask.InitializeLogger() + + b, err := bitmask.InitializeBitmask() + if err != nil { + log.Println("error: cannot initialize bitmask") + } + ctx.bm = b +} + +func startVPN() { + err := ctx.bm.StartVPN(ctx.Provider) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func stopVPN() { + err := ctx.bm.StopVPN() + if err != nil { + log.Println(err) + } +} + +// initializeContext initializes an empty connStatus and assigns it to the +// global ctx holder. This is expected to be called only once, so the public +// api uses the sync.Once primitive to call this. +func initializeContext(provider, appName string) { + var st status = off + ctx = &connectionCtx{ + AppName: appName, + Provider: provider, + Donate: false, + Status: st, + } + go trigger(OnStatusChanged) + initializeBitmask() +} diff --git a/pkg/backend/callbacks.go b/pkg/backend/callbacks.go new file mode 100644 index 0000000..5ea3b04 --- /dev/null +++ b/pkg/backend/callbacks.go @@ -0,0 +1,63 @@ +package backend + +import ( + "fmt" + "reflect" + "sync" + "unsafe" +) + +/* NOTE! ATCHUNG! what follow are not silly comments. Well, *this one* is, but + the lines after this are not. + Those are inline C functions, that are invoked by CGO later on. + it's also crucial that you don't any extra space between the function + block and the 'import "C"' line. */ + +// typedef void (*cb)(); +// inline void _do_callback(cb f) { +// f(); +// } +import "C" + +/* callbacks into C-land */ + +var mut sync.Mutex +var stmut sync.Mutex +var cbs = make(map[string](*[0]byte)) +var initOnce sync.Once + +// Events are just a enumeration of all the posible events that C functions can +// be interested in subscribing to. You cannot subscribe to an event that is +// not listed here. +type Events struct { + OnStatusChanged string +} + +const OnStatusChanged string = "OnStatusChanged" + +// subscribe registers a callback from C-land. +// This callback needs to be passed as a void* C function pointer. +func subscribe(event string, fp unsafe.Pointer) { + mut.Lock() + defer mut.Unlock() + e := &Events{} + v := reflect.Indirect(reflect.ValueOf(&e)) + hf := v.Elem().FieldByName(event) + if reflect.ValueOf(hf).IsZero() { + fmt.Println("ERROR: not a valid event:", event) + } else { + cbs[event] = (*[0]byte)(fp) + } +} + +// trigger fires a callback from C-land. +func trigger(event string) { + mut.Lock() + defer mut.Unlock() + cb := cbs[event] + if cb != nil { + C._do_callback(cb) + } else { + fmt.Println("ERROR: this event does not have subscribers:", event) + } +} diff --git a/pkg/backend/mocks.go b/pkg/backend/mocks.go new file mode 100644 index 0000000..a8ede73 --- /dev/null +++ b/pkg/backend/mocks.go @@ -0,0 +1,32 @@ +package backend + +import ( + "log" + "net/http" +) + +/* mock http server: easy way to mocking vpn behavior on ui interaction. This +* should also show a good way of writing functionality tests just for the Qml +* layer */ + +func mockUIOn(w http.ResponseWriter, r *http.Request) { + log.Println("changing status: on") + setStatus(on) +} + +func mockUIOff(w http.ResponseWriter, r *http.Request) { + log.Println("changing status: off") + setStatus(off) +} + +func mockUIFailed(w http.ResponseWriter, r *http.Request) { + log.Println("changing status: failed") + setStatus(failed) +} + +func mockUI() { + http.HandleFunc("/on", mockUIOn) + http.HandleFunc("/off", mockUIOff) + http.HandleFunc("/failed", mockUIFailed) + http.ListenAndServe(":8080", nil) +} diff --git a/pkg/backend/status.go b/pkg/backend/status.go new file mode 100644 index 0000000..e2d31db --- /dev/null +++ b/pkg/backend/status.go @@ -0,0 +1,122 @@ +package backend + +import ( + "bytes" + "encoding/json" + "log" + + "0xacab.org/leap/bitmask-vpn/pkg/bitmask" +) + +const ( + offStr = "off" + startingStr = "starting" + onStr = "on" + stoppingStr = "stopping" + failedStr = "failed" +) + +// ctx will be our glorious global object. +// if we ever switch again to a provider-agnostic app, we should keep a map here. +var ctx *connectionCtx + +// the status type reflects the current VPN status. Go code is responsible for updating +// it; the C gui just watches its changes and pulls its updates via the serialized +// context object. + +type status int + +const ( + off status = iota + starting + on + stopping + failed + unknown +) + +func (s status) String() string { + return [...]string{offStr, startingStr, onStr, stoppingStr, failedStr}[s] +} + +func (s status) MarshalJSON() ([]byte, error) { + b := bytes.NewBufferString(`"`) + b.WriteString(s.String()) + b.WriteString(`"`) + return b.Bytes(), nil +} + +func (s status) fromString(st string) status { + switch st { + case offStr: + return off + case startingStr: + return starting + case onStr: + return on + case stoppingStr: + return stopping + case failedStr: + return failed + default: + return unknown + } +} + +// The connectionCtx keeps the global state that is passed around to C-land. It +// also serves as the primary way of passing requests from the frontend to the +// Go-core, by letting the UI write some of these variables and processing +// them. + +type connectionCtx struct { + AppName string `json:"appName"` + Provider string `json:"provider"` + Donate bool `json:"donate"` + Status status `json:"status"` + bm bitmask.Bitmask +} + +func (c connectionCtx) toJson() ([]byte, error) { + stmut.Lock() + defer stmut.Unlock() + b, err := json.Marshal(c) + if err != nil { + log.Println(err) + return nil, err + } + return b, nil +} + +func (c connectionCtx) updateStatus() { + if stStr, err := c.bm.GetStatus(); err != nil { + log.Printf("Error getting status: %v", err) + } else { + setStatusFromStr(stStr) + } + + statusCh := c.bm.GetStatusCh() + for { + select { + case stStr := <-statusCh: + setStatusFromStr(stStr) + } + } +} + +func setStatus(st status) { + stmut.Lock() + defer stmut.Unlock() + ctx.Status = st + go trigger(OnStatusChanged) +} + +func toggleDonate() { + stmut.Lock() + defer stmut.Unlock() + ctx.Donate = !ctx.Donate + go trigger(OnStatusChanged) +} + +func setStatusFromStr(stStr string) { + setStatus(unknown.fromString(stStr)) +} |