Merge ~alfonsosanchezbeato/snappy-hwe-snaps/+git/wifi-connect:use-nm-for-ap into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect:master

Proposed by Alfonso Sanchez-Beato
Status: Merged
Approved by: Kyle Nitzsche
Approved revision: 223cd235a40065079971211ab2a3a72749b3d730
Merged at revision: 31120c1e68e4276e6988b9e5425af72717b519c6
Proposed branch: ~alfonsosanchezbeato/snappy-hwe-snaps/+git/wifi-connect:use-nm-for-ap
Merge into: ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect:master
Diff against target: 6530 lines (+1724/-2847)
30 files modified
avahi/avahi_test.go (+4/-24)
daemon/daemon.go (+424/-170)
daemon/daemon_test.go (+6/-218)
dependencies.tsv (+6/-6)
dev/null (+0/-514)
hooks/configure.go (+40/-18)
netman/dbus.go (+458/-174)
netman/dbus_test.go (+39/-26)
run-checks (+1/-1)
scriptlets/country-codes (+250/-243)
scriptlets/fetch_country_codes.sh (+2/-2)
scriptlets/fill_country_codes.sh (+8/-13)
server/handlers.go (+72/-134)
server/handlers_test.go (+52/-195)
server/launcher.go (+23/-93)
server/launcher_test.go (+7/-60)
server/manager.go (+16/-19)
server/manager_test.go (+27/-38)
server/router.go (+25/-10)
service/service.go (+5/-141)
service/service_test.go (+26/-0)
snapcraft.yaml (+53/-31)
static/js/jquery.min.js (+4/-0)
static/templates/management.html (+1/-1)
static/templates/operational.html (+1/-1)
utils/config.go (+65/-147)
utils/config_test.go (+50/-487)
utils/ssid.go (+45/-0)
utils/utils.go (+11/-50)
utils/utils_test.go (+3/-31)
Reviewer Review Type Date Requested Status
Alfonso Sanchez-Beato continuous-integration Approve
Kyle Nitzsche Approve
System Enablement Bot continuous-integration Needs Fixing
Review via email: mp+380488@code.launchpad.net

Commit message

Refactor of wifi-connect to use now NM instead of wifi-ap.

Description of the change

Refactor of wifi-connect to use now NM instead of wifi-ap.

To post a comment you must log in.
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :

PASSED: Successfully build documentation, rev: 223cd235a40065079971211ab2a3a72749b3d730

Generated documentation is available at https://jenkins.canonical.com/system-enablement/job/snappy-hwe-snaps-snap-docs/1305/

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

I'm going to approve this "Very Large Merge™" without more than a quick scan since you are taking the lead on adjusting the code base for new opps.

review: Approve
Revision history for this message
Alfonso Sanchez-Beato (alfonsosanchezbeato) wrote :

Thanks Kyle!

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/server/middleware.go b/avahi/avahi_test.go
2similarity index 63%
3rename from server/middleware.go
4rename to avahi/avahi_test.go
5index 99bd6f9..486121f 100644
6--- a/server/middleware.go
7+++ b/avahi/avahi_test.go
8@@ -1,5 +1,3 @@
9-// -*- Mode: Go; indent-tabs-mode: t -*-
10-
11 /*
12 * Copyright (C) 2017 Canonical Ltd
13 *
14@@ -17,30 +15,12 @@
15 *
16 */
17
18-package server
19+package avahi
20
21 import (
22- "net/http"
23-
24- "launchpad.net/wifi-connect/netman"
25- "launchpad.net/wifi-connect/wifiap"
26+ "testing"
27 )
28
29-var wifiapClient wifiap.Operations
30-var netmanClient netman.Operations
31-
32-// Middleware to pre-process web service requests
33-func Middleware(inner http.Handler) http.Handler {
34- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35-
36- if wifiapClient == nil {
37- wifiapClient = wifiap.DefaultClient()
38- }
39-
40- if netmanClient == nil {
41- netmanClient = netman.DefaultClient()
42- }
43-
44- inner.ServeHTTP(w, r)
45- })
46+func TestAvahi(t *testing.T) {
47+ // TODO Fill it when we really use avahi
48 }
49diff --git a/cmd/main.go b/cmd/main.go
50deleted file mode 100644
51index 2eebb48..0000000
52--- a/cmd/main.go
53+++ /dev/null
54@@ -1,262 +0,0 @@
55-/*
56- * Copyright (C) 2017 Canonical Ltd
57- *
58- * This program is free software: you can redistribute it and/or modify
59- * it under the terms of the GNU General Public License version 3 as
60- * published by the Free Software Foundation.
61- *
62- * This program is distributed in the hope that it will be useful,
63- * but WITHOUT ANY WARRANTY; without even the implied warranty of
64- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
65- * GNU General Public License for more details.
66- *
67- * You should have received a copy of the GNU General Public License
68- * along with this program. If not, see <http://www.gnu.org/licenses/>.
69- *
70- */
71-
72-package main
73-
74-import (
75- "bufio"
76- "fmt"
77- "log"
78- "os"
79- "os/signal"
80- "strings"
81- "sync"
82-
83- "launchpad.net/wifi-connect/netman"
84- "launchpad.net/wifi-connect/server"
85- "launchpad.net/wifi-connect/utils"
86- "launchpad.net/wifi-connect/wifiap"
87-)
88-
89-func help() string {
90-
91- text :=
92- `Usage: sudo wifi-connect COMMAND [VALUE]
93-
94-Commands:
95- stop: Disables wifi-connect from automatic control, leaving system
96- in current state
97- start: Enables wifi-connect as automatic controller, restarting from
98- a clean state
99- show-ap: Show AP configuration
100- ssid VALUE: Set the AP ssid (causes AP restart if it is UP)
101- passphrase VALUE: Set the AP passphrase (cause AP restart if it is UP)
102-`
103- return text
104-}
105-
106-// checkSudo return false if the current user is not root, else true
107-func checkSudo() bool {
108- if os.Geteuid() != 0 {
109- fmt.Println("Error: This command requires sudo")
110- return false
111- }
112- return true
113-}
114-
115-func waitForCtrlC() {
116- var endWaiter sync.WaitGroup
117- endWaiter.Add(1)
118- var signalChannel chan os.Signal
119- signalChannel = make(chan os.Signal, 1)
120- signal.Notify(signalChannel, os.Interrupt)
121- go func() {
122- <-signalChannel
123- endWaiter.Done()
124- }()
125- endWaiter.Wait()
126-}
127-
128-func main() {
129-
130- log.SetFlags(log.Lshortfile)
131- log.SetPrefix("== wifi-connect: ")
132-
133- if len(os.Args) < 2 {
134- fmt.Println("Error: no command arguments provided")
135- return
136- }
137- args := os.Args[1:]
138-
139- switch args[0] {
140- case "help":
141- fmt.Printf("%s\n", help())
142- case "-help":
143- fmt.Printf("%s\n", help())
144- case "-h":
145- fmt.Printf("%s\n", help())
146- case "--help":
147- fmt.Printf("%s\n", help())
148- case "stop":
149- if !checkSudo() {
150- return
151- }
152- err := utils.WriteFlagFile(os.Getenv("SNAP_COMMON") + "/manualMode")
153- if err != nil {
154- fmt.Println(err)
155- return
156- }
157- fmt.Println("entering MANUAL Mode. Wifi-connect has stopped managing state. Use 'start' to restore normal operations")
158- case "start":
159- if !checkSudo() {
160- return
161- }
162- err := utils.RemoveFlagFile(os.Getenv("SNAP_COMMON") + "/manualMode")
163- if err != nil {
164- fmt.Println("Error:", err)
165- return
166- }
167- fmt.Println("Entering NORMAL Mode.")
168- case "show-ap":
169- if !checkSudo() {
170- return
171- }
172- wifiAPClient := wifiap.DefaultClient()
173- result, err := wifiAPClient.Show()
174- if err != nil {
175- fmt.Println("Error:", err)
176- return
177- }
178- if result != nil {
179- utils.PrintMapSorted(result)
180- return
181- }
182- case "ssid":
183- if !checkSudo() {
184- return
185- }
186- if len(os.Args) < 3 {
187- fmt.Println("Error: no ssid provided")
188- return
189- }
190- wifiAPClient := wifiap.DefaultClient()
191- wifiAPClient.SetSsid(os.Args[2])
192- case "passphrase":
193- if !checkSudo() {
194- return
195- }
196- if len(os.Args) < 3 {
197- fmt.Println("Error: no passphrase provided")
198- return
199- }
200- if len(os.Args[2]) < 13 {
201- fmt.Println("Error: passphrase must be at least 13 chars long")
202- return
203- }
204- wifiAPClient := wifiap.DefaultClient()
205- wifiAPClient.SetPassphrase(os.Args[2])
206- case "get-devices":
207- c := netman.DefaultClient()
208- devices := c.GetDevices()
209- for d := range devices {
210- fmt.Println(d)
211- }
212- case "get-wifi-devices":
213- c := netman.DefaultClient()
214- devices := c.GetWifiDevices(c.GetDevices())
215- for d := range devices {
216- fmt.Println(d)
217- }
218- case "get-ssids":
219- c := netman.DefaultClient()
220- SSIDs, _, _ := c.Ssids()
221- var out string
222- for _, ssid := range SSIDs {
223- out += strings.TrimSpace(ssid.Ssid) + ","
224- }
225- if len(out) > 0 {
226- fmt.Printf("%s\n", out[:len(out)-1])
227- }
228- case "check-connected":
229- c := netman.DefaultClient()
230- if c.ConnectedWifi(c.GetWifiDevices(c.GetDevices())) {
231- fmt.Println("Device is connected")
232- } else {
233- fmt.Println("Device is not connected")
234- }
235-
236- case "check-connected-wifi":
237- c := netman.DefaultClient()
238- if c.ConnectedWifi(c.GetWifiDevices(c.GetDevices())) {
239- fmt.Println("Device is connected to external wifi AP")
240- } else {
241- fmt.Println("Device is not connected to external wifi AP")
242- }
243- case "disconnect-wifi":
244- c := netman.DefaultClient()
245- c.DisconnectWifi(c.GetWifiDevices(c.GetDevices()))
246- case "wifis-managed":
247- c := netman.DefaultClient()
248- wifis, err := c.WifisManaged(c.GetWifiDevices(c.GetDevices()))
249- if err != nil {
250- fmt.Println(err)
251- return
252- }
253- for k, v := range wifis {
254- fmt.Printf("%s : %s\n", k, v)
255- }
256- case "manage-iface":
257- if len(os.Args) < 3 {
258- fmt.Println("Error: no interface provided")
259- return
260- }
261- c := netman.DefaultClient()
262- c.SetIfaceManaged(os.Args[2], true, c.GetWifiDevices(c.GetDevices()))
263- case "unmanage-iface":
264- if len(os.Args) < 3 {
265- fmt.Println("Error: no interface provided")
266- return
267- }
268- c := netman.DefaultClient()
269- c.SetIfaceManaged(os.Args[2], false, c.GetWifiDevices(c.GetDevices()))
270- case "connect":
271- c := netman.DefaultClient()
272- SSIDs, ap2device, ssid2ap := c.Ssids()
273- for _, ssid := range SSIDs {
274- fmt.Printf(" %v\n", ssid.Ssid)
275- }
276- reader := bufio.NewReader(os.Stdin)
277- fmt.Print("Connect to AP. Enter SSID: ")
278- ssid, _ := reader.ReadString('\n')
279- ssid = strings.TrimSpace(ssid)
280- fmt.Print("Enter phasprase: ")
281- pw, _ := reader.ReadString('\n')
282- pw = strings.TrimSpace(pw)
283- c.ConnectAp(ssid, pw, ap2device, ssid2ap)
284- case "management":
285- server.Address = server.TestingAddress
286- if err := server.StartManagementServer(); err != nil {
287- fmt.Printf("Could not start management server: %v\n", err)
288- return
289- }
290- waitForCtrlC()
291- case "operational":
292- server.Address = server.TestingAddress
293- if err := server.StartOperationalServer(); err != nil {
294- fmt.Printf("Could not start operational server: %v\n", err)
295- return
296- }
297- waitForCtrlC()
298- case "set-portal-password":
299- if len(os.Args) < 3 {
300- fmt.Println("Error: no string to hash provided")
301- return
302- }
303- if len(os.Args[2]) < 8 {
304- fmt.Println("Error: password must be at least 8 characters long")
305- return
306- }
307- b, err := utils.HashIt(os.Args[2])
308- if err != nil {
309- fmt.Println("Error hashing:", err)
310- return
311- }
312- fmt.Println(string(b))
313- default:
314- fmt.Println("Error. Your command is not supported. Please try 'help'")
315- }
316-}
317diff --git a/daemon/daemon.go b/daemon/daemon.go
318index 1830491..d1af3a9 100644
319--- a/daemon/daemon.go
320+++ b/daemon/daemon.go
321@@ -18,242 +18,496 @@
322 package daemon
323
324 import (
325- "encoding/json"
326 "fmt"
327- "io/ioutil"
328 "log"
329+ "net"
330 "os"
331- "path/filepath"
332+ "os/exec"
333+ "os/signal"
334+ "strconv"
335+ "syscall"
336+ "time"
337
338- "launchpad.net/wifi-connect/avahi"
339+ "github.com/godbus/dbus"
340+ "launchpad.net/wifi-connect/netman"
341 "launchpad.net/wifi-connect/server"
342 "launchpad.net/wifi-connect/utils"
343- "launchpad.net/wifi-connect/wifiap"
344 )
345
346-// enum to track current system state
347+type serviceState int
348+
349 const (
350- STARTING = 0 + iota
351- MANAGING
352- OPERATING
353- MANUAL
354+ stStarting serviceState = iota
355+ stManaging
356+ stOperating
357 )
358
359-var manualFlagPath string
360-var waitFlagPath string
361-var previousState = STARTING
362-var state = STARTING
363-
364-// PreConfigFile is the path to the file that stores the hash of the portals password
365-var PreConfigFile = filepath.Join(os.Getenv("SNAP_COMMON"), "pre-config.json")
366-
367-// PreConfig is the struct representing a configuration
368-type PreConfig struct {
369- Passphrase string `json:"wifi.security-passphrase,omitempty"`
370- Ssid string `json:"wifi.ssid,omitempty"`
371- Interface string `json:"wifi.interface,omitempty"`
372- Password string `json:"portal.password,omitempty"`
373- Operational bool `json:"portal.operational,omitempty"` //whether to show the operational portal
374- NoResetCreds bool `json:"portal.no-reset-creds,omitempty"` //whether user must reset passphrase and password on first use of mgmt portal
375+func (s serviceState) String() string {
376+ return [...]string{"Starting", "Managing", "Operating"}[s]
377 }
378
379-// Client is the base type for both testing and runtime
380-type Client struct {
381-}
382+const (
383+ waitBeforeScanSec = 2
384+)
385
386-// GetClient returns a client for runtime or testing
387-func GetClient() *Client {
388- return &Client{}
389+// Service models the high level wifi-connect service
390+type Service struct {
391+ config *utils.Config
392+ cnetman *netman.Client
393+ devicePath string
394+ apSettingsPath string
395+ apActiveConnPath string
396+ ssidLs utils.SsidList
397+ dnsmasqCmd *exec.Cmd
398+ chSigs chan os.Signal
399+ chUserEvs chan server.UserEvent
400+ // Arguments for port forwarding rule
401+ iptblForwArgs []string
402 }
403
404-// used to clase the operational http server
405-var err error
406-
407-// GetManualFlagPath returns the current path
408-func (c *Client) GetManualFlagPath() string {
409- return manualFlagPath
410-}
411+// Creates the run time configuration based on available wifi interfaces and the
412+// configuration loaded from config.json.
413+func (s *Service) setDefaults() error {
414+ var err error
415+ // If no interface is selected by configuration, just take the first one
416+ // returned by GetWifiDevices().
417+ if s.config.Wifi.Interface == "" {
418+ devPaths := s.cnetman.GetWifiDevices()
419+ if len(devPaths) == 0 {
420+ return fmt.Errorf("no wifi device found")
421+ }
422+ s.devicePath = devPaths[0]
423+ iface, err := s.cnetman.GetIfaceFromDevicePath(devPaths[0])
424+ if err != nil {
425+ return err
426+ }
427+ s.config.Wifi.Interface = iface
428+ } else {
429+ s.devicePath, err = s.cnetman.
430+ GetDevicePathFromIface(s.config.Wifi.Interface)
431+ if err != nil {
432+ return err
433+ }
434+ }
435+ log.Printf("setDefaults: using interface %s", s.config.Wifi.Interface)
436
437-// SetManualFlagPath sets the current path
438-func (c *Client) SetManualFlagPath(s string) {
439- manualFlagPath = s
440-}
441+ if len(s.config.Portal.Password) > 0 {
442+ log.Print("setDefaults: portal password being set")
443+ _, err := utils.HashIt(s.config.Portal.Password)
444+ if err != nil {
445+ log.Printf("setDefaults: password err: %v", err)
446+ return err
447+ }
448+ }
449
450-// GetWaitFlagPath returns the current path
451-func (c *Client) GetWaitFlagPath() string {
452- return waitFlagPath
453+ return nil
454 }
455
456-// SetWaitFlagPath sets the current path
457-func (c *Client) SetWaitFlagPath(s string) {
458- waitFlagPath = s
459-}
460+// GetService returns a service object for runtime or testing
461+func GetService(cfg *utils.Config) *Service {
462+ if cfg.Portal.Operational {
463+ log.Print("operational portal is enabled")
464+ }
465
466-// GetPreviousState returns the daemon previous state
467-func (c *Client) GetPreviousState() int {
468- return previousState
469-}
470+ chSigs := make(chan os.Signal, 1)
471+ signal.Notify(chSigs, syscall.SIGINT, syscall.SIGTERM)
472
473-// SetPreviousState sets daemon previous state
474-func (c *Client) SetPreviousState(i int) {
475- previousState = i
476- return
477-}
478+ chUserEvs := make(chan server.UserEvent)
479
480-// GetState returns the daemon state
481-func (c *Client) GetState() int {
482- return state
483-}
484+ cnetman := netman.DefaultClient()
485
486-// SetState sets the daemon state and updates the previous state
487-func (c *Client) SetState(i int) {
488- previousState = state
489- state = i
490-}
491+ service := &Service{config: cfg, cnetman: cnetman, chSigs: chSigs,
492+ chUserEvs: chUserEvs}
493
494-// CheckWaitApConnect returns true if the flag wait file exists
495-// and false if it does not
496-func (c *Client) CheckWaitApConnect() bool {
497- if _, err := os.Stat(waitFlagPath); os.IsNotExist(err) {
498- return false
499- }
500- return true
501+ return service
502 }
503
504-// ManualMode enables the daemon to loop without action if in manual mode
505-// It returns true if the manual mode flag wait file exists
506-// and false if it does not. If it does not exist and the mode is MANUAL, the
507-// state is set to STARTING. If it does exist and the mode is not MANUAL, state
508-// is set to MANUAL
509-func (c *Client) ManualMode() bool {
510- if _, err := os.Stat(manualFlagPath); os.IsNotExist(err) {
511- if state == MANUAL {
512- c.SetState(STARTING)
513- log.Print("entering STARTING mode")
514- }
515- return false
516- }
517- if state != MANUAL {
518- c.SetState(MANUAL)
519- log.Print("entering MANUAL mode")
520- }
521- return true
522+// Clean-up left-overs from previous executions
523+func (s *Service) cleanUpPrevRuns() {
524+ // Remove old hotspots from previous executions
525+ s.cnetman.DeleteWifiConnections(netman.ApWifiConn)
526 }
527
528-// IsApUpWithoutSSIDs corrects an possible but unlikely case.
529-// if wifiap is UP and there are no known SSIDs, bring it down so on next
530-// loop iter we start again and can get SSIDs. returns true when ip is
531-// UP and has no ssids
532-func (c *Client) IsApUpWithoutSSIDs(cw *wifiap.Client) bool {
533- wifiUp, _ := cw.Enabled()
534- if !wifiUp {
535- return false
536- }
537- ssids, _ := utils.ReadSsidsFile()
538- if len(ssids) < 1 {
539- log.Print("wifi-ap is UP but has no SSIDS")
540- return true // ap is up with no ssids
541- }
542- return false
543+// Returns true if wifi is connected to an AP
544+func (s *Service) isConnectedToAnAP() bool {
545+ return s.cnetman.ConnectedToAnAP(s.devicePath)
546 }
547
548-// ManagementServerUp starts the management server if it is
549-// not running
550-func (c *Client) ManagementServerUp() {
551+// startManagementServer starts the management server if it is not running
552+func (s *Service) startManagementServer() {
553 if server.Current != server.Management && server.State == server.Stopped {
554- err = server.StartManagementServer()
555+ err := server.StartManagementServer(s.chUserEvs, s.config, &s.ssidLs)
556 if err != nil {
557- log.Printf("Error start Mamagement portal: %v", err)
558+ log.Printf("Error starting Management portal: %v", err)
559 }
560 // init mDNS
561- avahi.InitMDNS()
562+ //avahi.InitMDNS()
563 }
564 }
565
566-// ManagementServerDown stops the management server if it is running
567-// also remove the wait flag file, thus resetting proper State
568-func (c *Client) ManagementServerDown() {
569- if server.Current == server.Management && (server.State == server.Running || server.State == server.Starting) {
570- err = server.ShutdownManagementServer()
571+// stopManagementServer stops the management server if it is running
572+func (s *Service) stopManagementServer() {
573+ if server.Current == server.Management && server.State == server.Running {
574+ err := server.ShutdownManagementServer()
575 if err != nil {
576- log.Printf("Error stopping the Management portal: %v", err)
577+ log.Printf("Error stopping Management portal: %v", err)
578 }
579- //remove flag fie so daemon resumes normal control
580- utils.RemoveFlagFile(os.Getenv("SNAP_COMMON") + "/startingApConnect")
581 }
582 }
583
584-// OperationalServerUp starts the operational server if it is
585-// not running
586-func (c *Client) OperationalServerUp() {
587+// startOperationalServer starts the operational server if it is not running
588+func (s *Service) startOperationalServer() {
589+ if s.config.Portal.Operational == false {
590+ return
591+ }
592+ log.Print("Starting operational server")
593 if server.Current != server.Operational && server.State == server.Stopped {
594- err = server.StartOperationalServer()
595+ err := server.StartOperationalServer(s.chUserEvs, s.config)
596 if err != nil {
597- log.Printf("Error starting the Operational portal: %v", err)
598+ log.Printf("Error starting Operational portal: %v", err)
599 }
600 // init mDNS
601- avahi.InitMDNS()
602+ //avahi.InitMDNS()
603 }
604 }
605
606-// OperationalServerDown stops the operational server if it is running
607-func (c *Client) OperationalServerDown() {
608- if server.Current == server.Operational && (server.State == server.Running || server.State == server.Starting) {
609- err = server.ShutdownOperationalServer()
610+// stopOperationalServer stops the operational server if it is running
611+func (s *Service) stopOperationalServer() {
612+ if server.Current == server.Operational && server.State == server.Running {
613+ err := server.ShutdownOperationalServer()
614 if err != nil {
615 log.Printf("Error stopping Operational portal: %v", err)
616 }
617 }
618 }
619
620-// LoadPreConfig returns a PreConfig based on the pre-config.json, if present, and an error to indicate
621-// possible json unmarshal failure
622-func LoadPreConfig() (*PreConfig, error) {
623- config := &PreConfig{}
624- content, err := ioutil.ReadFile(PreConfigFile)
625+func (s *Service) connectToAp(ssid, pwd string) error {
626+ s.exitManagingMode()
627+
628+ log.Printf("Trying to connect")
629+ _, _, err := s.cnetman.ConnectToAp(ssid, pwd, s.devicePath)
630+ if err != nil {
631+ log.Printf("Failed connecting to %v: %v", ssid, err)
632+ s.enterManagingMode()
633+ }
634+ return err
635+}
636+
637+func (s *Service) doWifiScan() {
638+ s.exitManagingMode()
639+
640+ // wait a bit so we get ssids when scanning
641+ // TODO with a modern enough NM we could Rescan and wait for the
642+ // LastScan property to be signalled. But that requires NM >= 1.12.
643+ time.Sleep(waitBeforeScanSec * time.Second)
644+
645+ // Do some retries, just in case
646+ for i := 0; i < 5; i++ {
647+ ssids, _, _ := s.cnetman.Ssids(s.devicePath)
648+ if len(ssids) == 0 {
649+ log.Print("no ssids!!")
650+ time.Sleep(waitBeforeScanSec * time.Second)
651+ continue
652+ }
653+
654+ ssidNames := make([]string, len(ssids))
655+ for i, ssid := range ssids {
656+ ssidNames[i] = ssid.Ssid
657+ }
658+ s.ssidLs.SetSsidList(ssidNames)
659+ break
660+ }
661+
662+ s.enterManagingMode()
663+}
664+
665+func (s *Service) enterOperatingMode() {
666+ s.startOperationalServer()
667+}
668+
669+func (s *Service) exitOperatingMode() {
670+ s.stopOperationalServer()
671+}
672+
673+func (s *Service) enterManagingMode() {
674+ log.Print("Entering MANAGEMENT mode")
675+
676+ // Remove all connections to APs that could re-connect
677+ s.cnetman.DeleteWifiConnections(netman.StaWifiConn)
678+ if err := s.cnetman.DisconnectDevice(s.devicePath); err != nil {
679+ log.Printf("Error when disconnecting device: %v", err)
680+ }
681+
682+ // wait a bit so we get ssids when scanning
683+ // TODO with a modern enough NM we could Rescan and wait for the
684+ // LastScan property to be signalled. But that requires NM >= 1.12.
685+ time.Sleep(waitBeforeScanSec * time.Second)
686+
687+ // Do an scan of currently available SSIDs
688+ ssids, _, _ := s.cnetman.Ssids(s.devicePath)
689+ if len(ssids) == 0 {
690+ log.Print("No SSIDs found when scanning")
691+ }
692+ ssidNames := make([]string, len(ssids))
693+ for i, ssid := range ssids {
694+ ssidNames[i] = ssid.Ssid
695+ }
696+ s.ssidLs.SetSsidList(ssidNames)
697+
698+ ip, subnet, err := net.ParseCIDR(s.config.Wifi.Subnet)
699+ if err != nil {
700+ log.Printf("Error while parsing %s: %v", s.config.Wifi.Subnet, err)
701+ return
702+ }
703+ if ip.To4() == nil {
704+ log.Printf("Error: %v is not an IPv4 subnet", s.config.Wifi.Subnet)
705+ return
706+ }
707+ maskedBits, _ := subnet.Mask.Size()
708+ if maskedBits > 28 {
709+ log.Printf("Error: %v needs a mask smaller than 29", s.config.Wifi.Subnet)
710+ return
711+ }
712+ apIP := ip.Mask(subnet.Mask)
713+ apIP[3] |= 1
714+ ipStr := apIP.String()
715+
716+ log.Printf("starting AP with name %s, subnet %s/%d",
717+ s.config.Wifi.Ssid, ipStr, maskedBits)
718+ s.apSettingsPath, s.apActiveConnPath, err =
719+ s.cnetman.CreateAccessPoint(s.config.Wifi.Ssid,
720+ s.config.Wifi.Passphrase, s.config.Wifi.Interface,
721+ ipStr, uint(maskedBits))
722+ if err != nil {
723+ log.Printf("Cannot create AP: %v", err)
724+ return
725+ }
726+
727+ s.startManagementServer()
728+
729+ // DHCP range is X.X.X.10 - X.X.X.15
730+ dhcpFirst := make(net.IP, net.IPv4len)
731+ dhcpLast := make(net.IP, net.IPv4len)
732+ copy(dhcpFirst, subnet.IP)
733+ copy(dhcpLast, subnet.IP)
734+ dhcpFirst[3] = 10
735+ dhcpLast[3] = 15
736+ dhcpRange := dhcpFirst.String() + "," + dhcpLast.String()
737+ // Run DNS and DHCP server. The DNS will always return the interface
738+ // address for any query, redirecting all request to our server.
739+ // With "--user=root --group=" we get the right code path so we avoid
740+ // problems when changing user/group from the snap.
741+ s.dnsmasqCmd =
742+ exec.Command("dnsmasq", "--conf-file", "--no-hosts",
743+ "--keep-in-foreground", "--bind-interfaces",
744+ "--except-interface=lo", "--clear-on-reload",
745+ "--strict-order", "--listen-address="+ipStr,
746+ "--dhcp-range="+dhcpRange+",60m", "--dhcp-lease-max=50",
747+ "--address=/#/"+ipStr,
748+ "--pid-file=/tmp/dnsmasq-wlp3s0.pid",
749+ "--user=root", "--group=")
750+ log.Printf("Running %v", s.dnsmasqCmd)
751+ err = s.dnsmasqCmd.Start()
752 if err != nil {
753- return config, err
754+ log.Fatalf("Cannot run %v: %v", s.dnsmasqCmd, err)
755 }
756- err = json.Unmarshal(content, config)
757+
758+ // Redirect all http to our server
759+ s.iptblForwArgs = []string{"PREROUTING", "-p", "tcp", "-m", "tcp",
760+ "-s", subnet.String(),
761+ "--dport", "80", "-j", "DNAT",
762+ "--to-destination",
763+ ipStr + ":" + strconv.Itoa(s.config.Portal.Port)}
764+ iptblArgs := append([]string{"-t", "nat", "-A"}, s.iptblForwArgs...)
765+ iptblCmd := exec.Command("iptables", iptblArgs...)
766+ log.Printf("Running %v", iptblCmd)
767+ err = iptblCmd.Run()
768 if err != nil {
769- return config, err
770+ log.Printf("Error running %v: %v", iptblCmd, err)
771 }
772- return config, nil
773 }
774
775-// SetDefaults creates the run time configuration based on wifi-ap and the pre-config.json
776-// configuration file, if any. The configuration is returned with an error.
777-func (c *Client) SetDefaults(cw wifiap.Operations, config *PreConfig) error {
778+func (s *Service) exitManagingMode() {
779+ log.Printf("Exiting MANAGEMENT mode")
780+
781+ if s.dnsmasqCmd != nil {
782+ log.Printf("Stopping dnsmasq")
783+ s.dnsmasqCmd.Process.Signal(syscall.SIGTERM)
784+ s.dnsmasqCmd.Process.Wait()
785+ s.dnsmasqCmd = nil
786+ }
787+
788+ // Remove http redirection
789+ iptblArgs := append([]string{"-t", "nat", "-D"}, s.iptblForwArgs...)
790+ iptblCmd := exec.Command("iptables", iptblArgs...)
791+ log.Printf("Running %v", iptblCmd)
792+ err := iptblCmd.Run()
793 if err != nil {
794- log.Printf("SetDefaults: preconfig unmarshall errorr: %v", err)
795- }
796- ap, errShow := cw.Show()
797- if errShow != nil {
798- log.Printf("SetDefaults: wifi-ap.Show err: %v", errShow)
799- }
800- if ap["wifi.security-passphrase"] != config.Passphrase {
801- if len(config.Passphrase) > 0 {
802- err = cw.SetPassphrase(config.Passphrase)
803- log.Print("SetDefaults wifi-ap passphrase being set")
804- if err != nil {
805- log.Printf("SetDefaults: passphrase err: %v", err)
806- return err
807- }
808- }
809+ log.Printf("Error running %v: %v", iptblCmd, err)
810 }
811- if len(config.Password) > 0 {
812- fmt.Println("== wifi-connect/SetDefaults portal password being set")
813- _, err = utils.HashIt(config.Password)
814- if err != nil {
815- log.Printf("SetDefaults: password err: %v", err)
816- return err
817+
818+ s.cnetman.DeleteWifiConnections(netman.ApWifiConn)
819+ if err := s.cnetman.DisconnectDevice(s.devicePath); err != nil {
820+ log.Printf("Error when disconnecting device: %v", err)
821+ }
822+
823+ s.stopManagementServer()
824+}
825+
826+// event: NM is up event received
827+func (s *Service) netManUpEv(current serviceState) serviceState {
828+ log.Printf("NetworkManager is up")
829+ if current != stStarting {
830+ // Spurious signal?
831+ return current
832+ }
833+
834+ err := s.setDefaults()
835+ if err != nil {
836+ log.Printf("setDefaults error: %v", err)
837+ }
838+ s.cleanUpPrevRuns()
839+
840+ var state serviceState
841+ if s.config.ConfigWifiOnce &&
842+ utils.FlagFileExists(utils.WifiConfigDoneFlagFile) {
843+ state = stOperating
844+ s.enterOperatingMode()
845+ } else {
846+ // Wait some time on first run to let the wifi connect
847+ // TODO wait for DBus signal instead
848+ time.Sleep(10 * time.Second)
849+ if s.isConnectedToAnAP() {
850+ state = stOperating
851+ s.enterOperatingMode()
852+ } else {
853+ state = stManaging
854+ s.enterManagingMode()
855 }
856 }
857- if config.Operational {
858- log.Print("SetDefaults: operational portal is enabled")
859+ return state
860+}
861+
862+func (s *Service) stopServices(state serviceState) {
863+ switch state {
864+ case stManaging:
865+ s.exitManagingMode()
866+ case stOperating:
867+ s.exitOperatingMode()
868+ }
869+}
870+
871+// event: NM is down event received
872+func (s *Service) netManDownEv(current serviceState) serviceState {
873+ log.Printf("NetworkManager is down")
874+ s.stopServices(current)
875+ return stStarting
876+}
877+
878+// event: user sends command to connect
879+func (s *Service) userConnectEv(current serviceState,
880+ ssid, password string) serviceState {
881+
882+ if current != stManaging {
883+ log.Printf("Connect event, but not in managing mode")
884+ return current
885+ }
886+
887+ // connectToAp exits managing mode on success
888+ err := s.connectToAp(ssid, password)
889+ if err != nil {
890+ log.Printf("Failed to connect: %v", err)
891+ return current
892 }
893- if config.NoResetCreds {
894- log.Print("SetDefaults: reset creds requirement is disabled")
895+
896+ utils.WriteFlagFile(utils.WifiConfigDoneFlagFile)
897+ s.enterOperatingMode()
898+
899+ return stOperating
900+}
901+
902+// event: user sends command to refresh wifi list
903+func (s *Service) userRefreshEv(current serviceState) {
904+ if current != stManaging {
905+ log.Printf("Refresh event, but not in managing mode")
906 }
907- return nil
908+
909+ s.doWifiScan()
910+}
911+
912+// event: user sends command to disconnect
913+func (s *Service) userDisconnectEv(current serviceState) serviceState {
914+ if current != stOperating {
915+ log.Printf("Disconnect event, but not in operating mode")
916+ return current
917+ }
918+
919+ s.exitOperatingMode()
920+ utils.RemoveFlagFile(utils.WifiConfigDoneFlagFile)
921+ s.enterManagingMode()
922+
923+ return stManaging
924+}
925+
926+// Start state machine
927+func (s *Service) Start() {
928+
929+ chDbus := make(chan *dbus.Signal, 30)
930+ defer close(chDbus)
931+ if err := s.cnetman.RegisterForNameOwnerChanged(chDbus); err != nil {
932+ log.Printf("Error: cannot register for NameOwnerChanged: %v", err)
933+ os.Exit(1)
934+ }
935+ defer s.cnetman.UnregisterForNameOwnerChanged(chDbus)
936+
937+ state := stStarting
938+ nmUp, err := s.cnetman.CheckNetworkManagerRunning()
939+ if nmUp == false {
940+ log.Printf("Waiting for NetworkManager service: %v", err)
941+ } else {
942+ state = s.netManUpEv(state)
943+ }
944+ spuriousSignal := false
945+
946+MainLoop:
947+ for {
948+ if spuriousSignal == false {
949+ log.Printf("State is %v", state)
950+ } else {
951+ spuriousSignal = false
952+ }
953+ select {
954+ case uEv := <-s.chUserEvs:
955+ log.Print("Event received: ", uEv.EvType)
956+ switch uEv.EvType {
957+ case server.UserEventConnect:
958+ state = s.userConnectEv(state,
959+ uEv.Params[server.UeConnSsid],
960+ uEv.Params[server.UeConnPassword])
961+ case server.UserEventRefresh:
962+ s.userRefreshEv(state)
963+ case server.UserEventDisconnect:
964+ state = s.userDisconnectEv(state)
965+ }
966+ case sig := <-s.chSigs:
967+ log.Printf("Exiting on %v signal", sig)
968+ break MainLoop
969+ case dbusSig := <-chDbus:
970+ if dbusSig.Name == "org.freedesktop.DBus.NameOwnerChanged" &&
971+ dbusSig.Body[0].(string) == "org.freedesktop.NetworkManager" {
972+ // Check if there is a current owner
973+ if dbusSig.Body[2].(string) == "" {
974+ state = s.netManDownEv(state)
975+ } else {
976+ state = s.netManUpEv(state)
977+ }
978+ } else {
979+ // We do not want to print the state for signals
980+ // that we are not interested in...
981+ spuriousSignal = true
982+ }
983+ }
984+ }
985+
986+ s.stopServices(state)
987 }
988diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go
989index 07fe109..2af53e8 100644
990--- a/daemon/daemon_test.go
991+++ b/daemon/daemon_test.go
992@@ -18,225 +18,13 @@
993 package daemon
994
995 import (
996- "fmt"
997- "io/ioutil"
998- "net/http"
999- "os"
1000- "strings"
1001 "testing"
1002-
1003- "launchpad.net/wifi-connect/utils"
1004 )
1005
1006-func TestManualFlagPath(t *testing.T) {
1007- mfpInit := "mfp"
1008- client := GetClient()
1009- client.SetManualFlagPath(mfpInit)
1010- mfp := client.GetManualFlagPath()
1011- if mfp != mfpInit {
1012- t.Errorf("ManualFlag path should be %s but is %s\n", mfpInit, mfp)
1013- }
1014-}
1015-
1016-func TestWaitFlagPath(t *testing.T) {
1017- wfpInit := "wfp"
1018- client := GetClient()
1019- client.SetWaitFlagPath(wfpInit)
1020- wfp := client.GetWaitFlagPath()
1021- if wfp != wfpInit {
1022- t.Errorf("WaitFlag path should be %s but is %s", wfp, wfpInit)
1023- }
1024-}
1025-
1026-func TestState(t *testing.T) {
1027- client := GetClient()
1028- client.SetState(MANAGING)
1029- client.SetPreviousState(STARTING)
1030- ps := client.GetPreviousState()
1031- if ps != STARTING {
1032- t.Errorf("Previous state should be %d but is %d", STARTING, ps)
1033- }
1034- s := client.GetState()
1035- if s != MANAGING {
1036- t.Errorf("State should be %d but is %d", MANAGING, s)
1037- }
1038-}
1039-
1040-func TestCheckWaitApConnect(t *testing.T) {
1041- client := GetClient()
1042- wfp := "thispathnevershouldexist"
1043- client.SetWaitFlagPath(wfp)
1044- if client.CheckWaitApConnect() {
1045- t.Errorf("CheckWaitApConnect returns true but should return false")
1046- }
1047- wfp = "../static/tests/waitFile"
1048- client.SetWaitFlagPath(wfp)
1049- if !client.CheckWaitApConnect() {
1050- t.Errorf("CheckWaitApConnect returns false but should return true")
1051- }
1052-}
1053-
1054-func TestManualMode(t *testing.T) {
1055- mfp := "thisfileshouldneverexist"
1056- client := GetClient()
1057- client.SetManualFlagPath(mfp)
1058- client.SetState(MANUAL)
1059- if client.ManualMode() {
1060- t.Errorf("ManualMode returns true but should return false")
1061- }
1062- if client.GetState() != STARTING {
1063- t.Errorf("ManualMode should set state to STARTING when not in manual mode but does not")
1064- }
1065- mfp = "../static/tests/manualMode"
1066- client.SetManualFlagPath(mfp)
1067- client.SetState(STARTING)
1068- if !client.ManualMode() {
1069- t.Errorf("ManualMode returns false but should return true")
1070- }
1071- if client.GetState() != MANUAL {
1072- t.Errorf("ManualMode should set state to MANUAL when in manual mode but does not")
1073- }
1074-}
1075-
1076-type mockWifiap struct{}
1077-
1078-func (mock *mockWifiap) Do(req *http.Request) (*http.Response, error) {
1079- fmt.Println("==== MY do called")
1080- url := req.URL.String()
1081- if url != "http://unix/v1/configuration" {
1082- return nil, fmt.Errorf("Not valid request URL: %v", url)
1083- }
1084-
1085- if req.Method != "GET" {
1086- return nil, fmt.Errorf("Method is not valid. Expected GET, got %v", req.Method)
1087- }
1088-
1089- rawBody := `{"result":{
1090- "debug":false,
1091- "dhcp.lease-time": "12h",
1092- "dhcp.range-start": "10.0.60.2",
1093- "dhcp.range-stop": "10.0.60.199",
1094- "disabled": true,
1095- "share.disabled": false,
1096- "share-network-interface": "tun0",
1097- "wifi-address": "10.0.60.1",
1098- "wifi.channel": "6",
1099- "wifi.hostapd-driver": "nl80211",
1100- "wifi.interface": "wlan0",
1101- "wifi.interface-mode": "direct",
1102- "wifi.netmask": "255.255.255.0",
1103- "wifi.operation-mode": "g",
1104- "wifi.security": "
1105- "wifi.security-passphrase": "passphrase123",
1106- "wifi.ssid": "AP"},"status":"OK","status-code":200,""sync"}`
1107-
1108- response := http.Response{
1109- StatusCode: 200,
1110- Status: "200 OK",
1111- Body: ioutil.NopCloser(strings.NewReader(rawBody)),
1112- }
1113-
1114- return &response, nil
1115-}
1116-
1117-func (mock *mockWifiap) Show() (map[string]interface{}, error) {
1118- wifiAp := make(map[string]interface{})
1119- wifiAp["wifi.security-passphrase"] = "randompassphrase"
1120- return wifiAp, nil
1121-}
1122-
1123-func (mock *mockWifiap) Enabled() (bool, error) {
1124- return true, nil
1125-}
1126-
1127-func (mock *mockWifiap) Enable() error {
1128- return nil
1129-}
1130-func (mock *mockWifiap) Disable() error {
1131- return nil
1132-}
1133-func (mock *mockWifiap) SetSsid(s string) error {
1134- return nil
1135-}
1136-
1137-func (mock *mockWifiap) SetPassphrase(p string) error {
1138- return nil
1139-}
1140-
1141-func (mock *mockWifiap) Set(map[string]interface{}) error {
1142- return nil
1143-}
1144-
1145-func TestLoadPreConfig(t *testing.T) {
1146- PreConfigFile = "../static/tests/pre-config0.json"
1147- config, err := LoadPreConfig()
1148- if err != nil {
1149- t.Errorf("Unexpected error using LoadPreConfig: %s", err)
1150- }
1151- if config.Passphrase != "abcdefghijklmnop" {
1152- t.Errorf("Passphrase of %s expected but got %s:", "abcdefghijklmnop", config.Passphrase)
1153- }
1154- if !config.Operational {
1155- t.Errorf("portal.operational was set to false but the loaded config is %t", config.Operational)
1156- }
1157- if !config.NoResetCreds {
1158- t.Errorf("portal.no-reset-creds was set to true but the loaded config is %t", config.NoResetCreds)
1159- }
1160-
1161-}
1162-
1163-func TestSetDefaults(t *testing.T) {
1164- client := GetClient()
1165- PreConfigFile = "../static/tests/pre-config0.json"
1166- hfp := "/tmp/hash"
1167- if _, err := os.Stat(hfp); err == nil {
1168- err = os.Remove(hfp)
1169- if err != nil {
1170- t.Errorf("Could not remove previous file version")
1171- }
1172- }
1173- config, _ := LoadPreConfig()
1174- utils.SetHashFile(hfp)
1175- client.SetDefaults(&mockWifiap{}, config)
1176- expectedPassphrase := "abcdefghijklmnop"
1177- expectedPassword := "qwerzxcv"
1178- if config.Passphrase != expectedPassphrase {
1179- t.Errorf("SetDefaults: Preconfig passphrase should be %s but is %s", expectedPassphrase, config.Passphrase)
1180- }
1181- if os.IsNotExist(err) {
1182- t.Errorf("SetDefaults should have created %s but did not", hfp)
1183- }
1184- res, _ := utils.MatchingHash(expectedPassword)
1185- if !res {
1186- t.Errorf("SetDefaults: Preconfig password hash did not match actual")
1187- }
1188- if !config.Operational {
1189- t.Errorf("SetDefaults: Preconfig portal.operational should be true (set) but is %t", config.Operational)
1190- }
1191- if !config.NoResetCreds {
1192- t.Errorf("SetDefaults: Preconfig portal.no-reset-creds should be true (set) but is %t", config.NoResetCreds)
1193- }
1194-
1195- if _, err := os.Stat(hfp); err == nil {
1196- err = os.Remove(hfp)
1197- if err != nil {
1198- t.Errorf("Could not remove previous file version")
1199- }
1200- }
1201- PreConfigFile = "../static/tests/pre-config1.json"
1202- config, _ = LoadPreConfig()
1203- client.SetDefaults(&mockWifiap{}, config)
1204- if len(config.Passphrase) > 0 {
1205- t.Errorf("SetDefaults: Preconfig passphrase was not set but is %s", config.Passphrase)
1206- }
1207- res2, _ := utils.MatchingHash(expectedPassword)
1208- if res2 {
1209- t.Errorf("SetDefaults: Preconfig password was not set, but the hash matched")
1210- }
1211- if config.Operational {
1212- t.Errorf("SetDefaults: Preconfig portal.no-operational should be false (unset) but is %t", config.Operational)
1213- }
1214- if config.NoResetCreds {
1215- t.Errorf("SetDefaults: Preconfig portal.no-reset-creds should be false (unnset) but is %t", config.NoResetCreds)
1216- }
1217+func TestGetService(t *testing.T) {
1218+ // cfg := utils.Config{Wifi: utils.WifiConfig{Ssid: "myssid"}, ConfigWifiOnce: false}
1219+ // service := GetService(&cfg)
1220+ // if service == nil {
1221+ // t.Error("Cannot get service")
1222+ // }
1223 }
1224diff --git a/dependencies.tsv b/dependencies.tsv
1225index e5f04d8..e9c02fa 100644
1226--- a/dependencies.tsv
1227+++ b/dependencies.tsv
1228@@ -1,7 +1,7 @@
1229-github.com/go-yaml/yaml git cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b 2017-04-07T18:21:22Z
1230-github.com/godbus/dbus git fe0e1d54eaeda11a9979659a8d32f459e88bee75 2017-03-03T19:03:06Z
1231-github.com/gorilla/context git 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 2016-08-17T18:46:32Z
1232-github.com/gorilla/mux git 757bef944d0f21880861c2dd9c871ca543023cba 2016-09-20T23:08:13Z
1233+github.com/godbus/dbus git 06fc4b473149e499166adbb9e31c7365a8ea146f 2020-02-14T23:16:04Z
1234+github.com/gorilla/mux git 75dcda0896e109a2a22c9315bca3bb21b87b2ba5 2020-01-12T19:17:43Z
1235+golang.org/x/crypto git 8b5121be2f68d8fc40bb06467003bdde1040a094 2020-01-24T22:56:46Z
1236+golang.org/x/net git 13f9640d40b9cc418fb53703dfbd177679788ceb 2019-10-04T11:05:52Z
1237+golang.org/x/text git 342b2e1fbaa52c93f31447ad2c6abc048c63e475 2019-04-25T21:42:06Z
1238 github.com/presotto/go-mdns-sd git 343772046ec1b3840b8591799a7bbcc68ea47b4b 2015-11-03T06:16:58Z
1239-github.com/reiver/go-oi git 431c83978379297f04f85f6eb94f129f25ab741d 2016-03-25T06:16:15Z
1240-github.com/reiver/go-telnet git 6b696f32801a8f8dd07947f1e1fdb1a7dc4766ff 2016-03-30T05:09:16Z
1241+gopkg.in/check.v1 git 4f90aeace3a26ad7021961c297b22c42160c7b25 2016-01-05T16:49:36Z
1242diff --git a/hooks/configure.go b/hooks/configure.go
1243index 80cfe91..7775eab 100644
1244--- a/hooks/configure.go
1245+++ b/hooks/configure.go
1246@@ -20,15 +20,14 @@
1247 package main
1248
1249 import (
1250- "encoding/json"
1251- "io/ioutil"
1252 "os"
1253 "os/exec"
1254 "path/filepath"
1255+ "strconv"
1256 "strings"
1257 "time"
1258
1259- "launchpad.net/wifi-connect/daemon"
1260+ "launchpad.net/wifi-connect/utils"
1261 )
1262
1263 // logFile is the file into which log msgs go. Needed because snap hook stdout
1264@@ -65,7 +64,6 @@ func (g *Get) SnapGet(key string) (string, error) {
1265 return "", err
1266 }
1267 return strings.TrimSpace(string(out)), nil
1268-
1269 }
1270
1271 // snapGetStr wraps SnapGet for string types and verifies the snap var is valid
1272@@ -75,12 +73,28 @@ func (c *Client) snapGetStr(key string, target *string) {
1273 return
1274 }
1275 if len(val) == 0 {
1276- log("configure error: key %s exists but has zero length" + key)
1277+ log("configure error: key " + key + " exists but has zero length")
1278 return
1279 }
1280 *target = val
1281 }
1282
1283+func (c *Client) snapGetInt(key string, target *int) {
1284+ val, err := c.getter.SnapGet(key)
1285+ if err != nil {
1286+ return
1287+ }
1288+ if len(val) == 0 {
1289+ log("configure error: key " + key + " exists but has zero length")
1290+ return
1291+ }
1292+ *target, err = strconv.Atoi(val)
1293+ if err != nil {
1294+ log("bad integer: " + err.Error())
1295+ *target = 0
1296+ }
1297+}
1298+
1299 // snapGetBool wraps SnapGet for bool types and verifies the snap var is valid
1300 func (c *Client) snapGetBool(key string, target *bool) {
1301 val, err := c.getter.SnapGet(key)
1302@@ -88,7 +102,7 @@ func (c *Client) snapGetBool(key string, target *bool) {
1303 return
1304 }
1305 if len(val) == 0 {
1306- log("configure error: key %s exists but has zero length" + key)
1307+ log("configure error: key " + key + " exists but has zero length")
1308 return
1309 }
1310
1311@@ -109,20 +123,28 @@ func log(msg string) {
1312 func main() {
1313 log("Configure hook running")
1314 client := GetClient()
1315- preConfig := &daemon.PreConfig{}
1316- client.snapGetStr("wifi.security-passphrase", &preConfig.Passphrase)
1317- client.snapGetStr("portal.password", &preConfig.Password)
1318- client.snapGetBool("portal.operational", &preConfig.Operational)
1319- client.snapGetBool("portal.no-reset-creds", &preConfig.NoResetCreds)
1320
1321- b, err := json.Marshal(preConfig)
1322- if err != nil {
1323- log("Marshall error: " + err.Error())
1324- return
1325- }
1326+ config, _ := utils.LoadConfig()
1327+
1328+ client.snapGetBool("config-wifi-once", &config.ConfigWifiOnce)
1329+
1330+ client.snapGetStr("wifi.ssid", &config.Wifi.Ssid)
1331+ client.snapGetStr("wifi.security-passphrase", &config.Wifi.Passphrase)
1332+ client.snapGetStr("wifi.interface", &config.Wifi.Interface)
1333+ client.snapGetStr("wifi.country-code", &config.Wifi.CountryCode)
1334+ client.snapGetInt("wifi.channel", &config.Wifi.Channel)
1335+ client.snapGetStr("wifi.operation-mode", &config.Wifi.OperationMode)
1336+ client.snapGetStr("wifi.subnet", &config.Wifi.Subnet)
1337+
1338+ client.snapGetStr("portal.password", &config.Portal.Password)
1339+ client.snapGetInt("portal.port", &config.Portal.Port)
1340+ client.snapGetBool("portal.no-reset-creds", &config.Portal.NoResetCredentials)
1341+ client.snapGetBool("portal.operational", &config.Portal.Operational)
1342+
1343+ utils.WriteConfigFile(config)
1344
1345- err = ioutil.WriteFile(daemon.PreConfigFile, b, 0644)
1346+ err := exec.Command("snapctl", "restart", os.Getenv("SNAP_NAME")).Run()
1347 if err != nil {
1348- log("configure hook: " + err.Error())
1349+ log("Error while re-starting the service: " + err.Error())
1350 }
1351 }
1352diff --git a/netman/dbus.go b/netman/dbus.go
1353index 55564bd..b6ce186 100644
1354--- a/netman/dbus.go
1355+++ b/netman/dbus.go
1356@@ -20,31 +20,45 @@ package netman
1357 import (
1358 "errors"
1359 "fmt"
1360- "io"
1361 "log"
1362- "os"
1363 "strings"
1364 "time"
1365
1366 "github.com/godbus/dbus"
1367 )
1368
1369+// WifiConnType enum for types of wifi connections
1370+type WifiConnType int
1371+
1372+// All wifi conns, just STA or just AP
1373+const (
1374+ AllWifiConn WifiConnType = iota
1375+ StaWifiConn
1376+ ApWifiConn
1377+)
1378+
1379 // Operations defines operations for this package client
1380 type Operations interface {
1381 GetDevices() []string
1382- GetWifiDevices(devices []string) []string
1383+ GetWifiDevices() []string
1384+ GetIfaceFromDevicePath(connSettPath string) (string, error)
1385+ GetDevicePathFromIface(iface string) (string, error)
1386 GetAccessPoints(devices []string, ap2device map[string]string) []string
1387- ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error
1388- Ssids() ([]SSID, map[string]string, map[string]string)
1389+ ConnectToAp(ssid, password, device string) (string, string, error)
1390+ Ssids(devicePath string) ([]SSID, map[string]string, map[string]string)
1391 Connected(devices []string) bool
1392- ConnectedWifi(wifiDevices []string) bool
1393- DeleteWifiConnections()
1394+ ConnectedToAnAP(wifiDevice string) bool
1395+ DisconnectDevice(device string) error
1396+ DeleteWifiConnection(connSett string) error
1397+ DeleteWifiConnections(wifiType int)
1398 DisconnectWifi(wifiDevices []string) int
1399 SetIfaceManaged(iface string, state bool, devices []string) string
1400 WifisManaged(wifiDevices []string) (map[string]string, error)
1401- Unmanage() error
1402- Manage() error
1403- ScanAndWriteSsidsToFile(filepath string) bool
1404+ Unmanage(wifiIface string) error
1405+ Manage(wifiIface string) error
1406+ CreateAccessPoint(ssid string, password string, wifiPath string,
1407+ ipAddress string, prefix uint) (string, string, error)
1408+ ActivateConnection(devicePath string, connPath string) (string, error)
1409 }
1410
1411 // Client type to support unit test mock and runtime execution
1412@@ -64,16 +78,20 @@ type Objecter interface {
1413 Object(dest string, path dbus.ObjectPath) dbus.BusObject
1414 }
1415
1416-// Object is the mock implementation of the godbus Object function
1417-func (d *DbusClient) Object(dest string, path dbus.ObjectPath) dbus.BusObject {
1418- obj := d.Connection.Object(dest, path)
1419- return obj
1420+func getSystemBus() *dbus.Conn {
1421+ conn, err := dbus.SystemBus()
1422+ if err != nil {
1423+ log.Printf("Error: Failed to connect to system bus: %v", err)
1424+ panic(1)
1425+ }
1426+ return conn
1427 }
1428
1429 // DefaultClient is the runtime client object
1430 func DefaultClient() *Client {
1431 conn := getSystemBus()
1432- obj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
1433+ obj := conn.Object("org.freedesktop.NetworkManager",
1434+ "/org/freedesktop/NetworkManager")
1435 return &Client{
1436 dbusClient: DbusClient{
1437 test: false,
1438@@ -102,13 +120,10 @@ func setObject(c *Client, iface string, path dbus.ObjectPath) {
1439
1440 // GetDevices returns NetMan (NetworkManager) devices
1441 func (c *Client) GetDevices() []string {
1442- if !c.dbusClient.test {
1443- c.dbusClient.Connection = getSystemBus()
1444- }
1445- c.dbusClient.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
1446 setObject(c, "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
1447 var devices []string
1448- err := c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.GetAllDevices", 0).Store(&devices)
1449+ err := c.dbusClient.BusObj.Call(
1450+ "org.freedesktop.NetworkManager.GetAllDevices", 0).Store(&devices)
1451 if err != nil {
1452 log.Printf("Error getting devices: %v", err)
1453 }
1454@@ -116,22 +131,20 @@ func (c *Client) GetDevices() []string {
1455 }
1456
1457 // GetWifiDevices returns wifi NetMan devices
1458-func (c *Client) GetWifiDevices(devices []string) []string {
1459+func (c *Client) GetWifiDevices() []string {
1460+ devices := c.GetDevices()
1461 var wifiDevices []string
1462 for _, d := range devices {
1463 objPath := dbus.ObjectPath(d)
1464- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1465 setObject(c, "org.freedesktop.NetworkManager", objPath)
1466- deviceType, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.DeviceType")
1467- if err2 != nil {
1468- log.Printf("Error getting wifi devices: %v", err2)
1469+ deviceType, err := c.dbusClient.BusObj.GetProperty(
1470+ "org.freedesktop.NetworkManager.Device.DeviceType")
1471+ if err != nil {
1472+ log.Printf("Error getting DeviceType: %v", err)
1473 continue
1474 }
1475 var wifiType uint32
1476 wifiType = 2
1477- if deviceType.Value() == nil {
1478- break
1479- }
1480 if deviceType.Value() != wifiType {
1481 continue
1482 }
1483@@ -140,15 +153,52 @@ func (c *Client) GetWifiDevices(devices []string) []string {
1484 return wifiDevices
1485 }
1486
1487-//GetAccessPoints returns NetMan known external APs
1488+// GetIfaceFromDevicePath gets interface name from NM's dbus device path
1489+func (c *Client) GetIfaceFromDevicePath(connSettPath string) (string, error) {
1490+ setObject(c, "org.freedesktop.NetworkManager",
1491+ dbus.ObjectPath(connSettPath))
1492+ iface, err := c.dbusClient.BusObj.GetProperty(
1493+ "org.freedesktop.NetworkManager.Device.Interface")
1494+ if err != nil {
1495+ return "", err
1496+ }
1497+ return iface.Value().(string), err
1498+}
1499+
1500+// GetDevicePathFromIface gets NM's DBus object path for a device from the interface name
1501+func (c *Client) GetDevicePathFromIface(iface string) (string, error) {
1502+ devPaths := c.GetDevices()
1503+ matchDevPath := ""
1504+ for _, devPath := range devPaths {
1505+ setObject(c, "org.freedesktop.NetworkManager",
1506+ dbus.ObjectPath(devPath))
1507+ devIface, err := c.dbusClient.BusObj.GetProperty(
1508+ "org.freedesktop.NetworkManager.Device.Interface")
1509+ if err != nil {
1510+ log.Printf("Error getting wifi devices: %v", err)
1511+ continue
1512+ }
1513+ if devIface.Value().(string) == iface {
1514+ matchDevPath = devPath
1515+ break
1516+ }
1517+ }
1518+ if matchDevPath == "" {
1519+ return "", errors.New("Interface not found")
1520+ }
1521+ return matchDevPath, nil
1522+}
1523+
1524+// GetAccessPoints returns NetMan known external APs
1525 func (c *Client) GetAccessPoints(devices []string, ap2device map[string]string) []string {
1526 var APs []string
1527 for _, d := range devices {
1528 var aps []string
1529 objPath := dbus.ObjectPath(d)
1530- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1531 setObject(c, "org.freedesktop.NetworkManager", objPath)
1532- err := c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Device.Wireless.GetAllAccessPoints", 0).Store(&aps)
1533+ err := c.dbusClient.BusObj.Call("org.freedesktop."+
1534+ "NetworkManager.Device.Wireless.GetAllAccessPoints",
1535+ 0).Store(&aps)
1536 if err != nil {
1537 log.Printf("Error getting accesspoints: %v", err)
1538 continue
1539@@ -175,9 +225,9 @@ func (c *Client) getSsids(APs []string, ssid2ap map[string]string) []SSID {
1540 var SSIDs []SSID
1541 for _, ap := range APs {
1542 objPath := dbus.ObjectPath(ap)
1543- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1544 setObject(c, "org.freedesktop.NetworkManager", objPath)
1545- ssid, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.AccessPoint.Ssid")
1546+ ssid, err := c.dbusClient.BusObj.GetProperty(
1547+ "org.freedesktop.NetworkManager.AccessPoint.Ssid")
1548 if err != nil {
1549 log.Printf("Error getting accesspoint's ssids: %v", err)
1550 continue
1551@@ -196,60 +246,43 @@ func (c *Client) getSsids(APs []string, ssid2ap map[string]string) []SSID {
1552 Ssid := SSID{Ssid: ssidStr, ApPath: ap}
1553 SSIDs = append(SSIDs, Ssid)
1554 ssid2ap[strings.TrimSpace(ssidStr)] = ap
1555- //TODO: exclude ssid of device's own AP (the wifi-ap one)
1556 }
1557 return SSIDs
1558 }
1559
1560-// ConnectAp attempts to Connect to an external AP
1561-func (c *Client) ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error {
1562- inner1 := make(map[string]dbus.Variant)
1563- inner1["security"] = dbus.MakeVariant("802-11-wireless-security")
1564-
1565- inner2 := make(map[string]dbus.Variant)
1566- inner2["key-mgmt"] = dbus.MakeVariant("wpa-psk")
1567- inner2["psk"] = dbus.MakeVariant(p)
1568+// ConnectToAp attempts to connect to an external AP
1569+func (c *Client) ConnectToAp(ssid, password, device string) (string, string, error) {
1570
1571- outer := make(map[string]map[string]dbus.Variant)
1572- outer["802-11-wireless"] = inner1
1573- outer["802-11-wireless-security"] = inner2
1574+ wirelessSett := make(map[string]dbus.Variant)
1575+ wirelessSett["ssid"] = dbus.MakeVariant([]byte(ssid))
1576
1577- c.dbusClient.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
1578- setObject(c, "org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager"))
1579- c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.AddAndActivateConnection", 0, outer, dbus.ObjectPath(ap2device[ssid2ap[ssid]]), dbus.ObjectPath(ssid2ap[ssid]))
1580+ wirelessSecSett := make(map[string]dbus.Variant)
1581+ wirelessSecSett["key-mgmt"] = dbus.MakeVariant("wpa-psk")
1582+ wirelessSecSett["psk"] = dbus.MakeVariant(password)
1583
1584- // loop until connected or until max loops
1585- trying := true
1586- idx := -1
1587- for trying {
1588- idx++
1589- time.Sleep(1000 * time.Millisecond)
1590- if c.Connected(c.GetWifiDevices(c.GetDevices())) {
1591- return nil
1592- }
1593- if idx == 19 {
1594- return errors.New("wifi-connect: cannot connect to AP")
1595- }
1596- }
1597- return nil
1598-}
1599+ settings := make(map[string]map[string]dbus.Variant)
1600+ settings["802-11-wireless"] = wirelessSett
1601+ settings["802-11-wireless-security"] = wirelessSecSett
1602
1603-func getSystemBus() *dbus.Conn {
1604- conn, err := dbus.SystemBus()
1605+ setObject(c, "org.freedesktop.NetworkManager",
1606+ dbus.ObjectPath("/org/freedesktop/NetworkManager"))
1607+ var connPath, actConnPath string
1608+ err := c.dbusClient.BusObj.Call(
1609+ "org.freedesktop.NetworkManager.AddAndActivateConnection",
1610+ 0, settings, dbus.ObjectPath(device),
1611+ dbus.ObjectPath("/")).Store(&connPath, &actConnPath)
1612 if err != nil {
1613- log.Printf("Error: Failed to connect to system bus: %v", err)
1614- panic(1)
1615+ return "", "", err
1616 }
1617- return conn
1618+
1619+ return connPath, actConnPath, c.WaitConnectionActivated(actConnPath)
1620 }
1621
1622 // Ssids returns known SSIDs
1623-func (c *Client) Ssids() ([]SSID, map[string]string, map[string]string) {
1624+func (c *Client) Ssids(devicePath string) ([]SSID, map[string]string, map[string]string) {
1625 ap2device := make(map[string]string)
1626 ssid2ap := make(map[string]string)
1627- devices := c.GetDevices()
1628- wifiDevices := c.GetWifiDevices(devices)
1629- APs := c.GetAccessPoints(wifiDevices, ap2device)
1630+ APs := c.GetAccessPoints([]string{devicePath}, ap2device)
1631 SSIDs := c.getSsids(APs, ssid2ap)
1632 return SSIDs, ap2device, ssid2ap
1633 }
1634@@ -258,20 +291,22 @@ func (c *Client) Ssids() ([]SSID, map[string]string, map[string]string) {
1635 func (c *Client) Connected(devices []string) bool {
1636 for _, d := range devices {
1637 objPath := dbus.ObjectPath(d)
1638- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1639 setObject(c, "org.freedesktop.NetworkManager", objPath)
1640- dType, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.DeviceType")
1641+ dType, err := c.dbusClient.BusObj.
1642+ GetProperty("org.freedesktop.NetworkManager.Device.DeviceType")
1643 if err != nil {
1644 log.Printf("Error getting device type: %v", err)
1645 continue
1646 }
1647- state, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.State")
1648+ state, err2 := c.dbusClient.BusObj.
1649+ GetProperty("org.freedesktop.NetworkManager.Device.State")
1650 if err2 != nil {
1651 log.Printf("Error getting device state: %v", err2)
1652 continue
1653 }
1654 // only handle eth and wifi device type
1655- if dbus.Variant.Value(dType) != uint32(1) && dbus.Variant.Value(dType) != uint32(2) {
1656+ if dbus.Variant.Value(dType) != uint32(1) &&
1657+ dbus.Variant.Value(dType) != uint32(2) {
1658 continue
1659 }
1660 if dbus.Variant.Value(state) == uint32(100) {
1661@@ -281,75 +316,180 @@ func (c *Client) Connected(devices []string) bool {
1662 return false
1663 }
1664
1665-// ConnectedWifi checks if any passed wifi devices are connected
1666-func (c *Client) ConnectedWifi(wifiDevices []string) bool {
1667- for _, d := range wifiDevices {
1668- objPath := dbus.ObjectPath(d)
1669- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1670- setObject(c, "org.freedesktop.NetworkManager", objPath)
1671- state, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.State")
1672- if err != nil {
1673- log.Printf("Error getting device state: %v", err)
1674- continue
1675- }
1676- if dbus.Variant.Value(state) == uint32(100) {
1677- return true
1678- }
1679+// ConnectedToAnAP checks if the passed device is connected to an AP. If the
1680+// device is itself an AP, we do not consider that a connection.
1681+func (c *Client) ConnectedToAnAP(wifiDevice string) bool {
1682+ objPath := dbus.ObjectPath(wifiDevice)
1683+ setObject(c, "org.freedesktop.NetworkManager", objPath)
1684+
1685+ // GetAppliedConnection will return the current active
1686+ // connection for the device, or nothing if the device is not
1687+ // active. Checking for existence of "802-11-wireless" will
1688+ // rule out both non-wifi and non-active wifi devices.
1689+ settings := make(map[string]interface{})
1690+ var connID uint64
1691+ c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager."+
1692+ "Device.GetAppliedConnection", 0, uint32(0)).
1693+ Store(&settings, &connID)
1694+ wifiSett, ok := settings["802-11-wireless"].(map[string]interface{})
1695+ if !ok {
1696+ return false
1697 }
1698- return false
1699+ // If mode is not set, it is infrastructure mode
1700+ mode, ok := wifiSett["mode"].(string)
1701+ if !ok {
1702+ return true
1703+ }
1704+ if mode == "ap" {
1705+ // We ARE an AP, so not connected to one
1706+ return false
1707+ }
1708+ return true
1709+}
1710+
1711+// DeleteWifiConnection deletes a wifi connection using the object path to the
1712+// connetion settings TODO wait for Removed signal?
1713+func (c *Client) DeleteWifiConnection(connSett string) error {
1714+ setObject(c, "org.freedesktop.NetworkManager",
1715+ dbus.ObjectPath(connSett))
1716+
1717+ call := c.dbusClient.BusObj.Call(
1718+ "org.freedesktop.NetworkManager.Settings.Connection.Delete", 0)
1719+ return call.Err
1720 }
1721
1722-// DeleteWifiConnections deletes all wifi connections (type 802-11-wireless)
1723-func (c *Client) DeleteWifiConnections() {
1724+// DeleteWifiConnections deletes all wifi connections for a given type
1725+func (c *Client) DeleteWifiConnections(wifiType WifiConnType) {
1726 objPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")
1727- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1728 setObject(c, "org.freedesktop.NetworkManager", objPath)
1729
1730- //connect all connections
1731+ // get all connections
1732 var conns []string
1733- err := c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Settings.ListConnections", 0).Store(&conns)
1734+ err := c.dbusClient.BusObj.Call(
1735+ "org.freedesktop.NetworkManager.Settings.ListConnections", 0).
1736+ Store(&conns)
1737 if err != nil {
1738 log.Printf("Error getting connections: %v", err)
1739 }
1740
1741- //Delete all 802-11-wireless connections to clean netman cruft
1742+ // Delete 802-11-wireless connections for the given type
1743 for _, conn := range conns {
1744 objPath := dbus.ObjectPath(conn)
1745- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1746 setObject(c, "org.freedesktop.NetworkManager", objPath)
1747 settings := make(map[string]interface{})
1748- c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&settings)
1749- _, ok := settings["802-11-wireless"]
1750- if ok {
1751- objPath := dbus.ObjectPath(conn)
1752- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1753- setObject(c, "org.freedesktop.NetworkManager", objPath)
1754- c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Delete", 0)
1755+ c.dbusClient.BusObj.Call("org.freedesktop."+
1756+ "NetworkManager.Settings.Connection.GetSettings", 0).
1757+ Store(&settings)
1758+ wifiSett, ok := settings["802-11-wireless"].(map[string]interface{})
1759+ if !ok {
1760+ continue
1761+ }
1762+ isAp := false
1763+ mode, ok := wifiSett["mode"].(string)
1764+ if ok && mode == "ap" {
1765+ isAp = true
1766+ }
1767+ switch wifiType {
1768+ case StaWifiConn:
1769+ if isAp == true {
1770+ continue
1771+ }
1772+ case ApWifiConn:
1773+ if isAp == false {
1774+ continue
1775+ }
1776+ }
1777+ err = c.DeleteWifiConnection(conn)
1778+ if err != nil {
1779+ log.Printf("Error while deleting %s: %v", conn, err)
1780 }
1781 }
1782 }
1783
1784-// DisconnectWifi disconnects every interface passed. return shows number of disconnect calls made. Also deletes all 802-11-wireless connections as netman cruft we don't need.
1785-func (c *Client) DisconnectWifi(wifiDevices []string) int {
1786- c.DeleteWifiConnections()
1787- ran := 0
1788- for _, d := range wifiDevices {
1789- objPath := dbus.ObjectPath(d)
1790- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1791- setObject(c, "org.freedesktop.NetworkManager", objPath)
1792- c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Device.Disconnect", 0)
1793- ran++
1794+// DisconnectDevice disconnects a device
1795+func (c *Client) DisconnectDevice(device string) error {
1796+ objPath := dbus.ObjectPath(device)
1797+ setObject(c, "org.freedesktop.NetworkManager", objPath)
1798+ err := c.dbusClient.BusObj.Call(
1799+ "org.freedesktop.NetworkManager.Device.Disconnect", 0).Err
1800+ if err != nil {
1801+ if err.Error() == "This device is not active" {
1802+ return nil
1803+ }
1804+ return err
1805 }
1806- return ran
1807+
1808+ return c.waitDeviceDisconnect(device)
1809 }
1810
1811-// SetIfaceManaged sets passed device to be managed/unmanaged by network manager, return iface set, if any
1812+func (c *Client) waitDeviceDisconnect(device string) error {
1813+ rule := "type='signal',sender='org.freedesktop.NetworkManager'," +
1814+ "path='" + device + "'," +
1815+ "interface='org.freedesktop.NetworkManager.Device'," +
1816+ "member='StateChanged'"
1817+ setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
1818+ err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Err
1819+ if err != nil {
1820+ return err
1821+ }
1822+ defer func() {
1823+ setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
1824+ err := c.dbusClient.BusObj.Call(
1825+ "org.freedesktop.DBus.RemoveMatch", 0, rule).Err
1826+ if err != nil {
1827+ log.Printf("Error when calling RemoveMatch: %v", err)
1828+ }
1829+ }()
1830+
1831+ chSig := make(chan *dbus.Signal, 10)
1832+ defer close(chSig)
1833+ c.dbusClient.Connection.Signal(chSig)
1834+ defer c.dbusClient.Connection.RemoveSignal(chSig)
1835+
1836+ // Get current state, as we might have missed the signal while we were
1837+ // registering for it (the connection already existed).
1838+ devPath := dbus.ObjectPath(device)
1839+ setObject(c, "org.freedesktop.NetworkManager", devPath)
1840+ state, err := c.dbusClient.BusObj.GetProperty(
1841+ "org.freedesktop.NetworkManager.Device.State")
1842+ if err != nil {
1843+ return err
1844+ }
1845+ // NM_DEVICE_STATE_DISCONNECTED = 30
1846+ if state.Value() == 30 {
1847+ return nil
1848+ }
1849+
1850+SignalLoop:
1851+ for {
1852+ select {
1853+ case sig := <-chSig:
1854+ // Note that we need to make sure the signal comes from
1855+ // the right object, as we will receive here any signal
1856+ // to which the process has registered.
1857+ // NM_DEVICE_STATE_DISCONNECTED = 30
1858+ if sig.Path == devPath &&
1859+ sig.Name ==
1860+ "org.freedesktop.NetworkManager.Device.StateChanged" &&
1861+ sig.Body[0].(uint32) == 30 {
1862+ break SignalLoop
1863+ }
1864+ case <-time.After(20 * time.Second):
1865+ return fmt.Errorf("Timeout while waiting for disconnection")
1866+ }
1867+ }
1868+
1869+ return nil
1870+}
1871+
1872+// SetIfaceManaged sets passed device to be managed/unmanaged by network
1873+// manager, return iface set, if any
1874 func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) string {
1875 for _, d := range devices {
1876 objPath := dbus.ObjectPath(d)
1877- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1878 setObject(c, "org.freedesktop.NetworkManager", objPath)
1879- intface, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Interface")
1880+ intface, err2 := c.dbusClient.BusObj.GetProperty(
1881+ "org.freedesktop.NetworkManager.Device.Interface")
1882 if err2 != nil {
1883 log.Printf("Error in SetIfaceManaged() geting interface: %v", err2)
1884 return ""
1885@@ -357,9 +497,11 @@ func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) str
1886 if iface != intface.Value().(string) {
1887 continue
1888 }
1889- managed, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Managed")
1890+ managed, err := c.dbusClient.BusObj.GetProperty(
1891+ "org.freedesktop.NetworkManager.Device.Managed")
1892 if err != nil {
1893- log.Printf("Error in SetIfaceManaged() fetching device managed: %v", err)
1894+ log.Printf("Error in SetIfaceManaged() fetching device managed: %v",
1895+ err)
1896 return ""
1897 }
1898 switch state {
1899@@ -373,21 +515,26 @@ func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) str
1900 }
1901 }
1902
1903- c.dbusClient.BusObj.Call("org.freedesktop.DBus.Properties.Set", 0, "org.freedesktop.NetworkManager.Device", "Managed", dbus.MakeVariant(state))
1904+ c.dbusClient.BusObj.Call("org.freedesktop.DBus.Properties.Set",
1905+ 0, "org.freedesktop.NetworkManager.Device",
1906+ "Managed", dbus.MakeVariant(state))
1907 // loop until interface is in desired managed state or max iters reached
1908 idx := -1
1909 for {
1910 idx++
1911 time.Sleep(1000 * time.Millisecond)
1912- managedState, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.State")
1913+ managedState, err := c.dbusClient.BusObj.GetProperty(
1914+ "org.freedesktop.NetworkManager.Device.State")
1915 if err == nil {
1916 switch state {
1917 case true:
1918- if managedState.Value() == uint32(30) { //NM_DEVICE_STATE_DISCONNECTED
1919+ if managedState.Value() == uint32(30) {
1920+ //NM_DEVICE_STATE_DISCONNECTED
1921 return iface
1922 }
1923 case false:
1924- if managedState.Value() == uint32(10) { //NM_DEVICE_STATE_UNMANAGED
1925+ if managedState.Value() == uint32(10) {
1926+ //NM_DEVICE_STATE_UNMANAGED
1927 return iface
1928 }
1929 }
1930@@ -400,21 +547,25 @@ func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) str
1931 return "" //no iface state changed
1932 }
1933
1934-// WifisManaged returns map[iface]device of wifi iterfaces that are managed by network manager
1935+// WifisManaged returns map[iface]device of wifi interfaces that are
1936+// managed by network manager
1937 func (c *Client) WifisManaged(wifiDevices []string) (map[string]string, error) {
1938 ifaces := make(map[string]string)
1939 for _, d := range wifiDevices {
1940 objPath := dbus.ObjectPath(d)
1941- c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
1942 setObject(c, "org.freedesktop.NetworkManager", objPath)
1943- managed, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Managed")
1944+ managed, err := c.dbusClient.BusObj.GetProperty(
1945+ "org.freedesktop.NetworkManager.Device.Managed")
1946 if err != nil {
1947- log.Printf("Error in WifisManaged() getting device managed: %v", err)
1948+ log.Printf("Error in WifisManaged() getting device managed: %v",
1949+ err)
1950 return ifaces, err
1951 }
1952- iface, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Interface")
1953+ iface, err2 := c.dbusClient.BusObj.GetProperty(
1954+ "org.freedesktop.NetworkManager.Device.Interface")
1955 if err2 != nil {
1956- log.Printf("Error in WifisManaged() getting device interface: %v", err)
1957+ log.Printf("Error in WifisManaged() getting device interface: %v",
1958+ err)
1959 return ifaces, err2
1960 }
1961 if managed.Value().(bool) == true {
1962@@ -424,75 +575,208 @@ func (c *Client) WifisManaged(wifiDevices []string) (map[string]string, error) {
1963 return ifaces, nil
1964 }
1965
1966-// Unmanage sets wlan0 to be Unmanaged by network manager if it
1967+// Unmanage sets an interface to be Unmanaged by network manager if it
1968 // is managed
1969-func (c *Client) Unmanage() error {
1970- ifaces, err := c.WifisManaged(c.GetWifiDevices(c.GetDevices()))
1971+func (c *Client) Unmanage(wifiIface string) error {
1972+ ifaces, err := c.WifisManaged(c.GetWifiDevices())
1973 if err != nil {
1974 return fmt.Errorf("Error getting managed wifis: %v", err)
1975 }
1976- if _, ok := ifaces["wlan0"]; ok {
1977- if c.SetIfaceManaged("wlan0", false, c.GetWifiDevices(c.GetDevices())) == "" {
1978+ if _, ok := ifaces[wifiIface]; ok {
1979+ if c.SetIfaceManaged(wifiIface, false,
1980+ c.GetWifiDevices()) == "" {
1981 return fmt.Errorf("No interface was set to unmanaged")
1982 }
1983 }
1984 return nil
1985 }
1986
1987-// Manage sets wlan0 to not managed by network manager
1988-func (c *Client) Manage() error {
1989- if c.SetIfaceManaged("wlan0", true, c.GetWifiDevices(c.GetDevices())) == "" {
1990+// Manage sets an interface to not managed by network manager
1991+func (c *Client) Manage(wifiIface string) error {
1992+ if c.SetIfaceManaged(wifiIface, true,
1993+ c.GetWifiDevices()) == "" {
1994 return fmt.Errorf("No interface was set to managed")
1995 }
1996 return nil
1997 }
1998
1999-// ScanAndWriteSsidsToFile scans for ssids and writes to file
2000-func (c *Client) ScanAndWriteSsidsToFile(filepath string) bool {
2001+// CreateAccessPoint creates an AP with given SSID and password, for the wifi
2002+// interface specified. Returns the path to the connection settings and the
2003+// active connection.
2004+func (c *Client) CreateAccessPoint(ssid string, password string,
2005+ iface string, ipAddress string, prefix uint) (string, string, error) {
2006
2007- var err error
2008- var file *os.File
2009- if _, err = os.Stat(filepath); os.IsNotExist(err) {
2010- file, err = os.Create(filepath)
2011- if err != nil {
2012- log.Printf("Error touching ssids file: %v", err)
2013- return false
2014- }
2015+ wifiPath, err := c.GetDevicePathFromIface(iface)
2016+ if err != nil {
2017+ return "", "", err
2018 }
2019
2020- file, err = os.OpenFile(filepath, os.O_RDWR, 0644)
2021+ setObject(c, "org.freedesktop.NetworkManager",
2022+ "/org/freedesktop/NetworkManager")
2023+
2024+ connectSett := make(map[string]dbus.Variant)
2025+ connectSett["id"] = dbus.MakeVariant(ssid)
2026+ connectSett["autoconnect"] = dbus.MakeVariant(false)
2027+
2028+ wirelessSett := make(map[string]dbus.Variant)
2029+ wirelessSett["ssid"] = dbus.MakeVariant([]byte(ssid))
2030+ wirelessSett["mode"] = dbus.MakeVariant("ap")
2031+
2032+ securitySett := make(map[string]dbus.Variant)
2033+ securitySett["key-mgmt"] = dbus.MakeVariant("wpa-psk")
2034+ securitySett["psk"] = dbus.MakeVariant(password)
2035+ securitySett["proto"] = dbus.MakeVariant([]string{"rsn"})
2036+ securitySett["pairwise"] = dbus.MakeVariant([]string{"ccmp"})
2037+ securitySett["group"] = dbus.MakeVariant([]string{"ccmp"})
2038+
2039+ ipv4Sett := make(map[string]dbus.Variant)
2040+ ipv4Sett["method"] = dbus.MakeVariant("manual")
2041+ ipv4Address := make(map[string]dbus.Variant)
2042+ ipv4Address["address"] = dbus.MakeVariant(ipAddress)
2043+ ipv4Address["prefix"] = dbus.MakeVariant(uint32(prefix))
2044+ ipv4AddressData := make([]map[string]dbus.Variant, 1)
2045+ ipv4AddressData[0] = ipv4Address
2046+ ipv4Sett["address-data"] = dbus.MakeVariant(ipv4AddressData)
2047+
2048+ ipv6Sett := make(map[string]dbus.Variant)
2049+ ipv6Sett["method"] = dbus.MakeVariant("ignore")
2050+
2051+ settings := make(map[string]map[string]dbus.Variant)
2052+ settings["connection"] = connectSett
2053+ settings["802-11-wireless"] = wirelessSett
2054+ settings["802-11-wireless-security"] = securitySett
2055+ settings["ipv4"] = ipv4Sett
2056+ settings["ipv6"] = ipv6Sett
2057+
2058+ var connPath, actConnPath string
2059+ err = c.dbusClient.BusObj.Call(
2060+ "org.freedesktop.NetworkManager.AddAndActivateConnection", 0,
2061+ settings, dbus.ObjectPath(wifiPath), dbus.ObjectPath("/")).
2062+ Store(&connPath, &actConnPath)
2063 if err != nil {
2064- log.Printf("Error opening ssids file: %v", err)
2065- return false
2066+ return "", "", err
2067 }
2068
2069- defer file.Close()
2070+ log.Printf("Connection setting in %s", connPath)
2071+ log.Printf("Active connection config in %s", actConnPath)
2072+ return connPath, actConnPath, c.WaitConnectionActivated(actConnPath)
2073+}
2074+
2075+// ActivateConnection starts a connection on a device
2076+func (c *Client) ActivateConnection(devicePath string,
2077+ connPath string) (string, error) {
2078
2079- return c.scanSsids(file)
2080+ setObject(c, "org.freedesktop.NetworkManager",
2081+ "/org/freedesktop/NetworkManager")
2082
2083+ var activeConnPath string
2084+ err := c.dbusClient.BusObj.Call(
2085+ "org.freedesktop.NetworkManager.ActivateConnection", 0,
2086+ dbus.ObjectPath(connPath), dbus.ObjectPath(devicePath),
2087+ dbus.ObjectPath("/")).Store(&activeConnPath)
2088+ if err != nil {
2089+ return "", err
2090+ }
2091+ return activeConnPath, c.WaitConnectionActivated(activeConnPath)
2092 }
2093
2094-// ScanSsids sets wlan0 to be managed and then scans
2095-// for ssids. If found, write the ssids (comma separated)
2096-// to writer and return true, else return false.
2097-func (c *Client) scanSsids(writer io.Writer) bool {
2098- c.Manage()
2099- SSIDs, _, _ := c.Ssids()
2100- //only write SSIDs when found
2101- if len(SSIDs) > 0 {
2102- var out string
2103- for _, ssid := range SSIDs {
2104- out += strings.TrimSpace(ssid.Ssid) + ","
2105- }
2106- out = out[:len(out)-1]
2107- _, err := writer.Write([]byte(out))
2108+// WaitConnectionActivated waits until a connection has been activated
2109+func (c *Client) WaitConnectionActivated(actConnPath string) error {
2110+ rule := "type='signal',sender='org.freedesktop.NetworkManager'," +
2111+ "path='" + actConnPath + "'," +
2112+ "interface='org.freedesktop.NetworkManager.Connection.Active'," +
2113+ "member='StateChanged'"
2114+ setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
2115+ err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Err
2116+ if err != nil {
2117+ return err
2118+ }
2119+ defer func() {
2120+ setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
2121+ err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.RemoveMatch", 0, rule).Err
2122 if err != nil {
2123- log.Printf("Error writing SSID(s): %v ", err)
2124- } else {
2125- log.Print("SSID(s) obtained")
2126- return true
2127+ log.Printf("Error when calling RemoveMatch: %v", err)
2128 }
2129+ }()
2130+
2131+ chSig := make(chan *dbus.Signal, 10)
2132+ defer close(chSig)
2133+ c.dbusClient.Connection.Signal(chSig)
2134+ defer c.dbusClient.Connection.RemoveSignal(chSig)
2135+
2136+ // Get current state, as we might have missed the signal while we were
2137+ // registering for it (the connection already existed).
2138+ setObject(c, "org.freedesktop.NetworkManager", dbus.ObjectPath(actConnPath))
2139+ state, err := c.dbusClient.BusObj.GetProperty(
2140+ "org.freedesktop.NetworkManager.Connection.Active.State")
2141+ if err != nil {
2142+ return err
2143 }
2144- log.Print("No SSID found")
2145- return false
2146+ // NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
2147+ if state.Value() == 2 {
2148+ return nil
2149+ }
2150+
2151+SignalLoop:
2152+ for {
2153+ select {
2154+ case sig := <-chSig:
2155+ // Note that we need to make sure the signal comes from
2156+ // the right object, as we will receive here any signal
2157+ // to which the process has registered.
2158+ // NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
2159+ if sig.Path == dbus.ObjectPath(actConnPath) &&
2160+ sig.Body[0].(uint32) == 2 {
2161+ break SignalLoop
2162+ }
2163+ case <-time.After(20 * time.Second):
2164+ return fmt.Errorf("Timeout while waiting for conn activation")
2165+ }
2166+ }
2167+
2168+ return nil
2169+}
2170+
2171+// CheckNetworkManagerRunning finds out whether NM is running or not
2172+func (c *Client) CheckNetworkManagerRunning() (bool, error) {
2173+ setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
2174+ err := c.dbusClient.BusObj.Call(
2175+ "org.freedesktop.DBus.GetNameOwner", 0,
2176+ "org.freedesktop.NetworkManager").Err
2177+ if err != nil {
2178+ return false, err
2179+ }
2180+ return true, nil
2181+}
2182+
2183+// RegisterForNameOwnerChanged registers for changes in DBus names
2184+func (c *Client) RegisterForNameOwnerChanged(chSig chan<- *dbus.Signal) error {
2185+ rule := "type='signal',sender='org.freedesktop.DBus'," +
2186+ "path='/org/freedesktop/DBus'," +
2187+ "interface='org.freedesktop.DBus'," +
2188+ "member='NameOwnerChanged'"
2189+ setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
2190+ err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Err
2191+ if err != nil {
2192+ return err
2193+ }
2194+
2195+ c.dbusClient.Connection.Signal(chSig)
2196+ return nil
2197+}
2198+
2199+// UnregisterForNameOwnerChanged unregisters for changes in DBus names
2200+func (c *Client) UnregisterForNameOwnerChanged(chSig chan<- *dbus.Signal) error {
2201+ rule := "type='signal',sender='org.freedesktop.DBus'," +
2202+ "path='/org/freedesktop/DBus'," +
2203+ "interface='org.freedesktop.DBus'," +
2204+ "member='NameOwnerChanged'"
2205+ setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
2206+ err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.RemoveMatch", 0, rule).Err
2207+ if err != nil {
2208+ log.Printf("Error unregistering for NameOwnerChanged: %v", err)
2209+ }
2210+
2211+ c.dbusClient.Connection.RemoveSignal(chSig)
2212+ return err
2213 }
2214diff --git a/netman/dbus_test.go b/netman/dbus_test.go
2215index c27a2f6..3110d1e 100644
2216--- a/netman/dbus_test.go
2217+++ b/netman/dbus_test.go
2218@@ -18,6 +18,7 @@
2219 package netman
2220
2221 import (
2222+ "context"
2223 "errors"
2224 "fmt"
2225 "strconv"
2226@@ -77,16 +78,23 @@ func (mock *mockObj) Call(method string, flags dbus.Flags, args ...interface{})
2227 return call
2228 }
2229
2230-func (mock *mockObj) Go(method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call {
2231+func (mock *mockObj) Go(method string, flags dbus.Flags, ch chan *dbus.Call,
2232+ args ...interface{}) *dbus.Call {
2233 call := makeCall()
2234 return call
2235 }
2236
2237+func (mock *mockObj) GoWithContext(ctx context.Context, method string,
2238+ flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call {
2239+ return nil
2240+}
2241+
2242 func (mock *mockObj) GetProperty(p string) (dbus.Variant, error) {
2243 switch p {
2244 case "org.freedesktop.NetworkManager.Device.DeviceType":
2245 if len(mock.wifiDevices) < 2 { // 2 of three devices are wifi
2246- mock.wifiDevices = append(mock.wifiDevices, "wifi"+strconv.Itoa(len(mock.wifiDevices)))
2247+ mock.wifiDevices = append(mock.wifiDevices,
2248+ "wifi"+strconv.Itoa(len(mock.wifiDevices)))
2249 return dbus.MakeVariant(uint32(2)), nil
2250 }
2251 return dbus.MakeVariant(uint32(1)), nil
2252@@ -123,6 +131,25 @@ func (mock *mockObj) GetProperty(p string) (dbus.Variant, error) {
2253 return dbus.MakeVariant("GetProperty error"), errors.New("no such property found")
2254 }
2255
2256+func (mock *mockObj) AddMatchSignal(iface, member string,
2257+ options ...dbus.MatchOption) *dbus.Call {
2258+ return nil
2259+}
2260+
2261+func (mock *mockObj) RemoveMatchSignal(iface, member string,
2262+ options ...dbus.MatchOption) *dbus.Call {
2263+ return nil
2264+}
2265+
2266+func (mock *mockObj) CallWithContext(ctx context.Context, method string,
2267+ flags dbus.Flags, args ...interface{}) *dbus.Call {
2268+ return nil
2269+}
2270+
2271+func (mock *mockObj) SetProperty(p string, v interface{}) error {
2272+ return nil
2273+}
2274+
2275 func (mock *mockObj) Destination() string {
2276 return "destination"
2277 }
2278@@ -160,11 +187,10 @@ func TestGetDevices(t *testing.T) {
2279
2280 func TestGetWifiDevices(t *testing.T) {
2281 client := NewClient(&mockObj{})
2282- devices := client.GetDevices()
2283- wifiDevices := client.GetWifiDevices(devices)
2284+ wifiDevices := client.GetWifiDevices()
2285 found1 := false
2286 found2 := false
2287- for _, v := range devices {
2288+ for _, v := range wifiDevices {
2289 switch v {
2290 case "/d/1":
2291 found1 = true
2292@@ -176,16 +202,16 @@ func TestGetWifiDevices(t *testing.T) {
2293 t.Errorf("An expected device was not found")
2294 }
2295 if len(wifiDevices) != 2 {
2296- t.Errorf("Two wifi device should have been found but, found: %d", len(wifiDevices))
2297+ t.Errorf("Two wifi device should have been found but, found: %d",
2298+ len(wifiDevices))
2299 }
2300 fmt.Printf("===== Found wifi devices: %v\n", wifiDevices)
2301 }
2302
2303 func TestGetAPs(t *testing.T) {
2304 client := NewClient(&mockObj{})
2305- devices := client.GetDevices()
2306 ap2device := make(map[string]string)
2307- wifiDevices := client.GetWifiDevices(devices)
2308+ wifiDevices := client.GetWifiDevices()
2309 aps := client.GetAccessPoints(wifiDevices, ap2device)
2310 if len(aps) != 4 {
2311 t.Errorf("4 APs should have been found, but found: %d", len(aps))
2312@@ -195,8 +221,7 @@ func TestGetAPs(t *testing.T) {
2313
2314 func TestGetSsids(t *testing.T) {
2315 client := NewClient(&mockObj{})
2316- devices := client.GetDevices()
2317- wifiDevices := client.GetWifiDevices(devices)
2318+ wifiDevices := client.GetWifiDevices()
2319 ap2device := make(map[string]string)
2320 ssid2ap := make(map[string]string)
2321 aps := client.GetAccessPoints(wifiDevices, ap2device)
2322@@ -209,8 +234,8 @@ func TestGetSsids(t *testing.T) {
2323
2324 func TestSsids(t *testing.T) {
2325 client := NewClient(&mockObj{})
2326- ssids, _, _ := client.Ssids()
2327- if len(ssids) != 4 {
2328+ ssids, _, _ := client.Ssids("/d/3")
2329+ if len(ssids) != 2 {
2330 t.Errorf("4 SSIDs should have been found, but found: %d", len(ssids))
2331 }
2332 fmt.Printf("===== Ssids() (ssid/ap): %v\n", ssids)
2333@@ -232,26 +257,14 @@ func TestConnectedWifi(t *testing.T) {
2334 mock := &mockObj{}
2335 mock.connect = true
2336 client := NewClient(mock)
2337- if !client.ConnectedWifi([]string{"d1"}) {
2338+ if !client.Connected([]string{"d1"}) {
2339 t.Errorf("Should have found Wificonnected state, but did not")
2340 }
2341- if client.ConnectedWifi([]string{}) {
2342+ if client.Connected([]string{}) {
2343 t.Errorf("Should have found no connection since there are no devices, but did not")
2344 }
2345 }
2346
2347-func TestDiscconnectWifi(t *testing.T) {
2348- client := NewClient(&mockObj{})
2349- res := client.DisconnectWifi([]string{})
2350- if res != 0 {
2351- t.Errorf("0 Disconnect call expected, but found: %d", res)
2352- }
2353- res = client.DisconnectWifi([]string{"d1"})
2354- if res != 1 {
2355- t.Errorf("1 Disconnect call expected, but found: %d", res)
2356- }
2357-}
2358-
2359 func TestSetIfaceManaged(t *testing.T) {
2360 mock := &mockObj{}
2361 client := NewClient(mock)
2362diff --git a/run-checks b/run-checks
2363index 562c14d..d922caf 100755
2364--- a/run-checks
2365+++ b/run-checks
2366@@ -85,7 +85,7 @@ if [ ! -z "$STATIC" ]; then
2367
2368 # golint
2369 echo Install golint
2370- go get github.com/golang/lint/golint
2371+ go get -u golang.org/x/lint/golint
2372 export PATH=$PATH:$GOPATH/bin
2373
2374 echo Running lint
2375diff --git a/scriptlets/country-codes b/scriptlets/country-codes
2376index d4e8a97..16c718a 100644
2377--- a/scriptlets/country-codes
2378+++ b/scriptlets/country-codes
2379@@ -1,243 +1,250 @@
2380-<html><head><title>ISO-3166-1 Country Names</title></head>
2381-<body bgcolor=#ffffff>
2382-<h2>ISO-3166-1 Country Names</h2>
2383-<a href="/iso3166/">Reproduced with permission from ISO</a>
2384-<p>
2385-Follow links for ISO-3166-2 subdivision codes
2386-<p>
2387-<a href=iso.AD.html>AD</a> : ANDORRA<br>
2388-<a href=iso.AE.html>AE</a> : UNITED ARAB EMIRATES<br>
2389-<a href=iso.AF.html>AF</a> : AFGHANISTAN<br>
2390-<a href=iso.AG.html>AG</a> : ANTIGUA AND BARBUDA<br>
2391-<a href=iso.AI.html>AI</a> : ANGUILLA<br>
2392-<a href=iso.AL.html>AL</a> : ALBANIA<br>
2393-<a href=iso.AM.html>AM</a> : ARMENIA<br>
2394-<a href=iso.AN.html>AN</a> : NETHERLANDS ANTILLES<br>
2395-<a href=iso.AO.html>AO</a> : ANGOLA<br>
2396-<a href=iso.AQ.html>AQ</a> : ANTARCTICA<br>
2397-<a href=iso.AR.html>AR</a> : ARGENTINA<br>
2398-<a href=iso.AS.html>AS</a> : AMERICAN SAMOA<br>
2399-<a href=iso.AT.html>AT</a> : AUSTRIA<br>
2400-<a href=iso.AU.html>AU</a> : AUSTRALIA<br>
2401-<a href=iso.AW.html>AW</a> : ARUBA<br>
2402-<a href=iso.AZ.html>AZ</a> : AZERBAIJAN<br>
2403-<a href=iso.BA.html>BA</a> : BOSNIA AND HERZEGOVINA<br>
2404-<a href=iso.BB.html>BB</a> : BARBADOS<br>
2405-<a href=iso.BD.html>BD</a> : BANGLADESH<br>
2406-<a href=iso.BE.html>BE</a> : BELGIUM<br>
2407-<a href=iso.BF.html>BF</a> : BURKINA FASO<br>
2408-<a href=iso.BG.html>BG</a> : BULGARIA<br>
2409-<a href=iso.BH.html>BH</a> : BAHRAIN<br>
2410-<a href=iso.BI.html>BI</a> : BURUNDI<br>
2411-<a href=iso.BJ.html>BJ</a> : BENIN<br>
2412-<a href=iso.BM.html>BM</a> : BERMUDA<br>
2413-<a href=iso.BN.html>BN</a> : BRUNEI DARUSSALAM<br>
2414-<a href=iso.BO.html>BO</a> : BOLIVIA<br>
2415-<a href=iso.BR.html>BR</a> : BRAZIL<br>
2416-<a href=iso.BS.html>BS</a> : BAHAMAS<br>
2417-<a href=iso.BT.html>BT</a> : BHUTAN<br>
2418-<a href=iso.BV.html>BV</a> : BOUVET ISLAND<br>
2419-<a href=iso.BW.html>BW</a> : BOTSWANA<br>
2420-<a href=iso.BY.html>BY</a> : BELARUS<br>
2421-<a href=iso.BZ.html>BZ</a> : BELIZE<br>
2422-<a href=iso.CA.html>CA</a> : CANADA<br>
2423-<a href=iso.CC.html>CC</a> : COCOS (KEELING) ISLANDS<br>
2424-<a href=iso.CD.html>CD</a> : CONGO, THE DEMOCRATIC REPUBLIC OF THE<br>
2425-<a href=iso.CF.html>CF</a> : CENTRAL AFRICAN REPUBLIC<br>
2426-<a href=iso.CG.html>CG</a> : CONGO<br>
2427-<a href=iso.CH.html>CH</a> : SWITZERLAND<br>
2428-<a href=iso.CI.html>CI</a> : C�TE D'IVOIRE<br>
2429-<a href=iso.CK.html>CK</a> : COOK ISLANDS<br>
2430-<a href=iso.CL.html>CL</a> : CHILE<br>
2431-<a href=iso.CM.html>CM</a> : CAMEROON<br>
2432-<a href=iso.CN.html>CN</a> : CHINA<br>
2433-<a href=iso.CO.html>CO</a> : COLOMBIA<br>
2434-<a href=iso.CR.html>CR</a> : COSTA RICA<br>
2435-<a href=iso.CU.html>CU</a> : CUBA<br>
2436-<a href=iso.CV.html>CV</a> : CAPE VERDE<br>
2437-<a href=iso.CX.html>CX</a> : CHRISTMAS ISLAND<br>
2438-<a href=iso.CY.html>CY</a> : CYPRUS<br>
2439-<a href=iso.CZ.html>CZ</a> : CZECH REPUBLIC<br>
2440-<a href=iso.DE.html>DE</a> : GERMANY<br>
2441-<a href=iso.DJ.html>DJ</a> : DJIBOUTI<br>
2442-<a href=iso.DK.html>DK</a> : DENMARK<br>
2443-<a href=iso.DM.html>DM</a> : DOMINICA<br>
2444-<a href=iso.DO.html>DO</a> : DOMINICAN REPUBLIC<br>
2445-<a href=iso.DZ.html>DZ</a> : ALGERIA<br>
2446-<a href=iso.EC.html>EC</a> : ECUADOR<br>
2447-<a href=iso.EE.html>EE</a> : ESTONIA<br>
2448-<a href=iso.EG.html>EG</a> : EGYPT<br>
2449-<a href=iso.EH.html>EH</a> : WESTERN SARARA<br>
2450-<a href=iso.ER.html>ER</a> : ERITREA<br>
2451-<a href=iso.ES.html>ES</a> : SPAIN<br>
2452-<a href=iso.ET.html>ET</a> : ETHIOPIA<br>
2453-<a href=iso.FI.html>FI</a> : FINLAND<br>
2454-<a href=iso.FJ.html>FJ</a> : FIJI<br>
2455-<a href=iso.FK.html>FK</a> : FALKLAND ISLANDS (MALVINAS)<br>
2456-<a href=iso.FM.html>FM</a> : MICRONESIA, FEDERATED STATES OF<br>
2457-<a href=iso.FO.html>FO</a> : FAROE ISLANDS<br>
2458-<a href=iso.FR.html>FR</a> : FRANCE<br>
2459-<a href=iso.GA.html>GA</a> : GABON<br>
2460-<a href=iso.GB.html>GB</a> : UNITED KINGDOM<br>
2461-<a href=iso.GD.html>GD</a> : GRENADA<br>
2462-<a href=iso.GE.html>GE</a> : GEORGIA<br>
2463-<a href=iso.GF.html>GF</a> : FRENCH GUIANA<br>
2464-<a href=iso.GH.html>GH</a> : GHANA<br>
2465-<a href=iso.GI.html>GI</a> : GIBRALTAR<br>
2466-<a href=iso.GL.html>GL</a> : GREENLAND<br>
2467-<a href=iso.GM.html>GM</a> : GAMBIA<br>
2468-<a href=iso.GN.html>GN</a> : GUINEA<br>
2469-<a href=iso.GP.html>GP</a> : GUADELOUPE<br>
2470-<a href=iso.GQ.html>GQ</a> : EQUATORIAL GUINEA<br>
2471-<a href=iso.GR.html>GR</a> : GREECE<br>
2472-<a href=iso.GS.html>GS</a> : SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS<br>
2473-<a href=iso.GT.html>GT</a> : GUATEMALA<br>
2474-<a href=iso.GU.html>GU</a> : GUAM<br>
2475-<a href=iso.GW.html>GW</a> : GUINEA-BISSAU<br>
2476-<a href=iso.GY.html>GY</a> : GUYANA<br>
2477-<a href=iso.HK.html>HK</a> : HONG KONG<br>
2478-<a href=iso.HM.html>HM</a> : HEARD ISLAND AND MCDONALD ISLANDS<br>
2479-<a href=iso.HN.html>HN</a> : HONDURAS<br>
2480-<a href=iso.HR.html>HR</a> : CROATIA<br>
2481-<a href=iso.HT.html>HT</a> : HAITI<br>
2482-<a href=iso.HU.html>HU</a> : HUNGARY<br>
2483-<a href=iso.ID.html>ID</a> : INDONESIA<br>
2484-<a href=iso.IE.html>IE</a> : IRELAND<br>
2485-<a href=iso.IL.html>IL</a> : ISRAEL<br>
2486-<a href=iso.IN.html>IN</a> : INDIA<br>
2487-<a href=iso.IO.html>IO</a> : BRITISH INDIAN OCEAN TERRITORY<br>
2488-<a href=iso.IQ.html>IQ</a> : IRAQ<br>
2489-<a href=iso.IR.html>IR</a> : IRAN, ISLAMIC REPUBLIC OF<br>
2490-<a href=iso.IS.html>IS</a> : ICELAND<br>
2491-<a href=iso.IT.html>IT</a> : ITALY<br>
2492-<a href=iso.JM.html>JM</a> : JAMAICA<br>
2493-<a href=iso.JO.html>JO</a> : JORDAN<br>
2494-<a href=iso.JP.html>JP</a> : JAPAN<br>
2495-<a href=iso.KE.html>KE</a> : KENYA<br>
2496-<a href=iso.KG.html>KG</a> : KYRGYZSTAN<br>
2497-<a href=iso.KH.html>KH</a> : CAMBODIA<br>
2498-<a href=iso.KI.html>KI</a> : KIRIBATI<br>
2499-<a href=iso.KM.html>KM</a> : COMOROS<br>
2500-<a href=iso.KN.html>KN</a> : SAINT KITTS AND NEVIS<br>
2501-<a href=iso.KP.html>KP</a> : KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF<br>
2502-<a href=iso.KR.html>KR</a> : KOREA, REPUBLIC OF<br>
2503-<a href=iso.KW.html>KW</a> : KUWAIT<br>
2504-<a href=iso.KY.html>KY</a> : CAYMAN ISLANDS<br>
2505-<a href=iso.KZ.html>KZ</a> : KAZAKHSTAN<br>
2506-<a href=iso.LA.html>LA</a> : LAO PEOPLE'S DEMOCRATIC REPUBLIC<br>
2507-<a href=iso.LB.html>LB</a> : LEBANON<br>
2508-<a href=iso.LC.html>LC</a> : SAINT LUCIA<br>
2509-<a href=iso.LI.html>LI</a> : LIECHTENSTEIN<br>
2510-<a href=iso.LK.html>LK</a> : SRI LANKA<br>
2511-<a href=iso.LR.html>LR</a> : LIBERIA<br>
2512-<a href=iso.LS.html>LS</a> : LESOTHO<br>
2513-<a href=iso.LT.html>LT</a> : LITHUANIA<br>
2514-<a href=iso.LU.html>LU</a> : LUXEMBOURG<br>
2515-<a href=iso.LV.html>LV</a> : LATVIA<br>
2516-<a href=iso.LY.html>LY</a> : LIBYAN ARAB JAMABIRIYA<br>
2517-<a href=iso.MA.html>MA</a> : MOROCCO<br>
2518-<a href=iso.MC.html>MC</a> : MONACO<br>
2519-<a href=iso.MD.html>MD</a> : MOLDOVA, REPUBLIC OF<br>
2520-<a href=iso.MG.html>MG</a> : MADAGASCAR<br>
2521-<a href=iso.MH.html>MH</a> : MARSHALL ISLANDS<br>
2522-<a href=iso.MK.html>MK</a> : MACEDONIA, THE FORMER YUGOSLAV REPU8LIC OF<br>
2523-<a href=iso.ML.html>ML</a> : MALI<br>
2524-<a href=iso.MM.html>MM</a> : MYANMAR<br>
2525-<a href=iso.MN.html>MN</a> : MONGOLIA<br>
2526-<a href=iso.MO.html>MO</a> : MACAU<br>
2527-<a href=iso.MP.html>MP</a> : NORTHERN MARIANA ISLANDS<br>
2528-<a href=iso.MQ.html>MQ</a> : MARTINIQUE<br>
2529-<a href=iso.MR.html>MR</a> : MAURITANIA<br>
2530-<a href=iso.MS.html>MS</a> : MONTSERRAT<br>
2531-<a href=iso.MT.html>MT</a> : MALTA<br>
2532-<a href=iso.MU.html>MU</a> : MAURITIUS<br>
2533-<a href=iso.MV.html>MV</a> : MALDIVES<br>
2534-<a href=iso.MW.html>MW</a> : MALAWI<br>
2535-<a href=iso.MX.html>MX</a> : MEXICO<br>
2536-<a href=iso.MY.html>MY</a> : MALAYSIA<br>
2537-<a href=iso.MZ.html>MZ</a> : MOZAMBIQUE<br>
2538-<a href=iso.NA.html>NA</a> : NAMIBIA<br>
2539-<a href=iso.NC.html>NC</a> : NEW CALEDONIA<br>
2540-<a href=iso.NE.html>NE</a> : NIGER<br>
2541-<a href=iso.NF.html>NF</a> : NORFOLK ISLAND<br>
2542-<a href=iso.NG.html>NG</a> : NIGERIA<br>
2543-<a href=iso.NI.html>NI</a> : NICARAGUA<br>
2544-<a href=iso.NL.html>NL</a> : NETHERLANDS<br>
2545-<a href=iso.NO.html>NO</a> : NORWAY<br>
2546-<a href=iso.NP.html>NP</a> : NEPAL<br>
2547-<a href=iso.NU.html>NU</a> : NIUE<br>
2548-<a href=iso.NZ.html>NZ</a> : NEW ZEALAND<br>
2549-<a href=iso.OM.html>OM</a> : OMAN<br>
2550-<a href=iso.PA.html>PA</a> : PANAMA<br>
2551-<a href=iso.PE.html>PE</a> : PERU<br>
2552-<a href=iso.PF.html>PF</a> : FRENCH POLYNESIA<br>
2553-<a href=iso.PG.html>PG</a> : PAPUA NEW GUINEA<br>
2554-<a href=iso.PH.html>PH</a> : PHILIPPINES<br>
2555-<a href=iso.PK.html>PK</a> : PAKISTAN<br>
2556-<a href=iso.PL.html>PL</a> : POLAND<br>
2557-<a href=iso.PM.html>PM</a> : SAINT PIERRE AND MIQUELON<br>
2558-<a href=iso.PN.html>PN</a> : PITCAIRN<br>
2559-<a href=iso.PR.html>PR</a> : PUERTO RICO<br>
2560-<a href=iso.PT.html>PT</a> : PORTUGAL<br>
2561-<a href=iso.PW.html>PW</a> : PALAU<br>
2562-<a href=iso.PY.html>PY</a> : PARAGUAY<br>
2563-<a href=iso.QA.html>QA</a> : QATAR<br>
2564-<a href=iso.RE.html>RE</a> : R�UNION<br>
2565-<a href=iso.RO.html>RO</a> : ROMANIA<br>
2566-<a href=iso.RU.html>RU</a> : RUSSIAN FEDERATION<br>
2567-<a href=iso.RW.html>RW</a> : RWANDA<br>
2568-<a href=iso.SA.html>SA</a> : SAUDI ARABIA<br>
2569-<a href=iso.SB.html>SB</a> : SOLOMON ISLANDS<br>
2570-<a href=iso.SC.html>SC</a> : SEYCHELLES<br>
2571-<a href=iso.SD.html>SD</a> : SUDAN<br>
2572-<a href=iso.SE.html>SE</a> : SWEDEN<br>
2573-<a href=iso.SG.html>SG</a> : SINGAPORE<br>
2574-<a href=iso.SH.html>SH</a> : SAINT HELENA<br>
2575-<a href=iso.SI.html>SI</a> : SLOVENIA<br>
2576-<a href=iso.SJ.html>SJ</a> : SVALBARD AND JAN MAYEN<br>
2577-<a href=iso.SK.html>SK</a> : SLOVAKIA<br>
2578-<a href=iso.SL.html>SL</a> : SIERRA LEONE<br>
2579-<a href=iso.SM.html>SM</a> : SAN MARINO<br>
2580-<a href=iso.SN.html>SN</a> : SENEGAL<br>
2581-<a href=iso.SO.html>SO</a> : SOMALIA<br>
2582-<a href=iso.SR.html>SR</a> : SURINAME<br>
2583-<a href=iso.ST.html>ST</a> : SAO TOME AND PRINCIPE<br>
2584-<a href=iso.SV.html>SV</a> : EL SALVADOR<br>
2585-<a href=iso.SY.html>SY</a> : SYRIAN ARAB REPUBLIC<br>
2586-<a href=iso.SZ.html>SZ</a> : SWAZILAND<br>
2587-<a href=iso.TC.html>TC</a> : TURKS AND CAICOS ISLANDS<br>
2588-<a href=iso.TD.html>TD</a> : CHAD<br>
2589-<a href=iso.TF.html>TF</a> : FRENCH SOUTHERN TERRITORIES<br>
2590-<a href=iso.TG.html>TG</a> : TOGO<br>
2591-<a href=iso.TH.html>TH</a> : THAILAND<br>
2592-<a href=iso.TJ.html>TJ</a> : TAJIKISTAN<br>
2593-<a href=iso.TK.html>TK</a> : TOKELAU<br>
2594-<a href=iso.TM.html>TM</a> : TURKMENISTAN<br>
2595-<a href=iso.TN.html>TN</a> : TUNISIA<br>
2596-<a href=iso.TO.html>TO</a> : TONGA<br>
2597-<a href=iso.TP.html>TP</a> : EAST TIMOR<br>
2598-<a href=iso.TR.html>TR</a> : TURKEY<br>
2599-<a href=iso.TT.html>TT</a> : TRINIDAD AND TOBAGO<br>
2600-<a href=iso.TV.html>TV</a> : TUVALU<br>
2601-<a href=iso.TW.html>TW</a> : TAIWAN, PROVINCE OF CHINA<br>
2602-<a href=iso.TZ.html>TZ</a> : TANZANIA, UNITED REPUBLIC OF<br>
2603-<a href=iso.UA.html>UA</a> : UKRAINE<br>
2604-<a href=iso.UG.html>UG</a> : UGANDA<br>
2605-<a href=iso.UM.html>UM</a> : UNITED STATES MINOR OUTLYING ISLANDS<br>
2606-<a href=iso.US.html>US</a> : UNITED STATES<br>
2607-<a href=iso.UY.html>UY</a> : URUGUAY<br>
2608-<a href=iso.UZ.html>UZ</a> : UZBEKISTAN<br>
2609-<a href=iso.VE.html>VE</a> : VENEZUELA<br>
2610-<a href=iso.VG.html>VG</a> : VIRGIN ISLANDS, BRITISH<br>
2611-<a href=iso.VI.html>VI</a> : VIRGIN ISLANDS, U.S.<br>
2612-<a href=iso.VN.html>VN</a> : VIET NAM<br>
2613-<a href=iso.VU.html>VU</a> : VANUATU<br>
2614-<a href=iso.WF.html>WF</a> : WALLIS AND FUTUNA<br>
2615-<a href=iso.WS.html>WS</a> : SAMOA<br>
2616-<a href=iso.YE.html>YE</a> : YEMEN<br>
2617-<a href=iso.YT.html>YT</a> : MAYOTTE<br>
2618-<a href=iso.YU.html>YU</a> : YUGOSLAVIA<br>
2619-<a href=iso.ZA.html>ZA</a> : SOUTH AFRICA<br>
2620-<a href=iso.ZM.html>ZM</a> : ZAMBIA<br>
2621-<a href=iso.ZW.html>ZW</a> : ZIMBABWE<br>
2622-</body></html>
2623+Name,Code
2624+Afghanistan,AF
2625+Åland Islands,AX
2626+Albania,AL
2627+Algeria,DZ
2628+American Samoa,AS
2629+Andorra,AD
2630+Angola,AO
2631+Anguilla,AI
2632+Antarctica,AQ
2633+Antigua and Barbuda,AG
2634+Argentina,AR
2635+Armenia,AM
2636+Aruba,AW
2637+Australia,AU
2638+Austria,AT
2639+Azerbaijan,AZ
2640+Bahamas,BS
2641+Bahrain,BH
2642+Bangladesh,BD
2643+Barbados,BB
2644+Belarus,BY
2645+Belgium,BE
2646+Belize,BZ
2647+Benin,BJ
2648+Bermuda,BM
2649+Bhutan,BT
2650+"Bolivia, Plurinational State of",BO
2651+"Bonaire, Sint Eustatius and Saba",BQ
2652+Bosnia and Herzegovina,BA
2653+Botswana,BW
2654+Bouvet Island,BV
2655+Brazil,BR
2656+British Indian Ocean Territory,IO
2657+Brunei Darussalam,BN
2658+Bulgaria,BG
2659+Burkina Faso,BF
2660+Burundi,BI
2661+Cambodia,KH
2662+Cameroon,CM
2663+Canada,CA
2664+Cape Verde,CV
2665+Cayman Islands,KY
2666+Central African Republic,CF
2667+Chad,TD
2668+Chile,CL
2669+China,CN
2670+Christmas Island,CX
2671+Cocos (Keeling) Islands,CC
2672+Colombia,CO
2673+Comoros,KM
2674+Congo,CG
2675+"Congo, the Democratic Republic of the",CD
2676+Cook Islands,CK
2677+Costa Rica,CR
2678+Côte d'Ivoire,CI
2679+Croatia,HR
2680+Cuba,CU
2681+Curaçao,CW
2682+Cyprus,CY
2683+Czech Republic,CZ
2684+Denmark,DK
2685+Djibouti,DJ
2686+Dominica,DM
2687+Dominican Republic,DO
2688+Ecuador,EC
2689+Egypt,EG
2690+El Salvador,SV
2691+Equatorial Guinea,GQ
2692+Eritrea,ER
2693+Estonia,EE
2694+Ethiopia,ET
2695+Falkland Islands (Malvinas),FK
2696+Faroe Islands,FO
2697+Fiji,FJ
2698+Finland,FI
2699+France,FR
2700+French Guiana,GF
2701+French Polynesia,PF
2702+French Southern Territories,TF
2703+Gabon,GA
2704+Gambia,GM
2705+Georgia,GE
2706+Germany,DE
2707+Ghana,GH
2708+Gibraltar,GI
2709+Greece,GR
2710+Greenland,GL
2711+Grenada,GD
2712+Guadeloupe,GP
2713+Guam,GU
2714+Guatemala,GT
2715+Guernsey,GG
2716+Guinea,GN
2717+Guinea-Bissau,GW
2718+Guyana,GY
2719+Haiti,HT
2720+Heard Island and McDonald Islands,HM
2721+Holy See (Vatican City State),VA
2722+Honduras,HN
2723+Hong Kong,HK
2724+Hungary,HU
2725+Iceland,IS
2726+India,IN
2727+Indonesia,ID
2728+"Iran, Islamic Republic of",IR
2729+Iraq,IQ
2730+Ireland,IE
2731+Isle of Man,IM
2732+Israel,IL
2733+Italy,IT
2734+Jamaica,JM
2735+Japan,JP
2736+Jersey,JE
2737+Jordan,JO
2738+Kazakhstan,KZ
2739+Kenya,KE
2740+Kiribati,KI
2741+"Korea, Democratic People's Republic of",KP
2742+"Korea, Republic of",KR
2743+Kuwait,KW
2744+Kyrgyzstan,KG
2745+Lao People's Democratic Republic,LA
2746+Latvia,LV
2747+Lebanon,LB
2748+Lesotho,LS
2749+Liberia,LR
2750+Libya,LY
2751+Liechtenstein,LI
2752+Lithuania,LT
2753+Luxembourg,LU
2754+Macao,MO
2755+"Macedonia, the Former Yugoslav Republic of",MK
2756+Madagascar,MG
2757+Malawi,MW
2758+Malaysia,MY
2759+Maldives,MV
2760+Mali,ML
2761+Malta,MT
2762+Marshall Islands,MH
2763+Martinique,MQ
2764+Mauritania,MR
2765+Mauritius,MU
2766+Mayotte,YT
2767+Mexico,MX
2768+"Micronesia, Federated States of",FM
2769+"Moldova, Republic of",MD
2770+Monaco,MC
2771+Mongolia,MN
2772+Montenegro,ME
2773+Montserrat,MS
2774+Morocco,MA
2775+Mozambique,MZ
2776+Myanmar,MM
2777+Namibia,NA
2778+Nauru,NR
2779+Nepal,NP
2780+Netherlands,NL
2781+New Caledonia,NC
2782+New Zealand,NZ
2783+Nicaragua,NI
2784+Niger,NE
2785+Nigeria,NG
2786+Niue,NU
2787+Norfolk Island,NF
2788+Northern Mariana Islands,MP
2789+Norway,NO
2790+Oman,OM
2791+Pakistan,PK
2792+Palau,PW
2793+"Palestine, State of",PS
2794+Panama,PA
2795+Papua New Guinea,PG
2796+Paraguay,PY
2797+Peru,PE
2798+Philippines,PH
2799+Pitcairn,PN
2800+Poland,PL
2801+Portugal,PT
2802+Puerto Rico,PR
2803+Qatar,QA
2804+Réunion,RE
2805+Romania,RO
2806+Russian Federation,RU
2807+Rwanda,RW
2808+Saint Barthélemy,BL
2809+"Saint Helena, Ascension and Tristan da Cunha",SH
2810+Saint Kitts and Nevis,KN
2811+Saint Lucia,LC
2812+Saint Martin (French part),MF
2813+Saint Pierre and Miquelon,PM
2814+Saint Vincent and the Grenadines,VC
2815+Samoa,WS
2816+San Marino,SM
2817+Sao Tome and Principe,ST
2818+Saudi Arabia,SA
2819+Senegal,SN
2820+Serbia,RS
2821+Seychelles,SC
2822+Sierra Leone,SL
2823+Singapore,SG
2824+Sint Maarten (Dutch part),SX
2825+Slovakia,SK
2826+Slovenia,SI
2827+Solomon Islands,SB
2828+Somalia,SO
2829+South Africa,ZA
2830+South Georgia and the South Sandwich Islands,GS
2831+South Sudan,SS
2832+Spain,ES
2833+Sri Lanka,LK
2834+Sudan,SD
2835+Suriname,SR
2836+Svalbard and Jan Mayen,SJ
2837+Swaziland,SZ
2838+Sweden,SE
2839+Switzerland,CH
2840+Syrian Arab Republic,SY
2841+"Taiwan, Province of China",TW
2842+Tajikistan,TJ
2843+"Tanzania, United Republic of",TZ
2844+Thailand,TH
2845+Timor-Leste,TL
2846+Togo,TG
2847+Tokelau,TK
2848+Tonga,TO
2849+Trinidad and Tobago,TT
2850+Tunisia,TN
2851+Turkey,TR
2852+Turkmenistan,TM
2853+Turks and Caicos Islands,TC
2854+Tuvalu,TV
2855+Uganda,UG
2856+Ukraine,UA
2857+United Arab Emirates,AE
2858+United Kingdom,GB
2859+United States,US
2860+United States Minor Outlying Islands,UM
2861+Uruguay,UY
2862+Uzbekistan,UZ
2863+Vanuatu,VU
2864+"Venezuela, Bolivarian Republic of",VE
2865+Viet Nam,VN
2866+"Virgin Islands, British",VG
2867+"Virgin Islands, U.S.",VI
2868+Wallis and Futuna,WF
2869+Western Sahara,EH
2870+Yemen,YE
2871+Zambia,ZM
2872+Zimbabwe,ZW
2873diff --git a/scriptlets/fetch_country_codes.sh b/scriptlets/fetch_country_codes.sh
2874index 8dae525..0850b77 100755
2875--- a/scriptlets/fetch_country_codes.sh
2876+++ b/scriptlets/fetch_country_codes.sh
2877@@ -3,8 +3,8 @@
2878 # store them locally to be used in compilation time.
2879 # This operation must be executed by hand when wanted to update
2880 # current ones shown in config page
2881-set -e
2882+set -ex
2883
2884 cd "$(dirname "$0")"
2885
2886-curl -X GET http://geotags.com/iso3166/countries.html > country-codes
2887\ No newline at end of file
2888+curl -X GET https://raw.githubusercontent.com/umpirsky/country-list/master/data/en_US/country.csv > country-codes
2889diff --git a/scriptlets/fill_country_codes.sh b/scriptlets/fill_country_codes.sh
2890index b8babe4..028b77a 100755
2891--- a/scriptlets/fill_country_codes.sh
2892+++ b/scriptlets/fill_country_codes.sh
2893@@ -4,9 +4,9 @@
2894 # Process is easy, get it from remote path, format using sed, and replace
2895 # html page were they will be used
2896
2897-set -e
2898+set -e
2899
2900-COUNTRY_CODES=../../../scriptlets/country-codes
2901+COUNTRY_CODES=./scriptlets/country-codes
2902
2903 if [ ! -e $COUNTRY_CODES ]; then
2904 echo "======================================================="
2905@@ -19,21 +19,16 @@ if [ ! -e $COUNTRY_CODES ]; then
2906 exit 1
2907 fi
2908
2909-# think that this is a scriptlet, executed in parts/<the_part>/build folder
2910-cp $COUNTRY_CODES .
2911-
2912-# remove non processable lines
2913-sed -i '/^<a href=iso/!d' country-codes
2914-
2915-# process lines to change its format to be html select options
2916-sed -i '/<a href=iso/ s/<a href=iso.*html>/<option value="/
2917-s/<\/a>\ :\ /">/
2918-s/<br>/<\/option>/' country-codes
2919+# remove first line and create html
2920+tail -n +2 $COUNTRY_CODES | sed 's/\r$//' |
2921+ while read -r ln; do
2922+ printf '<option value="%s">%s</option>\n' "${ln##*,}" "${ln%,*}"
2923+ done > country-codes
2924
2925 # add world wide default option at beginning
2926 sed -i '1s;^;<option value="XX">\-WORLD WIDE\-<\/option>\n;' country-codes
2927
2928 # replace mark in management.html template with real country-codes
2929-sed -e '/\[COUNTRY_CODE_OPTIONS\]/ {' -e 'r country-codes' -e 'd' -e '}' -i static/templates/management.html
2930+sed -e '/\[COUNTRY_CODE_OPTIONS\]/ {' -e 'r country-codes' -e 'd' -e '}' -i static/templates/management.html
2931
2932 echo "country codes filled ok"
2933diff --git a/server/handlers.go b/server/handlers.go
2934index 45ec7fe..b7e71ff 100644
2935--- a/server/handlers.go
2936+++ b/server/handlers.go
2937@@ -24,10 +24,8 @@ import (
2938 "net/http"
2939 "os"
2940 "path/filepath"
2941- "text/template"
2942- "time"
2943-
2944 "strconv"
2945+ "text/template"
2946
2947 "launchpad.net/wifi-connect/utils"
2948 )
2949@@ -42,9 +40,6 @@ const (
2950 // ResourcesPath absolute path to web static resources
2951 var ResourcesPath = filepath.Join(os.Getenv("SNAP"), "static")
2952
2953-// first time management portal is accessed this file is created.
2954-var firstConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), ".first_config")
2955-
2956 var cw interface{}
2957
2958 // Data interface representing any data included in a template
2959@@ -66,7 +61,36 @@ type ConnectingData struct {
2960 Ssid string
2961 }
2962
2963-type noData struct {
2964+// UserEvent stores data from events generated by the user, that is
2965+// sent back to the service main loop
2966+type UserEvent struct {
2967+ EvType int
2968+ Params map[string]string
2969+}
2970+
2971+// Event types for UserEvent.evType
2972+const (
2973+ UserEventConnect = 0 + iota
2974+ UserEventRefresh
2975+ UserEventDisconnect
2976+)
2977+
2978+// Parameters for UserEventConnect
2979+const (
2980+ UeConnSsid = "ssid"
2981+ UeConnPassword = "password"
2982+)
2983+
2984+// ManagementServer models the management server
2985+type ManagementServer struct {
2986+ chUserEvs chan<- UserEvent
2987+ config utils.Config
2988+ ssidLs *utils.SsidList
2989+}
2990+
2991+// OperationalServer models the operational server
2992+type OperationalServer struct {
2993+ chUserEvs chan<- UserEvent
2994 }
2995
2996 func execTemplate(w http.ResponseWriter, templatePath string, data Data) {
2997@@ -81,7 +105,7 @@ func execTemplate(w http.ResponseWriter, templatePath string, data Data) {
2998
2999 err = t.Execute(w, data)
3000 if err != nil {
3001- msg := fmt.Sprintf("Error executing the template at %v : %v", templatePath, err)
3002+ msg := fmt.Sprintf("Error executing the template at %v: %v", templatePath, err)
3003 log.Print(msg)
3004 http.Error(w, msg, http.StatusInternalServerError)
3005 return
3006@@ -89,53 +113,36 @@ func execTemplate(w http.ResponseWriter, templatePath string, data Data) {
3007 }
3008
3009 // ManagementHandler handles management portal
3010-func ManagementHandler(w http.ResponseWriter, r *http.Request) {
3011+func (ms *ManagementServer) ManagementHandler(w http.ResponseWriter, r *http.Request) {
3012
3013 if utils.MustSetConfig() {
3014
3015- config, err := utils.ReadConfig()
3016- if err != nil {
3017- msg := fmt.Sprintf("Error reading configuration: %v", err)
3018- log.Println(msg)
3019- http.Error(w, msg, http.StatusInternalServerError)
3020- return
3021- }
3022-
3023- if !config.Portal.NoResetCredentials {
3024- execTemplate(w, managementTemplatePath, ManagementData{Config: config, Page: "config"})
3025+ if !ms.config.Portal.NoResetCredentials {
3026+ execTemplate(w, managementTemplatePath,
3027+ ManagementData{Config: &ms.config, Page: "config"})
3028 return
3029 }
3030
3031 }
3032
3033- ssids, err := utils.ReadSsidsFile()
3034- if err != nil {
3035- log.Printf("Error reading SSIDs file: %v", err)
3036- http.Error(w, err.Error(), http.StatusInternalServerError)
3037- return
3038- }
3039-
3040- execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"})
3041+ execTemplate(w, managementTemplatePath,
3042+ ManagementData{Ssids: ms.ssidLs.GetSsidList(), Page: "ssids"})
3043 }
3044
3045 // SaveConfigHandler saves config received as form post parameters
3046-func SaveConfigHandler(w http.ResponseWriter, r *http.Request) {
3047- // read previous config
3048- config, err := utils.ReadConfig()
3049- if err != nil {
3050- msg := fmt.Sprintf("Error reading previous stored config: %v", err)
3051- log.Println(msg)
3052- http.Error(w, msg, http.StatusInternalServerError)
3053- return
3054- }
3055+func (ms *ManagementServer) SaveConfigHandler(w http.ResponseWriter, r *http.Request) {
3056+ // start from previous config
3057+ config := ms.config
3058
3059 r.ParseForm()
3060
3061+ var err error
3062 config.Wifi.Ssid = utils.ParseFormParamSingleValue(r.Form, "Ssid")
3063 config.Wifi.Passphrase = utils.ParseFormParamSingleValue(r.Form, "Passphrase")
3064 config.Wifi.Interface = utils.ParseFormParamSingleValue(r.Form, "Interface")
3065 config.Wifi.CountryCode = utils.ParseFormParamSingleValue(r.Form, "CountryCode")
3066- config.Wifi.Channel, err = strconv.Atoi(utils.ParseFormParamSingleValue(r.Form, "Channel"))
3067+ config.Wifi.Channel, err = strconv.Atoi(
3068+ utils.ParseFormParamSingleValue(r.Form, "Channel"))
3069 if err != nil {
3070 msg := fmt.Sprintf("Error parsing channel form value: %v", err)
3071 log.Println(msg)
3072@@ -145,7 +152,7 @@ func SaveConfigHandler(w http.ResponseWriter, r *http.Request) {
3073 config.Wifi.OperationMode = utils.ParseFormParamSingleValue(r.Form, "OperationMode")
3074 config.Portal.Password = utils.ParseFormParamSingleValue(r.Form, "PortalPassword")
3075
3076- err = utils.WriteConfig(config)
3077+ err = utils.WriteConfig(&config)
3078 if err != nil {
3079 msg := fmt.Sprintf("Error saving config: %v", err)
3080 log.Println(msg)
3081@@ -153,20 +160,12 @@ func SaveConfigHandler(w http.ResponseWriter, r *http.Request) {
3082 return
3083 }
3084
3085- //after saving config, redirect to management portal, showing available ssids
3086- ssids, err := utils.ReadSsidsFile()
3087- if err != nil {
3088- msg := fmt.Sprintf("== wifi-connect/handler: Error reading SSIDs file: %v\n", err)
3089- log.Println(msg)
3090- http.Error(w, msg, http.StatusInternalServerError)
3091- return
3092- }
3093-
3094- execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"})
3095+ execTemplate(w, managementTemplatePath,
3096+ ManagementData{Ssids: ms.ssidLs.GetSsidList(), Page: "ssids"})
3097 }
3098
3099 // ConnectHandler reads form got ssid and password and tries to connect to that network
3100-func ConnectHandler(w http.ResponseWriter, r *http.Request) {
3101+func (ms *ManagementServer) ConnectHandler(w http.ResponseWriter, r *http.Request) {
3102 r.ParseForm()
3103
3104 pwd := ""
3105@@ -184,55 +183,15 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) {
3106 }
3107 ssid := ssids[0]
3108
3109- go func() {
3110- log.Printf("Connecting to %v", ssid)
3111-
3112- // While the connecting is attempted, the flag file must be
3113- // present so the service loops harmlessly
3114- waitPath := os.Getenv("SNAP_COMMON") + "/startingApConnect"
3115- err1 := utils.WriteFlagFile(waitPath)
3116- if err1 != nil {
3117- log.Print("Error writing flag file")
3118- return
3119- }
3120-
3121- err := wifiapClient.Disable()
3122- if err != nil {
3123- msg := fmt.Sprintf("Error disabling AP: %v", err)
3124- log.Print(msg)
3125- http.Error(w, msg, http.StatusInternalServerError)
3126- return
3127- }
3128-
3129- // manage iface by netman
3130- netmanClient.SetIfaceManaged("wlan0", true, netmanClient.GetWifiDevices(netmanClient.GetDevices()))
3131- _, ap2device, ssid2ap := netmanClient.Ssids()
3132-
3133- // attempt to connect to external AP
3134- err = netmanClient.ConnectAp(ssid, pwd, ap2device, ssid2ap)
3135- //TODO signal user in portal on failure to connect
3136- if err != nil {
3137- msg := fmt.Sprintf("Failed connecting to %v", ssid)
3138- log.Println(msg)
3139- netmanClient.DeleteWifiConnections()
3140- //note that this call takes time, perhaps a couple minutes, so
3141- //deleting the wait file happens after, which means it takes a couple
3142- //minutes for the wifi-connect service to restor system to Management
3143- //mode
3144- http.Error(w, msg, http.StatusInternalServerError)
3145- //remove WAIT flag file so that daemon starts checking state
3146- //and takes control again
3147- waitPath := os.Getenv("SNAP_COMMON") + "/startingApConnect"
3148- utils.RemoveFlagFile(waitPath)
3149- }
3150- }()
3151+ ms.chUserEvs <- UserEvent{EvType: UserEventConnect,
3152+ Params: map[string]string{UeConnSsid: ssid, UeConnPassword: pwd}}
3153 }
3154
3155 type disconnectData struct {
3156 }
3157
3158 // OperationalHandler display Opertational mode page
3159-func OperationalHandler(w http.ResponseWriter, r *http.Request) {
3160+func (os *OperationalServer) OperationalHandler(w http.ResponseWriter, r *http.Request) {
3161 data := disconnectData{}
3162 execTemplate(w, operationalTemplatePath, data)
3163 }
3164@@ -242,8 +201,8 @@ type hashResponse struct {
3165 HashMatch bool
3166 }
3167
3168-// HashItHandler returns a hash of the password as json
3169-func HashItHandler(w http.ResponseWriter, r *http.Request) {
3170+// hashItHandler returns a hash of the password as json
3171+func hashItHandler(w http.ResponseWriter, r *http.Request) {
3172 r.ParseForm()
3173 hashMe := r.Form["Hash"]
3174 hashed, errH := utils.MatchingHash(hashMe[0])
3175@@ -262,52 +221,31 @@ func HashItHandler(w http.ResponseWriter, r *http.Request) {
3176 w.Write(b)
3177 }
3178
3179+// HashItHandler hashes the password
3180+func (os *OperationalServer) HashItHandler(w http.ResponseWriter, r *http.Request) {
3181+ hashItHandler(w, r)
3182+}
3183+
3184+// HashItHandler hashes the password
3185+func (ms *ManagementServer) HashItHandler(w http.ResponseWriter, r *http.Request) {
3186+ hashItHandler(w, r)
3187+}
3188+
3189 // DisconnectHandler allows user to disconnect from external AP
3190-func DisconnectHandler(w http.ResponseWriter, r *http.Request) {
3191- netmanClient.DisconnectWifi(netmanClient.GetWifiDevices(netmanClient.GetDevices()))
3192+func (os *OperationalServer) DisconnectHandler(w http.ResponseWriter, r *http.Request) {
3193+ os.chUserEvs <- UserEvent{EvType: UserEventDisconnect}
3194 }
3195
3196 // RefreshHandler handles ssids refreshment
3197-func RefreshHandler(w http.ResponseWriter, r *http.Request) {
3198+func (ms *ManagementServer) RefreshHandler(w http.ResponseWriter, r *http.Request) {
3199
3200 // show same page. After refresh operation, management page should show a refresh alert
3201- ManagementHandler(w, r)
3202-
3203- go func() {
3204- if err := netmanClient.Unmanage(); err != nil {
3205- fmt.Println(err)
3206- return
3207- }
3208-
3209- apUp, err := wifiapClient.Enabled()
3210- if err != nil {
3211- fmt.Println(Sprintf("An error happened while requesting current AP status: %v\n", err))
3212- return
3213- }
3214-
3215- if apUp {
3216- err := wifiapClient.Disable()
3217- if err != nil {
3218- fmt.Println(Sprintf("An error happened while bringing AP down: %v\n", err))
3219- return
3220- }
3221- }
3222+ ms.ManagementHandler(w, r)
3223
3224- for found := netmanClient.ScanAndWriteSsidsToFile(utils.SsidsFile); !found; found = netmanClient.ScanAndWriteSsidsToFile(utils.SsidsFile) {
3225- time.Sleep(5 * time.Second)
3226- }
3227-
3228- if err := netmanClient.Unmanage(); err != nil {
3229- fmt.Println(err)
3230- return
3231- }
3232-
3233- err = wifiapClient.Enable()
3234- if err != nil {
3235- fmt.Println(Sprintf("An error happened while bringing AP up: %v\n", err))
3236- return
3237- }
3238+ ms.chUserEvs <- UserEvent{EvType: UserEventRefresh}
3239+}
3240
3241- fmt.Println("== wifi-connect/RefreshHandler: starting wifi-ap")
3242- }()
3243+// RedirectHandler redirects to our main URL
3244+func (ms *ManagementServer) RedirectHandler(w http.ResponseWriter, r *http.Request) {
3245+ http.Redirect(w, r, "http://device-wifi-connect.com", http.StatusFound)
3246 }
3247diff --git a/server/handlers_test.go b/server/handlers_test.go
3248index f4278b7..27a81e3 100644
3249--- a/server/handlers_test.go
3250+++ b/server/handlers_test.go
3251@@ -26,124 +26,19 @@ import (
3252 "strings"
3253 "testing"
3254
3255- "launchpad.net/wifi-connect/netman"
3256 "launchpad.net/wifi-connect/utils"
3257 )
3258
3259-type wifiapClientMock struct{}
3260-
3261-func (c *wifiapClientMock) Show() (map[string]interface{}, error) {
3262- return nil, nil
3263-}
3264-
3265-func (c *wifiapClientMock) Enable() error {
3266- return nil
3267-}
3268-
3269-func (c *wifiapClientMock) Disable() error {
3270- return nil
3271-}
3272-
3273-func (c *wifiapClientMock) Enabled() (bool, error) {
3274- return true, nil
3275-}
3276-
3277-func (c *wifiapClientMock) SetSsid(string) error {
3278- return nil
3279-}
3280-
3281-func (c *wifiapClientMock) SetPassphrase(string) error {
3282- return nil
3283-}
3284-
3285-func (c *wifiapClientMock) Set(map[string]interface{}) error {
3286- return nil
3287-}
3288-
3289-type netmanClientMock struct{}
3290-
3291-func (c *netmanClientMock) GetDevices() []string {
3292- return []string{"/d/1"}
3293-}
3294-
3295-func (c *netmanClientMock) GetWifiDevices(devices []string) []string {
3296- return []string{"/d/1"}
3297-}
3298-
3299-func (c *netmanClientMock) GetAccessPoints(devices []string, ap2device map[string]string) []string {
3300- return []string{"/ap/1"}
3301-}
3302-
3303-func (c *netmanClientMock) ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error {
3304- return nil
3305-}
3306-
3307-func (c *netmanClientMock) Ssids() ([]netman.SSID, map[string]string, map[string]string) {
3308- myssid := netman.SSID{Ssid: "myssid", ApPath: "/ap/1"}
3309- return []netman.SSID{myssid}, map[string]string{"/ap/1": "/d/1"}, map[string]string{"myssid": "/ap/1"}
3310-}
3311-
3312-func (c *netmanClientMock) Connected(devices []string) bool {
3313- return false
3314-}
3315-
3316-func (c *netmanClientMock) ConnectedWifi(wifiDevices []string) bool {
3317- return false
3318-}
3319-
3320-func (c *netmanClientMock) DisconnectWifi(wifiDevices []string) int {
3321- return 0
3322-}
3323-
3324-func (c *netmanClientMock) SetIfaceManaged(iface string, state bool, devices []string) string {
3325- return "wlan0"
3326-}
3327-
3328-func (c *netmanClientMock) WifisManaged(wifiDevices []string) (map[string]string, error) {
3329- return map[string]string{"wlan0": "/d/1"}, nil
3330-}
3331-func (c *netmanClientMock) Unmanage() error {
3332- return nil
3333-}
3334-func (c *netmanClientMock) Manage() error {
3335- return nil
3336-}
3337-
3338-func (c *netmanClientMock) ScanAndWriteSsidsToFile(filepath string) bool {
3339- return true
3340-}
3341-
3342-func (c *netmanClientMock) DeleteWifiConnections() {}
3343-
3344 func TestManagementHandler(t *testing.T) {
3345-
3346- // mock settings to not to ask for saving a first configuration
3347- utils.ReadConfig = func() (*utils.Config, error) {
3348- return &utils.Config{
3349- Wifi: &utils.WifiConfig{
3350- Ssid: "Ubuntu",
3351- Passphrase: "17Soj8/Sxh14lcpD",
3352- Interface: "wlp2s0",
3353- CountryCode: "0x31",
3354- Channel: 6,
3355- OperationMode: "g",
3356- },
3357- Portal: &utils.PortalConfig{
3358- Password: "the_password",
3359- NoResetCredentials: true,
3360- NoOperational: false,
3361- },
3362- }, nil
3363- }
3364- utils.MustSetConfig = func() bool { return false }
3365+ chUserEvs := make(chan UserEvent)
3366+ ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{},
3367+ ssidLs: &utils.SsidList{}}
3368
3369 ResourcesPath = "../static"
3370- SsidsFile := "../static/tests/ssids"
3371- utils.SetSsidsFile(SsidsFile)
3372
3373 w := httptest.NewRecorder()
3374 r, _ := http.NewRequest("GET", "/", nil)
3375- http.HandlerFunc(ManagementHandler).ServeHTTP(w, r)
3376+ http.HandlerFunc(ms.ManagementHandler).ServeHTTP(w, r)
3377
3378 if w.Code != http.StatusOK {
3379 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)
3380@@ -155,9 +50,9 @@ func TestManagementHandler(t *testing.T) {
3381 }
3382
3383 func TestConnectHandler(t *testing.T) {
3384-
3385- wifiapClient = &wifiapClientMock{}
3386- netmanClient = &netmanClientMock{}
3387+ chUserEvs := make(chan UserEvent)
3388+ ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{},
3389+ ssidLs: &utils.SsidList{}}
3390
3391 ResourcesPath = "../static"
3392
3393@@ -169,7 +64,21 @@ func TestConnectHandler(t *testing.T) {
3394 r, _ := http.NewRequest("POST", "/connect", bytes.NewBufferString(form.Encode()))
3395 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
3396 r.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
3397- http.HandlerFunc(ConnectHandler).ServeHTTP(w, r)
3398+
3399+ go func() {
3400+ uEv := <-chUserEvs
3401+ if uEv.EvType != UserEventConnect {
3402+ t.Errorf("Bad event %v", uEv.EvType)
3403+ }
3404+ if uEv.Params[UeConnSsid] != "myssid" {
3405+ t.Errorf("Bad ssid: %s", uEv.Params[UeConnSsid])
3406+ }
3407+ if uEv.Params[UeConnPassword] != "mypassphrase" {
3408+ t.Errorf("Bad password: %s", uEv.Params[UeConnPassword])
3409+ }
3410+ }()
3411+
3412+ http.HandlerFunc(ms.ConnectHandler).ServeHTTP(w, r)
3413
3414 if w.Code != http.StatusOK {
3415 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)
3416@@ -177,14 +86,21 @@ func TestConnectHandler(t *testing.T) {
3417 }
3418
3419 func TestDisconnectHandler(t *testing.T) {
3420-
3421- netmanClient = &netmanClientMock{}
3422-
3423+ chUserEvs := make(chan UserEvent)
3424+ os := &OperationalServer{chUserEvs: chUserEvs}
3425 ResourcesPath = "../static"
3426
3427 w := httptest.NewRecorder()
3428 r, _ := http.NewRequest("GET", "/disconnect", nil)
3429- http.HandlerFunc(DisconnectHandler).ServeHTTP(w, r)
3430+
3431+ go func() {
3432+ uEv := <-chUserEvs
3433+ if uEv.EvType != UserEventDisconnect {
3434+ t.Errorf("Bad event %v", uEv.EvType)
3435+ }
3436+ }()
3437+
3438+ http.HandlerFunc(os.DisconnectHandler).ServeHTTP(w, r)
3439
3440 if w.Code != http.StatusOK {
3441 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)
3442@@ -192,17 +108,22 @@ func TestDisconnectHandler(t *testing.T) {
3443 }
3444
3445 func TestRefreshHandler(t *testing.T) {
3446-
3447- wifiapClient = &wifiapClientMock{}
3448- netmanClient = &netmanClientMock{}
3449-
3450+ chUserEvs := make(chan UserEvent)
3451+ ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{},
3452+ ssidLs: &utils.SsidList{}}
3453 ResourcesPath = "../static"
3454- SsidsFile := "../static/tests/ssids"
3455- utils.SetSsidsFile(SsidsFile)
3456
3457 w := httptest.NewRecorder()
3458 r, _ := http.NewRequest("GET", "/refresh", nil)
3459- http.HandlerFunc(RefreshHandler).ServeHTTP(w, r)
3460+
3461+ go func() {
3462+ uEv := <-chUserEvs
3463+ if uEv.EvType != UserEventRefresh {
3464+ t.Errorf("Bad event %v", uEv.EvType)
3465+ }
3466+ }()
3467+
3468+ http.HandlerFunc(ms.RefreshHandler).ServeHTTP(w, r)
3469
3470 if w.Code != http.StatusOK {
3471 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)
3472@@ -214,11 +135,13 @@ func TestRefreshHandler(t *testing.T) {
3473 }
3474
3475 func TestOperationalHandler(t *testing.T) {
3476+ chUserEvs := make(chan UserEvent)
3477+ os := &OperationalServer{chUserEvs: chUserEvs}
3478 ResourcesPath = "../static"
3479
3480 w := httptest.NewRecorder()
3481 r, _ := http.NewRequest("GET", "/", nil)
3482- http.HandlerFunc(OperationalHandler).ServeHTTP(w, r)
3483+ http.HandlerFunc(os.OperationalHandler).ServeHTTP(w, r)
3484
3485 if w.Code != http.StatusOK {
3486 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)
3487@@ -230,83 +153,17 @@ func TestOperationalHandler(t *testing.T) {
3488 }
3489
3490 func TestInvalidTemplateHandler(t *testing.T) {
3491+ chUserEvs := make(chan UserEvent)
3492+ ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{},
3493+ ssidLs: &utils.SsidList{}}
3494
3495 ResourcesPath = "/invalidpath"
3496
3497 w := httptest.NewRecorder()
3498 r, _ := http.NewRequest("GET", "/", nil)
3499- http.HandlerFunc(ManagementHandler).ServeHTTP(w, r)
3500+ http.HandlerFunc(ms.ManagementHandler).ServeHTTP(w, r)
3501
3502 if w.Code != http.StatusInternalServerError {
3503 t.Errorf("Expected status %d, got: %d", http.StatusInternalServerError, w.Code)
3504 }
3505 }
3506-
3507-func TestReadSsidsFile(t *testing.T) {
3508-
3509- SsidsFile := "../static/tests/ssids"
3510- utils.SetSsidsFile(SsidsFile)
3511- ssids, err := utils.ReadSsidsFile()
3512- if err != nil {
3513- t.Errorf("Unexpected error reading ssids file: %v", err)
3514- }
3515-
3516- if len(ssids) != 4 {
3517- t.Error("Expected 4 elements in csv record")
3518- }
3519-
3520- set := make(map[string]bool)
3521- for _, v := range ssids {
3522- set[v] = true
3523- }
3524-
3525- if !set["mynetwork"] {
3526- t.Error("mynetwork value not found")
3527- }
3528- if !set["yournetwork"] {
3529- t.Error("yournetwork value not found")
3530- }
3531- if !set["hernetwork"] {
3532- t.Error("hernetwork value not found")
3533- }
3534- if !set["hisnetwork"] {
3535- t.Error("hisnetwork value not found")
3536- }
3537-}
3538-
3539-func TestReadSsidsFileWithOnlyOne(t *testing.T) {
3540-
3541- SsidsFile := "../static/tests/ssids_onlyonessid"
3542- utils.SetSsidsFile(SsidsFile)
3543- ssids, err := utils.ReadSsidsFile()
3544- if err != nil {
3545- t.Errorf("Unexpected error reading ssids file: %v", err)
3546- }
3547-
3548- if len(ssids) != 1 {
3549- t.Error("Expected 1 elements in csv record")
3550- }
3551-
3552- set := make(map[string]bool)
3553- for _, v := range ssids {
3554- set[v] = true
3555- }
3556-
3557- if !set["mynetwork"] {
3558- t.Error("mynetwork value not found")
3559- }
3560-}
3561-
3562-func TestReadEmptySsidsFile(t *testing.T) {
3563-
3564- SsidsFile := "../static/tests/ssids_empty"
3565- utils.SetSsidsFile(SsidsFile)
3566- ssids, err := utils.ReadSsidsFile()
3567- if err != nil {
3568- t.Errorf("Unexpected error reading ssids file: %v", err)
3569- }
3570-
3571- if len(ssids) != 0 {
3572- t.Error("Expected 0 elements in csv record")
3573- }
3574-}
3575diff --git a/server/launcher.go b/server/launcher.go
3576index 7317df0..3615781 100644
3577--- a/server/launcher.go
3578+++ b/server/launcher.go
3579@@ -19,118 +19,50 @@ package server
3580
3581 import (
3582 "log"
3583- "net"
3584 "net/http"
3585- "time"
3586-
3587- "launchpad.net/wifi-connect/utils"
3588+ "strconv"
3589 )
3590
3591+// RunningState enum defining which server is up and running
3592+type RunningState int
3593+
3594 // Server running state
3595 const (
3596- Stopped RunningState = 0 + iota
3597- Starting
3598+ Stopped RunningState = iota
3599 Running
3600- Stopping
3601 )
3602
3603-const (
3604- // TestingAddress listening point for testing servers
3605- TestingAddress = ":8081"
3606-)
3607-
3608-// RunningState enum defining which server is up and running
3609-type RunningState int
3610-
3611-// Address where server is listening
3612-var Address = ":8080"
3613-
3614 // State holds current server state
3615 var State = Stopped
3616
3617-var listener net.Listener
3618+var server *http.Server
3619 var done chan bool
3620
3621-type tcpKeepAliveListener struct {
3622- *net.TCPListener
3623-}
3624-
3625-// WaitForState waits for server reach certain state
3626-func WaitForState(state RunningState) bool {
3627- retries := 10
3628- idle := 10 * time.Millisecond
3629- for ; retries > 0 && State != state; retries-- {
3630- time.Sleep(idle)
3631- idle *= 2
3632- }
3633- return State == state
3634-}
3635-
3636-// Accept accepts incoming tcp connections
3637-func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
3638- tc, err := ln.AcceptTCP()
3639- if err != nil {
3640- return tc, err
3641- }
3642- tc.SetKeepAlive(true)
3643- tc.SetKeepAlivePeriod(3 * time.Minute)
3644- return tc, nil
3645-}
3646-
3647-func listenAndServe(addr string, handler http.Handler) error {
3648+func listenAndServe(port int, handler http.Handler) error {
3649
3650 if State != Stopped {
3651- return Errorf("Server is not in proper stopped state before trying to start it")
3652- }
3653-
3654- if utils.RunningOn(addr) {
3655- return Errorf("Another instance is running in same address %v", addr)
3656+ return Errorf("Server is not in stopped state before trying to start it")
3657 }
3658
3659- State = Starting
3660-
3661- srv := &http.Server{Addr: addr, Handler: handler}
3662- // channel needed to communicate real server shutdown, as after calling listener.Close()
3663- // it can take several milliseconds to really stop the listening.
3664+ server = &http.Server{Addr: ":" + strconv.Itoa(port), Handler: handler}
3665+ // channel needed to communicate real server shutdown, as after calling
3666+ // server.Close() it can take several milliseconds to really stop the
3667+ // listening.
3668 done = make(chan bool)
3669
3670- var err error
3671- listener, err = net.Listen("tcp", addr)
3672- if err != nil {
3673- return err
3674- }
3675-
3676- // launch goroutine to check server state changes after startup is triggered
3677- go func() {
3678- retries := 10
3679- idle := 10 * time.Millisecond
3680- for ; !utils.RunningOn(addr) && retries > 0; retries-- {
3681- time.Sleep(idle)
3682- idle *= 2
3683- }
3684-
3685- if retries == 0 {
3686- log.Print("Server could not be started")
3687- return
3688- }
3689-
3690- State = Running
3691- }()
3692-
3693 // launching server in a goroutine for not blocking
3694- go func() {
3695- if listener != nil {
3696- err := srv.Serve(tcpKeepAliveListener{listener.(*net.TCPListener)})
3697- if err != nil {
3698- log.Printf("HTTP Server closing - %v", err)
3699- }
3700- // notify server real stop
3701- done <- true
3702+ go func(server *http.Server) {
3703+ err := server.ListenAndServe()
3704+ if err != nil {
3705+ log.Printf("HTTP Server closing - %v", err)
3706 }
3707+ // notify server real stop
3708+ done <- true
3709
3710 close(done)
3711- }()
3712+ }(server)
3713
3714+ State = Running
3715 return nil
3716 }
3717
3718@@ -140,18 +72,16 @@ func stop() error {
3719 return Errorf("Already stopped")
3720 }
3721
3722- if listener == nil {
3723+ if server == nil {
3724 State = Stopped
3725 return Errorf("Already closed")
3726 }
3727
3728- State = Stopping
3729-
3730- err := listener.Close()
3731+ err := server.Close()
3732 if err != nil {
3733 return err
3734 }
3735- listener = nil
3736+ server = nil
3737
3738 // wait for server real shutdown confirmation
3739 <-done
3740diff --git a/server/launcher_test.go b/server/launcher_test.go
3741index c377c79..eb252d3 100644
3742--- a/server/launcher_test.go
3743+++ b/server/launcher_test.go
3744@@ -19,77 +19,24 @@ package server
3745
3746 import (
3747 "testing"
3748+ "time"
3749
3750- telnet "github.com/reiver/go-telnet"
3751+ "launchpad.net/wifi-connect/utils"
3752 )
3753
3754 func TestLaunchAndStop(t *testing.T) {
3755+ thePort := 8444
3756
3757- thePort := ":14444"
3758-
3759- err := listenAndServe(thePort, nil)
3760- if err != nil {
3761- t.Errorf("Start server failed: %v", err)
3762- }
3763-
3764- // telnet to check server is alive
3765- caller := telnet.StandardCaller
3766- err = telnet.DialToAndCall("localhost"+thePort, caller)
3767- if err != nil {
3768- t.Errorf("Failed to telnet localhost server at port %v: %v", thePort, err)
3769- }
3770-
3771- err = stop()
3772- if err != nil {
3773- t.Errorf("Stop server error: %v", err)
3774- }
3775-}
3776-
3777-// This test is being skipped because of a problem with jenkins where it fails.
3778-// We cannot reproduce the failure outside of jenkins. Additionally, we are
3779-// changing the http server and will not track state in the same way in the future.
3780-// These tests will change.
3781-func SkipTestStates(t *testing.T) {
3782-
3783- WaitForState(Stopped)
3784-
3785- if State != Stopped {
3786- t.Error("Not in initial state")
3787- }
3788-
3789- thePort := ":14444"
3790-
3791- err := listenAndServe(thePort, nil)
3792+ chUserEvs := make(chan UserEvent)
3793+ err := listenAndServe(thePort, managementHandler(chUserEvs,
3794+ &utils.Config{}, &utils.SsidList{}))
3795 if err != nil {
3796 t.Errorf("Start server failed: %v", err)
3797 }
3798
3799- if State != Starting && State != Running {
3800- t.Error("Not in proper start(ing) state")
3801- }
3802-
3803- WaitForState(Running)
3804-
3805- // try a bad transition
3806- err = listenAndServe(thePort, nil)
3807- if err == nil {
3808- t.Error("An error should be thrown when trying to start an already running instance")
3809- }
3810-
3811+ time.Sleep(1 * time.Second)
3812 err = stop()
3813 if err != nil {
3814 t.Errorf("Stop server error: %v", err)
3815 }
3816-
3817- if State != Stopping && State != Stopped {
3818- t.Error("Not in proper stop(ing) state")
3819- }
3820-
3821- WaitForState(Stopped)
3822-
3823- // try bad transitions
3824- err = stop()
3825- if err == nil {
3826- t.Error("An error should be thrown when trying to stop a stopped instance")
3827- }
3828 }
3829diff --git a/server/manager.go b/server/manager.go
3830index 6b11c23..c668974 100644
3831--- a/server/manager.go
3832+++ b/server/manager.go
3833@@ -18,8 +18,6 @@
3834 package server
3835
3836 import (
3837- "os"
3838-
3839 "launchpad.net/wifi-connect/utils"
3840 )
3841
3842@@ -37,23 +35,20 @@ type Server int
3843 var Current = None
3844
3845 // StartManagementServer starts server in management mode
3846-func StartManagementServer() error {
3847+func StartManagementServer(chUserEvs chan<- UserEvent, config *utils.Config,
3848+ ssidLs *utils.SsidList) error {
3849+
3850 if Current != None {
3851 Current = None
3852- return Errorf("Not in a valid status. Please stop first any other server instance before starting this one")
3853+ return Errorf("Invalid state: other server is running")
3854 }
3855
3856 // change current instance asap we manage this server
3857 Current = Management
3858
3859- waitPath := os.Getenv("SNAP_COMMON") + "/startingApConnect"
3860- err := utils.WriteFlagFile(waitPath)
3861- if err != nil {
3862- Current = None
3863- return err
3864- }
3865-
3866- err = listenAndServe(Address, managementHandler())
3867+ // Pass around program config to handlers
3868+ err := listenAndServe(config.Portal.Port,
3869+ managementHandler(chUserEvs, config, ssidLs))
3870 if err != nil {
3871 Current = None
3872 return err
3873@@ -63,15 +58,15 @@ func StartManagementServer() error {
3874 }
3875
3876 // StartOperationalServer starts server in operational mode
3877-func StartOperationalServer() error {
3878+func StartOperationalServer(chUserEvs chan<- UserEvent, config *utils.Config) error {
3879 if Current != None {
3880- return Errorf("Not in a valid status. Please stop first any other server instance before starting this one")
3881+ return Errorf("Invalid state: other server is running")
3882 }
3883
3884 // change current instance asap we manage this server
3885 Current = Operational
3886
3887- err := listenAndServe(Address, operationalHandler())
3888+ err := listenAndServe(config.Portal.Port, operationalHandler(chUserEvs))
3889 if err != nil {
3890 Current = None
3891 return err
3892@@ -80,9 +75,10 @@ func StartOperationalServer() error {
3893 return nil
3894 }
3895
3896-// ShutdownManagementServer shutdown server management mode. If management server is not up, returns error
3897+// ShutdownManagementServer shutdown server management mode. If management
3898+// server is not up, returns error
3899 func ShutdownManagementServer() error {
3900- if Current != Management || (State != Running && State != Starting) {
3901+ if Current != Management || State != Running {
3902 return Errorf("Trying to stop management server when it is not running")
3903 }
3904
3905@@ -95,9 +91,10 @@ func ShutdownManagementServer() error {
3906 return nil
3907 }
3908
3909-// ShutdownOperationalServer shutdown server operational mode. If operational server is not up, returns error
3910+// ShutdownOperationalServer shutdown server operational mode. If operational
3911+// server is not up, returns error
3912 func ShutdownOperationalServer() error {
3913- if Current != Operational || (State != Running && State != Starting) {
3914+ if Current != Operational || State != Running {
3915 return Errorf("Trying to stop operational server when it is not running")
3916 }
3917
3918diff --git a/server/manager_test.go b/server/manager_test.go
3919index ea0661e..dbb38ae 100644
3920--- a/server/manager_test.go
3921+++ b/server/manager_test.go
3922@@ -1,29 +1,28 @@
3923 package server
3924
3925 import (
3926- "os"
3927 "testing"
3928+ "time"
3929+
3930+ "launchpad.net/wifi-connect/utils"
3931 )
3932
3933 func TestBasicServerTransitionStates(t *testing.T) {
3934
3935- Address = ":14444"
3936-
3937- os.Setenv("SNAP_COMMON", os.TempDir())
3938-
3939 if Current != None || State != Stopped {
3940 t.Errorf("Server is not in initial state")
3941 }
3942
3943- if err := StartManagementServer(); err != nil {
3944+ chUserEvs := make(chan UserEvent)
3945+ cfg := utils.Config{Portal: utils.PortalConfig{Port: 14444}}
3946+ if err := StartManagementServer(chUserEvs, &cfg, &utils.SsidList{}); err != nil {
3947 t.Errorf("Error starting management server %v", err)
3948 }
3949- if Current != Management || (State != Starting && State != Running) {
3950+ if Current != Management || State != Running {
3951 t.Errorf("Server is not in starting or in management status")
3952 }
3953
3954- WaitForState(Running)
3955-
3956+ time.Sleep(1 * time.Second)
3957 if err := ShutdownManagementServer(); err != nil {
3958 t.Errorf("Error stopping management server %v", err)
3959 }
3960@@ -32,17 +31,14 @@ func TestBasicServerTransitionStates(t *testing.T) {
3961 t.Errorf("Current server is not None")
3962 }
3963
3964- WaitForState(Stopped)
3965-
3966- if err := StartOperationalServer(); err != nil {
3967+ if err := StartOperationalServer(chUserEvs, &cfg); err != nil {
3968 t.Errorf("Error starting operational server %v", err)
3969 }
3970- if Current != Operational || (State != Starting && State != Running) {
3971+ if Current != Operational || State != Running {
3972 t.Errorf("Server is not in starting or in operational status")
3973 }
3974
3975- WaitForState(Running)
3976-
3977+ time.Sleep(1 * time.Second)
3978 if err := ShutdownOperationalServer(); err != nil {
3979 t.Errorf("Error stopping operational server %v", err)
3980 }
3981@@ -52,31 +48,26 @@ func TestBasicServerTransitionStates(t *testing.T) {
3982 }
3983
3984 func TestEdgeServerTransitionStates(t *testing.T) {
3985-
3986- Address = ":14444"
3987-
3988- os.Setenv("SNAP_COMMON", os.TempDir())
3989-
3990 if Current != None {
3991 t.Errorf("Server is not in initial state")
3992 }
3993
3994- if err := StartManagementServer(); err != nil {
3995+ chUserEvs := make(chan UserEvent)
3996+ cfg := utils.Config{Portal: utils.PortalConfig{Port: 14444}}
3997+ if err := StartManagementServer(chUserEvs, &cfg, &utils.SsidList{}); err != nil {
3998 t.Errorf("Error starting management server %v", err)
3999 }
4000- if Current != Management || (State != Starting && State != Running) {
4001+ if Current != Management || State != Running {
4002 t.Errorf("Server is not in starting or in management status")
4003 }
4004
4005- WaitForState(Running)
4006-
4007 // start operational server without stopping management must throw an error
4008 if err := StartOperationalServer; err == nil {
4009- t.Errorf(`Expected an error when trying to launch one server instance having
4010- the other active`)
4011+ t.Errorf("No error when trying to launch one server instance having " +
4012+ "the other active")
4013 }
4014 if Current != Management {
4015- t.Errorf("Server is not in management status after failed start operational server")
4016+ t.Errorf("Not in management status after failed start operational server")
4017 }
4018
4019 // stop wrong server must throw an error
4020@@ -84,9 +75,10 @@ func TestEdgeServerTransitionStates(t *testing.T) {
4021 t.Errorf("Expected an error when trying to shutdown wrong server")
4022 }
4023 if Current != Management {
4024- t.Errorf("Server is not in management status after failed start operational server")
4025+ t.Errorf("Not in management status after failed start operational server")
4026 }
4027
4028+ time.Sleep(1 * time.Second)
4029 if err := ShutdownManagementServer(); err != nil {
4030 t.Errorf("Error stopping management server %v", err)
4031 }
4032@@ -94,25 +86,21 @@ func TestEdgeServerTransitionStates(t *testing.T) {
4033 t.Errorf("Server is not in None status")
4034 }
4035
4036- WaitForState(Stopped)
4037-
4038 // analog tests with operational server
4039- if err := StartOperationalServer(); err != nil {
4040+ if err := StartOperationalServer(chUserEvs, &cfg); err != nil {
4041 t.Errorf("Error starting operational server %v", err)
4042 }
4043- if Current != Operational || (State != Starting && State != Running) {
4044+ if Current != Operational || State != Running {
4045 t.Errorf("Server is not in starting or in operational status")
4046 }
4047
4048- WaitForState(Running)
4049-
4050 // start management server without stopping operational must throw an error
4051 if err := StartManagementServer; err == nil {
4052- t.Errorf(`Expected an error when trying to launch one server instance having
4053- the other active`)
4054+ t.Errorf("Expected an error when trying to launch one server " +
4055+ "instance having the other active")
4056 }
4057 if Current != Operational {
4058- t.Errorf("Server is not in operational status after failed start operational server")
4059+ t.Errorf("Not in operational status after failed start operational server")
4060 }
4061
4062 // stop wrong server must throw an error
4063@@ -120,9 +108,10 @@ func TestEdgeServerTransitionStates(t *testing.T) {
4064 t.Errorf("Expected an error when trying to shutdown wrong server")
4065 }
4066 if Current != Operational {
4067- t.Errorf("Server is not in operational status after failed start operational server")
4068+ t.Errorf("Not in operational status after failed start operational server")
4069 }
4070
4071+ time.Sleep(1 * time.Second)
4072 if err := ShutdownOperationalServer(); err != nil {
4073 t.Errorf("Error stopping operational server %v", err)
4074 }
4075diff --git a/server/router.go b/server/router.go
4076index 20a3716..3af467e 100644
4077--- a/server/router.go
4078+++ b/server/router.go
4079@@ -21,18 +21,31 @@ import (
4080 "net/http"
4081
4082 "github.com/gorilla/mux"
4083+ "launchpad.net/wifi-connect/utils"
4084 )
4085
4086 // managementHandler handles requests for web UI when AP is up
4087-func managementHandler() *mux.Router {
4088+func managementHandler(chUserEvs chan<- UserEvent, config *utils.Config,
4089+ ssidLs *utils.SsidList) *mux.Router {
4090+
4091 router := mux.NewRouter()
4092+ ms := &ManagementServer{chUserEvs: chUserEvs, config: *config, ssidLs: ssidLs}
4093
4094 // Pages routes
4095- router.Handle("/", Middleware(http.HandlerFunc(ManagementHandler))).Methods("GET")
4096- router.Handle("/config", Middleware(http.HandlerFunc(SaveConfigHandler))).Methods("POST")
4097- router.Handle("/connect", Middleware(http.HandlerFunc(ConnectHandler))).Methods("POST")
4098- router.HandleFunc("/hashit", HashItHandler).Methods("POST")
4099- router.Handle("/refresh", Middleware(http.HandlerFunc(RefreshHandler))).Methods("GET")
4100+ router.HandleFunc("/", ms.ManagementHandler).Methods("GET")
4101+ router.HandleFunc("/config", ms.SaveConfigHandler).Methods("POST")
4102+ router.HandleFunc("/connect", ms.ConnectHandler).Methods("POST")
4103+ router.HandleFunc("/hashit", ms.HashItHandler).Methods("POST")
4104+ router.HandleFunc("/refresh", ms.RefreshHandler).Methods("GET")
4105+ // To allow detection of captive portal
4106+ // Android/ChromeOS
4107+ router.HandleFunc("/generate_204", ms.RedirectHandler).Methods("GET")
4108+ // iOS/Mac
4109+ router.HandleFunc("/hotspot-detect.html", ms.RedirectHandler).Methods("GET")
4110+ router.HandleFunc("/library/test/success.html", ms.RedirectHandler).Methods("GET")
4111+ // Windows
4112+ router.HandleFunc("/connecttest.txt", ms.RedirectHandler).Methods("GET")
4113+ router.HandleFunc("/redirect", ms.RedirectHandler).Methods("GET")
4114
4115 // Resources path
4116 fs := http.StripPrefix("/static/", http.FileServer(http.Dir(ResourcesPath)))
4117@@ -42,15 +55,17 @@ func managementHandler() *mux.Router {
4118 }
4119
4120 // operationalHandler handles request for web UI when connected to external Wi-Fi
4121-func operationalHandler() *mux.Router {
4122+func operationalHandler(chUserEvs chan<- UserEvent) *mux.Router {
4123 router := mux.NewRouter()
4124+ os := &OperationalServer{chUserEvs: chUserEvs}
4125
4126- router.HandleFunc("/", OperationalHandler).Methods("GET")
4127- router.Handle("/disconnect", Middleware(http.HandlerFunc(DisconnectHandler))).Methods("GET")
4128- router.HandleFunc("/hashit", HashItHandler).Methods("POST")
4129+ router.HandleFunc("/", os.OperationalHandler).Methods("GET")
4130+ router.HandleFunc("/disconnect", os.DisconnectHandler).Methods("GET")
4131+ router.HandleFunc("/hashit", os.HashItHandler).Methods("POST")
4132
4133 // Resources path
4134 fs := http.StripPrefix("/static/", http.FileServer(http.Dir(ResourcesPath)))
4135 router.PathPrefix("/static/").Handler(fs)
4136+
4137 return router
4138 }
4139diff --git a/service/service.go b/service/service.go
4140index 3ea136c..3d30bc8 100644
4141--- a/service/service.go
4142+++ b/service/service.go
4143@@ -18,157 +18,21 @@
4144 package main
4145
4146 import (
4147- "fmt"
4148 "log"
4149- "os"
4150- "time"
4151
4152 "launchpad.net/wifi-connect/daemon"
4153- "launchpad.net/wifi-connect/netman"
4154 "launchpad.net/wifi-connect/utils"
4155- "launchpad.net/wifi-connect/wifiap"
4156 )
4157
4158-var prevConnected bool
4159-
4160 func main() {
4161-
4162- log.SetFlags(log.Lshortfile)
4163+ log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile)
4164 log.SetPrefix("== wifi-connect: ")
4165
4166- c := netman.DefaultClient()
4167- cw := wifiap.DefaultClient()
4168- client := daemon.GetClient()
4169-
4170- config, err := daemon.LoadPreConfig()
4171- if err != nil {
4172- log.Printf("Empty preconfiguration: %v", err)
4173- }
4174- err = client.SetDefaults(cw, config)
4175+ config, err := utils.LoadConfig()
4176 if err != nil {
4177- log.Printf("SetDetaults error: %v", err)
4178+ log.Printf("Empty configuration: %v", err)
4179 }
4180- first := true
4181- client.SetWaitFlagPath(os.Getenv("SNAP_COMMON") + "/startingApConnect")
4182- client.SetManualFlagPath(os.Getenv("SNAP_COMMON") + "/manualMode")
4183-
4184- client.ManagementServerDown()
4185- client.OperationalServerDown()
4186-
4187- connected := false
4188- prevConnected = false
4189-
4190- for {
4191- if first {
4192- log.Print("daemon STARTING")
4193- client.SetPreviousState(daemon.STARTING)
4194- client.SetState(daemon.STARTING)
4195- first = false
4196- //clean start require wifi AP down so we can get SSIDs
4197- cw.Disable()
4198- //remove previous State flags
4199- utils.RemoveFlagFile(client.GetWaitFlagPath())
4200- utils.RemoveFlagFile(client.GetManualFlagPath())
4201- //TODO only wait if wlan0 is managed
4202- //wait time period (TBD) on first run to allow wifi connections
4203- time.Sleep(10000 * time.Millisecond)
4204- }
4205-
4206- // wait 5 seconds on each iter
4207- time.Sleep(5000 * time.Millisecond)
4208
4209- config, err = daemon.LoadPreConfig()
4210- if err != nil {
4211- log.Printf("Empty preconfiguration: %v", err)
4212- }
4213- // loop without action if in manual mode
4214- if client.ManualMode() {
4215- continue
4216- }
4217-
4218- // start clean on exiting manual mode
4219- if client.GetPreviousState() == daemon.MANUAL {
4220- first = true
4221- continue
4222- }
4223- // the AP should not be up without SSIDS
4224- if client.IsApUpWithoutSSIDs(cw) {
4225- cw.Disable()
4226- continue
4227- }
4228-
4229- // log connected state if different
4230- prevConnected = connected
4231- connected = c.ConnectedWifi(c.GetWifiDevices(c.GetDevices()))
4232- if first || prevConnected != connected {
4233- if connected {
4234- log.Print("WIFI CONNECTED")
4235- } else {
4236- log.Print("WIFI NOT CONNECTED")
4237- }
4238- }
4239-
4240- // if an external wifi connection, we are in Operational mode
4241- // and we stay here until there is an external wifi connection
4242- if connected {
4243- //log.Print("operational config:", config.Operational)
4244- client.SetState(daemon.OPERATING)
4245- if client.GetPreviousState() != daemon.OPERATING {
4246- log.Print("entering OPERATIONAL mode")
4247- client.ManagementServerDown()
4248- if config.Operational {
4249- //log.Print("about to put up oper server")
4250- client.OperationalServerUp()
4251- }
4252- }
4253- continue
4254- }
4255-
4256- // wait/loop until wait flag file is gone
4257- // this stops daemon State changing until the management portal
4258- // is done, either stopped or the user has attempted to connect to
4259- // an external AP
4260- if client.CheckWaitApConnect() {
4261- continue
4262- }
4263-
4264- client.SetState(daemon.MANAGING)
4265- if client.GetPreviousState() != daemon.MANAGING || !client.CheckWaitApConnect() {
4266- // if wlan0 managed, set Unmanaged so that we can bring up wifi-ap
4267- // properly if needed
4268- if err := c.Unmanage(); err != nil {
4269- log.Print(err)
4270- continue
4271- }
4272-
4273- //wifi-ap UP?
4274- wifiUp, err := cw.Enabled()
4275- if err != nil {
4276- log.Printf("Error checking wifi-ap.Enabled(): %v", err)
4277- continue // try again since no better course of action
4278- }
4279-
4280- if !wifiUp {
4281- log.Print("entering MANAGEMENT mode")
4282- found := c.ScanAndWriteSsidsToFile(utils.SsidsFile)
4283- if err := c.Unmanage(); err != nil {
4284- fmt.Println(err)
4285- continue
4286- }
4287- if !found {
4288- log.Print("No SSIDs found. Continuing to scan for SSIDS...")
4289- continue
4290- }
4291- log.Printf("starting wifi-ap")
4292- if err := cw.Enable(); err != nil {
4293- log.Print(err)
4294- continue
4295- }
4296- //if client.GetPreviousState() == daemon.OPERATING {
4297- client.OperationalServerDown()
4298- //}
4299- client.ManagementServerUp()
4300- }
4301- }
4302- }
4303+ service := daemon.GetService(config)
4304+ service.Start()
4305 }
4306diff --git a/service/service_test.go b/service/service_test.go
4307new file mode 100644
4308index 0000000..795dc6a
4309--- /dev/null
4310+++ b/service/service_test.go
4311@@ -0,0 +1,26 @@
4312+/*
4313+ * Copyright (C) 2017 Canonical Ltd
4314+ *
4315+ * This program is free software: you can redistribute it and/or modify
4316+ * it under the terms of the GNU General Public License version 3 as
4317+ * published by the Free Software Foundation.
4318+ *
4319+ * This program is distributed in the hope that it will be useful,
4320+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
4321+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4322+ * GNU General Public License for more details.
4323+ *
4324+ * You should have received a copy of the GNU General Public License
4325+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
4326+ *
4327+ */
4328+
4329+package main
4330+
4331+import (
4332+ "testing"
4333+)
4334+
4335+func TestService(t *testing.T) {
4336+ // TODO Fill it
4337+}
4338diff --git a/snapcraft.yaml b/snapcraft.yaml
4339index a2af4b2..b7e3be7 100644
4340--- a/snapcraft.yaml
4341+++ b/snapcraft.yaml
4342@@ -1,5 +1,5 @@
4343 name: wifi-connect
4344-version: 0.11
4345+version: '0.2'
4346 summary: Connect your device to external wifi over temp wifi AP
4347 description: |
4348 A solution to enable your device to connect to an external
4349@@ -11,51 +11,73 @@ description: |
4350 https://code.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect
4351 grade: stable
4352 confinement: strict
4353+base: core18
4354
4355 apps:
4356- wifi-connect:
4357- command: cmd
4358- plugs: [network, network-bind, network-manager, control]
4359 daemon:
4360- command: service
4361+ command: wificonnectd
4362 daemon: simple
4363- plugs: [network-manager, control, network-bind]
4364+ plugs: [network-manager, network-bind, firewall-control]
4365
4366 hooks:
4367- configure:
4368- plugs: [network]
4369-
4370-plugs:
4371- control:
4372- interface: content
4373- content: socket-directory
4374- target: $SNAP_COMMON
4375- default-provider: wifi-ap
4376+ configure:
4377+ plugs: [network]
4378+
4379+# For dnsmasq, dir where leases are stored
4380+layout:
4381+ /var/lib/misc:
4382+ bind: $SNAP_DATA
4383
4384 parts:
4385- go:
4386- plugin: go
4387- source: .
4388- go-importpath: launchpad.net/wifi-connect
4389- build-packages:
4390- # needed by go get
4391- - bzr
4392- install: |
4393+
4394+ go-binaries:
4395+ plugin: dump
4396+ source: .
4397+ source-type: local
4398+ build-snaps: [ "go" ]
4399+ build-packages: [ "bzr", "git", "python", "build-essential" ]
4400+ stage-packages: [ "dnsmasq" ]
4401+ override-build: |
4402+ set -ex
4403+ export GOPATH=$(mktemp -d)
4404+ (
4405+ src_path="$GOPATH"/src/launchpad.net/wifi-connect
4406+ mkdir -p "$src_path"
4407+ cp -a avahi daemon hooks netman server service utils static \
4408+ dependencies.tsv mdlint.py README.md run-checks "$src_path"
4409+ cd "$src_path"
4410+ go get launchpad.net/godeps
4411+ export PATH=$PATH:$GOPATH/bin
4412+ godeps -u dependencies.tsv
4413+ mkdir -p "$SNAPCRAFT_PART_INSTALL"/bin/
4414+ cd "$GOPATH"/src/launchpad.net/wifi-connect/
4415+ go build -o "$SNAPCRAFT_PART_INSTALL"/bin/wificonnectd service/service.go
4416+ # configure hook
4417+ mkdir -p "$SNAPCRAFT_PART_INSTALL"/snap/hooks
4418+ go build -o "$SNAPCRAFT_PART_INSTALL"/snap/hooks/configure hooks/configure.go
4419+ )
4420 # set environment var SKIP_TESTS to 'y' or 'yes' if you want not to execute
4421 # this part unit tests in your next compilation.
4422+ SKIP_TESTS=no
4423 if [ "$SKIP_TESTS" = "yes" ] || [ "$SKIP_TESTS" = "y" ]; then
4424- echo "skipping unit tests"
4425+ echo "skipping unit tests"
4426 else
4427- export GOPATH=$PWD/../go
4428- cd $GOPATH/src/launchpad.net/wifi-connect
4429- ./run-checks all
4430+ cd "$GOPATH"/src/launchpad.net/wifi-connect
4431+ ./run-checks all
4432 fi
4433- # configure hook
4434- mkdir -p $SNAPCRAFT_PART_INSTALL/snap/hooks
4435- mv $SNAPCRAFT_PART_INSTALL/bin/hooks $SNAPCRAFT_PART_INSTALL/snap/hooks/configure
4436+ cd
4437+ rm -rf "$GOPATH"
4438+ stage:
4439+ - bin/wificonnectd
4440+ - snap/hooks/configure
4441+ - usr/sbin/dnsmasq
4442+
4443 assets:
4444 plugin: dump
4445 source: .
4446- prepare: ../../../scriptlets/fill_country_codes.sh
4447+ override-build: |
4448+ "$SNAPCRAFT_PROJECT_DIR"/scriptlets/fill_country_codes.sh
4449+ snapcraftctl build
4450 stage:
4451 - static
4452+ - -static/tests
4453diff --git a/static/js/jquery.min.js b/static/js/jquery.min.js
4454new file mode 100644
4455index 0000000..644d35e
4456--- /dev/null
4457+++ b/static/js/jquery.min.js
4458@@ -0,0 +1,4 @@
4459+/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */
4460+!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=Array.isArray(d)))?(e?(e=!1,f=c&&Array.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,N,e),g(f,c,O,e)):(f++,j.call(a,g(f,c,N,e),g(f,c,O,e),g(f,c,N,c.notifyWith))):(d!==N&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S),
4461+a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},U=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function V(){this.expando=r.expando+V.uid++}V.uid=1,V.prototype={cache:function(a){var b=a[this.expando];return b||(b={},U(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){Array.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(L)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var W=new V,X=new V,Y=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Z=/[A-Z]/g;function $(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:Y.test(a)?JSON.parse(a):a)}function _(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Z,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=$(c)}catch(e){}X.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return X.hasData(a)||W.hasData(a)},data:function(a,b,c){return X.access(a,b,c)},removeData:function(a,b){X.remove(a,b)},_data:function(a,b,c){return W.access(a,b,c)},_removeData:function(a,b){W.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=X.get(f),1===f.nodeType&&!W.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),_(f,d,e[d])));W.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){X.set(this,a)}):T(this,function(b){var c;if(f&&void 0===b){if(c=X.get(f,a),void 0!==c)return c;if(c=_(f,a),void 0!==c)return c}else this.each(function(){X.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=W.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var aa=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ba=new RegExp("^(?:([+-])=|)("+aa+")([a-z%]*)$","i"),ca=["Top","Right","Bottom","Left"],da=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},ea=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function fa(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&ba.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var ga={};function ha(a){var b,c=a.ownerDocument,d=a.nodeName,e=ga[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),ga[d]=e,e)}function ia(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=W.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&da(d)&&(e[f]=ha(d))):"none"!==c&&(e[f]="none",W.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ia(this,!0)},hide:function(){return ia(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){da(this)?r(this).show():r(this).hide()})}});var ja=/^(?:checkbox|radio)$/i,ka=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c<d;c++)W.set(a[c],"globalEval",!b||W.get(b[c],"globalEval"))}var pa=/<|&#?\w+;/;function qa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(pa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ka.exec(f)||["",""])[1].toLowerCase(),i=ma[h]||ma._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==xa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===xa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&B(this,"input"))return this.click(),!1},_default:function(a){return B(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?va:wa,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:wa,isPropagationStopped:wa,isImmediatePropagationStopped:wa,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=va,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=va,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=va,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&sa.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&ta.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return ya(this,a,b,c,d)},one:function(a,b,c,d){return ya(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=wa),this.each(function(){r.event.remove(this,a,c,b)})}});var za=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/<script|<style|<link/i,Ba=/checked\s*(?:[^=]|=\s*.checked.)/i,Ca=/^true\/(.*)/,Da=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}X.hasData(a)&&(h=X.access(a),i=r.extend({},h),X.set(b,i))}}function Ia(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ja.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ja(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,na(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Ga),l=0;l<i;l++)j=h[l],la.test(j.type||"")&&!W.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Da,""),k))}return a}function Ka(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(na(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&oa(na(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(za,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d<e;d++)Ia(f[d],g[d]);if(b)if(c)for(f=f||na(a),g=g||na(h),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);else Ha(a,h);return g=na(h,"script"),g.length>0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(na(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ja(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(na(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var La=/^margin/,Ma=new RegExp("^("+aa+")(?!px)[a-z%]+$","i"),Na=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",ra.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,ra.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Oa(a,b,c){var d,e,f,g,h=a.style;return c=c||Na(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&Ma.test(g)&&La.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Pa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Qa=/^(none|table(?!-c[ea]).+)/,Ra=/^--/,Sa={position:"absolute",visibility:"hidden",display:"block"},Ta={letterSpacing:"0",fontWeight:"400"},Ua=["Webkit","Moz","ms"],Va=d.createElement("div").style;function Wa(a){if(a in Va)return a;var b=a[0].toUpperCase()+a.slice(1),c=Ua.length;while(c--)if(a=Ua[c]+b,a in Va)return a}function Xa(a){var b=r.cssProps[a];return b||(b=r.cssProps[a]=Wa(a)||a),b}function Ya(a,b,c){var d=ba.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Za(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ca[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ca[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ca[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ca[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ca[f]+"Width",!0,e)));return g}function $a(a,b,c){var d,e=Na(a),f=Oa(a,b,e),g="border-box"===r.css(a,"boxSizing",!1,e);return Ma.test(f)?f:(d=g&&(o.boxSizingReliable()||f===a.style[b]),"auto"===f&&(f=a["offset"+b[0].toUpperCase()+b.slice(1)]),f=parseFloat(f)||0,f+Za(a,b,c||(g?"border":"content"),d,e)+"px")}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Oa(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=Ra.test(b),j=a.style;return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:j[b]:(f=typeof c,"string"===f&&(e=ba.exec(c))&&e[1]&&(c=fa(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(j[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i?j.setProperty(b,c):j[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b),i=Ra.test(b);return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Oa(a,b,d)),"normal"===e&&b in Ta&&(e=Ta[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Qa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?$a(a,b,d):ea(a,Sa,function(){return $a(a,b,d)})},set:function(a,c,d){var e,f=d&&Na(a),g=d&&Za(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=ba.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Ya(a,c,g)}}}),r.cssHooks.marginLeft=Pa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Oa(a,"marginLeft"))||a.getBoundingClientRect().left-ea(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ca[d]+b]=f[d]||f[d-2]||f[0];return e}},La.test(a)||(r.cssHooks[a+b].set=Ya)}),r.fn.extend({css:function(a,b){return T(this,function(a,b,c){var d,e,f={},g=0;if(Array.isArray(b)){for(d=Na(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function ib(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&da(a),q=W.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],cb.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=W.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ia([a],!0),j=a.style.display||j,k=r.css(a,"display"),ia([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=W.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ia([a],!0),m.done(function(){p||ia([a]),W.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=hb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function jb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],Array.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kb(a,b,c){var d,e,f=0,g=kb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=ab||fb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(i||h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:ab||fb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jb(k,j.opts.specialEasing);f<g;f++)if(d=kb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,hb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j}r.Animation=r.extend(kb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return fa(c.elem,a,ba.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(L);for(var c,d=0,e=a.length;d<e;d++)c=a[d],kb.tweeners[c]=kb.tweeners[c]||[],kb.tweeners[c].unshift(b)},prefilters:[ib],prefilter:function(a,b){b?kb.prefilters.unshift(a):kb.prefilters.push(a)}}),r.speed=function(a,b,c){var d=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off?d.duration=0:"number"!=typeof d.duration&&(d.duration in r.fx.speeds?d.duration=r.fx.speeds[d.duration]:d.duration=r.fx.speeds._default),null!=d.queue&&d.queue!==!0||(d.queue="fx"),d.old=d.complete,d.complete=function(){r.isFunction(d.old)&&d.old.call(this),d.queue&&r.dequeue(this,d.queue)},d},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(da).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=kb(this,r.extend({},a),f);(e||W.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=W.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&db.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=W.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gb(b,!0),a,d,e)}}),r.each({slideDown:gb("show"),slideUp:gb("hide"),slideToggle:gb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(ab=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),ab=void 0},r.fx.timer=function(a){r.timers.push(a),r.fx.start()},r.fx.interval=13,r.fx.start=function(){bb||(bb=!0,eb())},r.fx.stop=function(){bb=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var lb,mb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return T(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),
4462+null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!B(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Tb=[],Ub=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Tb.pop()||r.expando+"_"+ub++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Ub.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ub.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Ub,"$1"+e):b.jsonp!==!1&&(b.url+=(vb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Tb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=C.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=qa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=pb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length},r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),b=f.ownerDocument,c=b.documentElement,e=b.defaultView,{top:d.top+e.pageYOffset-c.clientTop,left:d.left+e.pageXOffset-c.clientLeft}):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),B(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||ra})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return T(this,function(a,d,e){var f;return r.isWindow(a)?f=a:9===a.nodeType&&(f=a.defaultView),void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Pa(o.pixelPosition,function(a,c){if(c)return c=Oa(a,b),Ma.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return T(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.holdReady=function(a){a?r.readyWait++:r.ready(!0)},r.isArray=Array.isArray,r.parseJSON=JSON.parse,r.nodeName=B,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Vb=a.jQuery,Wb=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Wb),b&&a.jQuery===r&&(a.jQuery=Vb),r},b||(a.jQuery=a.$=r),r});
4463diff --git a/static/templates/management.html b/static/templates/management.html
4464index f11bb00..4d68f2b 100644
4465--- a/static/templates/management.html
4466+++ b/static/templates/management.html
4467@@ -3,7 +3,7 @@
4468 <head>
4469 <meta charset="utf-8" />
4470 <meta name="viewport" content="width=device-width, initial-scale=1" />
4471- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
4472+ <script src="/static/js/jquery.min.js"></script>
4473 <title>Select Wi-Fi</title>
4474
4475 <link rel="stylesheet" href="/static/css/application.css" />
4476diff --git a/static/templates/operational.html b/static/templates/operational.html
4477index 27d4f94..9d2cd89 100644
4478--- a/static/templates/operational.html
4479+++ b/static/templates/operational.html
4480@@ -5,7 +5,7 @@
4481 <meta name="viewport" content="width=device-width, initial-scale=1" />
4482 <title>Wifi Connected</title>
4483 <link rel="stylesheet" href="/static/css/application.css" />
4484- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
4485+ <script src="/static/js/jquery.min.js"></script>
4486 </head>
4487
4488 <body>
4489diff --git a/utils/config.go b/utils/config.go
4490index bd23686..2cba34c 100644
4491--- a/utils/config.go
4492+++ b/utils/config.go
4493@@ -4,41 +4,47 @@ import (
4494 "encoding/json"
4495 "fmt"
4496 "io/ioutil"
4497- "log"
4498 "os"
4499 "path/filepath"
4500 "strconv"
4501 "strings"
4502-
4503- "launchpad.net/wifi-connect/wifiap"
4504 )
4505
4506-var configFile = filepath.Join(os.Getenv("SNAP_COMMON"), "pre-config.json")
4507+var configFile = filepath.Join(os.Getenv("SNAP_COMMON"), "config.json")
4508 var mustConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), "config_done.flag")
4509
4510-var wifiapClient wifiap.Operations = wifiap.DefaultClient()
4511-
4512 // Config this project config got from wifi-ap + custom wifi-connect params
4513 type Config struct {
4514- Wifi *WifiConfig
4515- Portal *PortalConfig
4516+ Wifi WifiConfig `json:"wifi"`
4517+ Portal PortalConfig `json:"portal"`
4518+ // If set, once we have connected to an AP once, we do not enter
4519+ // managing mode when the service is re-started, even if we are
4520+ // not able to reconnect to said AP. To change that, we would
4521+ // have to use the operational portal.
4522+ ConfigWifiOnce bool `json:"config-wifi-once"`
4523 }
4524
4525 // WifiConfig config specific parameters for wifi configuration
4526 type WifiConfig struct {
4527- Ssid string `json:"wifi.ssid"`
4528- Passphrase string `json:"wifi.security-passphrase"`
4529- Interface string `json:"wifi.interface"`
4530- CountryCode string `json:"wifi.country-code"`
4531- Channel int `json:"wifi.channel"`
4532- OperationMode string `json:"wifi.operation-mode"`
4533+ Ssid string `json:"ssid"`
4534+ Passphrase string `json:"security-passphrase"`
4535+ Interface string `json:"interface"`
4536+ CountryCode string `json:"country-code"`
4537+ Channel int `json:"channel"`
4538+ OperationMode string `json:"operation-mode"`
4539+ // Subnet for our AP, in CIDR format
4540+ Subnet string `json:"subnet"`
4541 }
4542
4543 // PortalConfig config specific parameters for portals configuration
4544 type PortalConfig struct {
4545- Password string //`json:"portal.password"`
4546- NoResetCredentials bool `json:"portal.no-reset-creds"`
4547- NoOperational bool `json:"portal.no-operational"`
4548+ Password string `json:"password"`
4549+ // Port where we will be listening
4550+ Port int `json:"port"`
4551+ // whether user must reset passphrase and password on first use of mgmt portal
4552+ NoResetCredentials bool `json:"no-reset-creds"`
4553+ // whether to show the operational portal
4554+ Operational bool `json:"operational"`
4555 }
4556
4557 func (c *Config) String() string {
4558@@ -52,8 +58,11 @@ func (c *Config) String() string {
4559 func (c *PortalConfig) String() string {
4560 s := []string{
4561 strings.Join([]string{"Password: ", c.Password}, " "),
4562- strings.Join([]string{"NoResetCredentials:", strconv.FormatBool(c.NoResetCredentials)}, " "),
4563- strings.Join([]string{"NoOperational:", strconv.FormatBool(c.NoOperational)}, " "),
4564+ strings.Join([]string{"Port:", strconv.Itoa(c.Port)}, " "),
4565+ strings.Join([]string{"NoResetCredentials:",
4566+ strconv.FormatBool(c.NoResetCredentials)}, " "),
4567+ strings.Join([]string{"Operational:",
4568+ strconv.FormatBool(c.Operational)}, " "),
4569 }
4570 return fmt.Sprintf(strings.Join(s, "\n"))
4571 }
4572@@ -66,161 +75,67 @@ func (c *WifiConfig) String() string {
4573 strings.Join([]string{"CountryCode:", c.CountryCode}, " "),
4574 strings.Join([]string{"Channel:", strconv.Itoa(c.Channel)}, " "),
4575 strings.Join([]string{"OperationMode:", c.OperationMode}, " "),
4576+ strings.Join([]string{"Subnet:", c.Subnet}, " "),
4577 }
4578 return fmt.Sprintf(strings.Join(s, "\n"))
4579 }
4580
4581-func defaultPortalConfig() *PortalConfig {
4582- return &PortalConfig{
4583- Password: "",
4584- NoResetCredentials: false,
4585- NoOperational: false,
4586- }
4587-}
4588-
4589-// config currently stored in local json file is completely storable in PortalConfig
4590-// If needed to scale, we could rewrite this method to support a more generic type
4591-func readLocalConfig() (*PortalConfig, error) {
4592- if _, err := os.Stat(configFile); os.IsNotExist(err) {
4593- log.Printf("Warn: not found local config file at %v\n", configFile)
4594- // in case there is no local config file, return a null pointer
4595- return nil, nil
4596- }
4597+// LoadConfig loads configuration file
4598+func LoadConfig() (*Config, error) {
4599+ config := &Config{}
4600+ // Set some defaults
4601+ config.Portal.Port = 8085
4602+ config.Wifi.Subnet = "10.25.52.1/24"
4603
4604- fileContents, err := ioutil.ReadFile(configFile)
4605+ content, err := ioutil.ReadFile(configFile)
4606 if err != nil {
4607- return nil, fmt.Errorf("Error reading json config file: %v", err)
4608+ return config, err
4609 }
4610-
4611- // parameters not available in config file will be se to default value
4612- portalConfig := defaultPortalConfig()
4613- err = json.Unmarshal(fileContents, portalConfig)
4614- if err != nil {
4615- return nil, fmt.Errorf("Error unmarshalling json config file contents: %v", err)
4616- }
4617-
4618- return portalConfig, nil
4619-}
4620-
4621-func writeLocalConfig(p *PortalConfig) error {
4622- // the only writable local config param is the password, stored as a hash
4623- _, err := HashIt(p.Password)
4624+ err = json.Unmarshal(content, config)
4625 if err != nil {
4626- return fmt.Errorf("Could not hash portal password to file: %v", err)
4627+ return config, err
4628 }
4629-
4630- return nil
4631+ return config, nil
4632 }
4633
4634-func readRemoteParam(m map[string]interface{}, key string, defaultValue interface{}) interface{} {
4635- val, ok := m[key]
4636- if !ok {
4637- val = defaultValue
4638- log.Printf("Warning: %v key was not found in remote config", key)
4639- }
4640-
4641- return val
4642-}
4643-
4644-func readRemoteConfig() (*WifiConfig, error) {
4645- settings, err := wifiapClient.Show()
4646+// WriteConfigFile writes configuration file
4647+func WriteConfigFile(config *Config) error {
4648+ b, err := json.Marshal(config)
4649 if err != nil {
4650- return nil, fmt.Errorf("Error reading wifi-ap remote configuration: %v", err)
4651- }
4652-
4653- // NOTE: Preprocessing for the case of wifi.channel.
4654- // In the case of the channel, it is returned as string from rest api, but we have to convert it
4655- // as it is handled as int internally.
4656- // In case wifi-ap provides this in future as int, this could be replaced by
4657- //
4658- // readRemoteParam(settings, "wifi.channel", 0).(int)
4659- channel, err := strconv.Atoi(readRemoteParam(settings, "wifi.channel", "0").(string))
4660- if err != nil {
4661- return nil, fmt.Errorf("Could not parse wifi.channel parameter: %v", err)
4662+ return err
4663 }
4664
4665- return &WifiConfig{
4666- Ssid: readRemoteParam(settings, "wifi.ssid", "").(string),
4667- Passphrase: readRemoteParam(settings, "wifi.security-passphrase", "").(string),
4668- Interface: readRemoteParam(settings, "wifi.interface", "").(string),
4669- CountryCode: readRemoteParam(settings, "wifi.country-code", "").(string),
4670- Channel: channel,
4671- OperationMode: readRemoteParam(settings, "wifi.operation-mode", "").(string),
4672- }, nil
4673-}
4674-
4675-func writeRemoteConfig(wc *WifiConfig) error {
4676- params := make(map[string]interface{})
4677- params["wifi.ssid"] = wc.Ssid
4678- params["wifi.security-passphrase"] = wc.Passphrase
4679- params["wifi.interface"] = wc.Interface
4680- params["wifi.country-code"] = wc.CountryCode
4681- params["wifi.channel"] = wc.Channel
4682- params["wifi.operation-mode"] = wc.OperationMode
4683-
4684- err := wifiapClient.Set(params)
4685+ err = ioutil.WriteFile(configFile, b, 0644)
4686 if err != nil {
4687- return fmt.Errorf("Error writing remote configuration: %v", err)
4688+ return err
4689 }
4690
4691 return nil
4692 }
4693
4694-// ReadConfig reads all config, remote and local, at the same time
4695-var ReadConfig = func() (*Config, error) {
4696- wifiConfig, err := readRemoteConfig()
4697- if err != nil {
4698- return nil, err
4699- }
4700-
4701- portalConfig, err := readLocalConfig()
4702+func writeConfig(p *PortalConfig) error {
4703+ // Just take password, stored as a hash
4704+ _, err := HashIt(p.Password)
4705 if err != nil {
4706- return nil, err
4707- }
4708-
4709- // if local config is nil, fill returning object with default values
4710- if portalConfig == nil {
4711- portalConfig = defaultPortalConfig()
4712+ return fmt.Errorf("Could not hash portal password to file: %v", err)
4713 }
4714
4715- return &Config{Wifi: wifiConfig, Portal: portalConfig}, nil
4716+ return nil
4717 }
4718
4719-// WriteConfig writes all remote and local config at the same time
4720+// WriteConfig writes configuration
4721 var WriteConfig = func(c *Config) error {
4722- previousRemoteConfig, err := readRemoteConfig()
4723- if err != nil {
4724- return fmt.Errorf("Error reading current remote config before applying new one: %v", err)
4725- }
4726-
4727- // only write remote config if it's different from current
4728- if *previousRemoteConfig != *c.Wifi {
4729- err = writeRemoteConfig(c.Wifi)
4730- if err != nil {
4731- // if an error happens writing remote config there is no need to restore
4732- // backup, as nothing shouldn't have been written
4733- return err
4734- }
4735- }
4736-
4737- err = writeLocalConfig(c.Portal)
4738+ err := writeConfig(&c.Portal)
4739 if err != nil {
4740- // rollback
4741- if previousRemoteConfig != nil {
4742- backupErr := writeRemoteConfig(previousRemoteConfig)
4743- if backupErr != nil {
4744- return fmt.Errorf("Could not restore previous remote configuration: %v\n after error: %v", backupErr, err)
4745- }
4746- }
4747 return err
4748 }
4749
4750- // write flag file for not asking more times for configuring snap before first use
4751- if MustSetConfig() {
4752- err = WriteFlagFile(mustConfigFlagFile)
4753- if err != nil {
4754- return fmt.Errorf("Error writing flag file after configuring for a first time")
4755- }
4756+ // write flag file for not asking more times for configuring snap before
4757+ // first use
4758+ err = WriteFlagFile(mustConfigFlagFile)
4759+ if err != nil {
4760+ return fmt.Errorf("Error writing flag file after " +
4761+ "configuring for a first time")
4762 }
4763
4764 return nil
4765@@ -228,8 +143,11 @@ var WriteConfig = func(c *Config) error {
4766
4767 // MustSetConfig true if one needs to configure snap before continuing
4768 var MustSetConfig = func() bool {
4769- if _, err := os.Stat(mustConfigFlagFile); os.IsNotExist(err) {
4770- return true
4771- }
4772+ // TODO Do not request portal configuration for the moment. It is not
4773+ // clear if it makes sense as we will be using wifi-connect only once
4774+ // to select an AP.
4775+ // if _, err := os.Stat(mustConfigFlagFile); os.IsNotExist(err) {
4776+ // return true
4777+ // }
4778 return false
4779 }
4780diff --git a/utils/config_test.go b/utils/config_test.go
4781index 1802a60..b4339ed 100644
4782--- a/utils/config_test.go
4783+++ b/utils/config_test.go
4784@@ -4,61 +4,37 @@ import (
4785 "fmt"
4786 "io/ioutil"
4787 "os"
4788- "path/filepath"
4789- "strconv"
4790- "sync"
4791 "testing"
4792- "time"
4793
4794 "gopkg.in/check.v1"
4795 )
4796
4797-const testLocalConfig = `
4798-{
4799- "portal.no-reset-creds": true,
4800- "portal.no-operational": false
4801+const testConfig1 = `{
4802+ "wifi": {
4803+ "ssid": "confconn",
4804+ "security-passphrase": "mypassphrase",
4805+ "interface": "wlp1s1",
4806+ "country-code": "ES",
4807+ "channel": 6 ,
4808+ "operation-mode": "g",
4809+ "subnet": "23.23.23.23/16"
4810+ },
4811+ "portal": {
4812+ "password": "mypassword",
4813+ "port": 9009,
4814+ "no-reset-creds": false,
4815+ "operational": true
4816+ },
4817+ "config-wifi-once": true
4818 }
4819 `
4820
4821-const testLocalConfigBadEntry = `
4822-{
4823- "portal.password": "the_password",
4824- "portal.no-reset-creds": true,
4825- "bad.parameter": "bad.value",
4826- "portal.no-operational": false
4827-}
4828-`
4829-
4830-const testLocalEmptyConfig = `
4831-{
4832-}
4833-`
4834-
4835-var testPortalConfig = &PortalConfig{"the_password", true, false}
4836-
4837-var rand uint32
4838-var randmu sync.Mutex
4839-
4840+// Hook up gocheck into the "go test" runner.
4841 func Test(t *testing.T) { check.TestingT(t) }
4842
4843-type S struct{}
4844-
4845-var _ = check.Suite(&S{})
4846+type CfgSuite struct{}
4847
4848-// ####################
4849-// Testing local config
4850-// ####################
4851-func randomName() string {
4852- randmu.Lock()
4853- r := rand
4854- if r == 0 {
4855- r = uint32(time.Now().UnixNano() + int64(os.Getpid()))
4856- }
4857- r = r*1664525 + 1013904223 // constants from Numerical Recipes
4858- rand = r
4859- randmu.Unlock()
4860- return strconv.Itoa(int(1e9 + r%1e9))[1:]
4861-}
4862+var _ = check.Suite(&CfgSuite{})
4863
4864 func createTempFile(content string) (*os.File, error) {
4865 contentAsBytes := []byte(content)
4866@@ -79,465 +55,52 @@ func createTempFile(content string) (*os.File, error) {
4867 return tmpfile, nil
4868 }
4869
4870-func verifyLocalConfig(c *check.C, cfg *PortalConfig, expectedPwd string, expectedNoResetCredentials bool, expectedNoOperational bool) {
4871- c.Assert(cfg.Password, check.Equals, expectedPwd)
4872- c.Assert(cfg.NoResetCredentials, check.Equals, expectedNoResetCredentials)
4873- c.Assert(cfg.NoOperational, check.Equals, expectedNoOperational)
4874-}
4875-
4876-func verifyDefaultLocalConfig(c *check.C, cfg *PortalConfig) {
4877- verifyLocalConfig(c, cfg, "", false, false)
4878-}
4879-
4880-func (s *S) TestReadLocalConfig(c *check.C) {
4881- f, err := createTempFile(testLocalConfig)
4882+func (s *CfgSuite) TestReadConfigDefaults(c *check.C) {
4883+ f, err := createTempFile("{}")
4884 c.Assert(err, check.IsNil)
4885
4886- defer os.Remove(f.Name())
4887 configFile = f.Name()
4888
4889- cfg, err := readLocalConfig()
4890- c.Assert(err, check.IsNil)
4891-
4892- verifyLocalConfig(c, cfg, "", true, false)
4893-}
4894+ cfg, err := LoadConfig()
4895
4896-func (s *S) TestReadLocalConfigBadEntry(c *check.C) {
4897- // No matter if there are additional not recognized params, only known should be marshalled
4898- f, err := createTempFile(testLocalConfigBadEntry)
4899 c.Assert(err, check.IsNil)
4900+ c.Assert(cfg.Wifi.Ssid, check.Equals, "")
4901+ c.Assert(cfg.Wifi.Passphrase, check.Equals, "")
4902+ c.Assert(cfg.Wifi.Interface, check.Equals, "")
4903+ c.Assert(cfg.Wifi.CountryCode, check.Equals, "")
4904+ c.Assert(cfg.Wifi.Channel, check.Equals, 0)
4905+ c.Assert(cfg.Wifi.OperationMode, check.Equals, "")
4906+ c.Assert(cfg.Wifi.Subnet, check.Equals, "10.25.52.1/24")
4907
4908- defer os.Remove(f.Name())
4909- configFile = f.Name()
4910-
4911- cfg, err := readLocalConfig()
4912- c.Assert(err, check.IsNil)
4913+ c.Assert(cfg.Portal.Password, check.Equals, "")
4914+ c.Assert(cfg.Portal.Port, check.Equals, 8085)
4915+ c.Assert(cfg.Portal.NoResetCredentials, check.Equals, false)
4916+ c.Assert(cfg.Portal.Operational, check.Equals, false)
4917
4918- verifyLocalConfig(c, cfg, "", true, false)
4919+ c.Assert(cfg.ConfigWifiOnce, check.Equals, false)
4920 }
4921
4922-func (s *S) TestReadLocalEmptyConfig(c *check.C) {
4923- // No matter if there are additional not recognized params, only known should be marshalled
4924- f, err := createTempFile(testLocalEmptyConfig)
4925+func (s *CfgSuite) TestReadConfig(c *check.C) {
4926+ f, err := createTempFile(testConfig1)
4927 c.Assert(err, check.IsNil)
4928
4929- defer os.Remove(f.Name())
4930 configFile = f.Name()
4931
4932- cfg, err := readLocalConfig()
4933- c.Assert(err, check.IsNil)
4934-
4935- verifyDefaultLocalConfig(c, cfg)
4936-}
4937-
4938-func (s *S) TestReadLocalNotExistingConfig(c *check.C) {
4939- configFile = "does/not/exists/config.json"
4940-
4941- cfg, err := readLocalConfig()
4942- c.Assert(err, check.IsNil)
4943- c.Assert(cfg, check.IsNil)
4944-}
4945-
4946-func (s *S) TestWriteLocalConfigFileDoesNotExists(c *check.C) {
4947- mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
4948- defer os.Remove(mustConfigFlagFile)
4949- HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName())
4950- defer os.Remove(HashFile)
4951+ cfg, err := LoadConfig()
4952
4953- err := writeLocalConfig(testPortalConfig)
4954 c.Assert(err, check.IsNil)
4955+ c.Assert(cfg.Wifi.Ssid, check.Equals, "confconn")
4956+ c.Assert(cfg.Wifi.Passphrase, check.Equals, "mypassphrase")
4957+ c.Assert(cfg.Wifi.Interface, check.Equals, "wlp1s1")
4958+ c.Assert(cfg.Wifi.CountryCode, check.Equals, "ES")
4959+ c.Assert(cfg.Wifi.Channel, check.Equals, 6)
4960+ c.Assert(cfg.Wifi.OperationMode, check.Equals, "g")
4961+ c.Assert(cfg.Wifi.Subnet, check.Equals, "23.23.23.23/16")
4962
4963- ok, err := MatchingHash(testPortalConfig.Password)
4964- c.Assert(err, check.IsNil)
4965- c.Assert(ok, check.Equals, true)
4966-}
4967-
4968-func (s *S) TestWriteLocalConfigFiletExists(c *check.C) {
4969- mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
4970- defer os.Remove(mustConfigFlagFile)
4971-
4972- f, err := createTempFile("whateverbadpasswordhash")
4973- c.Assert(err, check.IsNil)
4974-
4975- defer os.Remove(f.Name())
4976- HashFile = f.Name()
4977-
4978- err = writeLocalConfig(testPortalConfig)
4979- c.Assert(err, check.IsNil)
4980-
4981- ok, err := MatchingHash(testPortalConfig.Password)
4982- c.Assert(err, check.IsNil)
4983- c.Assert(ok, check.Equals, true)
4984-}
4985-
4986-// #####################
4987-// Testing remote config
4988-// #####################
4989-type wifiapClientMock struct {
4990- m map[string]interface{}
4991-}
4992-
4993-func (c *wifiapClientMock) Show() (map[string]interface{}, error) {
4994- return c.m, nil
4995-}
4996-
4997-func (c *wifiapClientMock) Enable() error {
4998- return nil
4999-}
5000-
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches