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 (community) 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
diff --git a/server/middleware.go b/avahi/avahi_test.go
0similarity index 63%0similarity index 63%
1rename from server/middleware.go1rename from server/middleware.go
2rename to avahi/avahi_test.go2rename to avahi/avahi_test.go
index 99bd6f9..486121f 100644
--- a/server/middleware.go
+++ b/avahi/avahi_test.go
@@ -1,5 +1,3 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*1/*
4 * Copyright (C) 2017 Canonical Ltd2 * Copyright (C) 2017 Canonical Ltd
5 *3 *
@@ -17,30 +15,12 @@
17 *15 *
18 */16 */
1917
20package server18package avahi
2119
22import (20import (
23 "net/http"21 "testing"
24
25 "launchpad.net/wifi-connect/netman"
26 "launchpad.net/wifi-connect/wifiap"
27)22)
2823
29var wifiapClient wifiap.Operations24func TestAvahi(t *testing.T) {
30var netmanClient netman.Operations25 // TODO Fill it when we really use avahi
31
32// Middleware to pre-process web service requests
33func 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}26}
diff --git a/cmd/main.go b/cmd/main.go
47deleted file mode 10064427deleted file mode 100644
index 2eebb48..0000000
--- a/cmd/main.go
+++ /dev/null
@@ -1,262 +0,0 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 */
17
18package main
19
20import (
21 "bufio"
22 "fmt"
23 "log"
24 "os"
25 "os/signal"
26 "strings"
27 "sync"
28
29 "launchpad.net/wifi-connect/netman"
30 "launchpad.net/wifi-connect/server"
31 "launchpad.net/wifi-connect/utils"
32 "launchpad.net/wifi-connect/wifiap"
33)
34
35func help() string {
36
37 text :=
38 `Usage: sudo wifi-connect COMMAND [VALUE]
39
40Commands:
41 stop: Disables wifi-connect from automatic control, leaving system
42 in current state
43 start: Enables wifi-connect as automatic controller, restarting from
44 a clean state
45 show-ap: Show AP configuration
46 ssid VALUE: Set the AP ssid (causes AP restart if it is UP)
47 passphrase VALUE: Set the AP passphrase (cause AP restart if it is UP)
48`
49 return text
50}
51
52// checkSudo return false if the current user is not root, else true
53func checkSudo() bool {
54 if os.Geteuid() != 0 {
55 fmt.Println("Error: This command requires sudo")
56 return false
57 }
58 return true
59}
60
61func waitForCtrlC() {
62 var endWaiter sync.WaitGroup
63 endWaiter.Add(1)
64 var signalChannel chan os.Signal
65 signalChannel = make(chan os.Signal, 1)
66 signal.Notify(signalChannel, os.Interrupt)
67 go func() {
68 <-signalChannel
69 endWaiter.Done()
70 }()
71 endWaiter.Wait()
72}
73
74func main() {
75
76 log.SetFlags(log.Lshortfile)
77 log.SetPrefix("== wifi-connect: ")
78
79 if len(os.Args) < 2 {
80 fmt.Println("Error: no command arguments provided")
81 return
82 }
83 args := os.Args[1:]
84
85 switch args[0] {
86 case "help":
87 fmt.Printf("%s\n", help())
88 case "-help":
89 fmt.Printf("%s\n", help())
90 case "-h":
91 fmt.Printf("%s\n", help())
92 case "--help":
93 fmt.Printf("%s\n", help())
94 case "stop":
95 if !checkSudo() {
96 return
97 }
98 err := utils.WriteFlagFile(os.Getenv("SNAP_COMMON") + "/manualMode")
99 if err != nil {
100 fmt.Println(err)
101 return
102 }
103 fmt.Println("entering MANUAL Mode. Wifi-connect has stopped managing state. Use 'start' to restore normal operations")
104 case "start":
105 if !checkSudo() {
106 return
107 }
108 err := utils.RemoveFlagFile(os.Getenv("SNAP_COMMON") + "/manualMode")
109 if err != nil {
110 fmt.Println("Error:", err)
111 return
112 }
113 fmt.Println("Entering NORMAL Mode.")
114 case "show-ap":
115 if !checkSudo() {
116 return
117 }
118 wifiAPClient := wifiap.DefaultClient()
119 result, err := wifiAPClient.Show()
120 if err != nil {
121 fmt.Println("Error:", err)
122 return
123 }
124 if result != nil {
125 utils.PrintMapSorted(result)
126 return
127 }
128 case "ssid":
129 if !checkSudo() {
130 return
131 }
132 if len(os.Args) < 3 {
133 fmt.Println("Error: no ssid provided")
134 return
135 }
136 wifiAPClient := wifiap.DefaultClient()
137 wifiAPClient.SetSsid(os.Args[2])
138 case "passphrase":
139 if !checkSudo() {
140 return
141 }
142 if len(os.Args) < 3 {
143 fmt.Println("Error: no passphrase provided")
144 return
145 }
146 if len(os.Args[2]) < 13 {
147 fmt.Println("Error: passphrase must be at least 13 chars long")
148 return
149 }
150 wifiAPClient := wifiap.DefaultClient()
151 wifiAPClient.SetPassphrase(os.Args[2])
152 case "get-devices":
153 c := netman.DefaultClient()
154 devices := c.GetDevices()
155 for d := range devices {
156 fmt.Println(d)
157 }
158 case "get-wifi-devices":
159 c := netman.DefaultClient()
160 devices := c.GetWifiDevices(c.GetDevices())
161 for d := range devices {
162 fmt.Println(d)
163 }
164 case "get-ssids":
165 c := netman.DefaultClient()
166 SSIDs, _, _ := c.Ssids()
167 var out string
168 for _, ssid := range SSIDs {
169 out += strings.TrimSpace(ssid.Ssid) + ","
170 }
171 if len(out) > 0 {
172 fmt.Printf("%s\n", out[:len(out)-1])
173 }
174 case "check-connected":
175 c := netman.DefaultClient()
176 if c.ConnectedWifi(c.GetWifiDevices(c.GetDevices())) {
177 fmt.Println("Device is connected")
178 } else {
179 fmt.Println("Device is not connected")
180 }
181
182 case "check-connected-wifi":
183 c := netman.DefaultClient()
184 if c.ConnectedWifi(c.GetWifiDevices(c.GetDevices())) {
185 fmt.Println("Device is connected to external wifi AP")
186 } else {
187 fmt.Println("Device is not connected to external wifi AP")
188 }
189 case "disconnect-wifi":
190 c := netman.DefaultClient()
191 c.DisconnectWifi(c.GetWifiDevices(c.GetDevices()))
192 case "wifis-managed":
193 c := netman.DefaultClient()
194 wifis, err := c.WifisManaged(c.GetWifiDevices(c.GetDevices()))
195 if err != nil {
196 fmt.Println(err)
197 return
198 }
199 for k, v := range wifis {
200 fmt.Printf("%s : %s\n", k, v)
201 }
202 case "manage-iface":
203 if len(os.Args) < 3 {
204 fmt.Println("Error: no interface provided")
205 return
206 }
207 c := netman.DefaultClient()
208 c.SetIfaceManaged(os.Args[2], true, c.GetWifiDevices(c.GetDevices()))
209 case "unmanage-iface":
210 if len(os.Args) < 3 {
211 fmt.Println("Error: no interface provided")
212 return
213 }
214 c := netman.DefaultClient()
215 c.SetIfaceManaged(os.Args[2], false, c.GetWifiDevices(c.GetDevices()))
216 case "connect":
217 c := netman.DefaultClient()
218 SSIDs, ap2device, ssid2ap := c.Ssids()
219 for _, ssid := range SSIDs {
220 fmt.Printf(" %v\n", ssid.Ssid)
221 }
222 reader := bufio.NewReader(os.Stdin)
223 fmt.Print("Connect to AP. Enter SSID: ")
224 ssid, _ := reader.ReadString('\n')
225 ssid = strings.TrimSpace(ssid)
226 fmt.Print("Enter phasprase: ")
227 pw, _ := reader.ReadString('\n')
228 pw = strings.TrimSpace(pw)
229 c.ConnectAp(ssid, pw, ap2device, ssid2ap)
230 case "management":
231 server.Address = server.TestingAddress
232 if err := server.StartManagementServer(); err != nil {
233 fmt.Printf("Could not start management server: %v\n", err)
234 return
235 }
236 waitForCtrlC()
237 case "operational":
238 server.Address = server.TestingAddress
239 if err := server.StartOperationalServer(); err != nil {
240 fmt.Printf("Could not start operational server: %v\n", err)
241 return
242 }
243 waitForCtrlC()
244 case "set-portal-password":
245 if len(os.Args) < 3 {
246 fmt.Println("Error: no string to hash provided")
247 return
248 }
249 if len(os.Args[2]) < 8 {
250 fmt.Println("Error: password must be at least 8 characters long")
251 return
252 }
253 b, err := utils.HashIt(os.Args[2])
254 if err != nil {
255 fmt.Println("Error hashing:", err)
256 return
257 }
258 fmt.Println(string(b))
259 default:
260 fmt.Println("Error. Your command is not supported. Please try 'help'")
261 }
262}
diff --git a/daemon/daemon.go b/daemon/daemon.go
index 1830491..d1af3a9 100644
--- a/daemon/daemon.go
+++ b/daemon/daemon.go
@@ -18,242 +18,496 @@
18package daemon18package daemon
1919
20import (20import (
21 "encoding/json"
22 "fmt"21 "fmt"
23 "io/ioutil"
24 "log"22 "log"
23 "net"
25 "os"24 "os"
26 "path/filepath"25 "os/exec"
26 "os/signal"
27 "strconv"
28 "syscall"
29 "time"
2730
28 "launchpad.net/wifi-connect/avahi"31 "github.com/godbus/dbus"
32 "launchpad.net/wifi-connect/netman"
29 "launchpad.net/wifi-connect/server"33 "launchpad.net/wifi-connect/server"
30 "launchpad.net/wifi-connect/utils"34 "launchpad.net/wifi-connect/utils"
31 "launchpad.net/wifi-connect/wifiap"
32)35)
3336
34// enum to track current system state37type serviceState int
38
35const (39const (
36 STARTING = 0 + iota40 stStarting serviceState = iota
37 MANAGING41 stManaging
38 OPERATING42 stOperating
39 MANUAL
40)43)
4144
42var manualFlagPath string45func (s serviceState) String() string {
43var waitFlagPath string46 return [...]string{"Starting", "Managing", "Operating"}[s]
44var previousState = STARTING
45var state = STARTING
46
47// PreConfigFile is the path to the file that stores the hash of the portals password
48var PreConfigFile = filepath.Join(os.Getenv("SNAP_COMMON"), "pre-config.json")
49
50// PreConfig is the struct representing a configuration
51type PreConfig struct {
52 Passphrase string `json:"wifi.security-passphrase,omitempty"`
53 Ssid string `json:"wifi.ssid,omitempty"`
54 Interface string `json:"wifi.interface,omitempty"`
55 Password string `json:"portal.password,omitempty"`
56 Operational bool `json:"portal.operational,omitempty"` //whether to show the operational portal
57 NoResetCreds bool `json:"portal.no-reset-creds,omitempty"` //whether user must reset passphrase and password on first use of mgmt portal
58}47}
5948
60// Client is the base type for both testing and runtime49const (
61type Client struct {50 waitBeforeScanSec = 2
62}51)
6352
64// GetClient returns a client for runtime or testing53// Service models the high level wifi-connect service
65func GetClient() *Client {54type Service struct {
66 return &Client{}55 config *utils.Config
56 cnetman *netman.Client
57 devicePath string
58 apSettingsPath string
59 apActiveConnPath string
60 ssidLs utils.SsidList
61 dnsmasqCmd *exec.Cmd
62 chSigs chan os.Signal
63 chUserEvs chan server.UserEvent
64 // Arguments for port forwarding rule
65 iptblForwArgs []string
67}66}
6867
69// used to clase the operational http server68// Creates the run time configuration based on available wifi interfaces and the
70var err error69// configuration loaded from config.json.
7170func (s *Service) setDefaults() error {
72// GetManualFlagPath returns the current path71 var err error
73func (c *Client) GetManualFlagPath() string {72 // If no interface is selected by configuration, just take the first one
74 return manualFlagPath73 // returned by GetWifiDevices().
75}74 if s.config.Wifi.Interface == "" {
75 devPaths := s.cnetman.GetWifiDevices()
76 if len(devPaths) == 0 {
77 return fmt.Errorf("no wifi device found")
78 }
79 s.devicePath = devPaths[0]
80 iface, err := s.cnetman.GetIfaceFromDevicePath(devPaths[0])
81 if err != nil {
82 return err
83 }
84 s.config.Wifi.Interface = iface
85 } else {
86 s.devicePath, err = s.cnetman.
87 GetDevicePathFromIface(s.config.Wifi.Interface)
88 if err != nil {
89 return err
90 }
91 }
92 log.Printf("setDefaults: using interface %s", s.config.Wifi.Interface)
7693
77// SetManualFlagPath sets the current path94 if len(s.config.Portal.Password) > 0 {
78func (c *Client) SetManualFlagPath(s string) {95 log.Print("setDefaults: portal password being set")
79 manualFlagPath = s96 _, err := utils.HashIt(s.config.Portal.Password)
80}97 if err != nil {
98 log.Printf("setDefaults: password err: %v", err)
99 return err
100 }
101 }
81102
82// GetWaitFlagPath returns the current path103 return nil
83func (c *Client) GetWaitFlagPath() string {
84 return waitFlagPath
85}104}
86105
87// SetWaitFlagPath sets the current path106// GetService returns a service object for runtime or testing
88func (c *Client) SetWaitFlagPath(s string) {107func GetService(cfg *utils.Config) *Service {
89 waitFlagPath = s108 if cfg.Portal.Operational {
90}109 log.Print("operational portal is enabled")
110 }
91111
92// GetPreviousState returns the daemon previous state112 chSigs := make(chan os.Signal, 1)
93func (c *Client) GetPreviousState() int {113 signal.Notify(chSigs, syscall.SIGINT, syscall.SIGTERM)
94 return previousState
95}
96114
97// SetPreviousState sets daemon previous state115 chUserEvs := make(chan server.UserEvent)
98func (c *Client) SetPreviousState(i int) {
99 previousState = i
100 return
101}
102116
103// GetState returns the daemon state117 cnetman := netman.DefaultClient()
104func (c *Client) GetState() int {
105 return state
106}
107118
108// SetState sets the daemon state and updates the previous state119 service := &Service{config: cfg, cnetman: cnetman, chSigs: chSigs,
109func (c *Client) SetState(i int) {120 chUserEvs: chUserEvs}
110 previousState = state
111 state = i
112}
113121
114// CheckWaitApConnect returns true if the flag wait file exists122 return service
115// and false if it does not
116func (c *Client) CheckWaitApConnect() bool {
117 if _, err := os.Stat(waitFlagPath); os.IsNotExist(err) {
118 return false
119 }
120 return true
121}123}
122124
123// ManualMode enables the daemon to loop without action if in manual mode125// Clean-up left-overs from previous executions
124// It returns true if the manual mode flag wait file exists126func (s *Service) cleanUpPrevRuns() {
125// and false if it does not. If it does not exist and the mode is MANUAL, the127 // Remove old hotspots from previous executions
126// state is set to STARTING. If it does exist and the mode is not MANUAL, state128 s.cnetman.DeleteWifiConnections(netman.ApWifiConn)
127// is set to MANUAL
128func (c *Client) ManualMode() bool {
129 if _, err := os.Stat(manualFlagPath); os.IsNotExist(err) {
130 if state == MANUAL {
131 c.SetState(STARTING)
132 log.Print("entering STARTING mode")
133 }
134 return false
135 }
136 if state != MANUAL {
137 c.SetState(MANUAL)
138 log.Print("entering MANUAL mode")
139 }
140 return true
141}129}
142130
143// IsApUpWithoutSSIDs corrects an possible but unlikely case.131// Returns true if wifi is connected to an AP
144// if wifiap is UP and there are no known SSIDs, bring it down so on next132func (s *Service) isConnectedToAnAP() bool {
145// loop iter we start again and can get SSIDs. returns true when ip is133 return s.cnetman.ConnectedToAnAP(s.devicePath)
146// UP and has no ssids
147func (c *Client) IsApUpWithoutSSIDs(cw *wifiap.Client) bool {
148 wifiUp, _ := cw.Enabled()
149 if !wifiUp {
150 return false
151 }
152 ssids, _ := utils.ReadSsidsFile()
153 if len(ssids) < 1 {
154 log.Print("wifi-ap is UP but has no SSIDS")
155 return true // ap is up with no ssids
156 }
157 return false
158}134}
159135
160// ManagementServerUp starts the management server if it is136// startManagementServer starts the management server if it is not running
161// not running137func (s *Service) startManagementServer() {
162func (c *Client) ManagementServerUp() {
163 if server.Current != server.Management && server.State == server.Stopped {138 if server.Current != server.Management && server.State == server.Stopped {
164 err = server.StartManagementServer()139 err := server.StartManagementServer(s.chUserEvs, s.config, &s.ssidLs)
165 if err != nil {140 if err != nil {
166 log.Printf("Error start Mamagement portal: %v", err)141 log.Printf("Error starting Management portal: %v", err)
167 }142 }
168 // init mDNS143 // init mDNS
169 avahi.InitMDNS()144 //avahi.InitMDNS()
170 }145 }
171}146}
172147
173// ManagementServerDown stops the management server if it is running148// stopManagementServer stops the management server if it is running
174// also remove the wait flag file, thus resetting proper State149func (s *Service) stopManagementServer() {
175func (c *Client) ManagementServerDown() {150 if server.Current == server.Management && server.State == server.Running {
176 if server.Current == server.Management && (server.State == server.Running || server.State == server.Starting) {151 err := server.ShutdownManagementServer()
177 err = server.ShutdownManagementServer()
178 if err != nil {152 if err != nil {
179 log.Printf("Error stopping the Management portal: %v", err)153 log.Printf("Error stopping Management portal: %v", err)
180 }154 }
181 //remove flag fie so daemon resumes normal control
182 utils.RemoveFlagFile(os.Getenv("SNAP_COMMON") + "/startingApConnect")
183 }155 }
184}156}
185157
186// OperationalServerUp starts the operational server if it is158// startOperationalServer starts the operational server if it is not running
187// not running159func (s *Service) startOperationalServer() {
188func (c *Client) OperationalServerUp() {160 if s.config.Portal.Operational == false {
161 return
162 }
163 log.Print("Starting operational server")
189 if server.Current != server.Operational && server.State == server.Stopped {164 if server.Current != server.Operational && server.State == server.Stopped {
190 err = server.StartOperationalServer()165 err := server.StartOperationalServer(s.chUserEvs, s.config)
191 if err != nil {166 if err != nil {
192 log.Printf("Error starting the Operational portal: %v", err)167 log.Printf("Error starting Operational portal: %v", err)
193 }168 }
194 // init mDNS169 // init mDNS
195 avahi.InitMDNS()170 //avahi.InitMDNS()
196 }171 }
197}172}
198173
199// OperationalServerDown stops the operational server if it is running174// stopOperationalServer stops the operational server if it is running
200func (c *Client) OperationalServerDown() {175func (s *Service) stopOperationalServer() {
201 if server.Current == server.Operational && (server.State == server.Running || server.State == server.Starting) {176 if server.Current == server.Operational && server.State == server.Running {
202 err = server.ShutdownOperationalServer()177 err := server.ShutdownOperationalServer()
203 if err != nil {178 if err != nil {
204 log.Printf("Error stopping Operational portal: %v", err)179 log.Printf("Error stopping Operational portal: %v", err)
205 }180 }
206 }181 }
207}182}
208183
209// LoadPreConfig returns a PreConfig based on the pre-config.json, if present, and an error to indicate184func (s *Service) connectToAp(ssid, pwd string) error {
210// possible json unmarshal failure185 s.exitManagingMode()
211func LoadPreConfig() (*PreConfig, error) {186
212 config := &PreConfig{}187 log.Printf("Trying to connect")
213 content, err := ioutil.ReadFile(PreConfigFile)188 _, _, err := s.cnetman.ConnectToAp(ssid, pwd, s.devicePath)
189 if err != nil {
190 log.Printf("Failed connecting to %v: %v", ssid, err)
191 s.enterManagingMode()
192 }
193 return err
194}
195
196func (s *Service) doWifiScan() {
197 s.exitManagingMode()
198
199 // wait a bit so we get ssids when scanning
200 // TODO with a modern enough NM we could Rescan and wait for the
201 // LastScan property to be signalled. But that requires NM >= 1.12.
202 time.Sleep(waitBeforeScanSec * time.Second)
203
204 // Do some retries, just in case
205 for i := 0; i < 5; i++ {
206 ssids, _, _ := s.cnetman.Ssids(s.devicePath)
207 if len(ssids) == 0 {
208 log.Print("no ssids!!")
209 time.Sleep(waitBeforeScanSec * time.Second)
210 continue
211 }
212
213 ssidNames := make([]string, len(ssids))
214 for i, ssid := range ssids {
215 ssidNames[i] = ssid.Ssid
216 }
217 s.ssidLs.SetSsidList(ssidNames)
218 break
219 }
220
221 s.enterManagingMode()
222}
223
224func (s *Service) enterOperatingMode() {
225 s.startOperationalServer()
226}
227
228func (s *Service) exitOperatingMode() {
229 s.stopOperationalServer()
230}
231
232func (s *Service) enterManagingMode() {
233 log.Print("Entering MANAGEMENT mode")
234
235 // Remove all connections to APs that could re-connect
236 s.cnetman.DeleteWifiConnections(netman.StaWifiConn)
237 if err := s.cnetman.DisconnectDevice(s.devicePath); err != nil {
238 log.Printf("Error when disconnecting device: %v", err)
239 }
240
241 // wait a bit so we get ssids when scanning
242 // TODO with a modern enough NM we could Rescan and wait for the
243 // LastScan property to be signalled. But that requires NM >= 1.12.
244 time.Sleep(waitBeforeScanSec * time.Second)
245
246 // Do an scan of currently available SSIDs
247 ssids, _, _ := s.cnetman.Ssids(s.devicePath)
248 if len(ssids) == 0 {
249 log.Print("No SSIDs found when scanning")
250 }
251 ssidNames := make([]string, len(ssids))
252 for i, ssid := range ssids {
253 ssidNames[i] = ssid.Ssid
254 }
255 s.ssidLs.SetSsidList(ssidNames)
256
257 ip, subnet, err := net.ParseCIDR(s.config.Wifi.Subnet)
258 if err != nil {
259 log.Printf("Error while parsing %s: %v", s.config.Wifi.Subnet, err)
260 return
261 }
262 if ip.To4() == nil {
263 log.Printf("Error: %v is not an IPv4 subnet", s.config.Wifi.Subnet)
264 return
265 }
266 maskedBits, _ := subnet.Mask.Size()
267 if maskedBits > 28 {
268 log.Printf("Error: %v needs a mask smaller than 29", s.config.Wifi.Subnet)
269 return
270 }
271 apIP := ip.Mask(subnet.Mask)
272 apIP[3] |= 1
273 ipStr := apIP.String()
274
275 log.Printf("starting AP with name %s, subnet %s/%d",
276 s.config.Wifi.Ssid, ipStr, maskedBits)
277 s.apSettingsPath, s.apActiveConnPath, err =
278 s.cnetman.CreateAccessPoint(s.config.Wifi.Ssid,
279 s.config.Wifi.Passphrase, s.config.Wifi.Interface,
280 ipStr, uint(maskedBits))
281 if err != nil {
282 log.Printf("Cannot create AP: %v", err)
283 return
284 }
285
286 s.startManagementServer()
287
288 // DHCP range is X.X.X.10 - X.X.X.15
289 dhcpFirst := make(net.IP, net.IPv4len)
290 dhcpLast := make(net.IP, net.IPv4len)
291 copy(dhcpFirst, subnet.IP)
292 copy(dhcpLast, subnet.IP)
293 dhcpFirst[3] = 10
294 dhcpLast[3] = 15
295 dhcpRange := dhcpFirst.String() + "," + dhcpLast.String()
296 // Run DNS and DHCP server. The DNS will always return the interface
297 // address for any query, redirecting all request to our server.
298 // With "--user=root --group=" we get the right code path so we avoid
299 // problems when changing user/group from the snap.
300 s.dnsmasqCmd =
301 exec.Command("dnsmasq", "--conf-file", "--no-hosts",
302 "--keep-in-foreground", "--bind-interfaces",
303 "--except-interface=lo", "--clear-on-reload",
304 "--strict-order", "--listen-address="+ipStr,
305 "--dhcp-range="+dhcpRange+",60m", "--dhcp-lease-max=50",
306 "--address=/#/"+ipStr,
307 "--pid-file=/tmp/dnsmasq-wlp3s0.pid",
308 "--user=root", "--group=")
309 log.Printf("Running %v", s.dnsmasqCmd)
310 err = s.dnsmasqCmd.Start()
214 if err != nil {311 if err != nil {
215 return config, err312 log.Fatalf("Cannot run %v: %v", s.dnsmasqCmd, err)
216 }313 }
217 err = json.Unmarshal(content, config)314
315 // Redirect all http to our server
316 s.iptblForwArgs = []string{"PREROUTING", "-p", "tcp", "-m", "tcp",
317 "-s", subnet.String(),
318 "--dport", "80", "-j", "DNAT",
319 "--to-destination",
320 ipStr + ":" + strconv.Itoa(s.config.Portal.Port)}
321 iptblArgs := append([]string{"-t", "nat", "-A"}, s.iptblForwArgs...)
322 iptblCmd := exec.Command("iptables", iptblArgs...)
323 log.Printf("Running %v", iptblCmd)
324 err = iptblCmd.Run()
218 if err != nil {325 if err != nil {
219 return config, err326 log.Printf("Error running %v: %v", iptblCmd, err)
220 }327 }
221 return config, nil
222}328}
223329
224// SetDefaults creates the run time configuration based on wifi-ap and the pre-config.json330func (s *Service) exitManagingMode() {
225// configuration file, if any. The configuration is returned with an error.331 log.Printf("Exiting MANAGEMENT mode")
226func (c *Client) SetDefaults(cw wifiap.Operations, config *PreConfig) error {332
333 if s.dnsmasqCmd != nil {
334 log.Printf("Stopping dnsmasq")
335 s.dnsmasqCmd.Process.Signal(syscall.SIGTERM)
336 s.dnsmasqCmd.Process.Wait()
337 s.dnsmasqCmd = nil
338 }
339
340 // Remove http redirection
341 iptblArgs := append([]string{"-t", "nat", "-D"}, s.iptblForwArgs...)
342 iptblCmd := exec.Command("iptables", iptblArgs...)
343 log.Printf("Running %v", iptblCmd)
344 err := iptblCmd.Run()
227 if err != nil {345 if err != nil {
228 log.Printf("SetDefaults: preconfig unmarshall errorr: %v", err)346 log.Printf("Error running %v: %v", iptblCmd, err)
229 }
230 ap, errShow := cw.Show()
231 if errShow != nil {
232 log.Printf("SetDefaults: wifi-ap.Show err: %v", errShow)
233 }
234 if ap["wifi.security-passphrase"] != config.Passphrase {
235 if len(config.Passphrase) > 0 {
236 err = cw.SetPassphrase(config.Passphrase)
237 log.Print("SetDefaults wifi-ap passphrase being set")
238 if err != nil {
239 log.Printf("SetDefaults: passphrase err: %v", err)
240 return err
241 }
242 }
243 }347 }
244 if len(config.Password) > 0 {348
245 fmt.Println("== wifi-connect/SetDefaults portal password being set")349 s.cnetman.DeleteWifiConnections(netman.ApWifiConn)
246 _, err = utils.HashIt(config.Password)350 if err := s.cnetman.DisconnectDevice(s.devicePath); err != nil {
247 if err != nil {351 log.Printf("Error when disconnecting device: %v", err)
248 log.Printf("SetDefaults: password err: %v", err)352 }
249 return err353
354 s.stopManagementServer()
355}
356
357// event: NM is up event received
358func (s *Service) netManUpEv(current serviceState) serviceState {
359 log.Printf("NetworkManager is up")
360 if current != stStarting {
361 // Spurious signal?
362 return current
363 }
364
365 err := s.setDefaults()
366 if err != nil {
367 log.Printf("setDefaults error: %v", err)
368 }
369 s.cleanUpPrevRuns()
370
371 var state serviceState
372 if s.config.ConfigWifiOnce &&
373 utils.FlagFileExists(utils.WifiConfigDoneFlagFile) {
374 state = stOperating
375 s.enterOperatingMode()
376 } else {
377 // Wait some time on first run to let the wifi connect
378 // TODO wait for DBus signal instead
379 time.Sleep(10 * time.Second)
380 if s.isConnectedToAnAP() {
381 state = stOperating
382 s.enterOperatingMode()
383 } else {
384 state = stManaging
385 s.enterManagingMode()
250 }386 }
251 }387 }
252 if config.Operational {388 return state
253 log.Print("SetDefaults: operational portal is enabled")389}
390
391func (s *Service) stopServices(state serviceState) {
392 switch state {
393 case stManaging:
394 s.exitManagingMode()
395 case stOperating:
396 s.exitOperatingMode()
397 }
398}
399
400// event: NM is down event received
401func (s *Service) netManDownEv(current serviceState) serviceState {
402 log.Printf("NetworkManager is down")
403 s.stopServices(current)
404 return stStarting
405}
406
407// event: user sends command to connect
408func (s *Service) userConnectEv(current serviceState,
409 ssid, password string) serviceState {
410
411 if current != stManaging {
412 log.Printf("Connect event, but not in managing mode")
413 return current
414 }
415
416 // connectToAp exits managing mode on success
417 err := s.connectToAp(ssid, password)
418 if err != nil {
419 log.Printf("Failed to connect: %v", err)
420 return current
254 }421 }
255 if config.NoResetCreds {422
256 log.Print("SetDefaults: reset creds requirement is disabled")423 utils.WriteFlagFile(utils.WifiConfigDoneFlagFile)
424 s.enterOperatingMode()
425
426 return stOperating
427}
428
429// event: user sends command to refresh wifi list
430func (s *Service) userRefreshEv(current serviceState) {
431 if current != stManaging {
432 log.Printf("Refresh event, but not in managing mode")
257 }433 }
258 return nil434
435 s.doWifiScan()
436}
437
438// event: user sends command to disconnect
439func (s *Service) userDisconnectEv(current serviceState) serviceState {
440 if current != stOperating {
441 log.Printf("Disconnect event, but not in operating mode")
442 return current
443 }
444
445 s.exitOperatingMode()
446 utils.RemoveFlagFile(utils.WifiConfigDoneFlagFile)
447 s.enterManagingMode()
448
449 return stManaging
450}
451
452// Start state machine
453func (s *Service) Start() {
454
455 chDbus := make(chan *dbus.Signal, 30)
456 defer close(chDbus)
457 if err := s.cnetman.RegisterForNameOwnerChanged(chDbus); err != nil {
458 log.Printf("Error: cannot register for NameOwnerChanged: %v", err)
459 os.Exit(1)
460 }
461 defer s.cnetman.UnregisterForNameOwnerChanged(chDbus)
462
463 state := stStarting
464 nmUp, err := s.cnetman.CheckNetworkManagerRunning()
465 if nmUp == false {
466 log.Printf("Waiting for NetworkManager service: %v", err)
467 } else {
468 state = s.netManUpEv(state)
469 }
470 spuriousSignal := false
471
472MainLoop:
473 for {
474 if spuriousSignal == false {
475 log.Printf("State is %v", state)
476 } else {
477 spuriousSignal = false
478 }
479 select {
480 case uEv := <-s.chUserEvs:
481 log.Print("Event received: ", uEv.EvType)
482 switch uEv.EvType {
483 case server.UserEventConnect:
484 state = s.userConnectEv(state,
485 uEv.Params[server.UeConnSsid],
486 uEv.Params[server.UeConnPassword])
487 case server.UserEventRefresh:
488 s.userRefreshEv(state)
489 case server.UserEventDisconnect:
490 state = s.userDisconnectEv(state)
491 }
492 case sig := <-s.chSigs:
493 log.Printf("Exiting on %v signal", sig)
494 break MainLoop
495 case dbusSig := <-chDbus:
496 if dbusSig.Name == "org.freedesktop.DBus.NameOwnerChanged" &&
497 dbusSig.Body[0].(string) == "org.freedesktop.NetworkManager" {
498 // Check if there is a current owner
499 if dbusSig.Body[2].(string) == "" {
500 state = s.netManDownEv(state)
501 } else {
502 state = s.netManUpEv(state)
503 }
504 } else {
505 // We do not want to print the state for signals
506 // that we are not interested in...
507 spuriousSignal = true
508 }
509 }
510 }
511
512 s.stopServices(state)
259}513}
diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go
index 07fe109..2af53e8 100644
--- a/daemon/daemon_test.go
+++ b/daemon/daemon_test.go
@@ -18,225 +18,13 @@
18package daemon18package daemon
1919
20import (20import (
21 "fmt"
22 "io/ioutil"
23 "net/http"
24 "os"
25 "strings"
26 "testing"21 "testing"
27
28 "launchpad.net/wifi-connect/utils"
29)22)
3023
31func TestManualFlagPath(t *testing.T) {24func TestGetService(t *testing.T) {
32 mfpInit := "mfp"25 // cfg := utils.Config{Wifi: utils.WifiConfig{Ssid: "myssid"}, ConfigWifiOnce: false}
33 client := GetClient()26 // service := GetService(&cfg)
34 client.SetManualFlagPath(mfpInit)27 // if service == nil {
35 mfp := client.GetManualFlagPath()28 // t.Error("Cannot get service")
36 if mfp != mfpInit {29 // }
37 t.Errorf("ManualFlag path should be %s but is %s\n", mfpInit, mfp)
38 }
39}
40
41func TestWaitFlagPath(t *testing.T) {
42 wfpInit := "wfp"
43 client := GetClient()
44 client.SetWaitFlagPath(wfpInit)
45 wfp := client.GetWaitFlagPath()
46 if wfp != wfpInit {
47 t.Errorf("WaitFlag path should be %s but is %s", wfp, wfpInit)
48 }
49}
50
51func TestState(t *testing.T) {
52 client := GetClient()
53 client.SetState(MANAGING)
54 client.SetPreviousState(STARTING)
55 ps := client.GetPreviousState()
56 if ps != STARTING {
57 t.Errorf("Previous state should be %d but is %d", STARTING, ps)
58 }
59 s := client.GetState()
60 if s != MANAGING {
61 t.Errorf("State should be %d but is %d", MANAGING, s)
62 }
63}
64
65func TestCheckWaitApConnect(t *testing.T) {
66 client := GetClient()
67 wfp := "thispathnevershouldexist"
68 client.SetWaitFlagPath(wfp)
69 if client.CheckWaitApConnect() {
70 t.Errorf("CheckWaitApConnect returns true but should return false")
71 }
72 wfp = "../static/tests/waitFile"
73 client.SetWaitFlagPath(wfp)
74 if !client.CheckWaitApConnect() {
75 t.Errorf("CheckWaitApConnect returns false but should return true")
76 }
77}
78
79func TestManualMode(t *testing.T) {
80 mfp := "thisfileshouldneverexist"
81 client := GetClient()
82 client.SetManualFlagPath(mfp)
83 client.SetState(MANUAL)
84 if client.ManualMode() {
85 t.Errorf("ManualMode returns true but should return false")
86 }
87 if client.GetState() != STARTING {
88 t.Errorf("ManualMode should set state to STARTING when not in manual mode but does not")
89 }
90 mfp = "../static/tests/manualMode"
91 client.SetManualFlagPath(mfp)
92 client.SetState(STARTING)
93 if !client.ManualMode() {
94 t.Errorf("ManualMode returns false but should return true")
95 }
96 if client.GetState() != MANUAL {
97 t.Errorf("ManualMode should set state to MANUAL when in manual mode but does not")
98 }
99}
100
101type mockWifiap struct{}
102
103func (mock *mockWifiap) Do(req *http.Request) (*http.Response, error) {
104 fmt.Println("==== MY do called")
105 url := req.URL.String()
106 if url != "http://unix/v1/configuration" {
107 return nil, fmt.Errorf("Not valid request URL: %v", url)
108 }
109
110 if req.Method != "GET" {
111 return nil, fmt.Errorf("Method is not valid. Expected GET, got %v", req.Method)
112 }
113
114 rawBody := `{"result":{
115 "debug":false,
116 "dhcp.lease-time": "12h",
117 "dhcp.range-start": "10.0.60.2",
118 "dhcp.range-stop": "10.0.60.199",
119 "disabled": true,
120 "share.disabled": false,
121 "share-network-interface": "tun0",
122 "wifi-address": "10.0.60.1",
123 "wifi.channel": "6",
124 "wifi.hostapd-driver": "nl80211",
125 "wifi.interface": "wlan0",
126 "wifi.interface-mode": "direct",
127 "wifi.netmask": "255.255.255.0",
128 "wifi.operation-mode": "g",
129 "wifi.security": "
130 "wifi.security-passphrase": "passphrase123",
131 "wifi.ssid": "AP"},"status":"OK","status-code":200,""sync"}`
132
133 response := http.Response{
134 StatusCode: 200,
135 Status: "200 OK",
136 Body: ioutil.NopCloser(strings.NewReader(rawBody)),
137 }
138
139 return &response, nil
140}
141
142func (mock *mockWifiap) Show() (map[string]interface{}, error) {
143 wifiAp := make(map[string]interface{})
144 wifiAp["wifi.security-passphrase"] = "randompassphrase"
145 return wifiAp, nil
146}
147
148func (mock *mockWifiap) Enabled() (bool, error) {
149 return true, nil
150}
151
152func (mock *mockWifiap) Enable() error {
153 return nil
154}
155func (mock *mockWifiap) Disable() error {
156 return nil
157}
158func (mock *mockWifiap) SetSsid(s string) error {
159 return nil
160}
161
162func (mock *mockWifiap) SetPassphrase(p string) error {
163 return nil
164}
165
166func (mock *mockWifiap) Set(map[string]interface{}) error {
167 return nil
168}
169
170func TestLoadPreConfig(t *testing.T) {
171 PreConfigFile = "../static/tests/pre-config0.json"
172 config, err := LoadPreConfig()
173 if err != nil {
174 t.Errorf("Unexpected error using LoadPreConfig: %s", err)
175 }
176 if config.Passphrase != "abcdefghijklmnop" {
177 t.Errorf("Passphrase of %s expected but got %s:", "abcdefghijklmnop", config.Passphrase)
178 }
179 if !config.Operational {
180 t.Errorf("portal.operational was set to false but the loaded config is %t", config.Operational)
181 }
182 if !config.NoResetCreds {
183 t.Errorf("portal.no-reset-creds was set to true but the loaded config is %t", config.NoResetCreds)
184 }
185
186}
187
188func TestSetDefaults(t *testing.T) {
189 client := GetClient()
190 PreConfigFile = "../static/tests/pre-config0.json"
191 hfp := "/tmp/hash"
192 if _, err := os.Stat(hfp); err == nil {
193 err = os.Remove(hfp)
194 if err != nil {
195 t.Errorf("Could not remove previous file version")
196 }
197 }
198 config, _ := LoadPreConfig()
199 utils.SetHashFile(hfp)
200 client.SetDefaults(&mockWifiap{}, config)
201 expectedPassphrase := "abcdefghijklmnop"
202 expectedPassword := "qwerzxcv"
203 if config.Passphrase != expectedPassphrase {
204 t.Errorf("SetDefaults: Preconfig passphrase should be %s but is %s", expectedPassphrase, config.Passphrase)
205 }
206 if os.IsNotExist(err) {
207 t.Errorf("SetDefaults should have created %s but did not", hfp)
208 }
209 res, _ := utils.MatchingHash(expectedPassword)
210 if !res {
211 t.Errorf("SetDefaults: Preconfig password hash did not match actual")
212 }
213 if !config.Operational {
214 t.Errorf("SetDefaults: Preconfig portal.operational should be true (set) but is %t", config.Operational)
215 }
216 if !config.NoResetCreds {
217 t.Errorf("SetDefaults: Preconfig portal.no-reset-creds should be true (set) but is %t", config.NoResetCreds)
218 }
219
220 if _, err := os.Stat(hfp); err == nil {
221 err = os.Remove(hfp)
222 if err != nil {
223 t.Errorf("Could not remove previous file version")
224 }
225 }
226 PreConfigFile = "../static/tests/pre-config1.json"
227 config, _ = LoadPreConfig()
228 client.SetDefaults(&mockWifiap{}, config)
229 if len(config.Passphrase) > 0 {
230 t.Errorf("SetDefaults: Preconfig passphrase was not set but is %s", config.Passphrase)
231 }
232 res2, _ := utils.MatchingHash(expectedPassword)
233 if res2 {
234 t.Errorf("SetDefaults: Preconfig password was not set, but the hash matched")
235 }
236 if config.Operational {
237 t.Errorf("SetDefaults: Preconfig portal.no-operational should be false (unset) but is %t", config.Operational)
238 }
239 if config.NoResetCreds {
240 t.Errorf("SetDefaults: Preconfig portal.no-reset-creds should be false (unnset) but is %t", config.NoResetCreds)
241 }
242}30}
diff --git a/dependencies.tsv b/dependencies.tsv
index e5f04d8..e9c02fa 100644
--- a/dependencies.tsv
+++ b/dependencies.tsv
@@ -1,7 +1,7 @@
1github.com/go-yaml/yaml git cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b 2017-04-07T18:21:22Z1github.com/godbus/dbus git 06fc4b473149e499166adbb9e31c7365a8ea146f 2020-02-14T23:16:04Z
2github.com/godbus/dbus git fe0e1d54eaeda11a9979659a8d32f459e88bee75 2017-03-03T19:03:06Z2github.com/gorilla/mux git 75dcda0896e109a2a22c9315bca3bb21b87b2ba5 2020-01-12T19:17:43Z
3github.com/gorilla/context git 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 2016-08-17T18:46:32Z3golang.org/x/crypto git 8b5121be2f68d8fc40bb06467003bdde1040a094 2020-01-24T22:56:46Z
4github.com/gorilla/mux git 757bef944d0f21880861c2dd9c871ca543023cba 2016-09-20T23:08:13Z4golang.org/x/net git 13f9640d40b9cc418fb53703dfbd177679788ceb 2019-10-04T11:05:52Z
5golang.org/x/text git 342b2e1fbaa52c93f31447ad2c6abc048c63e475 2019-04-25T21:42:06Z
5github.com/presotto/go-mdns-sd git 343772046ec1b3840b8591799a7bbcc68ea47b4b 2015-11-03T06:16:58Z6github.com/presotto/go-mdns-sd git 343772046ec1b3840b8591799a7bbcc68ea47b4b 2015-11-03T06:16:58Z
6github.com/reiver/go-oi git 431c83978379297f04f85f6eb94f129f25ab741d 2016-03-25T06:16:15Z7gopkg.in/check.v1 git 4f90aeace3a26ad7021961c297b22c42160c7b25 2016-01-05T16:49:36Z
7github.com/reiver/go-telnet git 6b696f32801a8f8dd07947f1e1fdb1a7dc4766ff 2016-03-30T05:09:16Z
diff --git a/hooks/configure.go b/hooks/configure.go
index 80cfe91..7775eab 100644
--- a/hooks/configure.go
+++ b/hooks/configure.go
@@ -20,15 +20,14 @@
20package main20package main
2121
22import (22import (
23 "encoding/json"
24 "io/ioutil"
25 "os"23 "os"
26 "os/exec"24 "os/exec"
27 "path/filepath"25 "path/filepath"
26 "strconv"
28 "strings"27 "strings"
29 "time"28 "time"
3029
31 "launchpad.net/wifi-connect/daemon"30 "launchpad.net/wifi-connect/utils"
32)31)
3332
34// logFile is the file into which log msgs go. Needed because snap hook stdout33// logFile is the file into which log msgs go. Needed because snap hook stdout
@@ -65,7 +64,6 @@ func (g *Get) SnapGet(key string) (string, error) {
65 return "", err64 return "", err
66 }65 }
67 return strings.TrimSpace(string(out)), nil66 return strings.TrimSpace(string(out)), nil
68
69}67}
7068
71// snapGetStr wraps SnapGet for string types and verifies the snap var is valid69// snapGetStr wraps SnapGet for string types and verifies the snap var is valid
@@ -75,12 +73,28 @@ func (c *Client) snapGetStr(key string, target *string) {
75 return73 return
76 }74 }
77 if len(val) == 0 {75 if len(val) == 0 {
78 log("configure error: key %s exists but has zero length" + key)76 log("configure error: key " + key + " exists but has zero length")
79 return77 return
80 }78 }
81 *target = val79 *target = val
82}80}
8381
82func (c *Client) snapGetInt(key string, target *int) {
83 val, err := c.getter.SnapGet(key)
84 if err != nil {
85 return
86 }
87 if len(val) == 0 {
88 log("configure error: key " + key + " exists but has zero length")
89 return
90 }
91 *target, err = strconv.Atoi(val)
92 if err != nil {
93 log("bad integer: " + err.Error())
94 *target = 0
95 }
96}
97
84// snapGetBool wraps SnapGet for bool types and verifies the snap var is valid98// snapGetBool wraps SnapGet for bool types and verifies the snap var is valid
85func (c *Client) snapGetBool(key string, target *bool) {99func (c *Client) snapGetBool(key string, target *bool) {
86 val, err := c.getter.SnapGet(key)100 val, err := c.getter.SnapGet(key)
@@ -88,7 +102,7 @@ func (c *Client) snapGetBool(key string, target *bool) {
88 return102 return
89 }103 }
90 if len(val) == 0 {104 if len(val) == 0 {
91 log("configure error: key %s exists but has zero length" + key)105 log("configure error: key " + key + " exists but has zero length")
92 return106 return
93 }107 }
94108
@@ -109,20 +123,28 @@ func log(msg string) {
109func main() {123func main() {
110 log("Configure hook running")124 log("Configure hook running")
111 client := GetClient()125 client := GetClient()
112 preConfig := &daemon.PreConfig{}
113 client.snapGetStr("wifi.security-passphrase", &preConfig.Passphrase)
114 client.snapGetStr("portal.password", &preConfig.Password)
115 client.snapGetBool("portal.operational", &preConfig.Operational)
116 client.snapGetBool("portal.no-reset-creds", &preConfig.NoResetCreds)
117126
118 b, err := json.Marshal(preConfig)127 config, _ := utils.LoadConfig()
119 if err != nil {128
120 log("Marshall error: " + err.Error())129 client.snapGetBool("config-wifi-once", &config.ConfigWifiOnce)
121 return130
122 }131 client.snapGetStr("wifi.ssid", &config.Wifi.Ssid)
132 client.snapGetStr("wifi.security-passphrase", &config.Wifi.Passphrase)
133 client.snapGetStr("wifi.interface", &config.Wifi.Interface)
134 client.snapGetStr("wifi.country-code", &config.Wifi.CountryCode)
135 client.snapGetInt("wifi.channel", &config.Wifi.Channel)
136 client.snapGetStr("wifi.operation-mode", &config.Wifi.OperationMode)
137 client.snapGetStr("wifi.subnet", &config.Wifi.Subnet)
138
139 client.snapGetStr("portal.password", &config.Portal.Password)
140 client.snapGetInt("portal.port", &config.Portal.Port)
141 client.snapGetBool("portal.no-reset-creds", &config.Portal.NoResetCredentials)
142 client.snapGetBool("portal.operational", &config.Portal.Operational)
143
144 utils.WriteConfigFile(config)
123145
124 err = ioutil.WriteFile(daemon.PreConfigFile, b, 0644)146 err := exec.Command("snapctl", "restart", os.Getenv("SNAP_NAME")).Run()
125 if err != nil {147 if err != nil {
126 log("configure hook: " + err.Error())148 log("Error while re-starting the service: " + err.Error())
127 }149 }
128}150}
diff --git a/netman/dbus.go b/netman/dbus.go
index 55564bd..b6ce186 100644
--- a/netman/dbus.go
+++ b/netman/dbus.go
@@ -20,31 +20,45 @@ package netman
20import (20import (
21 "errors"21 "errors"
22 "fmt"22 "fmt"
23 "io"
24 "log"23 "log"
25 "os"
26 "strings"24 "strings"
27 "time"25 "time"
2826
29 "github.com/godbus/dbus"27 "github.com/godbus/dbus"
30)28)
3129
30// WifiConnType enum for types of wifi connections
31type WifiConnType int
32
33// All wifi conns, just STA or just AP
34const (
35 AllWifiConn WifiConnType = iota
36 StaWifiConn
37 ApWifiConn
38)
39
32// Operations defines operations for this package client40// Operations defines operations for this package client
33type Operations interface {41type Operations interface {
34 GetDevices() []string42 GetDevices() []string
35 GetWifiDevices(devices []string) []string43 GetWifiDevices() []string
44 GetIfaceFromDevicePath(connSettPath string) (string, error)
45 GetDevicePathFromIface(iface string) (string, error)
36 GetAccessPoints(devices []string, ap2device map[string]string) []string46 GetAccessPoints(devices []string, ap2device map[string]string) []string
37 ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error47 ConnectToAp(ssid, password, device string) (string, string, error)
38 Ssids() ([]SSID, map[string]string, map[string]string)48 Ssids(devicePath string) ([]SSID, map[string]string, map[string]string)
39 Connected(devices []string) bool49 Connected(devices []string) bool
40 ConnectedWifi(wifiDevices []string) bool50 ConnectedToAnAP(wifiDevice string) bool
41 DeleteWifiConnections()51 DisconnectDevice(device string) error
52 DeleteWifiConnection(connSett string) error
53 DeleteWifiConnections(wifiType int)
42 DisconnectWifi(wifiDevices []string) int54 DisconnectWifi(wifiDevices []string) int
43 SetIfaceManaged(iface string, state bool, devices []string) string55 SetIfaceManaged(iface string, state bool, devices []string) string
44 WifisManaged(wifiDevices []string) (map[string]string, error)56 WifisManaged(wifiDevices []string) (map[string]string, error)
45 Unmanage() error57 Unmanage(wifiIface string) error
46 Manage() error58 Manage(wifiIface string) error
47 ScanAndWriteSsidsToFile(filepath string) bool59 CreateAccessPoint(ssid string, password string, wifiPath string,
60 ipAddress string, prefix uint) (string, string, error)
61 ActivateConnection(devicePath string, connPath string) (string, error)
48}62}
4963
50// Client type to support unit test mock and runtime execution64// Client type to support unit test mock and runtime execution
@@ -64,16 +78,20 @@ type Objecter interface {
64 Object(dest string, path dbus.ObjectPath) dbus.BusObject78 Object(dest string, path dbus.ObjectPath) dbus.BusObject
65}79}
6680
67// Object is the mock implementation of the godbus Object function81func getSystemBus() *dbus.Conn {
68func (d *DbusClient) Object(dest string, path dbus.ObjectPath) dbus.BusObject {82 conn, err := dbus.SystemBus()
69 obj := d.Connection.Object(dest, path)83 if err != nil {
70 return obj84 log.Printf("Error: Failed to connect to system bus: %v", err)
85 panic(1)
86 }
87 return conn
71}88}
7289
73// DefaultClient is the runtime client object90// DefaultClient is the runtime client object
74func DefaultClient() *Client {91func DefaultClient() *Client {
75 conn := getSystemBus()92 conn := getSystemBus()
76 obj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")93 obj := conn.Object("org.freedesktop.NetworkManager",
94 "/org/freedesktop/NetworkManager")
77 return &Client{95 return &Client{
78 dbusClient: DbusClient{96 dbusClient: DbusClient{
79 test: false,97 test: false,
@@ -102,13 +120,10 @@ func setObject(c *Client, iface string, path dbus.ObjectPath) {
102120
103// GetDevices returns NetMan (NetworkManager) devices121// GetDevices returns NetMan (NetworkManager) devices
104func (c *Client) GetDevices() []string {122func (c *Client) GetDevices() []string {
105 if !c.dbusClient.test {
106 c.dbusClient.Connection = getSystemBus()
107 }
108 c.dbusClient.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
109 setObject(c, "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")123 setObject(c, "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
110 var devices []string124 var devices []string
111 err := c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.GetAllDevices", 0).Store(&devices)125 err := c.dbusClient.BusObj.Call(
126 "org.freedesktop.NetworkManager.GetAllDevices", 0).Store(&devices)
112 if err != nil {127 if err != nil {
113 log.Printf("Error getting devices: %v", err)128 log.Printf("Error getting devices: %v", err)
114 }129 }
@@ -116,22 +131,20 @@ func (c *Client) GetDevices() []string {
116}131}
117132
118// GetWifiDevices returns wifi NetMan devices133// GetWifiDevices returns wifi NetMan devices
119func (c *Client) GetWifiDevices(devices []string) []string {134func (c *Client) GetWifiDevices() []string {
135 devices := c.GetDevices()
120 var wifiDevices []string136 var wifiDevices []string
121 for _, d := range devices {137 for _, d := range devices {
122 objPath := dbus.ObjectPath(d)138 objPath := dbus.ObjectPath(d)
123 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
124 setObject(c, "org.freedesktop.NetworkManager", objPath)139 setObject(c, "org.freedesktop.NetworkManager", objPath)
125 deviceType, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.DeviceType")140 deviceType, err := c.dbusClient.BusObj.GetProperty(
126 if err2 != nil {141 "org.freedesktop.NetworkManager.Device.DeviceType")
127 log.Printf("Error getting wifi devices: %v", err2)142 if err != nil {
143 log.Printf("Error getting DeviceType: %v", err)
128 continue144 continue
129 }145 }
130 var wifiType uint32146 var wifiType uint32
131 wifiType = 2147 wifiType = 2
132 if deviceType.Value() == nil {
133 break
134 }
135 if deviceType.Value() != wifiType {148 if deviceType.Value() != wifiType {
136 continue149 continue
137 }150 }
@@ -140,15 +153,52 @@ func (c *Client) GetWifiDevices(devices []string) []string {
140 return wifiDevices153 return wifiDevices
141}154}
142155
143//GetAccessPoints returns NetMan known external APs156// GetIfaceFromDevicePath gets interface name from NM's dbus device path
157func (c *Client) GetIfaceFromDevicePath(connSettPath string) (string, error) {
158 setObject(c, "org.freedesktop.NetworkManager",
159 dbus.ObjectPath(connSettPath))
160 iface, err := c.dbusClient.BusObj.GetProperty(
161 "org.freedesktop.NetworkManager.Device.Interface")
162 if err != nil {
163 return "", err
164 }
165 return iface.Value().(string), err
166}
167
168// GetDevicePathFromIface gets NM's DBus object path for a device from the interface name
169func (c *Client) GetDevicePathFromIface(iface string) (string, error) {
170 devPaths := c.GetDevices()
171 matchDevPath := ""
172 for _, devPath := range devPaths {
173 setObject(c, "org.freedesktop.NetworkManager",
174 dbus.ObjectPath(devPath))
175 devIface, err := c.dbusClient.BusObj.GetProperty(
176 "org.freedesktop.NetworkManager.Device.Interface")
177 if err != nil {
178 log.Printf("Error getting wifi devices: %v", err)
179 continue
180 }
181 if devIface.Value().(string) == iface {
182 matchDevPath = devPath
183 break
184 }
185 }
186 if matchDevPath == "" {
187 return "", errors.New("Interface not found")
188 }
189 return matchDevPath, nil
190}
191
192// GetAccessPoints returns NetMan known external APs
144func (c *Client) GetAccessPoints(devices []string, ap2device map[string]string) []string {193func (c *Client) GetAccessPoints(devices []string, ap2device map[string]string) []string {
145 var APs []string194 var APs []string
146 for _, d := range devices {195 for _, d := range devices {
147 var aps []string196 var aps []string
148 objPath := dbus.ObjectPath(d)197 objPath := dbus.ObjectPath(d)
149 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
150 setObject(c, "org.freedesktop.NetworkManager", objPath)198 setObject(c, "org.freedesktop.NetworkManager", objPath)
151 err := c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Device.Wireless.GetAllAccessPoints", 0).Store(&aps)199 err := c.dbusClient.BusObj.Call("org.freedesktop."+
200 "NetworkManager.Device.Wireless.GetAllAccessPoints",
201 0).Store(&aps)
152 if err != nil {202 if err != nil {
153 log.Printf("Error getting accesspoints: %v", err)203 log.Printf("Error getting accesspoints: %v", err)
154 continue204 continue
@@ -175,9 +225,9 @@ func (c *Client) getSsids(APs []string, ssid2ap map[string]string) []SSID {
175 var SSIDs []SSID225 var SSIDs []SSID
176 for _, ap := range APs {226 for _, ap := range APs {
177 objPath := dbus.ObjectPath(ap)227 objPath := dbus.ObjectPath(ap)
178 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
179 setObject(c, "org.freedesktop.NetworkManager", objPath)228 setObject(c, "org.freedesktop.NetworkManager", objPath)
180 ssid, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.AccessPoint.Ssid")229 ssid, err := c.dbusClient.BusObj.GetProperty(
230 "org.freedesktop.NetworkManager.AccessPoint.Ssid")
181 if err != nil {231 if err != nil {
182 log.Printf("Error getting accesspoint's ssids: %v", err)232 log.Printf("Error getting accesspoint's ssids: %v", err)
183 continue233 continue
@@ -196,60 +246,43 @@ func (c *Client) getSsids(APs []string, ssid2ap map[string]string) []SSID {
196 Ssid := SSID{Ssid: ssidStr, ApPath: ap}246 Ssid := SSID{Ssid: ssidStr, ApPath: ap}
197 SSIDs = append(SSIDs, Ssid)247 SSIDs = append(SSIDs, Ssid)
198 ssid2ap[strings.TrimSpace(ssidStr)] = ap248 ssid2ap[strings.TrimSpace(ssidStr)] = ap
199 //TODO: exclude ssid of device's own AP (the wifi-ap one)
200 }249 }
201 return SSIDs250 return SSIDs
202}251}
203252
204// ConnectAp attempts to Connect to an external AP253// ConnectToAp attempts to connect to an external AP
205func (c *Client) ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error {254func (c *Client) ConnectToAp(ssid, password, device string) (string, string, error) {
206 inner1 := make(map[string]dbus.Variant)
207 inner1["security"] = dbus.MakeVariant("802-11-wireless-security")
208
209 inner2 := make(map[string]dbus.Variant)
210 inner2["key-mgmt"] = dbus.MakeVariant("wpa-psk")
211 inner2["psk"] = dbus.MakeVariant(p)
212255
213 outer := make(map[string]map[string]dbus.Variant)256 wirelessSett := make(map[string]dbus.Variant)
214 outer["802-11-wireless"] = inner1257 wirelessSett["ssid"] = dbus.MakeVariant([]byte(ssid))
215 outer["802-11-wireless-security"] = inner2
216258
217 c.dbusClient.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")259 wirelessSecSett := make(map[string]dbus.Variant)
218 setObject(c, "org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager"))260 wirelessSecSett["key-mgmt"] = dbus.MakeVariant("wpa-psk")
219 c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.AddAndActivateConnection", 0, outer, dbus.ObjectPath(ap2device[ssid2ap[ssid]]), dbus.ObjectPath(ssid2ap[ssid]))261 wirelessSecSett["psk"] = dbus.MakeVariant(password)
220262
221 // loop until connected or until max loops263 settings := make(map[string]map[string]dbus.Variant)
222 trying := true264 settings["802-11-wireless"] = wirelessSett
223 idx := -1265 settings["802-11-wireless-security"] = wirelessSecSett
224 for trying {
225 idx++
226 time.Sleep(1000 * time.Millisecond)
227 if c.Connected(c.GetWifiDevices(c.GetDevices())) {
228 return nil
229 }
230 if idx == 19 {
231 return errors.New("wifi-connect: cannot connect to AP")
232 }
233 }
234 return nil
235}
236266
237func getSystemBus() *dbus.Conn {267 setObject(c, "org.freedesktop.NetworkManager",
238 conn, err := dbus.SystemBus()268 dbus.ObjectPath("/org/freedesktop/NetworkManager"))
269 var connPath, actConnPath string
270 err := c.dbusClient.BusObj.Call(
271 "org.freedesktop.NetworkManager.AddAndActivateConnection",
272 0, settings, dbus.ObjectPath(device),
273 dbus.ObjectPath("/")).Store(&connPath, &actConnPath)
239 if err != nil {274 if err != nil {
240 log.Printf("Error: Failed to connect to system bus: %v", err)275 return "", "", err
241 panic(1)
242 }276 }
243 return conn277
278 return connPath, actConnPath, c.WaitConnectionActivated(actConnPath)
244}279}
245280
246// Ssids returns known SSIDs281// Ssids returns known SSIDs
247func (c *Client) Ssids() ([]SSID, map[string]string, map[string]string) {282func (c *Client) Ssids(devicePath string) ([]SSID, map[string]string, map[string]string) {
248 ap2device := make(map[string]string)283 ap2device := make(map[string]string)
249 ssid2ap := make(map[string]string)284 ssid2ap := make(map[string]string)
250 devices := c.GetDevices()285 APs := c.GetAccessPoints([]string{devicePath}, ap2device)
251 wifiDevices := c.GetWifiDevices(devices)
252 APs := c.GetAccessPoints(wifiDevices, ap2device)
253 SSIDs := c.getSsids(APs, ssid2ap)286 SSIDs := c.getSsids(APs, ssid2ap)
254 return SSIDs, ap2device, ssid2ap287 return SSIDs, ap2device, ssid2ap
255}288}
@@ -258,20 +291,22 @@ func (c *Client) Ssids() ([]SSID, map[string]string, map[string]string) {
258func (c *Client) Connected(devices []string) bool {291func (c *Client) Connected(devices []string) bool {
259 for _, d := range devices {292 for _, d := range devices {
260 objPath := dbus.ObjectPath(d)293 objPath := dbus.ObjectPath(d)
261 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
262 setObject(c, "org.freedesktop.NetworkManager", objPath)294 setObject(c, "org.freedesktop.NetworkManager", objPath)
263 dType, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.DeviceType")295 dType, err := c.dbusClient.BusObj.
296 GetProperty("org.freedesktop.NetworkManager.Device.DeviceType")
264 if err != nil {297 if err != nil {
265 log.Printf("Error getting device type: %v", err)298 log.Printf("Error getting device type: %v", err)
266 continue299 continue
267 }300 }
268 state, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.State")301 state, err2 := c.dbusClient.BusObj.
302 GetProperty("org.freedesktop.NetworkManager.Device.State")
269 if err2 != nil {303 if err2 != nil {
270 log.Printf("Error getting device state: %v", err2)304 log.Printf("Error getting device state: %v", err2)
271 continue305 continue
272 }306 }
273 // only handle eth and wifi device type307 // only handle eth and wifi device type
274 if dbus.Variant.Value(dType) != uint32(1) && dbus.Variant.Value(dType) != uint32(2) {308 if dbus.Variant.Value(dType) != uint32(1) &&
309 dbus.Variant.Value(dType) != uint32(2) {
275 continue310 continue
276 }311 }
277 if dbus.Variant.Value(state) == uint32(100) {312 if dbus.Variant.Value(state) == uint32(100) {
@@ -281,75 +316,180 @@ func (c *Client) Connected(devices []string) bool {
281 return false316 return false
282}317}
283318
284// ConnectedWifi checks if any passed wifi devices are connected319// ConnectedToAnAP checks if the passed device is connected to an AP. If the
285func (c *Client) ConnectedWifi(wifiDevices []string) bool {320// device is itself an AP, we do not consider that a connection.
286 for _, d := range wifiDevices {321func (c *Client) ConnectedToAnAP(wifiDevice string) bool {
287 objPath := dbus.ObjectPath(d)322 objPath := dbus.ObjectPath(wifiDevice)
288 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)323 setObject(c, "org.freedesktop.NetworkManager", objPath)
289 setObject(c, "org.freedesktop.NetworkManager", objPath)324
290 state, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.State")325 // GetAppliedConnection will return the current active
291 if err != nil {326 // connection for the device, or nothing if the device is not
292 log.Printf("Error getting device state: %v", err)327 // active. Checking for existence of "802-11-wireless" will
293 continue328 // rule out both non-wifi and non-active wifi devices.
294 }329 settings := make(map[string]interface{})
295 if dbus.Variant.Value(state) == uint32(100) {330 var connID uint64
296 return true331 c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager."+
297 }332 "Device.GetAppliedConnection", 0, uint32(0)).
333 Store(&settings, &connID)
334 wifiSett, ok := settings["802-11-wireless"].(map[string]interface{})
335 if !ok {
336 return false
298 }337 }
299 return false338 // If mode is not set, it is infrastructure mode
339 mode, ok := wifiSett["mode"].(string)
340 if !ok {
341 return true
342 }
343 if mode == "ap" {
344 // We ARE an AP, so not connected to one
345 return false
346 }
347 return true
348}
349
350// DeleteWifiConnection deletes a wifi connection using the object path to the
351// connetion settings TODO wait for Removed signal?
352func (c *Client) DeleteWifiConnection(connSett string) error {
353 setObject(c, "org.freedesktop.NetworkManager",
354 dbus.ObjectPath(connSett))
355
356 call := c.dbusClient.BusObj.Call(
357 "org.freedesktop.NetworkManager.Settings.Connection.Delete", 0)
358 return call.Err
300}359}
301360
302// DeleteWifiConnections deletes all wifi connections (type 802-11-wireless)361// DeleteWifiConnections deletes all wifi connections for a given type
303func (c *Client) DeleteWifiConnections() {362func (c *Client) DeleteWifiConnections(wifiType WifiConnType) {
304 objPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")363 objPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")
305 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
306 setObject(c, "org.freedesktop.NetworkManager", objPath)364 setObject(c, "org.freedesktop.NetworkManager", objPath)
307365
308 //connect all connections366 // get all connections
309 var conns []string367 var conns []string
310 err := c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Settings.ListConnections", 0).Store(&conns)368 err := c.dbusClient.BusObj.Call(
369 "org.freedesktop.NetworkManager.Settings.ListConnections", 0).
370 Store(&conns)
311 if err != nil {371 if err != nil {
312 log.Printf("Error getting connections: %v", err)372 log.Printf("Error getting connections: %v", err)
313 }373 }
314374
315 //Delete all 802-11-wireless connections to clean netman cruft375 // Delete 802-11-wireless connections for the given type
316 for _, conn := range conns {376 for _, conn := range conns {
317 objPath := dbus.ObjectPath(conn)377 objPath := dbus.ObjectPath(conn)
318 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
319 setObject(c, "org.freedesktop.NetworkManager", objPath)378 setObject(c, "org.freedesktop.NetworkManager", objPath)
320 settings := make(map[string]interface{})379 settings := make(map[string]interface{})
321 c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&settings)380 c.dbusClient.BusObj.Call("org.freedesktop."+
322 _, ok := settings["802-11-wireless"]381 "NetworkManager.Settings.Connection.GetSettings", 0).
323 if ok {382 Store(&settings)
324 objPath := dbus.ObjectPath(conn)383 wifiSett, ok := settings["802-11-wireless"].(map[string]interface{})
325 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)384 if !ok {
326 setObject(c, "org.freedesktop.NetworkManager", objPath)385 continue
327 c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Delete", 0)386 }
387 isAp := false
388 mode, ok := wifiSett["mode"].(string)
389 if ok && mode == "ap" {
390 isAp = true
391 }
392 switch wifiType {
393 case StaWifiConn:
394 if isAp == true {
395 continue
396 }
397 case ApWifiConn:
398 if isAp == false {
399 continue
400 }
401 }
402 err = c.DeleteWifiConnection(conn)
403 if err != nil {
404 log.Printf("Error while deleting %s: %v", conn, err)
328 }405 }
329 }406 }
330}407}
331408
332// 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.409// DisconnectDevice disconnects a device
333func (c *Client) DisconnectWifi(wifiDevices []string) int {410func (c *Client) DisconnectDevice(device string) error {
334 c.DeleteWifiConnections()411 objPath := dbus.ObjectPath(device)
335 ran := 0412 setObject(c, "org.freedesktop.NetworkManager", objPath)
336 for _, d := range wifiDevices {413 err := c.dbusClient.BusObj.Call(
337 objPath := dbus.ObjectPath(d)414 "org.freedesktop.NetworkManager.Device.Disconnect", 0).Err
338 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)415 if err != nil {
339 setObject(c, "org.freedesktop.NetworkManager", objPath)416 if err.Error() == "This device is not active" {
340 c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Device.Disconnect", 0)417 return nil
341 ran++418 }
419 return err
342 }420 }
343 return ran421
422 return c.waitDeviceDisconnect(device)
344}423}
345424
346// SetIfaceManaged sets passed device to be managed/unmanaged by network manager, return iface set, if any425func (c *Client) waitDeviceDisconnect(device string) error {
426 rule := "type='signal',sender='org.freedesktop.NetworkManager'," +
427 "path='" + device + "'," +
428 "interface='org.freedesktop.NetworkManager.Device'," +
429 "member='StateChanged'"
430 setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
431 err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Err
432 if err != nil {
433 return err
434 }
435 defer func() {
436 setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
437 err := c.dbusClient.BusObj.Call(
438 "org.freedesktop.DBus.RemoveMatch", 0, rule).Err
439 if err != nil {
440 log.Printf("Error when calling RemoveMatch: %v", err)
441 }
442 }()
443
444 chSig := make(chan *dbus.Signal, 10)
445 defer close(chSig)
446 c.dbusClient.Connection.Signal(chSig)
447 defer c.dbusClient.Connection.RemoveSignal(chSig)
448
449 // Get current state, as we might have missed the signal while we were
450 // registering for it (the connection already existed).
451 devPath := dbus.ObjectPath(device)
452 setObject(c, "org.freedesktop.NetworkManager", devPath)
453 state, err := c.dbusClient.BusObj.GetProperty(
454 "org.freedesktop.NetworkManager.Device.State")
455 if err != nil {
456 return err
457 }
458 // NM_DEVICE_STATE_DISCONNECTED = 30
459 if state.Value() == 30 {
460 return nil
461 }
462
463SignalLoop:
464 for {
465 select {
466 case sig := <-chSig:
467 // Note that we need to make sure the signal comes from
468 // the right object, as we will receive here any signal
469 // to which the process has registered.
470 // NM_DEVICE_STATE_DISCONNECTED = 30
471 if sig.Path == devPath &&
472 sig.Name ==
473 "org.freedesktop.NetworkManager.Device.StateChanged" &&
474 sig.Body[0].(uint32) == 30 {
475 break SignalLoop
476 }
477 case <-time.After(20 * time.Second):
478 return fmt.Errorf("Timeout while waiting for disconnection")
479 }
480 }
481
482 return nil
483}
484
485// SetIfaceManaged sets passed device to be managed/unmanaged by network
486// manager, return iface set, if any
347func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) string {487func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) string {
348 for _, d := range devices {488 for _, d := range devices {
349 objPath := dbus.ObjectPath(d)489 objPath := dbus.ObjectPath(d)
350 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
351 setObject(c, "org.freedesktop.NetworkManager", objPath)490 setObject(c, "org.freedesktop.NetworkManager", objPath)
352 intface, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Interface")491 intface, err2 := c.dbusClient.BusObj.GetProperty(
492 "org.freedesktop.NetworkManager.Device.Interface")
353 if err2 != nil {493 if err2 != nil {
354 log.Printf("Error in SetIfaceManaged() geting interface: %v", err2)494 log.Printf("Error in SetIfaceManaged() geting interface: %v", err2)
355 return ""495 return ""
@@ -357,9 +497,11 @@ func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) str
357 if iface != intface.Value().(string) {497 if iface != intface.Value().(string) {
358 continue498 continue
359 }499 }
360 managed, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Managed")500 managed, err := c.dbusClient.BusObj.GetProperty(
501 "org.freedesktop.NetworkManager.Device.Managed")
361 if err != nil {502 if err != nil {
362 log.Printf("Error in SetIfaceManaged() fetching device managed: %v", err)503 log.Printf("Error in SetIfaceManaged() fetching device managed: %v",
504 err)
363 return ""505 return ""
364 }506 }
365 switch state {507 switch state {
@@ -373,21 +515,26 @@ func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) str
373 }515 }
374 }516 }
375517
376 c.dbusClient.BusObj.Call("org.freedesktop.DBus.Properties.Set", 0, "org.freedesktop.NetworkManager.Device", "Managed", dbus.MakeVariant(state))518 c.dbusClient.BusObj.Call("org.freedesktop.DBus.Properties.Set",
519 0, "org.freedesktop.NetworkManager.Device",
520 "Managed", dbus.MakeVariant(state))
377 // loop until interface is in desired managed state or max iters reached521 // loop until interface is in desired managed state or max iters reached
378 idx := -1522 idx := -1
379 for {523 for {
380 idx++524 idx++
381 time.Sleep(1000 * time.Millisecond)525 time.Sleep(1000 * time.Millisecond)
382 managedState, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.State")526 managedState, err := c.dbusClient.BusObj.GetProperty(
527 "org.freedesktop.NetworkManager.Device.State")
383 if err == nil {528 if err == nil {
384 switch state {529 switch state {
385 case true:530 case true:
386 if managedState.Value() == uint32(30) { //NM_DEVICE_STATE_DISCONNECTED531 if managedState.Value() == uint32(30) {
532 //NM_DEVICE_STATE_DISCONNECTED
387 return iface533 return iface
388 }534 }
389 case false:535 case false:
390 if managedState.Value() == uint32(10) { //NM_DEVICE_STATE_UNMANAGED536 if managedState.Value() == uint32(10) {
537 //NM_DEVICE_STATE_UNMANAGED
391 return iface538 return iface
392 }539 }
393 }540 }
@@ -400,21 +547,25 @@ func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) str
400 return "" //no iface state changed547 return "" //no iface state changed
401}548}
402549
403// WifisManaged returns map[iface]device of wifi iterfaces that are managed by network manager550// WifisManaged returns map[iface]device of wifi interfaces that are
551// managed by network manager
404func (c *Client) WifisManaged(wifiDevices []string) (map[string]string, error) {552func (c *Client) WifisManaged(wifiDevices []string) (map[string]string, error) {
405 ifaces := make(map[string]string)553 ifaces := make(map[string]string)
406 for _, d := range wifiDevices {554 for _, d := range wifiDevices {
407 objPath := dbus.ObjectPath(d)555 objPath := dbus.ObjectPath(d)
408 c.dbusClient.Object("org.freedesktop.NetworkManager", objPath)
409 setObject(c, "org.freedesktop.NetworkManager", objPath)556 setObject(c, "org.freedesktop.NetworkManager", objPath)
410 managed, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Managed")557 managed, err := c.dbusClient.BusObj.GetProperty(
558 "org.freedesktop.NetworkManager.Device.Managed")
411 if err != nil {559 if err != nil {
412 log.Printf("Error in WifisManaged() getting device managed: %v", err)560 log.Printf("Error in WifisManaged() getting device managed: %v",
561 err)
413 return ifaces, err562 return ifaces, err
414 }563 }
415 iface, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Interface")564 iface, err2 := c.dbusClient.BusObj.GetProperty(
565 "org.freedesktop.NetworkManager.Device.Interface")
416 if err2 != nil {566 if err2 != nil {
417 log.Printf("Error in WifisManaged() getting device interface: %v", err)567 log.Printf("Error in WifisManaged() getting device interface: %v",
568 err)
418 return ifaces, err2569 return ifaces, err2
419 }570 }
420 if managed.Value().(bool) == true {571 if managed.Value().(bool) == true {
@@ -424,75 +575,208 @@ func (c *Client) WifisManaged(wifiDevices []string) (map[string]string, error) {
424 return ifaces, nil575 return ifaces, nil
425}576}
426577
427// Unmanage sets wlan0 to be Unmanaged by network manager if it578// Unmanage sets an interface to be Unmanaged by network manager if it
428// is managed579// is managed
429func (c *Client) Unmanage() error {580func (c *Client) Unmanage(wifiIface string) error {
430 ifaces, err := c.WifisManaged(c.GetWifiDevices(c.GetDevices()))581 ifaces, err := c.WifisManaged(c.GetWifiDevices())
431 if err != nil {582 if err != nil {
432 return fmt.Errorf("Error getting managed wifis: %v", err)583 return fmt.Errorf("Error getting managed wifis: %v", err)
433 }584 }
434 if _, ok := ifaces["wlan0"]; ok {585 if _, ok := ifaces[wifiIface]; ok {
435 if c.SetIfaceManaged("wlan0", false, c.GetWifiDevices(c.GetDevices())) == "" {586 if c.SetIfaceManaged(wifiIface, false,
587 c.GetWifiDevices()) == "" {
436 return fmt.Errorf("No interface was set to unmanaged")588 return fmt.Errorf("No interface was set to unmanaged")
437 }589 }
438 }590 }
439 return nil591 return nil
440}592}
441593
442// Manage sets wlan0 to not managed by network manager594// Manage sets an interface to not managed by network manager
443func (c *Client) Manage() error {595func (c *Client) Manage(wifiIface string) error {
444 if c.SetIfaceManaged("wlan0", true, c.GetWifiDevices(c.GetDevices())) == "" {596 if c.SetIfaceManaged(wifiIface, true,
597 c.GetWifiDevices()) == "" {
445 return fmt.Errorf("No interface was set to managed")598 return fmt.Errorf("No interface was set to managed")
446 }599 }
447 return nil600 return nil
448}601}
449602
450// ScanAndWriteSsidsToFile scans for ssids and writes to file603// CreateAccessPoint creates an AP with given SSID and password, for the wifi
451func (c *Client) ScanAndWriteSsidsToFile(filepath string) bool {604// interface specified. Returns the path to the connection settings and the
605// active connection.
606func (c *Client) CreateAccessPoint(ssid string, password string,
607 iface string, ipAddress string, prefix uint) (string, string, error) {
452608
453 var err error609 wifiPath, err := c.GetDevicePathFromIface(iface)
454 var file *os.File610 if err != nil {
455 if _, err = os.Stat(filepath); os.IsNotExist(err) {611 return "", "", err
456 file, err = os.Create(filepath)
457 if err != nil {
458 log.Printf("Error touching ssids file: %v", err)
459 return false
460 }
461 }612 }
462613
463 file, err = os.OpenFile(filepath, os.O_RDWR, 0644)614 setObject(c, "org.freedesktop.NetworkManager",
615 "/org/freedesktop/NetworkManager")
616
617 connectSett := make(map[string]dbus.Variant)
618 connectSett["id"] = dbus.MakeVariant(ssid)
619 connectSett["autoconnect"] = dbus.MakeVariant(false)
620
621 wirelessSett := make(map[string]dbus.Variant)
622 wirelessSett["ssid"] = dbus.MakeVariant([]byte(ssid))
623 wirelessSett["mode"] = dbus.MakeVariant("ap")
624
625 securitySett := make(map[string]dbus.Variant)
626 securitySett["key-mgmt"] = dbus.MakeVariant("wpa-psk")
627 securitySett["psk"] = dbus.MakeVariant(password)
628 securitySett["proto"] = dbus.MakeVariant([]string{"rsn"})
629 securitySett["pairwise"] = dbus.MakeVariant([]string{"ccmp"})
630 securitySett["group"] = dbus.MakeVariant([]string{"ccmp"})
631
632 ipv4Sett := make(map[string]dbus.Variant)
633 ipv4Sett["method"] = dbus.MakeVariant("manual")
634 ipv4Address := make(map[string]dbus.Variant)
635 ipv4Address["address"] = dbus.MakeVariant(ipAddress)
636 ipv4Address["prefix"] = dbus.MakeVariant(uint32(prefix))
637 ipv4AddressData := make([]map[string]dbus.Variant, 1)
638 ipv4AddressData[0] = ipv4Address
639 ipv4Sett["address-data"] = dbus.MakeVariant(ipv4AddressData)
640
641 ipv6Sett := make(map[string]dbus.Variant)
642 ipv6Sett["method"] = dbus.MakeVariant("ignore")
643
644 settings := make(map[string]map[string]dbus.Variant)
645 settings["connection"] = connectSett
646 settings["802-11-wireless"] = wirelessSett
647 settings["802-11-wireless-security"] = securitySett
648 settings["ipv4"] = ipv4Sett
649 settings["ipv6"] = ipv6Sett
650
651 var connPath, actConnPath string
652 err = c.dbusClient.BusObj.Call(
653 "org.freedesktop.NetworkManager.AddAndActivateConnection", 0,
654 settings, dbus.ObjectPath(wifiPath), dbus.ObjectPath("/")).
655 Store(&connPath, &actConnPath)
464 if err != nil {656 if err != nil {
465 log.Printf("Error opening ssids file: %v", err)657 return "", "", err
466 return false
467 }658 }
468659
469 defer file.Close()660 log.Printf("Connection setting in %s", connPath)
661 log.Printf("Active connection config in %s", actConnPath)
662 return connPath, actConnPath, c.WaitConnectionActivated(actConnPath)
663}
664
665// ActivateConnection starts a connection on a device
666func (c *Client) ActivateConnection(devicePath string,
667 connPath string) (string, error) {
470668
471 return c.scanSsids(file)669 setObject(c, "org.freedesktop.NetworkManager",
670 "/org/freedesktop/NetworkManager")
472671
672 var activeConnPath string
673 err := c.dbusClient.BusObj.Call(
674 "org.freedesktop.NetworkManager.ActivateConnection", 0,
675 dbus.ObjectPath(connPath), dbus.ObjectPath(devicePath),
676 dbus.ObjectPath("/")).Store(&activeConnPath)
677 if err != nil {
678 return "", err
679 }
680 return activeConnPath, c.WaitConnectionActivated(activeConnPath)
473}681}
474682
475// ScanSsids sets wlan0 to be managed and then scans683// WaitConnectionActivated waits until a connection has been activated
476// for ssids. If found, write the ssids (comma separated)684func (c *Client) WaitConnectionActivated(actConnPath string) error {
477// to writer and return true, else return false.685 rule := "type='signal',sender='org.freedesktop.NetworkManager'," +
478func (c *Client) scanSsids(writer io.Writer) bool {686 "path='" + actConnPath + "'," +
479 c.Manage()687 "interface='org.freedesktop.NetworkManager.Connection.Active'," +
480 SSIDs, _, _ := c.Ssids()688 "member='StateChanged'"
481 //only write SSIDs when found689 setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
482 if len(SSIDs) > 0 {690 err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Err
483 var out string691 if err != nil {
484 for _, ssid := range SSIDs {692 return err
485 out += strings.TrimSpace(ssid.Ssid) + ","693 }
486 }694 defer func() {
487 out = out[:len(out)-1]695 setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
488 _, err := writer.Write([]byte(out))696 err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.RemoveMatch", 0, rule).Err
489 if err != nil {697 if err != nil {
490 log.Printf("Error writing SSID(s): %v ", err)698 log.Printf("Error when calling RemoveMatch: %v", err)
491 } else {
492 log.Print("SSID(s) obtained")
493 return true
494 }699 }
700 }()
701
702 chSig := make(chan *dbus.Signal, 10)
703 defer close(chSig)
704 c.dbusClient.Connection.Signal(chSig)
705 defer c.dbusClient.Connection.RemoveSignal(chSig)
706
707 // Get current state, as we might have missed the signal while we were
708 // registering for it (the connection already existed).
709 setObject(c, "org.freedesktop.NetworkManager", dbus.ObjectPath(actConnPath))
710 state, err := c.dbusClient.BusObj.GetProperty(
711 "org.freedesktop.NetworkManager.Connection.Active.State")
712 if err != nil {
713 return err
495 }714 }
496 log.Print("No SSID found")715 // NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
497 return false716 if state.Value() == 2 {
717 return nil
718 }
719
720SignalLoop:
721 for {
722 select {
723 case sig := <-chSig:
724 // Note that we need to make sure the signal comes from
725 // the right object, as we will receive here any signal
726 // to which the process has registered.
727 // NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
728 if sig.Path == dbus.ObjectPath(actConnPath) &&
729 sig.Body[0].(uint32) == 2 {
730 break SignalLoop
731 }
732 case <-time.After(20 * time.Second):
733 return fmt.Errorf("Timeout while waiting for conn activation")
734 }
735 }
736
737 return nil
738}
739
740// CheckNetworkManagerRunning finds out whether NM is running or not
741func (c *Client) CheckNetworkManagerRunning() (bool, error) {
742 setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
743 err := c.dbusClient.BusObj.Call(
744 "org.freedesktop.DBus.GetNameOwner", 0,
745 "org.freedesktop.NetworkManager").Err
746 if err != nil {
747 return false, err
748 }
749 return true, nil
750}
751
752// RegisterForNameOwnerChanged registers for changes in DBus names
753func (c *Client) RegisterForNameOwnerChanged(chSig chan<- *dbus.Signal) error {
754 rule := "type='signal',sender='org.freedesktop.DBus'," +
755 "path='/org/freedesktop/DBus'," +
756 "interface='org.freedesktop.DBus'," +
757 "member='NameOwnerChanged'"
758 setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
759 err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Err
760 if err != nil {
761 return err
762 }
763
764 c.dbusClient.Connection.Signal(chSig)
765 return nil
766}
767
768// UnregisterForNameOwnerChanged unregisters for changes in DBus names
769func (c *Client) UnregisterForNameOwnerChanged(chSig chan<- *dbus.Signal) error {
770 rule := "type='signal',sender='org.freedesktop.DBus'," +
771 "path='/org/freedesktop/DBus'," +
772 "interface='org.freedesktop.DBus'," +
773 "member='NameOwnerChanged'"
774 setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus")
775 err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.RemoveMatch", 0, rule).Err
776 if err != nil {
777 log.Printf("Error unregistering for NameOwnerChanged: %v", err)
778 }
779
780 c.dbusClient.Connection.RemoveSignal(chSig)
781 return err
498}782}
diff --git a/netman/dbus_test.go b/netman/dbus_test.go
index c27a2f6..3110d1e 100644
--- a/netman/dbus_test.go
+++ b/netman/dbus_test.go
@@ -18,6 +18,7 @@
18package netman18package netman
1919
20import (20import (
21 "context"
21 "errors"22 "errors"
22 "fmt"23 "fmt"
23 "strconv"24 "strconv"
@@ -77,16 +78,23 @@ func (mock *mockObj) Call(method string, flags dbus.Flags, args ...interface{})
77 return call78 return call
78}79}
7980
80func (mock *mockObj) Go(method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call {81func (mock *mockObj) Go(method string, flags dbus.Flags, ch chan *dbus.Call,
82 args ...interface{}) *dbus.Call {
81 call := makeCall()83 call := makeCall()
82 return call84 return call
83}85}
8486
87func (mock *mockObj) GoWithContext(ctx context.Context, method string,
88 flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call {
89 return nil
90}
91
85func (mock *mockObj) GetProperty(p string) (dbus.Variant, error) {92func (mock *mockObj) GetProperty(p string) (dbus.Variant, error) {
86 switch p {93 switch p {
87 case "org.freedesktop.NetworkManager.Device.DeviceType":94 case "org.freedesktop.NetworkManager.Device.DeviceType":
88 if len(mock.wifiDevices) < 2 { // 2 of three devices are wifi95 if len(mock.wifiDevices) < 2 { // 2 of three devices are wifi
89 mock.wifiDevices = append(mock.wifiDevices, "wifi"+strconv.Itoa(len(mock.wifiDevices)))96 mock.wifiDevices = append(mock.wifiDevices,
97 "wifi"+strconv.Itoa(len(mock.wifiDevices)))
90 return dbus.MakeVariant(uint32(2)), nil98 return dbus.MakeVariant(uint32(2)), nil
91 }99 }
92 return dbus.MakeVariant(uint32(1)), nil100 return dbus.MakeVariant(uint32(1)), nil
@@ -123,6 +131,25 @@ func (mock *mockObj) GetProperty(p string) (dbus.Variant, error) {
123 return dbus.MakeVariant("GetProperty error"), errors.New("no such property found")131 return dbus.MakeVariant("GetProperty error"), errors.New("no such property found")
124}132}
125133
134func (mock *mockObj) AddMatchSignal(iface, member string,
135 options ...dbus.MatchOption) *dbus.Call {
136 return nil
137}
138
139func (mock *mockObj) RemoveMatchSignal(iface, member string,
140 options ...dbus.MatchOption) *dbus.Call {
141 return nil
142}
143
144func (mock *mockObj) CallWithContext(ctx context.Context, method string,
145 flags dbus.Flags, args ...interface{}) *dbus.Call {
146 return nil
147}
148
149func (mock *mockObj) SetProperty(p string, v interface{}) error {
150 return nil
151}
152
126func (mock *mockObj) Destination() string {153func (mock *mockObj) Destination() string {
127 return "destination"154 return "destination"
128}155}
@@ -160,11 +187,10 @@ func TestGetDevices(t *testing.T) {
160187
161func TestGetWifiDevices(t *testing.T) {188func TestGetWifiDevices(t *testing.T) {
162 client := NewClient(&mockObj{})189 client := NewClient(&mockObj{})
163 devices := client.GetDevices()190 wifiDevices := client.GetWifiDevices()
164 wifiDevices := client.GetWifiDevices(devices)
165 found1 := false191 found1 := false
166 found2 := false192 found2 := false
167 for _, v := range devices {193 for _, v := range wifiDevices {
168 switch v {194 switch v {
169 case "/d/1":195 case "/d/1":
170 found1 = true196 found1 = true
@@ -176,16 +202,16 @@ func TestGetWifiDevices(t *testing.T) {
176 t.Errorf("An expected device was not found")202 t.Errorf("An expected device was not found")
177 }203 }
178 if len(wifiDevices) != 2 {204 if len(wifiDevices) != 2 {
179 t.Errorf("Two wifi device should have been found but, found: %d", len(wifiDevices))205 t.Errorf("Two wifi device should have been found but, found: %d",
206 len(wifiDevices))
180 }207 }
181 fmt.Printf("===== Found wifi devices: %v\n", wifiDevices)208 fmt.Printf("===== Found wifi devices: %v\n", wifiDevices)
182}209}
183210
184func TestGetAPs(t *testing.T) {211func TestGetAPs(t *testing.T) {
185 client := NewClient(&mockObj{})212 client := NewClient(&mockObj{})
186 devices := client.GetDevices()
187 ap2device := make(map[string]string)213 ap2device := make(map[string]string)
188 wifiDevices := client.GetWifiDevices(devices)214 wifiDevices := client.GetWifiDevices()
189 aps := client.GetAccessPoints(wifiDevices, ap2device)215 aps := client.GetAccessPoints(wifiDevices, ap2device)
190 if len(aps) != 4 {216 if len(aps) != 4 {
191 t.Errorf("4 APs should have been found, but found: %d", len(aps))217 t.Errorf("4 APs should have been found, but found: %d", len(aps))
@@ -195,8 +221,7 @@ func TestGetAPs(t *testing.T) {
195221
196func TestGetSsids(t *testing.T) {222func TestGetSsids(t *testing.T) {
197 client := NewClient(&mockObj{})223 client := NewClient(&mockObj{})
198 devices := client.GetDevices()224 wifiDevices := client.GetWifiDevices()
199 wifiDevices := client.GetWifiDevices(devices)
200 ap2device := make(map[string]string)225 ap2device := make(map[string]string)
201 ssid2ap := make(map[string]string)226 ssid2ap := make(map[string]string)
202 aps := client.GetAccessPoints(wifiDevices, ap2device)227 aps := client.GetAccessPoints(wifiDevices, ap2device)
@@ -209,8 +234,8 @@ func TestGetSsids(t *testing.T) {
209234
210func TestSsids(t *testing.T) {235func TestSsids(t *testing.T) {
211 client := NewClient(&mockObj{})236 client := NewClient(&mockObj{})
212 ssids, _, _ := client.Ssids()237 ssids, _, _ := client.Ssids("/d/3")
213 if len(ssids) != 4 {238 if len(ssids) != 2 {
214 t.Errorf("4 SSIDs should have been found, but found: %d", len(ssids))239 t.Errorf("4 SSIDs should have been found, but found: %d", len(ssids))
215 }240 }
216 fmt.Printf("===== Ssids() (ssid/ap): %v\n", ssids)241 fmt.Printf("===== Ssids() (ssid/ap): %v\n", ssids)
@@ -232,26 +257,14 @@ func TestConnectedWifi(t *testing.T) {
232 mock := &mockObj{}257 mock := &mockObj{}
233 mock.connect = true258 mock.connect = true
234 client := NewClient(mock)259 client := NewClient(mock)
235 if !client.ConnectedWifi([]string{"d1"}) {260 if !client.Connected([]string{"d1"}) {
236 t.Errorf("Should have found Wificonnected state, but did not")261 t.Errorf("Should have found Wificonnected state, but did not")
237 }262 }
238 if client.ConnectedWifi([]string{}) {263 if client.Connected([]string{}) {
239 t.Errorf("Should have found no connection since there are no devices, but did not")264 t.Errorf("Should have found no connection since there are no devices, but did not")
240 }265 }
241}266}
242267
243func TestDiscconnectWifi(t *testing.T) {
244 client := NewClient(&mockObj{})
245 res := client.DisconnectWifi([]string{})
246 if res != 0 {
247 t.Errorf("0 Disconnect call expected, but found: %d", res)
248 }
249 res = client.DisconnectWifi([]string{"d1"})
250 if res != 1 {
251 t.Errorf("1 Disconnect call expected, but found: %d", res)
252 }
253}
254
255func TestSetIfaceManaged(t *testing.T) {268func TestSetIfaceManaged(t *testing.T) {
256 mock := &mockObj{}269 mock := &mockObj{}
257 client := NewClient(mock)270 client := NewClient(mock)
diff --git a/run-checks b/run-checks
index 562c14d..d922caf 100755
--- a/run-checks
+++ b/run-checks
@@ -85,7 +85,7 @@ if [ ! -z "$STATIC" ]; then
8585
86 # golint86 # golint
87 echo Install golint87 echo Install golint
88 go get github.com/golang/lint/golint88 go get -u golang.org/x/lint/golint
89 export PATH=$PATH:$GOPATH/bin89 export PATH=$PATH:$GOPATH/bin
9090
91 echo Running lint91 echo Running lint
diff --git a/scriptlets/country-codes b/scriptlets/country-codes
index d4e8a97..16c718a 100644
--- a/scriptlets/country-codes
+++ b/scriptlets/country-codes
@@ -1,243 +1,250 @@
1<html><head><title>ISO-3166-1 Country Names</title></head>1Name,Code
2<body bgcolor=#ffffff>2Afghanistan,AF
3<h2>ISO-3166-1 Country Names</h2>3Åland Islands,AX
4<a href="/iso3166/">Reproduced with permission from ISO</a>4Albania,AL
5<p>5Algeria,DZ
6Follow links for ISO-3166-2 subdivision codes6American Samoa,AS
7<p>7Andorra,AD
8<a href=iso.AD.html>AD</a> : ANDORRA<br>8Angola,AO
9<a href=iso.AE.html>AE</a> : UNITED ARAB EMIRATES<br>9Anguilla,AI
10<a href=iso.AF.html>AF</a> : AFGHANISTAN<br>10Antarctica,AQ
11<a href=iso.AG.html>AG</a> : ANTIGUA AND BARBUDA<br>11Antigua and Barbuda,AG
12<a href=iso.AI.html>AI</a> : ANGUILLA<br>12Argentina,AR
13<a href=iso.AL.html>AL</a> : ALBANIA<br>13Armenia,AM
14<a href=iso.AM.html>AM</a> : ARMENIA<br>14Aruba,AW
15<a href=iso.AN.html>AN</a> : NETHERLANDS ANTILLES<br>15Australia,AU
16<a href=iso.AO.html>AO</a> : ANGOLA<br>16Austria,AT
17<a href=iso.AQ.html>AQ</a> : ANTARCTICA<br>17Azerbaijan,AZ
18<a href=iso.AR.html>AR</a> : ARGENTINA<br>18Bahamas,BS
19<a href=iso.AS.html>AS</a> : AMERICAN SAMOA<br>19Bahrain,BH
20<a href=iso.AT.html>AT</a> : AUSTRIA<br>20Bangladesh,BD
21<a href=iso.AU.html>AU</a> : AUSTRALIA<br>21Barbados,BB
22<a href=iso.AW.html>AW</a> : ARUBA<br>22Belarus,BY
23<a href=iso.AZ.html>AZ</a> : AZERBAIJAN<br>23Belgium,BE
24<a href=iso.BA.html>BA</a> : BOSNIA AND HERZEGOVINA<br>24Belize,BZ
25<a href=iso.BB.html>BB</a> : BARBADOS<br>25Benin,BJ
26<a href=iso.BD.html>BD</a> : BANGLADESH<br>26Bermuda,BM
27<a href=iso.BE.html>BE</a> : BELGIUM<br>27Bhutan,BT
28<a href=iso.BF.html>BF</a> : BURKINA FASO<br>28"Bolivia, Plurinational State of",BO
29<a href=iso.BG.html>BG</a> : BULGARIA<br>29"Bonaire, Sint Eustatius and Saba",BQ
30<a href=iso.BH.html>BH</a> : BAHRAIN<br>30Bosnia and Herzegovina,BA
31<a href=iso.BI.html>BI</a> : BURUNDI<br>31Botswana,BW
32<a href=iso.BJ.html>BJ</a> : BENIN<br>32Bouvet Island,BV
33<a href=iso.BM.html>BM</a> : BERMUDA<br>33Brazil,BR
34<a href=iso.BN.html>BN</a> : BRUNEI DARUSSALAM<br>34British Indian Ocean Territory,IO
35<a href=iso.BO.html>BO</a> : BOLIVIA<br>35Brunei Darussalam,BN
36<a href=iso.BR.html>BR</a> : BRAZIL<br>36Bulgaria,BG
37<a href=iso.BS.html>BS</a> : BAHAMAS<br>37Burkina Faso,BF
38<a href=iso.BT.html>BT</a> : BHUTAN<br>38Burundi,BI
39<a href=iso.BV.html>BV</a> : BOUVET ISLAND<br>39Cambodia,KH
40<a href=iso.BW.html>BW</a> : BOTSWANA<br>40Cameroon,CM
41<a href=iso.BY.html>BY</a> : BELARUS<br>41Canada,CA
42<a href=iso.BZ.html>BZ</a> : BELIZE<br>42Cape Verde,CV
43<a href=iso.CA.html>CA</a> : CANADA<br>43Cayman Islands,KY
44<a href=iso.CC.html>CC</a> : COCOS (KEELING) ISLANDS<br>44Central African Republic,CF
45<a href=iso.CD.html>CD</a> : CONGO, THE DEMOCRATIC REPUBLIC OF THE<br>45Chad,TD
46<a href=iso.CF.html>CF</a> : CENTRAL AFRICAN REPUBLIC<br>46Chile,CL
47<a href=iso.CG.html>CG</a> : CONGO<br>47China,CN
48<a href=iso.CH.html>CH</a> : SWITZERLAND<br>48Christmas Island,CX
49<a href=iso.CI.html>CI</a> : C�TE D'IVOIRE<br>49Cocos (Keeling) Islands,CC
50<a href=iso.CK.html>CK</a> : COOK ISLANDS<br>50Colombia,CO
51<a href=iso.CL.html>CL</a> : CHILE<br>51Comoros,KM
52<a href=iso.CM.html>CM</a> : CAMEROON<br>52Congo,CG
53<a href=iso.CN.html>CN</a> : CHINA<br>53"Congo, the Democratic Republic of the",CD
54<a href=iso.CO.html>CO</a> : COLOMBIA<br>54Cook Islands,CK
55<a href=iso.CR.html>CR</a> : COSTA RICA<br>55Costa Rica,CR
56<a href=iso.CU.html>CU</a> : CUBA<br>56Côte d'Ivoire,CI
57<a href=iso.CV.html>CV</a> : CAPE VERDE<br>57Croatia,HR
58<a href=iso.CX.html>CX</a> : CHRISTMAS ISLAND<br>58Cuba,CU
59<a href=iso.CY.html>CY</a> : CYPRUS<br>59Curaçao,CW
60<a href=iso.CZ.html>CZ</a> : CZECH REPUBLIC<br>60Cyprus,CY
61<a href=iso.DE.html>DE</a> : GERMANY<br>61Czech Republic,CZ
62<a href=iso.DJ.html>DJ</a> : DJIBOUTI<br>62Denmark,DK
63<a href=iso.DK.html>DK</a> : DENMARK<br>63Djibouti,DJ
64<a href=iso.DM.html>DM</a> : DOMINICA<br>64Dominica,DM
65<a href=iso.DO.html>DO</a> : DOMINICAN REPUBLIC<br>65Dominican Republic,DO
66<a href=iso.DZ.html>DZ</a> : ALGERIA<br>66Ecuador,EC
67<a href=iso.EC.html>EC</a> : ECUADOR<br>67Egypt,EG
68<a href=iso.EE.html>EE</a> : ESTONIA<br>68El Salvador,SV
69<a href=iso.EG.html>EG</a> : EGYPT<br>69Equatorial Guinea,GQ
70<a href=iso.EH.html>EH</a> : WESTERN SARARA<br>70Eritrea,ER
71<a href=iso.ER.html>ER</a> : ERITREA<br>71Estonia,EE
72<a href=iso.ES.html>ES</a> : SPAIN<br>72Ethiopia,ET
73<a href=iso.ET.html>ET</a> : ETHIOPIA<br>73Falkland Islands (Malvinas),FK
74<a href=iso.FI.html>FI</a> : FINLAND<br>74Faroe Islands,FO
75<a href=iso.FJ.html>FJ</a> : FIJI<br>75Fiji,FJ
76<a href=iso.FK.html>FK</a> : FALKLAND ISLANDS (MALVINAS)<br>76Finland,FI
77<a href=iso.FM.html>FM</a> : MICRONESIA, FEDERATED STATES OF<br>77France,FR
78<a href=iso.FO.html>FO</a> : FAROE ISLANDS<br>78French Guiana,GF
79<a href=iso.FR.html>FR</a> : FRANCE<br>79French Polynesia,PF
80<a href=iso.GA.html>GA</a> : GABON<br>80French Southern Territories,TF
81<a href=iso.GB.html>GB</a> : UNITED KINGDOM<br>81Gabon,GA
82<a href=iso.GD.html>GD</a> : GRENADA<br>82Gambia,GM
83<a href=iso.GE.html>GE</a> : GEORGIA<br>83Georgia,GE
84<a href=iso.GF.html>GF</a> : FRENCH GUIANA<br>84Germany,DE
85<a href=iso.GH.html>GH</a> : GHANA<br>85Ghana,GH
86<a href=iso.GI.html>GI</a> : GIBRALTAR<br>86Gibraltar,GI
87<a href=iso.GL.html>GL</a> : GREENLAND<br>87Greece,GR
88<a href=iso.GM.html>GM</a> : GAMBIA<br>88Greenland,GL
89<a href=iso.GN.html>GN</a> : GUINEA<br>89Grenada,GD
90<a href=iso.GP.html>GP</a> : GUADELOUPE<br>90Guadeloupe,GP
91<a href=iso.GQ.html>GQ</a> : EQUATORIAL GUINEA<br>91Guam,GU
92<a href=iso.GR.html>GR</a> : GREECE<br>92Guatemala,GT
93<a href=iso.GS.html>GS</a> : SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS<br>93Guernsey,GG
94<a href=iso.GT.html>GT</a> : GUATEMALA<br>94Guinea,GN
95<a href=iso.GU.html>GU</a> : GUAM<br>95Guinea-Bissau,GW
96<a href=iso.GW.html>GW</a> : GUINEA-BISSAU<br>96Guyana,GY
97<a href=iso.GY.html>GY</a> : GUYANA<br>97Haiti,HT
98<a href=iso.HK.html>HK</a> : HONG KONG<br>98Heard Island and McDonald Islands,HM
99<a href=iso.HM.html>HM</a> : HEARD ISLAND AND MCDONALD ISLANDS<br>99Holy See (Vatican City State),VA
100<a href=iso.HN.html>HN</a> : HONDURAS<br>100Honduras,HN
101<a href=iso.HR.html>HR</a> : CROATIA<br>101Hong Kong,HK
102<a href=iso.HT.html>HT</a> : HAITI<br>102Hungary,HU
103<a href=iso.HU.html>HU</a> : HUNGARY<br>103Iceland,IS
104<a href=iso.ID.html>ID</a> : INDONESIA<br>104India,IN
105<a href=iso.IE.html>IE</a> : IRELAND<br>105Indonesia,ID
106<a href=iso.IL.html>IL</a> : ISRAEL<br>106"Iran, Islamic Republic of",IR
107<a href=iso.IN.html>IN</a> : INDIA<br>107Iraq,IQ
108<a href=iso.IO.html>IO</a> : BRITISH INDIAN OCEAN TERRITORY<br>108Ireland,IE
109<a href=iso.IQ.html>IQ</a> : IRAQ<br>109Isle of Man,IM
110<a href=iso.IR.html>IR</a> : IRAN, ISLAMIC REPUBLIC OF<br>110Israel,IL
111<a href=iso.IS.html>IS</a> : ICELAND<br>111Italy,IT
112<a href=iso.IT.html>IT</a> : ITALY<br>112Jamaica,JM
113<a href=iso.JM.html>JM</a> : JAMAICA<br>113Japan,JP
114<a href=iso.JO.html>JO</a> : JORDAN<br>114Jersey,JE
115<a href=iso.JP.html>JP</a> : JAPAN<br>115Jordan,JO
116<a href=iso.KE.html>KE</a> : KENYA<br>116Kazakhstan,KZ
117<a href=iso.KG.html>KG</a> : KYRGYZSTAN<br>117Kenya,KE
118<a href=iso.KH.html>KH</a> : CAMBODIA<br>118Kiribati,KI
119<a href=iso.KI.html>KI</a> : KIRIBATI<br>119"Korea, Democratic People's Republic of",KP
120<a href=iso.KM.html>KM</a> : COMOROS<br>120"Korea, Republic of",KR
121<a href=iso.KN.html>KN</a> : SAINT KITTS AND NEVIS<br>121Kuwait,KW
122<a href=iso.KP.html>KP</a> : KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF<br>122Kyrgyzstan,KG
123<a href=iso.KR.html>KR</a> : KOREA, REPUBLIC OF<br>123Lao People's Democratic Republic,LA
124<a href=iso.KW.html>KW</a> : KUWAIT<br>124Latvia,LV
125<a href=iso.KY.html>KY</a> : CAYMAN ISLANDS<br>125Lebanon,LB
126<a href=iso.KZ.html>KZ</a> : KAZAKHSTAN<br>126Lesotho,LS
127<a href=iso.LA.html>LA</a> : LAO PEOPLE'S DEMOCRATIC REPUBLIC<br>127Liberia,LR
128<a href=iso.LB.html>LB</a> : LEBANON<br>128Libya,LY
129<a href=iso.LC.html>LC</a> : SAINT LUCIA<br>129Liechtenstein,LI
130<a href=iso.LI.html>LI</a> : LIECHTENSTEIN<br>130Lithuania,LT
131<a href=iso.LK.html>LK</a> : SRI LANKA<br>131Luxembourg,LU
132<a href=iso.LR.html>LR</a> : LIBERIA<br>132Macao,MO
133<a href=iso.LS.html>LS</a> : LESOTHO<br>133"Macedonia, the Former Yugoslav Republic of",MK
134<a href=iso.LT.html>LT</a> : LITHUANIA<br>134Madagascar,MG
135<a href=iso.LU.html>LU</a> : LUXEMBOURG<br>135Malawi,MW
136<a href=iso.LV.html>LV</a> : LATVIA<br>136Malaysia,MY
137<a href=iso.LY.html>LY</a> : LIBYAN ARAB JAMABIRIYA<br>137Maldives,MV
138<a href=iso.MA.html>MA</a> : MOROCCO<br>138Mali,ML
139<a href=iso.MC.html>MC</a> : MONACO<br>139Malta,MT
140<a href=iso.MD.html>MD</a> : MOLDOVA, REPUBLIC OF<br>140Marshall Islands,MH
141<a href=iso.MG.html>MG</a> : MADAGASCAR<br>141Martinique,MQ
142<a href=iso.MH.html>MH</a> : MARSHALL ISLANDS<br>142Mauritania,MR
143<a href=iso.MK.html>MK</a> : MACEDONIA, THE FORMER YUGOSLAV REPU8LIC OF<br>143Mauritius,MU
144<a href=iso.ML.html>ML</a> : MALI<br>144Mayotte,YT
145<a href=iso.MM.html>MM</a> : MYANMAR<br>145Mexico,MX
146<a href=iso.MN.html>MN</a> : MONGOLIA<br>146"Micronesia, Federated States of",FM
147<a href=iso.MO.html>MO</a> : MACAU<br>147"Moldova, Republic of",MD
148<a href=iso.MP.html>MP</a> : NORTHERN MARIANA ISLANDS<br>148Monaco,MC
149<a href=iso.MQ.html>MQ</a> : MARTINIQUE<br>149Mongolia,MN
150<a href=iso.MR.html>MR</a> : MAURITANIA<br>150Montenegro,ME
151<a href=iso.MS.html>MS</a> : MONTSERRAT<br>151Montserrat,MS
152<a href=iso.MT.html>MT</a> : MALTA<br>152Morocco,MA
153<a href=iso.MU.html>MU</a> : MAURITIUS<br>153Mozambique,MZ
154<a href=iso.MV.html>MV</a> : MALDIVES<br>154Myanmar,MM
155<a href=iso.MW.html>MW</a> : MALAWI<br>155Namibia,NA
156<a href=iso.MX.html>MX</a> : MEXICO<br>156Nauru,NR
157<a href=iso.MY.html>MY</a> : MALAYSIA<br>157Nepal,NP
158<a href=iso.MZ.html>MZ</a> : MOZAMBIQUE<br>158Netherlands,NL
159<a href=iso.NA.html>NA</a> : NAMIBIA<br>159New Caledonia,NC
160<a href=iso.NC.html>NC</a> : NEW CALEDONIA<br>160New Zealand,NZ
161<a href=iso.NE.html>NE</a> : NIGER<br>161Nicaragua,NI
162<a href=iso.NF.html>NF</a> : NORFOLK ISLAND<br>162Niger,NE
163<a href=iso.NG.html>NG</a> : NIGERIA<br>163Nigeria,NG
164<a href=iso.NI.html>NI</a> : NICARAGUA<br>164Niue,NU
165<a href=iso.NL.html>NL</a> : NETHERLANDS<br>165Norfolk Island,NF
166<a href=iso.NO.html>NO</a> : NORWAY<br>166Northern Mariana Islands,MP
167<a href=iso.NP.html>NP</a> : NEPAL<br>167Norway,NO
168<a href=iso.NU.html>NU</a> : NIUE<br>168Oman,OM
169<a href=iso.NZ.html>NZ</a> : NEW ZEALAND<br>169Pakistan,PK
170<a href=iso.OM.html>OM</a> : OMAN<br>170Palau,PW
171<a href=iso.PA.html>PA</a> : PANAMA<br>171"Palestine, State of",PS
172<a href=iso.PE.html>PE</a> : PERU<br>172Panama,PA
173<a href=iso.PF.html>PF</a> : FRENCH POLYNESIA<br>173Papua New Guinea,PG
174<a href=iso.PG.html>PG</a> : PAPUA NEW GUINEA<br>174Paraguay,PY
175<a href=iso.PH.html>PH</a> : PHILIPPINES<br>175Peru,PE
176<a href=iso.PK.html>PK</a> : PAKISTAN<br>176Philippines,PH
177<a href=iso.PL.html>PL</a> : POLAND<br>177Pitcairn,PN
178<a href=iso.PM.html>PM</a> : SAINT PIERRE AND MIQUELON<br>178Poland,PL
179<a href=iso.PN.html>PN</a> : PITCAIRN<br>179Portugal,PT
180<a href=iso.PR.html>PR</a> : PUERTO RICO<br>180Puerto Rico,PR
181<a href=iso.PT.html>PT</a> : PORTUGAL<br>181Qatar,QA
182<a href=iso.PW.html>PW</a> : PALAU<br>182Réunion,RE
183<a href=iso.PY.html>PY</a> : PARAGUAY<br>183Romania,RO
184<a href=iso.QA.html>QA</a> : QATAR<br>184Russian Federation,RU
185<a href=iso.RE.html>RE</a> : R�UNION<br>185Rwanda,RW
186<a href=iso.RO.html>RO</a> : ROMANIA<br>186Saint Barthélemy,BL
187<a href=iso.RU.html>RU</a> : RUSSIAN FEDERATION<br>187"Saint Helena, Ascension and Tristan da Cunha",SH
188<a href=iso.RW.html>RW</a> : RWANDA<br>188Saint Kitts and Nevis,KN
189<a href=iso.SA.html>SA</a> : SAUDI ARABIA<br>189Saint Lucia,LC
190<a href=iso.SB.html>SB</a> : SOLOMON ISLANDS<br>190Saint Martin (French part),MF
191<a href=iso.SC.html>SC</a> : SEYCHELLES<br>191Saint Pierre and Miquelon,PM
192<a href=iso.SD.html>SD</a> : SUDAN<br>192Saint Vincent and the Grenadines,VC
193<a href=iso.SE.html>SE</a> : SWEDEN<br>193Samoa,WS
194<a href=iso.SG.html>SG</a> : SINGAPORE<br>194San Marino,SM
195<a href=iso.SH.html>SH</a> : SAINT HELENA<br>195Sao Tome and Principe,ST
196<a href=iso.SI.html>SI</a> : SLOVENIA<br>196Saudi Arabia,SA
197<a href=iso.SJ.html>SJ</a> : SVALBARD AND JAN MAYEN<br>197Senegal,SN
198<a href=iso.SK.html>SK</a> : SLOVAKIA<br>198Serbia,RS
199<a href=iso.SL.html>SL</a> : SIERRA LEONE<br>199Seychelles,SC
200<a href=iso.SM.html>SM</a> : SAN MARINO<br>200Sierra Leone,SL
201<a href=iso.SN.html>SN</a> : SENEGAL<br>201Singapore,SG
202<a href=iso.SO.html>SO</a> : SOMALIA<br>202Sint Maarten (Dutch part),SX
203<a href=iso.SR.html>SR</a> : SURINAME<br>203Slovakia,SK
204<a href=iso.ST.html>ST</a> : SAO TOME AND PRINCIPE<br>204Slovenia,SI
205<a href=iso.SV.html>SV</a> : EL SALVADOR<br>205Solomon Islands,SB
206<a href=iso.SY.html>SY</a> : SYRIAN ARAB REPUBLIC<br>206Somalia,SO
207<a href=iso.SZ.html>SZ</a> : SWAZILAND<br>207South Africa,ZA
208<a href=iso.TC.html>TC</a> : TURKS AND CAICOS ISLANDS<br>208South Georgia and the South Sandwich Islands,GS
209<a href=iso.TD.html>TD</a> : CHAD<br>209South Sudan,SS
210<a href=iso.TF.html>TF</a> : FRENCH SOUTHERN TERRITORIES<br>210Spain,ES
211<a href=iso.TG.html>TG</a> : TOGO<br>211Sri Lanka,LK
212<a href=iso.TH.html>TH</a> : THAILAND<br>212Sudan,SD
213<a href=iso.TJ.html>TJ</a> : TAJIKISTAN<br>213Suriname,SR
214<a href=iso.TK.html>TK</a> : TOKELAU<br>214Svalbard and Jan Mayen,SJ
215<a href=iso.TM.html>TM</a> : TURKMENISTAN<br>215Swaziland,SZ
216<a href=iso.TN.html>TN</a> : TUNISIA<br>216Sweden,SE
217<a href=iso.TO.html>TO</a> : TONGA<br>217Switzerland,CH
218<a href=iso.TP.html>TP</a> : EAST TIMOR<br>218Syrian Arab Republic,SY
219<a href=iso.TR.html>TR</a> : TURKEY<br>219"Taiwan, Province of China",TW
220<a href=iso.TT.html>TT</a> : TRINIDAD AND TOBAGO<br>220Tajikistan,TJ
221<a href=iso.TV.html>TV</a> : TUVALU<br>221"Tanzania, United Republic of",TZ
222<a href=iso.TW.html>TW</a> : TAIWAN, PROVINCE OF CHINA<br>222Thailand,TH
223<a href=iso.TZ.html>TZ</a> : TANZANIA, UNITED REPUBLIC OF<br>223Timor-Leste,TL
224<a href=iso.UA.html>UA</a> : UKRAINE<br>224Togo,TG
225<a href=iso.UG.html>UG</a> : UGANDA<br>225Tokelau,TK
226<a href=iso.UM.html>UM</a> : UNITED STATES MINOR OUTLYING ISLANDS<br>226Tonga,TO
227<a href=iso.US.html>US</a> : UNITED STATES<br>227Trinidad and Tobago,TT
228<a href=iso.UY.html>UY</a> : URUGUAY<br>228Tunisia,TN
229<a href=iso.UZ.html>UZ</a> : UZBEKISTAN<br>229Turkey,TR
230<a href=iso.VE.html>VE</a> : VENEZUELA<br>230Turkmenistan,TM
231<a href=iso.VG.html>VG</a> : VIRGIN ISLANDS, BRITISH<br>231Turks and Caicos Islands,TC
232<a href=iso.VI.html>VI</a> : VIRGIN ISLANDS, U.S.<br>232Tuvalu,TV
233<a href=iso.VN.html>VN</a> : VIET NAM<br>233Uganda,UG
234<a href=iso.VU.html>VU</a> : VANUATU<br>234Ukraine,UA
235<a href=iso.WF.html>WF</a> : WALLIS AND FUTUNA<br>235United Arab Emirates,AE
236<a href=iso.WS.html>WS</a> : SAMOA<br>236United Kingdom,GB
237<a href=iso.YE.html>YE</a> : YEMEN<br>237United States,US
238<a href=iso.YT.html>YT</a> : MAYOTTE<br>238United States Minor Outlying Islands,UM
239<a href=iso.YU.html>YU</a> : YUGOSLAVIA<br>239Uruguay,UY
240<a href=iso.ZA.html>ZA</a> : SOUTH AFRICA<br>240Uzbekistan,UZ
241<a href=iso.ZM.html>ZM</a> : ZAMBIA<br>241Vanuatu,VU
242<a href=iso.ZW.html>ZW</a> : ZIMBABWE<br>242"Venezuela, Bolivarian Republic of",VE
243</body></html>243Viet Nam,VN
244"Virgin Islands, British",VG
245"Virgin Islands, U.S.",VI
246Wallis and Futuna,WF
247Western Sahara,EH
248Yemen,YE
249Zambia,ZM
250Zimbabwe,ZW
diff --git a/scriptlets/fetch_country_codes.sh b/scriptlets/fetch_country_codes.sh
index 8dae525..0850b77 100755
--- a/scriptlets/fetch_country_codes.sh
+++ b/scriptlets/fetch_country_codes.sh
@@ -3,8 +3,8 @@
3# store them locally to be used in compilation time.3# store them locally to be used in compilation time.
4# This operation must be executed by hand when wanted to update4# This operation must be executed by hand when wanted to update
5# current ones shown in config page5# current ones shown in config page
6set -e 6set -ex
77
8cd "$(dirname "$0")"8cd "$(dirname "$0")"
99
10curl -X GET http://geotags.com/iso3166/countries.html > country-codes
11\ No newline at end of file10\ No newline at end of file
11curl -X GET https://raw.githubusercontent.com/umpirsky/country-list/master/data/en_US/country.csv > country-codes
diff --git a/scriptlets/fill_country_codes.sh b/scriptlets/fill_country_codes.sh
index b8babe4..028b77a 100755
--- a/scriptlets/fill_country_codes.sh
+++ b/scriptlets/fill_country_codes.sh
@@ -4,9 +4,9 @@
4# Process is easy, get it from remote path, format using sed, and replace 4# Process is easy, get it from remote path, format using sed, and replace
5# html page were they will be used5# html page were they will be used
66
7set -e 7set -e
88
9COUNTRY_CODES=../../../scriptlets/country-codes9COUNTRY_CODES=./scriptlets/country-codes
1010
11if [ ! -e $COUNTRY_CODES ]; then11if [ ! -e $COUNTRY_CODES ]; then
12 echo "======================================================="12 echo "======================================================="
@@ -19,21 +19,16 @@ if [ ! -e $COUNTRY_CODES ]; then
19 exit 119 exit 1
20fi20fi
2121
22# think that this is a scriptlet, executed in parts/<the_part>/build folder22# remove first line and create html
23cp $COUNTRY_CODES .23tail -n +2 $COUNTRY_CODES | sed 's/\r$//' |
2424 while read -r ln; do
25# remove non processable lines25 printf '<option value="%s">%s</option>\n' "${ln##*,}" "${ln%,*}"
26sed -i '/^<a href=iso/!d' country-codes26 done > country-codes
27
28# process lines to change its format to be html select options
29sed -i '/<a href=iso/ s/<a href=iso.*html>/<option value="/
30s/<\/a>\ :\ /">/
31s/<br>/<\/option>/' country-codes
3227
33# add world wide default option at beginning28# add world wide default option at beginning
34sed -i '1s;^;<option value="XX">\-WORLD WIDE\-<\/option>\n;' country-codes29sed -i '1s;^;<option value="XX">\-WORLD WIDE\-<\/option>\n;' country-codes
3530
36# replace mark in management.html template with real country-codes31# replace mark in management.html template with real country-codes
37sed -e '/\[COUNTRY_CODE_OPTIONS\]/ {' -e 'r country-codes' -e 'd' -e '}' -i static/templates/management.html 32sed -e '/\[COUNTRY_CODE_OPTIONS\]/ {' -e 'r country-codes' -e 'd' -e '}' -i static/templates/management.html
3833
39echo "country codes filled ok"34echo "country codes filled ok"
diff --git a/server/handlers.go b/server/handlers.go
index 45ec7fe..b7e71ff 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -24,10 +24,8 @@ import (
24 "net/http"24 "net/http"
25 "os"25 "os"
26 "path/filepath"26 "path/filepath"
27 "text/template"
28 "time"
29
30 "strconv"27 "strconv"
28 "text/template"
3129
32 "launchpad.net/wifi-connect/utils"30 "launchpad.net/wifi-connect/utils"
33)31)
@@ -42,9 +40,6 @@ const (
42// ResourcesPath absolute path to web static resources40// ResourcesPath absolute path to web static resources
43var ResourcesPath = filepath.Join(os.Getenv("SNAP"), "static")41var ResourcesPath = filepath.Join(os.Getenv("SNAP"), "static")
4442
45// first time management portal is accessed this file is created.
46var firstConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), ".first_config")
47
48var cw interface{}43var cw interface{}
4944
50// Data interface representing any data included in a template45// Data interface representing any data included in a template
@@ -66,7 +61,36 @@ type ConnectingData struct {
66 Ssid string61 Ssid string
67}62}
6863
69type noData struct {64// UserEvent stores data from events generated by the user, that is
65// sent back to the service main loop
66type UserEvent struct {
67 EvType int
68 Params map[string]string
69}
70
71// Event types for UserEvent.evType
72const (
73 UserEventConnect = 0 + iota
74 UserEventRefresh
75 UserEventDisconnect
76)
77
78// Parameters for UserEventConnect
79const (
80 UeConnSsid = "ssid"
81 UeConnPassword = "password"
82)
83
84// ManagementServer models the management server
85type ManagementServer struct {
86 chUserEvs chan<- UserEvent
87 config utils.Config
88 ssidLs *utils.SsidList
89}
90
91// OperationalServer models the operational server
92type OperationalServer struct {
93 chUserEvs chan<- UserEvent
70}94}
7195
72func execTemplate(w http.ResponseWriter, templatePath string, data Data) {96func execTemplate(w http.ResponseWriter, templatePath string, data Data) {
@@ -81,7 +105,7 @@ func execTemplate(w http.ResponseWriter, templatePath string, data Data) {
81105
82 err = t.Execute(w, data)106 err = t.Execute(w, data)
83 if err != nil {107 if err != nil {
84 msg := fmt.Sprintf("Error executing the template at %v : %v", templatePath, err)108 msg := fmt.Sprintf("Error executing the template at %v: %v", templatePath, err)
85 log.Print(msg)109 log.Print(msg)
86 http.Error(w, msg, http.StatusInternalServerError)110 http.Error(w, msg, http.StatusInternalServerError)
87 return111 return
@@ -89,53 +113,36 @@ func execTemplate(w http.ResponseWriter, templatePath string, data Data) {
89}113}
90114
91// ManagementHandler handles management portal115// ManagementHandler handles management portal
92func ManagementHandler(w http.ResponseWriter, r *http.Request) {116func (ms *ManagementServer) ManagementHandler(w http.ResponseWriter, r *http.Request) {
93117
94 if utils.MustSetConfig() {118 if utils.MustSetConfig() {
95119
96 config, err := utils.ReadConfig()120 if !ms.config.Portal.NoResetCredentials {
97 if err != nil {121 execTemplate(w, managementTemplatePath,
98 msg := fmt.Sprintf("Error reading configuration: %v", err)122 ManagementData{Config: &ms.config, Page: "config"})
99 log.Println(msg)
100 http.Error(w, msg, http.StatusInternalServerError)
101 return
102 }
103
104 if !config.Portal.NoResetCredentials {
105 execTemplate(w, managementTemplatePath, ManagementData{Config: config, Page: "config"})
106 return123 return
107 }124 }
108125
109 }126 }
110127
111 ssids, err := utils.ReadSsidsFile()128 execTemplate(w, managementTemplatePath,
112 if err != nil {129 ManagementData{Ssids: ms.ssidLs.GetSsidList(), Page: "ssids"})
113 log.Printf("Error reading SSIDs file: %v", err)
114 http.Error(w, err.Error(), http.StatusInternalServerError)
115 return
116 }
117
118 execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"})
119}130}
120131
121// SaveConfigHandler saves config received as form post parameters132// SaveConfigHandler saves config received as form post parameters
122func SaveConfigHandler(w http.ResponseWriter, r *http.Request) {133func (ms *ManagementServer) SaveConfigHandler(w http.ResponseWriter, r *http.Request) {
123 // read previous config134 // start from previous config
124 config, err := utils.ReadConfig()135 config := ms.config
125 if err != nil {
126 msg := fmt.Sprintf("Error reading previous stored config: %v", err)
127 log.Println(msg)
128 http.Error(w, msg, http.StatusInternalServerError)
129 return
130 }
131136
132 r.ParseForm()137 r.ParseForm()
133138
139 var err error
134 config.Wifi.Ssid = utils.ParseFormParamSingleValue(r.Form, "Ssid")140 config.Wifi.Ssid = utils.ParseFormParamSingleValue(r.Form, "Ssid")
135 config.Wifi.Passphrase = utils.ParseFormParamSingleValue(r.Form, "Passphrase")141 config.Wifi.Passphrase = utils.ParseFormParamSingleValue(r.Form, "Passphrase")
136 config.Wifi.Interface = utils.ParseFormParamSingleValue(r.Form, "Interface")142 config.Wifi.Interface = utils.ParseFormParamSingleValue(r.Form, "Interface")
137 config.Wifi.CountryCode = utils.ParseFormParamSingleValue(r.Form, "CountryCode")143 config.Wifi.CountryCode = utils.ParseFormParamSingleValue(r.Form, "CountryCode")
138 config.Wifi.Channel, err = strconv.Atoi(utils.ParseFormParamSingleValue(r.Form, "Channel"))144 config.Wifi.Channel, err = strconv.Atoi(
145 utils.ParseFormParamSingleValue(r.Form, "Channel"))
139 if err != nil {146 if err != nil {
140 msg := fmt.Sprintf("Error parsing channel form value: %v", err)147 msg := fmt.Sprintf("Error parsing channel form value: %v", err)
141 log.Println(msg)148 log.Println(msg)
@@ -145,7 +152,7 @@ func SaveConfigHandler(w http.ResponseWriter, r *http.Request) {
145 config.Wifi.OperationMode = utils.ParseFormParamSingleValue(r.Form, "OperationMode")152 config.Wifi.OperationMode = utils.ParseFormParamSingleValue(r.Form, "OperationMode")
146 config.Portal.Password = utils.ParseFormParamSingleValue(r.Form, "PortalPassword")153 config.Portal.Password = utils.ParseFormParamSingleValue(r.Form, "PortalPassword")
147154
148 err = utils.WriteConfig(config)155 err = utils.WriteConfig(&config)
149 if err != nil {156 if err != nil {
150 msg := fmt.Sprintf("Error saving config: %v", err)157 msg := fmt.Sprintf("Error saving config: %v", err)
151 log.Println(msg)158 log.Println(msg)
@@ -153,20 +160,12 @@ func SaveConfigHandler(w http.ResponseWriter, r *http.Request) {
153 return160 return
154 }161 }
155162
156 //after saving config, redirect to management portal, showing available ssids163 execTemplate(w, managementTemplatePath,
157 ssids, err := utils.ReadSsidsFile()164 ManagementData{Ssids: ms.ssidLs.GetSsidList(), Page: "ssids"})
158 if err != nil {
159 msg := fmt.Sprintf("== wifi-connect/handler: Error reading SSIDs file: %v\n", err)
160 log.Println(msg)
161 http.Error(w, msg, http.StatusInternalServerError)
162 return
163 }
164
165 execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"})
166}165}
167166
168// ConnectHandler reads form got ssid and password and tries to connect to that network167// ConnectHandler reads form got ssid and password and tries to connect to that network
169func ConnectHandler(w http.ResponseWriter, r *http.Request) {168func (ms *ManagementServer) ConnectHandler(w http.ResponseWriter, r *http.Request) {
170 r.ParseForm()169 r.ParseForm()
171170
172 pwd := ""171 pwd := ""
@@ -184,55 +183,15 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) {
184 }183 }
185 ssid := ssids[0]184 ssid := ssids[0]
186185
187 go func() {186 ms.chUserEvs <- UserEvent{EvType: UserEventConnect,
188 log.Printf("Connecting to %v", ssid)187 Params: map[string]string{UeConnSsid: ssid, UeConnPassword: pwd}}
189
190 // While the connecting is attempted, the flag file must be
191 // present so the service loops harmlessly
192 waitPath := os.Getenv("SNAP_COMMON") + "/startingApConnect"
193 err1 := utils.WriteFlagFile(waitPath)
194 if err1 != nil {
195 log.Print("Error writing flag file")
196 return
197 }
198
199 err := wifiapClient.Disable()
200 if err != nil {
201 msg := fmt.Sprintf("Error disabling AP: %v", err)
202 log.Print(msg)
203 http.Error(w, msg, http.StatusInternalServerError)
204 return
205 }
206
207 // manage iface by netman
208 netmanClient.SetIfaceManaged("wlan0", true, netmanClient.GetWifiDevices(netmanClient.GetDevices()))
209 _, ap2device, ssid2ap := netmanClient.Ssids()
210
211 // attempt to connect to external AP
212 err = netmanClient.ConnectAp(ssid, pwd, ap2device, ssid2ap)
213 //TODO signal user in portal on failure to connect
214 if err != nil {
215 msg := fmt.Sprintf("Failed connecting to %v", ssid)
216 log.Println(msg)
217 netmanClient.DeleteWifiConnections()
218 //note that this call takes time, perhaps a couple minutes, so
219 //deleting the wait file happens after, which means it takes a couple
220 //minutes for the wifi-connect service to restor system to Management
221 //mode
222 http.Error(w, msg, http.StatusInternalServerError)
223 //remove WAIT flag file so that daemon starts checking state
224 //and takes control again
225 waitPath := os.Getenv("SNAP_COMMON") + "/startingApConnect"
226 utils.RemoveFlagFile(waitPath)
227 }
228 }()
229}188}
230189
231type disconnectData struct {190type disconnectData struct {
232}191}
233192
234// OperationalHandler display Opertational mode page193// OperationalHandler display Opertational mode page
235func OperationalHandler(w http.ResponseWriter, r *http.Request) {194func (os *OperationalServer) OperationalHandler(w http.ResponseWriter, r *http.Request) {
236 data := disconnectData{}195 data := disconnectData{}
237 execTemplate(w, operationalTemplatePath, data)196 execTemplate(w, operationalTemplatePath, data)
238}197}
@@ -242,8 +201,8 @@ type hashResponse struct {
242 HashMatch bool201 HashMatch bool
243}202}
244203
245// HashItHandler returns a hash of the password as json204// hashItHandler returns a hash of the password as json
246func HashItHandler(w http.ResponseWriter, r *http.Request) {205func hashItHandler(w http.ResponseWriter, r *http.Request) {
247 r.ParseForm()206 r.ParseForm()
248 hashMe := r.Form["Hash"]207 hashMe := r.Form["Hash"]
249 hashed, errH := utils.MatchingHash(hashMe[0])208 hashed, errH := utils.MatchingHash(hashMe[0])
@@ -262,52 +221,31 @@ func HashItHandler(w http.ResponseWriter, r *http.Request) {
262 w.Write(b)221 w.Write(b)
263}222}
264223
224// HashItHandler hashes the password
225func (os *OperationalServer) HashItHandler(w http.ResponseWriter, r *http.Request) {
226 hashItHandler(w, r)
227}
228
229// HashItHandler hashes the password
230func (ms *ManagementServer) HashItHandler(w http.ResponseWriter, r *http.Request) {
231 hashItHandler(w, r)
232}
233
265// DisconnectHandler allows user to disconnect from external AP234// DisconnectHandler allows user to disconnect from external AP
266func DisconnectHandler(w http.ResponseWriter, r *http.Request) {235func (os *OperationalServer) DisconnectHandler(w http.ResponseWriter, r *http.Request) {
267 netmanClient.DisconnectWifi(netmanClient.GetWifiDevices(netmanClient.GetDevices()))236 os.chUserEvs <- UserEvent{EvType: UserEventDisconnect}
268}237}
269238
270// RefreshHandler handles ssids refreshment239// RefreshHandler handles ssids refreshment
271func RefreshHandler(w http.ResponseWriter, r *http.Request) {240func (ms *ManagementServer) RefreshHandler(w http.ResponseWriter, r *http.Request) {
272241
273 // show same page. After refresh operation, management page should show a refresh alert242 // show same page. After refresh operation, management page should show a refresh alert
274 ManagementHandler(w, r)243 ms.ManagementHandler(w, r)
275
276 go func() {
277 if err := netmanClient.Unmanage(); err != nil {
278 fmt.Println(err)
279 return
280 }
281
282 apUp, err := wifiapClient.Enabled()
283 if err != nil {
284 fmt.Println(Sprintf("An error happened while requesting current AP status: %v\n", err))
285 return
286 }
287
288 if apUp {
289 err := wifiapClient.Disable()
290 if err != nil {
291 fmt.Println(Sprintf("An error happened while bringing AP down: %v\n", err))
292 return
293 }
294 }
295244
296 for found := netmanClient.ScanAndWriteSsidsToFile(utils.SsidsFile); !found; found = netmanClient.ScanAndWriteSsidsToFile(utils.SsidsFile) {245 ms.chUserEvs <- UserEvent{EvType: UserEventRefresh}
297 time.Sleep(5 * time.Second)246}
298 }
299
300 if err := netmanClient.Unmanage(); err != nil {
301 fmt.Println(err)
302 return
303 }
304
305 err = wifiapClient.Enable()
306 if err != nil {
307 fmt.Println(Sprintf("An error happened while bringing AP up: %v\n", err))
308 return
309 }
310247
311 fmt.Println("== wifi-connect/RefreshHandler: starting wifi-ap")248// RedirectHandler redirects to our main URL
312 }()249func (ms *ManagementServer) RedirectHandler(w http.ResponseWriter, r *http.Request) {
250 http.Redirect(w, r, "http://device-wifi-connect.com", http.StatusFound)
313}251}
diff --git a/server/handlers_test.go b/server/handlers_test.go
index f4278b7..27a81e3 100644
--- a/server/handlers_test.go
+++ b/server/handlers_test.go
@@ -26,124 +26,19 @@ import (
26 "strings"26 "strings"
27 "testing"27 "testing"
2828
29 "launchpad.net/wifi-connect/netman"
30 "launchpad.net/wifi-connect/utils"29 "launchpad.net/wifi-connect/utils"
31)30)
3231
33type wifiapClientMock struct{}
34
35func (c *wifiapClientMock) Show() (map[string]interface{}, error) {
36 return nil, nil
37}
38
39func (c *wifiapClientMock) Enable() error {
40 return nil
41}
42
43func (c *wifiapClientMock) Disable() error {
44 return nil
45}
46
47func (c *wifiapClientMock) Enabled() (bool, error) {
48 return true, nil
49}
50
51func (c *wifiapClientMock) SetSsid(string) error {
52 return nil
53}
54
55func (c *wifiapClientMock) SetPassphrase(string) error {
56 return nil
57}
58
59func (c *wifiapClientMock) Set(map[string]interface{}) error {
60 return nil
61}
62
63type netmanClientMock struct{}
64
65func (c *netmanClientMock) GetDevices() []string {
66 return []string{"/d/1"}
67}
68
69func (c *netmanClientMock) GetWifiDevices(devices []string) []string {
70 return []string{"/d/1"}
71}
72
73func (c *netmanClientMock) GetAccessPoints(devices []string, ap2device map[string]string) []string {
74 return []string{"/ap/1"}
75}
76
77func (c *netmanClientMock) ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error {
78 return nil
79}
80
81func (c *netmanClientMock) Ssids() ([]netman.SSID, map[string]string, map[string]string) {
82 myssid := netman.SSID{Ssid: "myssid", ApPath: "/ap/1"}
83 return []netman.SSID{myssid}, map[string]string{"/ap/1": "/d/1"}, map[string]string{"myssid": "/ap/1"}
84}
85
86func (c *netmanClientMock) Connected(devices []string) bool {
87 return false
88}
89
90func (c *netmanClientMock) ConnectedWifi(wifiDevices []string) bool {
91 return false
92}
93
94func (c *netmanClientMock) DisconnectWifi(wifiDevices []string) int {
95 return 0
96}
97
98func (c *netmanClientMock) SetIfaceManaged(iface string, state bool, devices []string) string {
99 return "wlan0"
100}
101
102func (c *netmanClientMock) WifisManaged(wifiDevices []string) (map[string]string, error) {
103 return map[string]string{"wlan0": "/d/1"}, nil
104}
105func (c *netmanClientMock) Unmanage() error {
106 return nil
107}
108func (c *netmanClientMock) Manage() error {
109 return nil
110}
111
112func (c *netmanClientMock) ScanAndWriteSsidsToFile(filepath string) bool {
113 return true
114}
115
116func (c *netmanClientMock) DeleteWifiConnections() {}
117
118func TestManagementHandler(t *testing.T) {32func TestManagementHandler(t *testing.T) {
11933 chUserEvs := make(chan UserEvent)
120 // mock settings to not to ask for saving a first configuration34 ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{},
121 utils.ReadConfig = func() (*utils.Config, error) {35 ssidLs: &utils.SsidList{}}
122 return &utils.Config{
123 Wifi: &utils.WifiConfig{
124 Ssid: "Ubuntu",
125 Passphrase: "17Soj8/Sxh14lcpD",
126 Interface: "wlp2s0",
127 CountryCode: "0x31",
128 Channel: 6,
129 OperationMode: "g",
130 },
131 Portal: &utils.PortalConfig{
132 Password: "the_password",
133 NoResetCredentials: true,
134 NoOperational: false,
135 },
136 }, nil
137 }
138 utils.MustSetConfig = func() bool { return false }
13936
140 ResourcesPath = "../static"37 ResourcesPath = "../static"
141 SsidsFile := "../static/tests/ssids"
142 utils.SetSsidsFile(SsidsFile)
14338
144 w := httptest.NewRecorder()39 w := httptest.NewRecorder()
145 r, _ := http.NewRequest("GET", "/", nil)40 r, _ := http.NewRequest("GET", "/", nil)
146 http.HandlerFunc(ManagementHandler).ServeHTTP(w, r)41 http.HandlerFunc(ms.ManagementHandler).ServeHTTP(w, r)
14742
148 if w.Code != http.StatusOK {43 if w.Code != http.StatusOK {
149 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)44 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)
@@ -155,9 +50,9 @@ func TestManagementHandler(t *testing.T) {
155}50}
15651
157func TestConnectHandler(t *testing.T) {52func TestConnectHandler(t *testing.T) {
15853 chUserEvs := make(chan UserEvent)
159 wifiapClient = &wifiapClientMock{}54 ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{},
160 netmanClient = &netmanClientMock{}55 ssidLs: &utils.SsidList{}}
16156
162 ResourcesPath = "../static"57 ResourcesPath = "../static"
16358
@@ -169,7 +64,21 @@ func TestConnectHandler(t *testing.T) {
169 r, _ := http.NewRequest("POST", "/connect", bytes.NewBufferString(form.Encode()))64 r, _ := http.NewRequest("POST", "/connect", bytes.NewBufferString(form.Encode()))
170 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")65 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
171 r.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))66 r.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
172 http.HandlerFunc(ConnectHandler).ServeHTTP(w, r)67
68 go func() {
69 uEv := <-chUserEvs
70 if uEv.EvType != UserEventConnect {
71 t.Errorf("Bad event %v", uEv.EvType)
72 }
73 if uEv.Params[UeConnSsid] != "myssid" {
74 t.Errorf("Bad ssid: %s", uEv.Params[UeConnSsid])
75 }
76 if uEv.Params[UeConnPassword] != "mypassphrase" {
77 t.Errorf("Bad password: %s", uEv.Params[UeConnPassword])
78 }
79 }()
80
81 http.HandlerFunc(ms.ConnectHandler).ServeHTTP(w, r)
17382
174 if w.Code != http.StatusOK {83 if w.Code != http.StatusOK {
175 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)84 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)
@@ -177,14 +86,21 @@ func TestConnectHandler(t *testing.T) {
177}86}
17887
179func TestDisconnectHandler(t *testing.T) {88func TestDisconnectHandler(t *testing.T) {
18089 chUserEvs := make(chan UserEvent)
181 netmanClient = &netmanClientMock{}90 os := &OperationalServer{chUserEvs: chUserEvs}
182
183 ResourcesPath = "../static"91 ResourcesPath = "../static"
18492
185 w := httptest.NewRecorder()93 w := httptest.NewRecorder()
186 r, _ := http.NewRequest("GET", "/disconnect", nil)94 r, _ := http.NewRequest("GET", "/disconnect", nil)
187 http.HandlerFunc(DisconnectHandler).ServeHTTP(w, r)95
96 go func() {
97 uEv := <-chUserEvs
98 if uEv.EvType != UserEventDisconnect {
99 t.Errorf("Bad event %v", uEv.EvType)
100 }
101 }()
102
103 http.HandlerFunc(os.DisconnectHandler).ServeHTTP(w, r)
188104
189 if w.Code != http.StatusOK {105 if w.Code != http.StatusOK {
190 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)106 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)
@@ -192,17 +108,22 @@ func TestDisconnectHandler(t *testing.T) {
192}108}
193109
194func TestRefreshHandler(t *testing.T) {110func TestRefreshHandler(t *testing.T) {
195111 chUserEvs := make(chan UserEvent)
196 wifiapClient = &wifiapClientMock{}112 ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{},
197 netmanClient = &netmanClientMock{}113 ssidLs: &utils.SsidList{}}
198
199 ResourcesPath = "../static"114 ResourcesPath = "../static"
200 SsidsFile := "../static/tests/ssids"
201 utils.SetSsidsFile(SsidsFile)
202115
203 w := httptest.NewRecorder()116 w := httptest.NewRecorder()
204 r, _ := http.NewRequest("GET", "/refresh", nil)117 r, _ := http.NewRequest("GET", "/refresh", nil)
205 http.HandlerFunc(RefreshHandler).ServeHTTP(w, r)118
119 go func() {
120 uEv := <-chUserEvs
121 if uEv.EvType != UserEventRefresh {
122 t.Errorf("Bad event %v", uEv.EvType)
123 }
124 }()
125
126 http.HandlerFunc(ms.RefreshHandler).ServeHTTP(w, r)
206127
207 if w.Code != http.StatusOK {128 if w.Code != http.StatusOK {
208 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)129 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)
@@ -214,11 +135,13 @@ func TestRefreshHandler(t *testing.T) {
214}135}
215136
216func TestOperationalHandler(t *testing.T) {137func TestOperationalHandler(t *testing.T) {
138 chUserEvs := make(chan UserEvent)
139 os := &OperationalServer{chUserEvs: chUserEvs}
217 ResourcesPath = "../static"140 ResourcesPath = "../static"
218141
219 w := httptest.NewRecorder()142 w := httptest.NewRecorder()
220 r, _ := http.NewRequest("GET", "/", nil)143 r, _ := http.NewRequest("GET", "/", nil)
221 http.HandlerFunc(OperationalHandler).ServeHTTP(w, r)144 http.HandlerFunc(os.OperationalHandler).ServeHTTP(w, r)
222145
223 if w.Code != http.StatusOK {146 if w.Code != http.StatusOK {
224 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)147 t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code)
@@ -230,83 +153,17 @@ func TestOperationalHandler(t *testing.T) {
230}153}
231154
232func TestInvalidTemplateHandler(t *testing.T) {155func TestInvalidTemplateHandler(t *testing.T) {
156 chUserEvs := make(chan UserEvent)
157 ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{},
158 ssidLs: &utils.SsidList{}}
233159
234 ResourcesPath = "/invalidpath"160 ResourcesPath = "/invalidpath"
235161
236 w := httptest.NewRecorder()162 w := httptest.NewRecorder()
237 r, _ := http.NewRequest("GET", "/", nil)163 r, _ := http.NewRequest("GET", "/", nil)
238 http.HandlerFunc(ManagementHandler).ServeHTTP(w, r)164 http.HandlerFunc(ms.ManagementHandler).ServeHTTP(w, r)
239165
240 if w.Code != http.StatusInternalServerError {166 if w.Code != http.StatusInternalServerError {
241 t.Errorf("Expected status %d, got: %d", http.StatusInternalServerError, w.Code)167 t.Errorf("Expected status %d, got: %d", http.StatusInternalServerError, w.Code)
242 }168 }
243}169}
244
245func TestReadSsidsFile(t *testing.T) {
246
247 SsidsFile := "../static/tests/ssids"
248 utils.SetSsidsFile(SsidsFile)
249 ssids, err := utils.ReadSsidsFile()
250 if err != nil {
251 t.Errorf("Unexpected error reading ssids file: %v", err)
252 }
253
254 if len(ssids) != 4 {
255 t.Error("Expected 4 elements in csv record")
256 }
257
258 set := make(map[string]bool)
259 for _, v := range ssids {
260 set[v] = true
261 }
262
263 if !set["mynetwork"] {
264 t.Error("mynetwork value not found")
265 }
266 if !set["yournetwork"] {
267 t.Error("yournetwork value not found")
268 }
269 if !set["hernetwork"] {
270 t.Error("hernetwork value not found")
271 }
272 if !set["hisnetwork"] {
273 t.Error("hisnetwork value not found")
274 }
275}
276
277func TestReadSsidsFileWithOnlyOne(t *testing.T) {
278
279 SsidsFile := "../static/tests/ssids_onlyonessid"
280 utils.SetSsidsFile(SsidsFile)
281 ssids, err := utils.ReadSsidsFile()
282 if err != nil {
283 t.Errorf("Unexpected error reading ssids file: %v", err)
284 }
285
286 if len(ssids) != 1 {
287 t.Error("Expected 1 elements in csv record")
288 }
289
290 set := make(map[string]bool)
291 for _, v := range ssids {
292 set[v] = true
293 }
294
295 if !set["mynetwork"] {
296 t.Error("mynetwork value not found")
297 }
298}
299
300func TestReadEmptySsidsFile(t *testing.T) {
301
302 SsidsFile := "../static/tests/ssids_empty"
303 utils.SetSsidsFile(SsidsFile)
304 ssids, err := utils.ReadSsidsFile()
305 if err != nil {
306 t.Errorf("Unexpected error reading ssids file: %v", err)
307 }
308
309 if len(ssids) != 0 {
310 t.Error("Expected 0 elements in csv record")
311 }
312}
diff --git a/server/launcher.go b/server/launcher.go
index 7317df0..3615781 100644
--- a/server/launcher.go
+++ b/server/launcher.go
@@ -19,118 +19,50 @@ package server
1919
20import (20import (
21 "log"21 "log"
22 "net"
23 "net/http"22 "net/http"
24 "time"23 "strconv"
25
26 "launchpad.net/wifi-connect/utils"
27)24)
2825
26// RunningState enum defining which server is up and running
27type RunningState int
28
29// Server running state29// Server running state
30const (30const (
31 Stopped RunningState = 0 + iota31 Stopped RunningState = iota
32 Starting
33 Running32 Running
34 Stopping
35)33)
3634
37const (
38 // TestingAddress listening point for testing servers
39 TestingAddress = ":8081"
40)
41
42// RunningState enum defining which server is up and running
43type RunningState int
44
45// Address where server is listening
46var Address = ":8080"
47
48// State holds current server state35// State holds current server state
49var State = Stopped36var State = Stopped
5037
51var listener net.Listener38var server *http.Server
52var done chan bool39var done chan bool
5340
54type tcpKeepAliveListener struct {41func listenAndServe(port int, handler http.Handler) error {
55 *net.TCPListener
56}
57
58// WaitForState waits for server reach certain state
59func WaitForState(state RunningState) bool {
60 retries := 10
61 idle := 10 * time.Millisecond
62 for ; retries > 0 && State != state; retries-- {
63 time.Sleep(idle)
64 idle *= 2
65 }
66 return State == state
67}
68
69// Accept accepts incoming tcp connections
70func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
71 tc, err := ln.AcceptTCP()
72 if err != nil {
73 return tc, err
74 }
75 tc.SetKeepAlive(true)
76 tc.SetKeepAlivePeriod(3 * time.Minute)
77 return tc, nil
78}
79
80func listenAndServe(addr string, handler http.Handler) error {
8142
82 if State != Stopped {43 if State != Stopped {
83 return Errorf("Server is not in proper stopped state before trying to start it")44 return Errorf("Server is not in stopped state before trying to start it")
84 }
85
86 if utils.RunningOn(addr) {
87 return Errorf("Another instance is running in same address %v", addr)
88 }45 }
8946
90 State = Starting47 server = &http.Server{Addr: ":" + strconv.Itoa(port), Handler: handler}
9148 // channel needed to communicate real server shutdown, as after calling
92 srv := &http.Server{Addr: addr, Handler: handler}49 // server.Close() it can take several milliseconds to really stop the
93 // channel needed to communicate real server shutdown, as after calling listener.Close()50 // listening.
94 // it can take several milliseconds to really stop the listening.
95 done = make(chan bool)51 done = make(chan bool)
9652
97 var err error
98 listener, err = net.Listen("tcp", addr)
99 if err != nil {
100 return err
101 }
102
103 // launch goroutine to check server state changes after startup is triggered
104 go func() {
105 retries := 10
106 idle := 10 * time.Millisecond
107 for ; !utils.RunningOn(addr) && retries > 0; retries-- {
108 time.Sleep(idle)
109 idle *= 2
110 }
111
112 if retries == 0 {
113 log.Print("Server could not be started")
114 return
115 }
116
117 State = Running
118 }()
119
120 // launching server in a goroutine for not blocking53 // launching server in a goroutine for not blocking
121 go func() {54 go func(server *http.Server) {
122 if listener != nil {55 err := server.ListenAndServe()
123 err := srv.Serve(tcpKeepAliveListener{listener.(*net.TCPListener)})56 if err != nil {
124 if err != nil {57 log.Printf("HTTP Server closing - %v", err)
125 log.Printf("HTTP Server closing - %v", err)
126 }
127 // notify server real stop
128 done <- true
129 }58 }
59 // notify server real stop
60 done <- true
13061
131 close(done)62 close(done)
132 }()63 }(server)
13364
65 State = Running
134 return nil66 return nil
135}67}
13668
@@ -140,18 +72,16 @@ func stop() error {
140 return Errorf("Already stopped")72 return Errorf("Already stopped")
141 }73 }
14274
143 if listener == nil {75 if server == nil {
144 State = Stopped76 State = Stopped
145 return Errorf("Already closed")77 return Errorf("Already closed")
146 }78 }
14779
148 State = Stopping80 err := server.Close()
149
150 err := listener.Close()
151 if err != nil {81 if err != nil {
152 return err82 return err
153 }83 }
154 listener = nil84 server = nil
15585
156 // wait for server real shutdown confirmation86 // wait for server real shutdown confirmation
157 <-done87 <-done
diff --git a/server/launcher_test.go b/server/launcher_test.go
index c377c79..eb252d3 100644
--- a/server/launcher_test.go
+++ b/server/launcher_test.go
@@ -19,77 +19,24 @@ package server
1919
20import (20import (
21 "testing"21 "testing"
22 "time"
2223
23 telnet "github.com/reiver/go-telnet"24 "launchpad.net/wifi-connect/utils"
24)25)
2526
26func TestLaunchAndStop(t *testing.T) {27func TestLaunchAndStop(t *testing.T) {
28 thePort := 8444
2729
28 thePort := ":14444"30 chUserEvs := make(chan UserEvent)
2931 err := listenAndServe(thePort, managementHandler(chUserEvs,
30 err := listenAndServe(thePort, nil)32 &utils.Config{}, &utils.SsidList{}))
31 if err != nil {
32 t.Errorf("Start server failed: %v", err)
33 }
34
35 // telnet to check server is alive
36 caller := telnet.StandardCaller
37 err = telnet.DialToAndCall("localhost"+thePort, caller)
38 if err != nil {
39 t.Errorf("Failed to telnet localhost server at port %v: %v", thePort, err)
40 }
41
42 err = stop()
43 if err != nil {
44 t.Errorf("Stop server error: %v", err)
45 }
46}
47
48// This test is being skipped because of a problem with jenkins where it fails.
49// We cannot reproduce the failure outside of jenkins. Additionally, we are
50// changing the http server and will not track state in the same way in the future.
51// These tests will change.
52func SkipTestStates(t *testing.T) {
53
54 WaitForState(Stopped)
55
56 if State != Stopped {
57 t.Error("Not in initial state")
58 }
59
60 thePort := ":14444"
61
62 err := listenAndServe(thePort, nil)
63 if err != nil {33 if err != nil {
64 t.Errorf("Start server failed: %v", err)34 t.Errorf("Start server failed: %v", err)
65 }35 }
6636
67 if State != Starting && State != Running {37 time.Sleep(1 * time.Second)
68 t.Error("Not in proper start(ing) state")
69 }
70
71 WaitForState(Running)
72
73 // try a bad transition
74 err = listenAndServe(thePort, nil)
75 if err == nil {
76 t.Error("An error should be thrown when trying to start an already running instance")
77 }
78
79 err = stop()38 err = stop()
80 if err != nil {39 if err != nil {
81 t.Errorf("Stop server error: %v", err)40 t.Errorf("Stop server error: %v", err)
82 }41 }
83
84 if State != Stopping && State != Stopped {
85 t.Error("Not in proper stop(ing) state")
86 }
87
88 WaitForState(Stopped)
89
90 // try bad transitions
91 err = stop()
92 if err == nil {
93 t.Error("An error should be thrown when trying to stop a stopped instance")
94 }
95}42}
diff --git a/server/manager.go b/server/manager.go
index 6b11c23..c668974 100644
--- a/server/manager.go
+++ b/server/manager.go
@@ -18,8 +18,6 @@
18package server18package server
1919
20import (20import (
21 "os"
22
23 "launchpad.net/wifi-connect/utils"21 "launchpad.net/wifi-connect/utils"
24)22)
2523
@@ -37,23 +35,20 @@ type Server int
37var Current = None35var Current = None
3836
39// StartManagementServer starts server in management mode37// StartManagementServer starts server in management mode
40func StartManagementServer() error {38func StartManagementServer(chUserEvs chan<- UserEvent, config *utils.Config,
39 ssidLs *utils.SsidList) error {
40
41 if Current != None {41 if Current != None {
42 Current = None42 Current = None
43 return Errorf("Not in a valid status. Please stop first any other server instance before starting this one")43 return Errorf("Invalid state: other server is running")
44 }44 }
4545
46 // change current instance asap we manage this server46 // change current instance asap we manage this server
47 Current = Management47 Current = Management
4848
49 waitPath := os.Getenv("SNAP_COMMON") + "/startingApConnect"49 // Pass around program config to handlers
50 err := utils.WriteFlagFile(waitPath)50 err := listenAndServe(config.Portal.Port,
51 if err != nil {51 managementHandler(chUserEvs, config, ssidLs))
52 Current = None
53 return err
54 }
55
56 err = listenAndServe(Address, managementHandler())
57 if err != nil {52 if err != nil {
58 Current = None53 Current = None
59 return err54 return err
@@ -63,15 +58,15 @@ func StartManagementServer() error {
63}58}
6459
65// StartOperationalServer starts server in operational mode60// StartOperationalServer starts server in operational mode
66func StartOperationalServer() error {61func StartOperationalServer(chUserEvs chan<- UserEvent, config *utils.Config) error {
67 if Current != None {62 if Current != None {
68 return Errorf("Not in a valid status. Please stop first any other server instance before starting this one")63 return Errorf("Invalid state: other server is running")
69 }64 }
7065
71 // change current instance asap we manage this server66 // change current instance asap we manage this server
72 Current = Operational67 Current = Operational
7368
74 err := listenAndServe(Address, operationalHandler())69 err := listenAndServe(config.Portal.Port, operationalHandler(chUserEvs))
75 if err != nil {70 if err != nil {
76 Current = None71 Current = None
77 return err72 return err
@@ -80,9 +75,10 @@ func StartOperationalServer() error {
80 return nil75 return nil
81}76}
8277
83// ShutdownManagementServer shutdown server management mode. If management server is not up, returns error78// ShutdownManagementServer shutdown server management mode. If management
79// server is not up, returns error
84func ShutdownManagementServer() error {80func ShutdownManagementServer() error {
85 if Current != Management || (State != Running && State != Starting) {81 if Current != Management || State != Running {
86 return Errorf("Trying to stop management server when it is not running")82 return Errorf("Trying to stop management server when it is not running")
87 }83 }
8884
@@ -95,9 +91,10 @@ func ShutdownManagementServer() error {
95 return nil91 return nil
96}92}
9793
98// ShutdownOperationalServer shutdown server operational mode. If operational server is not up, returns error94// ShutdownOperationalServer shutdown server operational mode. If operational
95// server is not up, returns error
99func ShutdownOperationalServer() error {96func ShutdownOperationalServer() error {
100 if Current != Operational || (State != Running && State != Starting) {97 if Current != Operational || State != Running {
101 return Errorf("Trying to stop operational server when it is not running")98 return Errorf("Trying to stop operational server when it is not running")
102 }99 }
103100
diff --git a/server/manager_test.go b/server/manager_test.go
index ea0661e..dbb38ae 100644
--- a/server/manager_test.go
+++ b/server/manager_test.go
@@ -1,29 +1,28 @@
1package server1package server
22
3import (3import (
4 "os"
5 "testing"4 "testing"
5 "time"
6
7 "launchpad.net/wifi-connect/utils"
6)8)
79
8func TestBasicServerTransitionStates(t *testing.T) {10func TestBasicServerTransitionStates(t *testing.T) {
911
10 Address = ":14444"
11
12 os.Setenv("SNAP_COMMON", os.TempDir())
13
14 if Current != None || State != Stopped {12 if Current != None || State != Stopped {
15 t.Errorf("Server is not in initial state")13 t.Errorf("Server is not in initial state")
16 }14 }
1715
18 if err := StartManagementServer(); err != nil {16 chUserEvs := make(chan UserEvent)
17 cfg := utils.Config{Portal: utils.PortalConfig{Port: 14444}}
18 if err := StartManagementServer(chUserEvs, &cfg, &utils.SsidList{}); err != nil {
19 t.Errorf("Error starting management server %v", err)19 t.Errorf("Error starting management server %v", err)
20 }20 }
21 if Current != Management || (State != Starting && State != Running) {21 if Current != Management || State != Running {
22 t.Errorf("Server is not in starting or in management status")22 t.Errorf("Server is not in starting or in management status")
23 }23 }
2424
25 WaitForState(Running)25 time.Sleep(1 * time.Second)
26
27 if err := ShutdownManagementServer(); err != nil {26 if err := ShutdownManagementServer(); err != nil {
28 t.Errorf("Error stopping management server %v", err)27 t.Errorf("Error stopping management server %v", err)
29 }28 }
@@ -32,17 +31,14 @@ func TestBasicServerTransitionStates(t *testing.T) {
32 t.Errorf("Current server is not None")31 t.Errorf("Current server is not None")
33 }32 }
3433
35 WaitForState(Stopped)34 if err := StartOperationalServer(chUserEvs, &cfg); err != nil {
36
37 if err := StartOperationalServer(); err != nil {
38 t.Errorf("Error starting operational server %v", err)35 t.Errorf("Error starting operational server %v", err)
39 }36 }
40 if Current != Operational || (State != Starting && State != Running) {37 if Current != Operational || State != Running {
41 t.Errorf("Server is not in starting or in operational status")38 t.Errorf("Server is not in starting or in operational status")
42 }39 }
4340
44 WaitForState(Running)41 time.Sleep(1 * time.Second)
45
46 if err := ShutdownOperationalServer(); err != nil {42 if err := ShutdownOperationalServer(); err != nil {
47 t.Errorf("Error stopping operational server %v", err)43 t.Errorf("Error stopping operational server %v", err)
48 }44 }
@@ -52,31 +48,26 @@ func TestBasicServerTransitionStates(t *testing.T) {
52}48}
5349
54func TestEdgeServerTransitionStates(t *testing.T) {50func TestEdgeServerTransitionStates(t *testing.T) {
55
56 Address = ":14444"
57
58 os.Setenv("SNAP_COMMON", os.TempDir())
59
60 if Current != None {51 if Current != None {
61 t.Errorf("Server is not in initial state")52 t.Errorf("Server is not in initial state")
62 }53 }
6354
64 if err := StartManagementServer(); err != nil {55 chUserEvs := make(chan UserEvent)
56 cfg := utils.Config{Portal: utils.PortalConfig{Port: 14444}}
57 if err := StartManagementServer(chUserEvs, &cfg, &utils.SsidList{}); err != nil {
65 t.Errorf("Error starting management server %v", err)58 t.Errorf("Error starting management server %v", err)
66 }59 }
67 if Current != Management || (State != Starting && State != Running) {60 if Current != Management || State != Running {
68 t.Errorf("Server is not in starting or in management status")61 t.Errorf("Server is not in starting or in management status")
69 }62 }
7063
71 WaitForState(Running)
72
73 // start operational server without stopping management must throw an error64 // start operational server without stopping management must throw an error
74 if err := StartOperationalServer; err == nil {65 if err := StartOperationalServer; err == nil {
75 t.Errorf(`Expected an error when trying to launch one server instance having 66 t.Errorf("No error when trying to launch one server instance having " +
76 the other active`)67 "the other active")
77 }68 }
78 if Current != Management {69 if Current != Management {
79 t.Errorf("Server is not in management status after failed start operational server")70 t.Errorf("Not in management status after failed start operational server")
80 }71 }
8172
82 // stop wrong server must throw an error73 // stop wrong server must throw an error
@@ -84,9 +75,10 @@ func TestEdgeServerTransitionStates(t *testing.T) {
84 t.Errorf("Expected an error when trying to shutdown wrong server")75 t.Errorf("Expected an error when trying to shutdown wrong server")
85 }76 }
86 if Current != Management {77 if Current != Management {
87 t.Errorf("Server is not in management status after failed start operational server")78 t.Errorf("Not in management status after failed start operational server")
88 }79 }
8980
81 time.Sleep(1 * time.Second)
90 if err := ShutdownManagementServer(); err != nil {82 if err := ShutdownManagementServer(); err != nil {
91 t.Errorf("Error stopping management server %v", err)83 t.Errorf("Error stopping management server %v", err)
92 }84 }
@@ -94,25 +86,21 @@ func TestEdgeServerTransitionStates(t *testing.T) {
94 t.Errorf("Server is not in None status")86 t.Errorf("Server is not in None status")
95 }87 }
9688
97 WaitForState(Stopped)
98
99 // analog tests with operational server89 // analog tests with operational server
100 if err := StartOperationalServer(); err != nil {90 if err := StartOperationalServer(chUserEvs, &cfg); err != nil {
101 t.Errorf("Error starting operational server %v", err)91 t.Errorf("Error starting operational server %v", err)
102 }92 }
103 if Current != Operational || (State != Starting && State != Running) {93 if Current != Operational || State != Running {
104 t.Errorf("Server is not in starting or in operational status")94 t.Errorf("Server is not in starting or in operational status")
105 }95 }
10696
107 WaitForState(Running)
108
109 // start management server without stopping operational must throw an error97 // start management server without stopping operational must throw an error
110 if err := StartManagementServer; err == nil {98 if err := StartManagementServer; err == nil {
111 t.Errorf(`Expected an error when trying to launch one server instance having 99 t.Errorf("Expected an error when trying to launch one server " +
112 the other active`)100 "instance having the other active")
113 }101 }
114 if Current != Operational {102 if Current != Operational {
115 t.Errorf("Server is not in operational status after failed start operational server")103 t.Errorf("Not in operational status after failed start operational server")
116 }104 }
117105
118 // stop wrong server must throw an error106 // stop wrong server must throw an error
@@ -120,9 +108,10 @@ func TestEdgeServerTransitionStates(t *testing.T) {
120 t.Errorf("Expected an error when trying to shutdown wrong server")108 t.Errorf("Expected an error when trying to shutdown wrong server")
121 }109 }
122 if Current != Operational {110 if Current != Operational {
123 t.Errorf("Server is not in operational status after failed start operational server")111 t.Errorf("Not in operational status after failed start operational server")
124 }112 }
125113
114 time.Sleep(1 * time.Second)
126 if err := ShutdownOperationalServer(); err != nil {115 if err := ShutdownOperationalServer(); err != nil {
127 t.Errorf("Error stopping operational server %v", err)116 t.Errorf("Error stopping operational server %v", err)
128 }117 }
diff --git a/server/router.go b/server/router.go
index 20a3716..3af467e 100644
--- a/server/router.go
+++ b/server/router.go
@@ -21,18 +21,31 @@ import (
21 "net/http"21 "net/http"
2222
23 "github.com/gorilla/mux"23 "github.com/gorilla/mux"
24 "launchpad.net/wifi-connect/utils"
24)25)
2526
26// managementHandler handles requests for web UI when AP is up27// managementHandler handles requests for web UI when AP is up
27func managementHandler() *mux.Router {28func managementHandler(chUserEvs chan<- UserEvent, config *utils.Config,
29 ssidLs *utils.SsidList) *mux.Router {
30
28 router := mux.NewRouter()31 router := mux.NewRouter()
32 ms := &ManagementServer{chUserEvs: chUserEvs, config: *config, ssidLs: ssidLs}
2933
30 // Pages routes34 // Pages routes
31 router.Handle("/", Middleware(http.HandlerFunc(ManagementHandler))).Methods("GET")35 router.HandleFunc("/", ms.ManagementHandler).Methods("GET")
32 router.Handle("/config", Middleware(http.HandlerFunc(SaveConfigHandler))).Methods("POST")36 router.HandleFunc("/config", ms.SaveConfigHandler).Methods("POST")
33 router.Handle("/connect", Middleware(http.HandlerFunc(ConnectHandler))).Methods("POST")37 router.HandleFunc("/connect", ms.ConnectHandler).Methods("POST")
34 router.HandleFunc("/hashit", HashItHandler).Methods("POST")38 router.HandleFunc("/hashit", ms.HashItHandler).Methods("POST")
35 router.Handle("/refresh", Middleware(http.HandlerFunc(RefreshHandler))).Methods("GET")39 router.HandleFunc("/refresh", ms.RefreshHandler).Methods("GET")
40 // To allow detection of captive portal
41 // Android/ChromeOS
42 router.HandleFunc("/generate_204", ms.RedirectHandler).Methods("GET")
43 // iOS/Mac
44 router.HandleFunc("/hotspot-detect.html", ms.RedirectHandler).Methods("GET")
45 router.HandleFunc("/library/test/success.html", ms.RedirectHandler).Methods("GET")
46 // Windows
47 router.HandleFunc("/connecttest.txt", ms.RedirectHandler).Methods("GET")
48 router.HandleFunc("/redirect", ms.RedirectHandler).Methods("GET")
3649
37 // Resources path50 // Resources path
38 fs := http.StripPrefix("/static/", http.FileServer(http.Dir(ResourcesPath)))51 fs := http.StripPrefix("/static/", http.FileServer(http.Dir(ResourcesPath)))
@@ -42,15 +55,17 @@ func managementHandler() *mux.Router {
42}55}
4356
44// operationalHandler handles request for web UI when connected to external Wi-Fi57// operationalHandler handles request for web UI when connected to external Wi-Fi
45func operationalHandler() *mux.Router {58func operationalHandler(chUserEvs chan<- UserEvent) *mux.Router {
46 router := mux.NewRouter()59 router := mux.NewRouter()
60 os := &OperationalServer{chUserEvs: chUserEvs}
4761
48 router.HandleFunc("/", OperationalHandler).Methods("GET")62 router.HandleFunc("/", os.OperationalHandler).Methods("GET")
49 router.Handle("/disconnect", Middleware(http.HandlerFunc(DisconnectHandler))).Methods("GET")63 router.HandleFunc("/disconnect", os.DisconnectHandler).Methods("GET")
50 router.HandleFunc("/hashit", HashItHandler).Methods("POST")64 router.HandleFunc("/hashit", os.HashItHandler).Methods("POST")
5165
52 // Resources path66 // Resources path
53 fs := http.StripPrefix("/static/", http.FileServer(http.Dir(ResourcesPath)))67 fs := http.StripPrefix("/static/", http.FileServer(http.Dir(ResourcesPath)))
54 router.PathPrefix("/static/").Handler(fs)68 router.PathPrefix("/static/").Handler(fs)
69
55 return router70 return router
56}71}
diff --git a/service/service.go b/service/service.go
index 3ea136c..3d30bc8 100644
--- a/service/service.go
+++ b/service/service.go
@@ -18,157 +18,21 @@
18package main18package main
1919
20import (20import (
21 "fmt"
22 "log"21 "log"
23 "os"
24 "time"
2522
26 "launchpad.net/wifi-connect/daemon"23 "launchpad.net/wifi-connect/daemon"
27 "launchpad.net/wifi-connect/netman"
28 "launchpad.net/wifi-connect/utils"24 "launchpad.net/wifi-connect/utils"
29 "launchpad.net/wifi-connect/wifiap"
30)25)
3126
32var prevConnected bool
33
34func main() {27func main() {
3528 log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile)
36 log.SetFlags(log.Lshortfile)
37 log.SetPrefix("== wifi-connect: ")29 log.SetPrefix("== wifi-connect: ")
3830
39 c := netman.DefaultClient()31 config, err := utils.LoadConfig()
40 cw := wifiap.DefaultClient()
41 client := daemon.GetClient()
42
43 config, err := daemon.LoadPreConfig()
44 if err != nil {
45 log.Printf("Empty preconfiguration: %v", err)
46 }
47 err = client.SetDefaults(cw, config)
48 if err != nil {32 if err != nil {
49 log.Printf("SetDetaults error: %v", err)33 log.Printf("Empty configuration: %v", err)
50 }34 }
51 first := true
52 client.SetWaitFlagPath(os.Getenv("SNAP_COMMON") + "/startingApConnect")
53 client.SetManualFlagPath(os.Getenv("SNAP_COMMON") + "/manualMode")
54
55 client.ManagementServerDown()
56 client.OperationalServerDown()
57
58 connected := false
59 prevConnected = false
60
61 for {
62 if first {
63 log.Print("daemon STARTING")
64 client.SetPreviousState(daemon.STARTING)
65 client.SetState(daemon.STARTING)
66 first = false
67 //clean start require wifi AP down so we can get SSIDs
68 cw.Disable()
69 //remove previous State flags
70 utils.RemoveFlagFile(client.GetWaitFlagPath())
71 utils.RemoveFlagFile(client.GetManualFlagPath())
72 //TODO only wait if wlan0 is managed
73 //wait time period (TBD) on first run to allow wifi connections
74 time.Sleep(10000 * time.Millisecond)
75 }
76
77 // wait 5 seconds on each iter
78 time.Sleep(5000 * time.Millisecond)
7935
80 config, err = daemon.LoadPreConfig()36 service := daemon.GetService(config)
81 if err != nil {37 service.Start()
82 log.Printf("Empty preconfiguration: %v", err)
83 }
84 // loop without action if in manual mode
85 if client.ManualMode() {
86 continue
87 }
88
89 // start clean on exiting manual mode
90 if client.GetPreviousState() == daemon.MANUAL {
91 first = true
92 continue
93 }
94 // the AP should not be up without SSIDS
95 if client.IsApUpWithoutSSIDs(cw) {
96 cw.Disable()
97 continue
98 }
99
100 // log connected state if different
101 prevConnected = connected
102 connected = c.ConnectedWifi(c.GetWifiDevices(c.GetDevices()))
103 if first || prevConnected != connected {
104 if connected {
105 log.Print("WIFI CONNECTED")
106 } else {
107 log.Print("WIFI NOT CONNECTED")
108 }
109 }
110
111 // if an external wifi connection, we are in Operational mode
112 // and we stay here until there is an external wifi connection
113 if connected {
114 //log.Print("operational config:", config.Operational)
115 client.SetState(daemon.OPERATING)
116 if client.GetPreviousState() != daemon.OPERATING {
117 log.Print("entering OPERATIONAL mode")
118 client.ManagementServerDown()
119 if config.Operational {
120 //log.Print("about to put up oper server")
121 client.OperationalServerUp()
122 }
123 }
124 continue
125 }
126
127 // wait/loop until wait flag file is gone
128 // this stops daemon State changing until the management portal
129 // is done, either stopped or the user has attempted to connect to
130 // an external AP
131 if client.CheckWaitApConnect() {
132 continue
133 }
134
135 client.SetState(daemon.MANAGING)
136 if client.GetPreviousState() != daemon.MANAGING || !client.CheckWaitApConnect() {
137 // if wlan0 managed, set Unmanaged so that we can bring up wifi-ap
138 // properly if needed
139 if err := c.Unmanage(); err != nil {
140 log.Print(err)
141 continue
142 }
143
144 //wifi-ap UP?
145 wifiUp, err := cw.Enabled()
146 if err != nil {
147 log.Printf("Error checking wifi-ap.Enabled(): %v", err)
148 continue // try again since no better course of action
149 }
150
151 if !wifiUp {
152 log.Print("entering MANAGEMENT mode")
153 found := c.ScanAndWriteSsidsToFile(utils.SsidsFile)
154 if err := c.Unmanage(); err != nil {
155 fmt.Println(err)
156 continue
157 }
158 if !found {
159 log.Print("No SSIDs found. Continuing to scan for SSIDS...")
160 continue
161 }
162 log.Printf("starting wifi-ap")
163 if err := cw.Enable(); err != nil {
164 log.Print(err)
165 continue
166 }
167 //if client.GetPreviousState() == daemon.OPERATING {
168 client.OperationalServerDown()
169 //}
170 client.ManagementServerUp()
171 }
172 }
173 }
174}38}
diff --git a/service/service_test.go b/service/service_test.go
175new file mode 10064439new file mode 100644
index 0000000..795dc6a
--- /dev/null
+++ b/service/service_test.go
@@ -0,0 +1,26 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 */
17
18package main
19
20import (
21 "testing"
22)
23
24func TestService(t *testing.T) {
25 // TODO Fill it
26}
diff --git a/snapcraft.yaml b/snapcraft.yaml
index a2af4b2..b7e3be7 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -1,5 +1,5 @@
1name: wifi-connect 1name: wifi-connect
2version: 0.112version: '0.2'
3summary: Connect your device to external wifi over temp wifi AP3summary: Connect your device to external wifi over temp wifi AP
4description: |4description: |
5 A solution to enable your device to connect to an external5 A solution to enable your device to connect to an external
@@ -11,51 +11,73 @@ description: |
11 https://code.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect11 https://code.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect
12grade: stable12grade: stable
13confinement: strict13confinement: strict
14base: core18
1415
15apps:16apps:
16 wifi-connect:
17 command: cmd
18 plugs: [network, network-bind, network-manager, control]
19 daemon:17 daemon:
20 command: service 18 command: wificonnectd
21 daemon: simple19 daemon: simple
22 plugs: [network-manager, control, network-bind]20 plugs: [network-manager, network-bind, firewall-control]
2321
24hooks:22hooks:
25 configure:23 configure:
26 plugs: [network]24 plugs: [network]
27 25
28plugs:26# For dnsmasq, dir where leases are stored
29 control:27layout:
30 interface: content28 /var/lib/misc:
31 content: socket-directory29 bind: $SNAP_DATA
32 target: $SNAP_COMMON
33 default-provider: wifi-ap
3430
35parts:31parts:
36 go:32
37 plugin: go33 go-binaries:
38 source: . 34 plugin: dump
39 go-importpath: launchpad.net/wifi-connect35 source: .
40 build-packages:36 source-type: local
41 # needed by go get37 build-snaps: [ "go" ]
42 - bzr38 build-packages: [ "bzr", "git", "python", "build-essential" ]
43 install: |39 stage-packages: [ "dnsmasq" ]
40 override-build: |
41 set -ex
42 export GOPATH=$(mktemp -d)
43 (
44 src_path="$GOPATH"/src/launchpad.net/wifi-connect
45 mkdir -p "$src_path"
46 cp -a avahi daemon hooks netman server service utils static \
47 dependencies.tsv mdlint.py README.md run-checks "$src_path"
48 cd "$src_path"
49 go get launchpad.net/godeps
50 export PATH=$PATH:$GOPATH/bin
51 godeps -u dependencies.tsv
52 mkdir -p "$SNAPCRAFT_PART_INSTALL"/bin/
53 cd "$GOPATH"/src/launchpad.net/wifi-connect/
54 go build -o "$SNAPCRAFT_PART_INSTALL"/bin/wificonnectd service/service.go
55 # configure hook
56 mkdir -p "$SNAPCRAFT_PART_INSTALL"/snap/hooks
57 go build -o "$SNAPCRAFT_PART_INSTALL"/snap/hooks/configure hooks/configure.go
58 )
44 # set environment var SKIP_TESTS to 'y' or 'yes' if you want not to execute59 # set environment var SKIP_TESTS to 'y' or 'yes' if you want not to execute
45 # this part unit tests in your next compilation.60 # this part unit tests in your next compilation.
61 SKIP_TESTS=no
46 if [ "$SKIP_TESTS" = "yes" ] || [ "$SKIP_TESTS" = "y" ]; then62 if [ "$SKIP_TESTS" = "yes" ] || [ "$SKIP_TESTS" = "y" ]; then
47 echo "skipping unit tests"63 echo "skipping unit tests"
48 else 64 else
49 export GOPATH=$PWD/../go65 cd "$GOPATH"/src/launchpad.net/wifi-connect
50 cd $GOPATH/src/launchpad.net/wifi-connect66 ./run-checks all
51 ./run-checks all
52 fi67 fi
53 # configure hook68 cd
54 mkdir -p $SNAPCRAFT_PART_INSTALL/snap/hooks69 rm -rf "$GOPATH"
55 mv $SNAPCRAFT_PART_INSTALL/bin/hooks $SNAPCRAFT_PART_INSTALL/snap/hooks/configure70 stage:
71 - bin/wificonnectd
72 - snap/hooks/configure
73 - usr/sbin/dnsmasq
74
56 assets:75 assets:
57 plugin: dump76 plugin: dump
58 source: .77 source: .
59 prepare: ../../../scriptlets/fill_country_codes.sh78 override-build: |
79 "$SNAPCRAFT_PROJECT_DIR"/scriptlets/fill_country_codes.sh
80 snapcraftctl build
60 stage:81 stage:
61 - static82 - static
83 - -static/tests
diff --git a/static/js/jquery.min.js b/static/js/jquery.min.js
62new file mode 10064484new file mode 100644
index 0000000..644d35e
--- /dev/null
+++ b/static/js/jquery.min.js
@@ -0,0 +1,4 @@
1/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */
2!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),
3a.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),
4null==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});
diff --git a/static/templates/management.html b/static/templates/management.html
index f11bb00..4d68f2b 100644
--- a/static/templates/management.html
+++ b/static/templates/management.html
@@ -3,7 +3,7 @@
3<head>3<head>
4 <meta charset="utf-8" />4 <meta charset="utf-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>6 <script src="/static/js/jquery.min.js"></script>
7 <title>Select Wi-Fi</title>7 <title>Select Wi-Fi</title>
88
9 <link rel="stylesheet" href="/static/css/application.css" />9 <link rel="stylesheet" href="/static/css/application.css" />
diff --git a/static/templates/operational.html b/static/templates/operational.html
index 27d4f94..9d2cd89 100644
--- a/static/templates/operational.html
+++ b/static/templates/operational.html
@@ -5,7 +5,7 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1" />5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title>Wifi Connected</title>6 <title>Wifi Connected</title>
7 <link rel="stylesheet" href="/static/css/application.css" />7 <link rel="stylesheet" href="/static/css/application.css" />
8 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>8 <script src="/static/js/jquery.min.js"></script>
9</head>9</head>
1010
11<body>11<body>
diff --git a/utils/config.go b/utils/config.go
index bd23686..2cba34c 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -4,41 +4,47 @@ import (
4 "encoding/json"4 "encoding/json"
5 "fmt"5 "fmt"
6 "io/ioutil"6 "io/ioutil"
7 "log"
8 "os"7 "os"
9 "path/filepath"8 "path/filepath"
10 "strconv"9 "strconv"
11 "strings"10 "strings"
12
13 "launchpad.net/wifi-connect/wifiap"
14)11)
1512
16var configFile = filepath.Join(os.Getenv("SNAP_COMMON"), "pre-config.json")13var configFile = filepath.Join(os.Getenv("SNAP_COMMON"), "config.json")
17var mustConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), "config_done.flag")14var mustConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), "config_done.flag")
1815
19var wifiapClient wifiap.Operations = wifiap.DefaultClient()
20
21// Config this project config got from wifi-ap + custom wifi-connect params16// Config this project config got from wifi-ap + custom wifi-connect params
22type Config struct {17type Config struct {
23 Wifi *WifiConfig18 Wifi WifiConfig `json:"wifi"`
24 Portal *PortalConfig19 Portal PortalConfig `json:"portal"`
20 // If set, once we have connected to an AP once, we do not enter
21 // managing mode when the service is re-started, even if we are
22 // not able to reconnect to said AP. To change that, we would
23 // have to use the operational portal.
24 ConfigWifiOnce bool `json:"config-wifi-once"`
25}25}
2626
27// WifiConfig config specific parameters for wifi configuration27// WifiConfig config specific parameters for wifi configuration
28type WifiConfig struct {28type WifiConfig struct {
29 Ssid string `json:"wifi.ssid"`29 Ssid string `json:"ssid"`
30 Passphrase string `json:"wifi.security-passphrase"`30 Passphrase string `json:"security-passphrase"`
31 Interface string `json:"wifi.interface"`31 Interface string `json:"interface"`
32 CountryCode string `json:"wifi.country-code"`32 CountryCode string `json:"country-code"`
33 Channel int `json:"wifi.channel"`33 Channel int `json:"channel"`
34 OperationMode string `json:"wifi.operation-mode"`34 OperationMode string `json:"operation-mode"`
35 // Subnet for our AP, in CIDR format
36 Subnet string `json:"subnet"`
35}37}
3638
37// PortalConfig config specific parameters for portals configuration39// PortalConfig config specific parameters for portals configuration
38type PortalConfig struct {40type PortalConfig struct {
39 Password string //`json:"portal.password"`41 Password string `json:"password"`
40 NoResetCredentials bool `json:"portal.no-reset-creds"`42 // Port where we will be listening
41 NoOperational bool `json:"portal.no-operational"`43 Port int `json:"port"`
44 // whether user must reset passphrase and password on first use of mgmt portal
45 NoResetCredentials bool `json:"no-reset-creds"`
46 // whether to show the operational portal
47 Operational bool `json:"operational"`
42}48}
4349
44func (c *Config) String() string {50func (c *Config) String() string {
@@ -52,8 +58,11 @@ func (c *Config) String() string {
52func (c *PortalConfig) String() string {58func (c *PortalConfig) String() string {
53 s := []string{59 s := []string{
54 strings.Join([]string{"Password: ", c.Password}, " "),60 strings.Join([]string{"Password: ", c.Password}, " "),
55 strings.Join([]string{"NoResetCredentials:", strconv.FormatBool(c.NoResetCredentials)}, " "),61 strings.Join([]string{"Port:", strconv.Itoa(c.Port)}, " "),
56 strings.Join([]string{"NoOperational:", strconv.FormatBool(c.NoOperational)}, " "),62 strings.Join([]string{"NoResetCredentials:",
63 strconv.FormatBool(c.NoResetCredentials)}, " "),
64 strings.Join([]string{"Operational:",
65 strconv.FormatBool(c.Operational)}, " "),
57 }66 }
58 return fmt.Sprintf(strings.Join(s, "\n"))67 return fmt.Sprintf(strings.Join(s, "\n"))
59}68}
@@ -66,161 +75,67 @@ func (c *WifiConfig) String() string {
66 strings.Join([]string{"CountryCode:", c.CountryCode}, " "),75 strings.Join([]string{"CountryCode:", c.CountryCode}, " "),
67 strings.Join([]string{"Channel:", strconv.Itoa(c.Channel)}, " "),76 strings.Join([]string{"Channel:", strconv.Itoa(c.Channel)}, " "),
68 strings.Join([]string{"OperationMode:", c.OperationMode}, " "),77 strings.Join([]string{"OperationMode:", c.OperationMode}, " "),
78 strings.Join([]string{"Subnet:", c.Subnet}, " "),
69 }79 }
70 return fmt.Sprintf(strings.Join(s, "\n"))80 return fmt.Sprintf(strings.Join(s, "\n"))
71}81}
7282
73func defaultPortalConfig() *PortalConfig {83// LoadConfig loads configuration file
74 return &PortalConfig{84func LoadConfig() (*Config, error) {
75 Password: "",85 config := &Config{}
76 NoResetCredentials: false,86 // Set some defaults
77 NoOperational: false,87 config.Portal.Port = 8085
78 }88 config.Wifi.Subnet = "10.25.52.1/24"
79}
80
81// config currently stored in local json file is completely storable in PortalConfig
82// If needed to scale, we could rewrite this method to support a more generic type
83func readLocalConfig() (*PortalConfig, error) {
84 if _, err := os.Stat(configFile); os.IsNotExist(err) {
85 log.Printf("Warn: not found local config file at %v\n", configFile)
86 // in case there is no local config file, return a null pointer
87 return nil, nil
88 }
8989
90 fileContents, err := ioutil.ReadFile(configFile)90 content, err := ioutil.ReadFile(configFile)
91 if err != nil {91 if err != nil {
92 return nil, fmt.Errorf("Error reading json config file: %v", err)92 return config, err
93 }93 }
9494 err = json.Unmarshal(content, config)
95 // parameters not available in config file will be se to default value
96 portalConfig := defaultPortalConfig()
97 err = json.Unmarshal(fileContents, portalConfig)
98 if err != nil {
99 return nil, fmt.Errorf("Error unmarshalling json config file contents: %v", err)
100 }
101
102 return portalConfig, nil
103}
104
105func writeLocalConfig(p *PortalConfig) error {
106 // the only writable local config param is the password, stored as a hash
107 _, err := HashIt(p.Password)
108 if err != nil {95 if err != nil {
109 return fmt.Errorf("Could not hash portal password to file: %v", err)96 return config, err
110 }97 }
11198 return config, nil
112 return nil
113}99}
114100
115func readRemoteParam(m map[string]interface{}, key string, defaultValue interface{}) interface{} {101// WriteConfigFile writes configuration file
116 val, ok := m[key]102func WriteConfigFile(config *Config) error {
117 if !ok {103 b, err := json.Marshal(config)
118 val = defaultValue
119 log.Printf("Warning: %v key was not found in remote config", key)
120 }
121
122 return val
123}
124
125func readRemoteConfig() (*WifiConfig, error) {
126 settings, err := wifiapClient.Show()
127 if err != nil {104 if err != nil {
128 return nil, fmt.Errorf("Error reading wifi-ap remote configuration: %v", err)105 return err
129 }
130
131 // NOTE: Preprocessing for the case of wifi.channel.
132 // In the case of the channel, it is returned as string from rest api, but we have to convert it
133 // as it is handled as int internally.
134 // In case wifi-ap provides this in future as int, this could be replaced by
135 //
136 // readRemoteParam(settings, "wifi.channel", 0).(int)
137 channel, err := strconv.Atoi(readRemoteParam(settings, "wifi.channel", "0").(string))
138 if err != nil {
139 return nil, fmt.Errorf("Could not parse wifi.channel parameter: %v", err)
140 }106 }
141107
142 return &WifiConfig{108 err = ioutil.WriteFile(configFile, b, 0644)
143 Ssid: readRemoteParam(settings, "wifi.ssid", "").(string),
144 Passphrase: readRemoteParam(settings, "wifi.security-passphrase", "").(string),
145 Interface: readRemoteParam(settings, "wifi.interface", "").(string),
146 CountryCode: readRemoteParam(settings, "wifi.country-code", "").(string),
147 Channel: channel,
148 OperationMode: readRemoteParam(settings, "wifi.operation-mode", "").(string),
149 }, nil
150}
151
152func writeRemoteConfig(wc *WifiConfig) error {
153 params := make(map[string]interface{})
154 params["wifi.ssid"] = wc.Ssid
155 params["wifi.security-passphrase"] = wc.Passphrase
156 params["wifi.interface"] = wc.Interface
157 params["wifi.country-code"] = wc.CountryCode
158 params["wifi.channel"] = wc.Channel
159 params["wifi.operation-mode"] = wc.OperationMode
160
161 err := wifiapClient.Set(params)
162 if err != nil {109 if err != nil {
163 return fmt.Errorf("Error writing remote configuration: %v", err)110 return err
164 }111 }
165112
166 return nil113 return nil
167}114}
168115
169// ReadConfig reads all config, remote and local, at the same time116func writeConfig(p *PortalConfig) error {
170var ReadConfig = func() (*Config, error) {117 // Just take password, stored as a hash
171 wifiConfig, err := readRemoteConfig()118 _, err := HashIt(p.Password)
172 if err != nil {
173 return nil, err
174 }
175
176 portalConfig, err := readLocalConfig()
177 if err != nil {119 if err != nil {
178 return nil, err120 return fmt.Errorf("Could not hash portal password to file: %v", err)
179 }
180
181 // if local config is nil, fill returning object with default values
182 if portalConfig == nil {
183 portalConfig = defaultPortalConfig()
184 }121 }
185122
186 return &Config{Wifi: wifiConfig, Portal: portalConfig}, nil123 return nil
187}124}
188125
189// WriteConfig writes all remote and local config at the same time126// WriteConfig writes configuration
190var WriteConfig = func(c *Config) error {127var WriteConfig = func(c *Config) error {
191 previousRemoteConfig, err := readRemoteConfig()128 err := writeConfig(&c.Portal)
192 if err != nil {
193 return fmt.Errorf("Error reading current remote config before applying new one: %v", err)
194 }
195
196 // only write remote config if it's different from current
197 if *previousRemoteConfig != *c.Wifi {
198 err = writeRemoteConfig(c.Wifi)
199 if err != nil {
200 // if an error happens writing remote config there is no need to restore
201 // backup, as nothing shouldn't have been written
202 return err
203 }
204 }
205
206 err = writeLocalConfig(c.Portal)
207 if err != nil {129 if err != nil {
208 // rollback
209 if previousRemoteConfig != nil {
210 backupErr := writeRemoteConfig(previousRemoteConfig)
211 if backupErr != nil {
212 return fmt.Errorf("Could not restore previous remote configuration: %v\n after error: %v", backupErr, err)
213 }
214 }
215 return err130 return err
216 }131 }
217132
218 // write flag file for not asking more times for configuring snap before first use133 // write flag file for not asking more times for configuring snap before
219 if MustSetConfig() {134 // first use
220 err = WriteFlagFile(mustConfigFlagFile)135 err = WriteFlagFile(mustConfigFlagFile)
221 if err != nil {136 if err != nil {
222 return fmt.Errorf("Error writing flag file after configuring for a first time")137 return fmt.Errorf("Error writing flag file after " +
223 }138 "configuring for a first time")
224 }139 }
225140
226 return nil141 return nil
@@ -228,8 +143,11 @@ var WriteConfig = func(c *Config) error {
228143
229// MustSetConfig true if one needs to configure snap before continuing144// MustSetConfig true if one needs to configure snap before continuing
230var MustSetConfig = func() bool {145var MustSetConfig = func() bool {
231 if _, err := os.Stat(mustConfigFlagFile); os.IsNotExist(err) {146 // TODO Do not request portal configuration for the moment. It is not
232 return true147 // clear if it makes sense as we will be using wifi-connect only once
233 }148 // to select an AP.
149 // if _, err := os.Stat(mustConfigFlagFile); os.IsNotExist(err) {
150 // return true
151 // }
234 return false152 return false
235}153}
diff --git a/utils/config_test.go b/utils/config_test.go
index 1802a60..b4339ed 100644
--- a/utils/config_test.go
+++ b/utils/config_test.go
@@ -4,61 +4,37 @@ import (
4 "fmt"4 "fmt"
5 "io/ioutil"5 "io/ioutil"
6 "os"6 "os"
7 "path/filepath"
8 "strconv"
9 "sync"
10 "testing"7 "testing"
11 "time"
128
13 "gopkg.in/check.v1"9 "gopkg.in/check.v1"
14)10)
1511
16const testLocalConfig = `12const testConfig1 = `{
17{13 "wifi": {
18 "portal.no-reset-creds": true,14 "ssid": "confconn",
19 "portal.no-operational": false 15 "security-passphrase": "mypassphrase",
16 "interface": "wlp1s1",
17 "country-code": "ES",
18 "channel": 6 ,
19 "operation-mode": "g",
20 "subnet": "23.23.23.23/16"
21 },
22 "portal": {
23 "password": "mypassword",
24 "port": 9009,
25 "no-reset-creds": false,
26 "operational": true
27 },
28 "config-wifi-once": true
20}29}
21`30`
2231
23const testLocalConfigBadEntry = `32// Hook up gocheck into the "go test" runner.
24{
25 "portal.password": "the_password",
26 "portal.no-reset-creds": true,
27 "bad.parameter": "bad.value",
28 "portal.no-operational": false
29}
30`
31
32const testLocalEmptyConfig = `
33{
34}
35`
36
37var testPortalConfig = &PortalConfig{"the_password", true, false}
38
39var rand uint32
40var randmu sync.Mutex
41
42func Test(t *testing.T) { check.TestingT(t) }33func Test(t *testing.T) { check.TestingT(t) }
4334
44type S struct{}35type CfgSuite struct{}
45
46var _ = check.Suite(&S{})
4736
48// ####################37var _ = check.Suite(&CfgSuite{})
49// Testing local config
50// ####################
51func randomName() string {
52 randmu.Lock()
53 r := rand
54 if r == 0 {
55 r = uint32(time.Now().UnixNano() + int64(os.Getpid()))
56 }
57 r = r*1664525 + 1013904223 // constants from Numerical Recipes
58 rand = r
59 randmu.Unlock()
60 return strconv.Itoa(int(1e9 + r%1e9))[1:]
61}
6238
63func createTempFile(content string) (*os.File, error) {39func createTempFile(content string) (*os.File, error) {
64 contentAsBytes := []byte(content)40 contentAsBytes := []byte(content)
@@ -79,465 +55,52 @@ func createTempFile(content string) (*os.File, error) {
79 return tmpfile, nil55 return tmpfile, nil
80}56}
8157
82func verifyLocalConfig(c *check.C, cfg *PortalConfig, expectedPwd string, expectedNoResetCredentials bool, expectedNoOperational bool) {58func (s *CfgSuite) TestReadConfigDefaults(c *check.C) {
83 c.Assert(cfg.Password, check.Equals, expectedPwd)59 f, err := createTempFile("{}")
84 c.Assert(cfg.NoResetCredentials, check.Equals, expectedNoResetCredentials)
85 c.Assert(cfg.NoOperational, check.Equals, expectedNoOperational)
86}
87
88func verifyDefaultLocalConfig(c *check.C, cfg *PortalConfig) {
89 verifyLocalConfig(c, cfg, "", false, false)
90}
91
92func (s *S) TestReadLocalConfig(c *check.C) {
93 f, err := createTempFile(testLocalConfig)
94 c.Assert(err, check.IsNil)60 c.Assert(err, check.IsNil)
9561
96 defer os.Remove(f.Name())
97 configFile = f.Name()62 configFile = f.Name()
9863
99 cfg, err := readLocalConfig()64 cfg, err := LoadConfig()
100 c.Assert(err, check.IsNil)
101
102 verifyLocalConfig(c, cfg, "", true, false)
103}
10465
105func (s *S) TestReadLocalConfigBadEntry(c *check.C) {
106 // No matter if there are additional not recognized params, only known should be marshalled
107 f, err := createTempFile(testLocalConfigBadEntry)
108 c.Assert(err, check.IsNil)66 c.Assert(err, check.IsNil)
67 c.Assert(cfg.Wifi.Ssid, check.Equals, "")
68 c.Assert(cfg.Wifi.Passphrase, check.Equals, "")
69 c.Assert(cfg.Wifi.Interface, check.Equals, "")
70 c.Assert(cfg.Wifi.CountryCode, check.Equals, "")
71 c.Assert(cfg.Wifi.Channel, check.Equals, 0)
72 c.Assert(cfg.Wifi.OperationMode, check.Equals, "")
73 c.Assert(cfg.Wifi.Subnet, check.Equals, "10.25.52.1/24")
10974
110 defer os.Remove(f.Name())75 c.Assert(cfg.Portal.Password, check.Equals, "")
111 configFile = f.Name()76 c.Assert(cfg.Portal.Port, check.Equals, 8085)
11277 c.Assert(cfg.Portal.NoResetCredentials, check.Equals, false)
113 cfg, err := readLocalConfig()78 c.Assert(cfg.Portal.Operational, check.Equals, false)
114 c.Assert(err, check.IsNil)
11579
116 verifyLocalConfig(c, cfg, "", true, false)80 c.Assert(cfg.ConfigWifiOnce, check.Equals, false)
117}81}
11882
119func (s *S) TestReadLocalEmptyConfig(c *check.C) {83func (s *CfgSuite) TestReadConfig(c *check.C) {
120 // No matter if there are additional not recognized params, only known should be marshalled84 f, err := createTempFile(testConfig1)
121 f, err := createTempFile(testLocalEmptyConfig)
122 c.Assert(err, check.IsNil)85 c.Assert(err, check.IsNil)
12386
124 defer os.Remove(f.Name())
125 configFile = f.Name()87 configFile = f.Name()
12688
127 cfg, err := readLocalConfig()89 cfg, err := LoadConfig()
128 c.Assert(err, check.IsNil)
129
130 verifyDefaultLocalConfig(c, cfg)
131}
132
133func (s *S) TestReadLocalNotExistingConfig(c *check.C) {
134 configFile = "does/not/exists/config.json"
135
136 cfg, err := readLocalConfig()
137 c.Assert(err, check.IsNil)
138 c.Assert(cfg, check.IsNil)
139}
140
141func (s *S) TestWriteLocalConfigFileDoesNotExists(c *check.C) {
142 mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
143 defer os.Remove(mustConfigFlagFile)
144 HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName())
145 defer os.Remove(HashFile)
14690
147 err := writeLocalConfig(testPortalConfig)
148 c.Assert(err, check.IsNil)91 c.Assert(err, check.IsNil)
92 c.Assert(cfg.Wifi.Ssid, check.Equals, "confconn")
93 c.Assert(cfg.Wifi.Passphrase, check.Equals, "mypassphrase")
94 c.Assert(cfg.Wifi.Interface, check.Equals, "wlp1s1")
95 c.Assert(cfg.Wifi.CountryCode, check.Equals, "ES")
96 c.Assert(cfg.Wifi.Channel, check.Equals, 6)
97 c.Assert(cfg.Wifi.OperationMode, check.Equals, "g")
98 c.Assert(cfg.Wifi.Subnet, check.Equals, "23.23.23.23/16")
14999
150 ok, err := MatchingHash(testPortalConfig.Password)
151 c.Assert(err, check.IsNil)
152 c.Assert(ok, check.Equals, true)
153}
154
155func (s *S) TestWriteLocalConfigFiletExists(c *check.C) {
156 mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
157 defer os.Remove(mustConfigFlagFile)
158
159 f, err := createTempFile("whateverbadpasswordhash")
160 c.Assert(err, check.IsNil)
161
162 defer os.Remove(f.Name())
163 HashFile = f.Name()
164
165 err = writeLocalConfig(testPortalConfig)
166 c.Assert(err, check.IsNil)
167
168 ok, err := MatchingHash(testPortalConfig.Password)
169 c.Assert(err, check.IsNil)
170 c.Assert(ok, check.Equals, true)
171}
172
173// #####################
174// Testing remote config
175// #####################
176type wifiapClientMock struct {
177 m map[string]interface{}
178}
179
180func (c *wifiapClientMock) Show() (map[string]interface{}, error) {
181 return c.m, nil
182}
183
184func (c *wifiapClientMock) Enable() error {
185 return nil
186}
187
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches