Merge ~alfonsosanchezbeato/snappy-hwe-snaps/+git/wifi-connect:use-nm-for-ap into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect:master
- Git
- lp:~alfonsosanchezbeato/snappy-hwe-snaps/+git/wifi-connect
- use-nm-for-ap
- Merge into master
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) |
Related bugs: |
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.
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:223cd235a40
https:/
Executed test runs:
FAILURE: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
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.
Alfonso Sanchez-Beato (alfonsosanchezbeato) wrote : | # |
Thanks Kyle!
Preview Diff
1 | diff --git a/server/middleware.go b/avahi/avahi_test.go | |||
2 | 0 | similarity index 63% | 0 | similarity index 63% |
3 | 1 | rename from server/middleware.go | 1 | rename from server/middleware.go |
4 | 2 | rename to avahi/avahi_test.go | 2 | rename to avahi/avahi_test.go |
5 | index 99bd6f9..486121f 100644 | |||
6 | --- a/server/middleware.go | |||
7 | +++ b/avahi/avahi_test.go | |||
8 | @@ -1,5 +1,3 @@ | |||
9 | 1 | // -*- Mode: Go; indent-tabs-mode: t -*- | ||
10 | 2 | |||
11 | 3 | /* | 1 | /* |
12 | 4 | * Copyright (C) 2017 Canonical Ltd | 2 | * Copyright (C) 2017 Canonical Ltd |
13 | 5 | * | 3 | * |
14 | @@ -17,30 +15,12 @@ | |||
15 | 17 | * | 15 | * |
16 | 18 | */ | 16 | */ |
17 | 19 | 17 | ||
19 | 20 | package server | 18 | package avahi |
20 | 21 | 19 | ||
21 | 22 | import ( | 20 | import ( |
26 | 23 | "net/http" | 21 | "testing" |
23 | 24 | |||
24 | 25 | "launchpad.net/wifi-connect/netman" | ||
25 | 26 | "launchpad.net/wifi-connect/wifiap" | ||
27 | 27 | ) | 22 | ) |
28 | 28 | 23 | ||
46 | 29 | var wifiapClient wifiap.Operations | 24 | func TestAvahi(t *testing.T) { |
47 | 30 | var netmanClient netman.Operations | 25 | // TODO Fill it when we really use avahi |
31 | 31 | |||
32 | 32 | // Middleware to pre-process web service requests | ||
33 | 33 | func Middleware(inner http.Handler) http.Handler { | ||
34 | 34 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
35 | 35 | |||
36 | 36 | if wifiapClient == nil { | ||
37 | 37 | wifiapClient = wifiap.DefaultClient() | ||
38 | 38 | } | ||
39 | 39 | |||
40 | 40 | if netmanClient == nil { | ||
41 | 41 | netmanClient = netman.DefaultClient() | ||
42 | 42 | } | ||
43 | 43 | |||
44 | 44 | inner.ServeHTTP(w, r) | ||
45 | 45 | }) | ||
48 | 46 | } | 26 | } |
49 | diff --git a/cmd/main.go b/cmd/main.go | |||
50 | 47 | deleted file mode 100644 | 27 | deleted file mode 100644 |
51 | index 2eebb48..0000000 | |||
52 | --- a/cmd/main.go | |||
53 | +++ /dev/null | |||
54 | @@ -1,262 +0,0 @@ | |||
55 | 1 | /* | ||
56 | 2 | * Copyright (C) 2017 Canonical Ltd | ||
57 | 3 | * | ||
58 | 4 | * This program is free software: you can redistribute it and/or modify | ||
59 | 5 | * it under the terms of the GNU General Public License version 3 as | ||
60 | 6 | * published by the Free Software Foundation. | ||
61 | 7 | * | ||
62 | 8 | * This program is distributed in the hope that it will be useful, | ||
63 | 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
64 | 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
65 | 11 | * GNU General Public License for more details. | ||
66 | 12 | * | ||
67 | 13 | * You should have received a copy of the GNU General Public License | ||
68 | 14 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
69 | 15 | * | ||
70 | 16 | */ | ||
71 | 17 | |||
72 | 18 | package main | ||
73 | 19 | |||
74 | 20 | import ( | ||
75 | 21 | "bufio" | ||
76 | 22 | "fmt" | ||
77 | 23 | "log" | ||
78 | 24 | "os" | ||
79 | 25 | "os/signal" | ||
80 | 26 | "strings" | ||
81 | 27 | "sync" | ||
82 | 28 | |||
83 | 29 | "launchpad.net/wifi-connect/netman" | ||
84 | 30 | "launchpad.net/wifi-connect/server" | ||
85 | 31 | "launchpad.net/wifi-connect/utils" | ||
86 | 32 | "launchpad.net/wifi-connect/wifiap" | ||
87 | 33 | ) | ||
88 | 34 | |||
89 | 35 | func help() string { | ||
90 | 36 | |||
91 | 37 | text := | ||
92 | 38 | `Usage: sudo wifi-connect COMMAND [VALUE] | ||
93 | 39 | |||
94 | 40 | Commands: | ||
95 | 41 | stop: Disables wifi-connect from automatic control, leaving system | ||
96 | 42 | in current state | ||
97 | 43 | start: Enables wifi-connect as automatic controller, restarting from | ||
98 | 44 | a clean state | ||
99 | 45 | show-ap: Show AP configuration | ||
100 | 46 | ssid VALUE: Set the AP ssid (causes AP restart if it is UP) | ||
101 | 47 | passphrase VALUE: Set the AP passphrase (cause AP restart if it is UP) | ||
102 | 48 | ` | ||
103 | 49 | return text | ||
104 | 50 | } | ||
105 | 51 | |||
106 | 52 | // checkSudo return false if the current user is not root, else true | ||
107 | 53 | func checkSudo() bool { | ||
108 | 54 | if os.Geteuid() != 0 { | ||
109 | 55 | fmt.Println("Error: This command requires sudo") | ||
110 | 56 | return false | ||
111 | 57 | } | ||
112 | 58 | return true | ||
113 | 59 | } | ||
114 | 60 | |||
115 | 61 | func waitForCtrlC() { | ||
116 | 62 | var endWaiter sync.WaitGroup | ||
117 | 63 | endWaiter.Add(1) | ||
118 | 64 | var signalChannel chan os.Signal | ||
119 | 65 | signalChannel = make(chan os.Signal, 1) | ||
120 | 66 | signal.Notify(signalChannel, os.Interrupt) | ||
121 | 67 | go func() { | ||
122 | 68 | <-signalChannel | ||
123 | 69 | endWaiter.Done() | ||
124 | 70 | }() | ||
125 | 71 | endWaiter.Wait() | ||
126 | 72 | } | ||
127 | 73 | |||
128 | 74 | func main() { | ||
129 | 75 | |||
130 | 76 | log.SetFlags(log.Lshortfile) | ||
131 | 77 | log.SetPrefix("== wifi-connect: ") | ||
132 | 78 | |||
133 | 79 | if len(os.Args) < 2 { | ||
134 | 80 | fmt.Println("Error: no command arguments provided") | ||
135 | 81 | return | ||
136 | 82 | } | ||
137 | 83 | args := os.Args[1:] | ||
138 | 84 | |||
139 | 85 | switch args[0] { | ||
140 | 86 | case "help": | ||
141 | 87 | fmt.Printf("%s\n", help()) | ||
142 | 88 | case "-help": | ||
143 | 89 | fmt.Printf("%s\n", help()) | ||
144 | 90 | case "-h": | ||
145 | 91 | fmt.Printf("%s\n", help()) | ||
146 | 92 | case "--help": | ||
147 | 93 | fmt.Printf("%s\n", help()) | ||
148 | 94 | case "stop": | ||
149 | 95 | if !checkSudo() { | ||
150 | 96 | return | ||
151 | 97 | } | ||
152 | 98 | err := utils.WriteFlagFile(os.Getenv("SNAP_COMMON") + "/manualMode") | ||
153 | 99 | if err != nil { | ||
154 | 100 | fmt.Println(err) | ||
155 | 101 | return | ||
156 | 102 | } | ||
157 | 103 | fmt.Println("entering MANUAL Mode. Wifi-connect has stopped managing state. Use 'start' to restore normal operations") | ||
158 | 104 | case "start": | ||
159 | 105 | if !checkSudo() { | ||
160 | 106 | return | ||
161 | 107 | } | ||
162 | 108 | err := utils.RemoveFlagFile(os.Getenv("SNAP_COMMON") + "/manualMode") | ||
163 | 109 | if err != nil { | ||
164 | 110 | fmt.Println("Error:", err) | ||
165 | 111 | return | ||
166 | 112 | } | ||
167 | 113 | fmt.Println("Entering NORMAL Mode.") | ||
168 | 114 | case "show-ap": | ||
169 | 115 | if !checkSudo() { | ||
170 | 116 | return | ||
171 | 117 | } | ||
172 | 118 | wifiAPClient := wifiap.DefaultClient() | ||
173 | 119 | result, err := wifiAPClient.Show() | ||
174 | 120 | if err != nil { | ||
175 | 121 | fmt.Println("Error:", err) | ||
176 | 122 | return | ||
177 | 123 | } | ||
178 | 124 | if result != nil { | ||
179 | 125 | utils.PrintMapSorted(result) | ||
180 | 126 | return | ||
181 | 127 | } | ||
182 | 128 | case "ssid": | ||
183 | 129 | if !checkSudo() { | ||
184 | 130 | return | ||
185 | 131 | } | ||
186 | 132 | if len(os.Args) < 3 { | ||
187 | 133 | fmt.Println("Error: no ssid provided") | ||
188 | 134 | return | ||
189 | 135 | } | ||
190 | 136 | wifiAPClient := wifiap.DefaultClient() | ||
191 | 137 | wifiAPClient.SetSsid(os.Args[2]) | ||
192 | 138 | case "passphrase": | ||
193 | 139 | if !checkSudo() { | ||
194 | 140 | return | ||
195 | 141 | } | ||
196 | 142 | if len(os.Args) < 3 { | ||
197 | 143 | fmt.Println("Error: no passphrase provided") | ||
198 | 144 | return | ||
199 | 145 | } | ||
200 | 146 | if len(os.Args[2]) < 13 { | ||
201 | 147 | fmt.Println("Error: passphrase must be at least 13 chars long") | ||
202 | 148 | return | ||
203 | 149 | } | ||
204 | 150 | wifiAPClient := wifiap.DefaultClient() | ||
205 | 151 | wifiAPClient.SetPassphrase(os.Args[2]) | ||
206 | 152 | case "get-devices": | ||
207 | 153 | c := netman.DefaultClient() | ||
208 | 154 | devices := c.GetDevices() | ||
209 | 155 | for d := range devices { | ||
210 | 156 | fmt.Println(d) | ||
211 | 157 | } | ||
212 | 158 | case "get-wifi-devices": | ||
213 | 159 | c := netman.DefaultClient() | ||
214 | 160 | devices := c.GetWifiDevices(c.GetDevices()) | ||
215 | 161 | for d := range devices { | ||
216 | 162 | fmt.Println(d) | ||
217 | 163 | } | ||
218 | 164 | case "get-ssids": | ||
219 | 165 | c := netman.DefaultClient() | ||
220 | 166 | SSIDs, _, _ := c.Ssids() | ||
221 | 167 | var out string | ||
222 | 168 | for _, ssid := range SSIDs { | ||
223 | 169 | out += strings.TrimSpace(ssid.Ssid) + "," | ||
224 | 170 | } | ||
225 | 171 | if len(out) > 0 { | ||
226 | 172 | fmt.Printf("%s\n", out[:len(out)-1]) | ||
227 | 173 | } | ||
228 | 174 | case "check-connected": | ||
229 | 175 | c := netman.DefaultClient() | ||
230 | 176 | if c.ConnectedWifi(c.GetWifiDevices(c.GetDevices())) { | ||
231 | 177 | fmt.Println("Device is connected") | ||
232 | 178 | } else { | ||
233 | 179 | fmt.Println("Device is not connected") | ||
234 | 180 | } | ||
235 | 181 | |||
236 | 182 | case "check-connected-wifi": | ||
237 | 183 | c := netman.DefaultClient() | ||
238 | 184 | if c.ConnectedWifi(c.GetWifiDevices(c.GetDevices())) { | ||
239 | 185 | fmt.Println("Device is connected to external wifi AP") | ||
240 | 186 | } else { | ||
241 | 187 | fmt.Println("Device is not connected to external wifi AP") | ||
242 | 188 | } | ||
243 | 189 | case "disconnect-wifi": | ||
244 | 190 | c := netman.DefaultClient() | ||
245 | 191 | c.DisconnectWifi(c.GetWifiDevices(c.GetDevices())) | ||
246 | 192 | case "wifis-managed": | ||
247 | 193 | c := netman.DefaultClient() | ||
248 | 194 | wifis, err := c.WifisManaged(c.GetWifiDevices(c.GetDevices())) | ||
249 | 195 | if err != nil { | ||
250 | 196 | fmt.Println(err) | ||
251 | 197 | return | ||
252 | 198 | } | ||
253 | 199 | for k, v := range wifis { | ||
254 | 200 | fmt.Printf("%s : %s\n", k, v) | ||
255 | 201 | } | ||
256 | 202 | case "manage-iface": | ||
257 | 203 | if len(os.Args) < 3 { | ||
258 | 204 | fmt.Println("Error: no interface provided") | ||
259 | 205 | return | ||
260 | 206 | } | ||
261 | 207 | c := netman.DefaultClient() | ||
262 | 208 | c.SetIfaceManaged(os.Args[2], true, c.GetWifiDevices(c.GetDevices())) | ||
263 | 209 | case "unmanage-iface": | ||
264 | 210 | if len(os.Args) < 3 { | ||
265 | 211 | fmt.Println("Error: no interface provided") | ||
266 | 212 | return | ||
267 | 213 | } | ||
268 | 214 | c := netman.DefaultClient() | ||
269 | 215 | c.SetIfaceManaged(os.Args[2], false, c.GetWifiDevices(c.GetDevices())) | ||
270 | 216 | case "connect": | ||
271 | 217 | c := netman.DefaultClient() | ||
272 | 218 | SSIDs, ap2device, ssid2ap := c.Ssids() | ||
273 | 219 | for _, ssid := range SSIDs { | ||
274 | 220 | fmt.Printf(" %v\n", ssid.Ssid) | ||
275 | 221 | } | ||
276 | 222 | reader := bufio.NewReader(os.Stdin) | ||
277 | 223 | fmt.Print("Connect to AP. Enter SSID: ") | ||
278 | 224 | ssid, _ := reader.ReadString('\n') | ||
279 | 225 | ssid = strings.TrimSpace(ssid) | ||
280 | 226 | fmt.Print("Enter phasprase: ") | ||
281 | 227 | pw, _ := reader.ReadString('\n') | ||
282 | 228 | pw = strings.TrimSpace(pw) | ||
283 | 229 | c.ConnectAp(ssid, pw, ap2device, ssid2ap) | ||
284 | 230 | case "management": | ||
285 | 231 | server.Address = server.TestingAddress | ||
286 | 232 | if err := server.StartManagementServer(); err != nil { | ||
287 | 233 | fmt.Printf("Could not start management server: %v\n", err) | ||
288 | 234 | return | ||
289 | 235 | } | ||
290 | 236 | waitForCtrlC() | ||
291 | 237 | case "operational": | ||
292 | 238 | server.Address = server.TestingAddress | ||
293 | 239 | if err := server.StartOperationalServer(); err != nil { | ||
294 | 240 | fmt.Printf("Could not start operational server: %v\n", err) | ||
295 | 241 | return | ||
296 | 242 | } | ||
297 | 243 | waitForCtrlC() | ||
298 | 244 | case "set-portal-password": | ||
299 | 245 | if len(os.Args) < 3 { | ||
300 | 246 | fmt.Println("Error: no string to hash provided") | ||
301 | 247 | return | ||
302 | 248 | } | ||
303 | 249 | if len(os.Args[2]) < 8 { | ||
304 | 250 | fmt.Println("Error: password must be at least 8 characters long") | ||
305 | 251 | return | ||
306 | 252 | } | ||
307 | 253 | b, err := utils.HashIt(os.Args[2]) | ||
308 | 254 | if err != nil { | ||
309 | 255 | fmt.Println("Error hashing:", err) | ||
310 | 256 | return | ||
311 | 257 | } | ||
312 | 258 | fmt.Println(string(b)) | ||
313 | 259 | default: | ||
314 | 260 | fmt.Println("Error. Your command is not supported. Please try 'help'") | ||
315 | 261 | } | ||
316 | 262 | } | ||
317 | diff --git a/daemon/daemon.go b/daemon/daemon.go | |||
318 | index 1830491..d1af3a9 100644 | |||
319 | --- a/daemon/daemon.go | |||
320 | +++ b/daemon/daemon.go | |||
321 | @@ -18,242 +18,496 @@ | |||
322 | 18 | package daemon | 18 | package daemon |
323 | 19 | 19 | ||
324 | 20 | import ( | 20 | import ( |
325 | 21 | "encoding/json" | ||
326 | 22 | "fmt" | 21 | "fmt" |
327 | 23 | "io/ioutil" | ||
328 | 24 | "log" | 22 | "log" |
329 | 23 | "net" | ||
330 | 25 | "os" | 24 | "os" |
332 | 26 | "path/filepath" | 25 | "os/exec" |
333 | 26 | "os/signal" | ||
334 | 27 | "strconv" | ||
335 | 28 | "syscall" | ||
336 | 29 | "time" | ||
337 | 27 | 30 | ||
339 | 28 | "launchpad.net/wifi-connect/avahi" | 31 | "github.com/godbus/dbus" |
340 | 32 | "launchpad.net/wifi-connect/netman" | ||
341 | 29 | "launchpad.net/wifi-connect/server" | 33 | "launchpad.net/wifi-connect/server" |
342 | 30 | "launchpad.net/wifi-connect/utils" | 34 | "launchpad.net/wifi-connect/utils" |
343 | 31 | "launchpad.net/wifi-connect/wifiap" | ||
344 | 32 | ) | 35 | ) |
345 | 33 | 36 | ||
347 | 34 | // enum to track current system state | 37 | type serviceState int |
348 | 38 | |||
349 | 35 | const ( | 39 | const ( |
354 | 36 | STARTING = 0 + iota | 40 | stStarting serviceState = iota |
355 | 37 | MANAGING | 41 | stManaging |
356 | 38 | OPERATING | 42 | stOperating |
353 | 39 | MANUAL | ||
357 | 40 | ) | 43 | ) |
358 | 41 | 44 | ||
375 | 42 | var manualFlagPath string | 45 | func (s serviceState) String() string { |
376 | 43 | var waitFlagPath string | 46 | return [...]string{"Starting", "Managing", "Operating"}[s] |
361 | 44 | var previousState = STARTING | ||
362 | 45 | var state = STARTING | ||
363 | 46 | |||
364 | 47 | // PreConfigFile is the path to the file that stores the hash of the portals password | ||
365 | 48 | var PreConfigFile = filepath.Join(os.Getenv("SNAP_COMMON"), "pre-config.json") | ||
366 | 49 | |||
367 | 50 | // PreConfig is the struct representing a configuration | ||
368 | 51 | type PreConfig struct { | ||
369 | 52 | Passphrase string `json:"wifi.security-passphrase,omitempty"` | ||
370 | 53 | Ssid string `json:"wifi.ssid,omitempty"` | ||
371 | 54 | Interface string `json:"wifi.interface,omitempty"` | ||
372 | 55 | Password string `json:"portal.password,omitempty"` | ||
373 | 56 | Operational bool `json:"portal.operational,omitempty"` //whether to show the operational portal | ||
374 | 57 | NoResetCreds bool `json:"portal.no-reset-creds,omitempty"` //whether user must reset passphrase and password on first use of mgmt portal | ||
377 | 58 | } | 47 | } |
378 | 59 | 48 | ||
382 | 60 | // Client is the base type for both testing and runtime | 49 | const ( |
383 | 61 | type Client struct { | 50 | waitBeforeScanSec = 2 |
384 | 62 | } | 51 | ) |
385 | 63 | 52 | ||
389 | 64 | // GetClient returns a client for runtime or testing | 53 | // Service models the high level wifi-connect service |
390 | 65 | func GetClient() *Client { | 54 | type Service struct { |
391 | 66 | return &Client{} | 55 | config *utils.Config |
392 | 56 | cnetman *netman.Client | ||
393 | 57 | devicePath string | ||
394 | 58 | apSettingsPath string | ||
395 | 59 | apActiveConnPath string | ||
396 | 60 | ssidLs utils.SsidList | ||
397 | 61 | dnsmasqCmd *exec.Cmd | ||
398 | 62 | chSigs chan os.Signal | ||
399 | 63 | chUserEvs chan server.UserEvent | ||
400 | 64 | // Arguments for port forwarding rule | ||
401 | 65 | iptblForwArgs []string | ||
402 | 67 | } | 66 | } |
403 | 68 | 67 | ||
411 | 69 | // used to clase the operational http server | 68 | // Creates the run time configuration based on available wifi interfaces and the |
412 | 70 | var err error | 69 | // configuration loaded from config.json. |
413 | 71 | 70 | func (s *Service) setDefaults() error { | |
414 | 72 | // GetManualFlagPath returns the current path | 71 | var err error |
415 | 73 | func (c *Client) GetManualFlagPath() string { | 72 | // If no interface is selected by configuration, just take the first one |
416 | 74 | return manualFlagPath | 73 | // returned by GetWifiDevices(). |
417 | 75 | } | 74 | if s.config.Wifi.Interface == "" { |
418 | 75 | devPaths := s.cnetman.GetWifiDevices() | ||
419 | 76 | if len(devPaths) == 0 { | ||
420 | 77 | return fmt.Errorf("no wifi device found") | ||
421 | 78 | } | ||
422 | 79 | s.devicePath = devPaths[0] | ||
423 | 80 | iface, err := s.cnetman.GetIfaceFromDevicePath(devPaths[0]) | ||
424 | 81 | if err != nil { | ||
425 | 82 | return err | ||
426 | 83 | } | ||
427 | 84 | s.config.Wifi.Interface = iface | ||
428 | 85 | } else { | ||
429 | 86 | s.devicePath, err = s.cnetman. | ||
430 | 87 | GetDevicePathFromIface(s.config.Wifi.Interface) | ||
431 | 88 | if err != nil { | ||
432 | 89 | return err | ||
433 | 90 | } | ||
434 | 91 | } | ||
435 | 92 | log.Printf("setDefaults: using interface %s", s.config.Wifi.Interface) | ||
436 | 76 | 93 | ||
441 | 77 | // SetManualFlagPath sets the current path | 94 | if len(s.config.Portal.Password) > 0 { |
442 | 78 | func (c *Client) SetManualFlagPath(s string) { | 95 | log.Print("setDefaults: portal password being set") |
443 | 79 | manualFlagPath = s | 96 | _, err := utils.HashIt(s.config.Portal.Password) |
444 | 80 | } | 97 | if err != nil { |
445 | 98 | log.Printf("setDefaults: password err: %v", err) | ||
446 | 99 | return err | ||
447 | 100 | } | ||
448 | 101 | } | ||
449 | 81 | 102 | ||
453 | 82 | // GetWaitFlagPath returns the current path | 103 | return nil |
451 | 83 | func (c *Client) GetWaitFlagPath() string { | ||
452 | 84 | return waitFlagPath | ||
454 | 85 | } | 104 | } |
455 | 86 | 105 | ||
460 | 87 | // SetWaitFlagPath sets the current path | 106 | // GetService returns a service object for runtime or testing |
461 | 88 | func (c *Client) SetWaitFlagPath(s string) { | 107 | func GetService(cfg *utils.Config) *Service { |
462 | 89 | waitFlagPath = s | 108 | if cfg.Portal.Operational { |
463 | 90 | } | 109 | log.Print("operational portal is enabled") |
464 | 110 | } | ||
465 | 91 | 111 | ||
470 | 92 | // GetPreviousState returns the daemon previous state | 112 | chSigs := make(chan os.Signal, 1) |
471 | 93 | func (c *Client) GetPreviousState() int { | 113 | signal.Notify(chSigs, syscall.SIGINT, syscall.SIGTERM) |
468 | 94 | return previousState | ||
469 | 95 | } | ||
472 | 96 | 114 | ||
478 | 97 | // SetPreviousState sets daemon previous state | 115 | chUserEvs := make(chan server.UserEvent) |
474 | 98 | func (c *Client) SetPreviousState(i int) { | ||
475 | 99 | previousState = i | ||
476 | 100 | return | ||
477 | 101 | } | ||
479 | 102 | 116 | ||
484 | 103 | // GetState returns the daemon state | 117 | cnetman := netman.DefaultClient() |
481 | 104 | func (c *Client) GetState() int { | ||
482 | 105 | return state | ||
483 | 106 | } | ||
485 | 107 | 118 | ||
491 | 108 | // SetState sets the daemon state and updates the previous state | 119 | service := &Service{config: cfg, cnetman: cnetman, chSigs: chSigs, |
492 | 109 | func (c *Client) SetState(i int) { | 120 | chUserEvs: chUserEvs} |
488 | 110 | previousState = state | ||
489 | 111 | state = i | ||
490 | 112 | } | ||
493 | 113 | 121 | ||
501 | 114 | // CheckWaitApConnect returns true if the flag wait file exists | 122 | return service |
495 | 115 | // and false if it does not | ||
496 | 116 | func (c *Client) CheckWaitApConnect() bool { | ||
497 | 117 | if _, err := os.Stat(waitFlagPath); os.IsNotExist(err) { | ||
498 | 118 | return false | ||
499 | 119 | } | ||
500 | 120 | return true | ||
502 | 121 | } | 123 | } |
503 | 122 | 124 | ||
522 | 123 | // ManualMode enables the daemon to loop without action if in manual mode | 125 | // Clean-up left-overs from previous executions |
523 | 124 | // It returns true if the manual mode flag wait file exists | 126 | func (s *Service) cleanUpPrevRuns() { |
524 | 125 | // and false if it does not. If it does not exist and the mode is MANUAL, the | 127 | // Remove old hotspots from previous executions |
525 | 126 | // state is set to STARTING. If it does exist and the mode is not MANUAL, state | 128 | s.cnetman.DeleteWifiConnections(netman.ApWifiConn) |
508 | 127 | // is set to MANUAL | ||
509 | 128 | func (c *Client) ManualMode() bool { | ||
510 | 129 | if _, err := os.Stat(manualFlagPath); os.IsNotExist(err) { | ||
511 | 130 | if state == MANUAL { | ||
512 | 131 | c.SetState(STARTING) | ||
513 | 132 | log.Print("entering STARTING mode") | ||
514 | 133 | } | ||
515 | 134 | return false | ||
516 | 135 | } | ||
517 | 136 | if state != MANUAL { | ||
518 | 137 | c.SetState(MANUAL) | ||
519 | 138 | log.Print("entering MANUAL mode") | ||
520 | 139 | } | ||
521 | 140 | return true | ||
526 | 141 | } | 129 | } |
527 | 142 | 130 | ||
543 | 143 | // IsApUpWithoutSSIDs corrects an possible but unlikely case. | 131 | // Returns true if wifi is connected to an AP |
544 | 144 | // if wifiap is UP and there are no known SSIDs, bring it down so on next | 132 | func (s *Service) isConnectedToAnAP() bool { |
545 | 145 | // loop iter we start again and can get SSIDs. returns true when ip is | 133 | return s.cnetman.ConnectedToAnAP(s.devicePath) |
531 | 146 | // UP and has no ssids | ||
532 | 147 | func (c *Client) IsApUpWithoutSSIDs(cw *wifiap.Client) bool { | ||
533 | 148 | wifiUp, _ := cw.Enabled() | ||
534 | 149 | if !wifiUp { | ||
535 | 150 | return false | ||
536 | 151 | } | ||
537 | 152 | ssids, _ := utils.ReadSsidsFile() | ||
538 | 153 | if len(ssids) < 1 { | ||
539 | 154 | log.Print("wifi-ap is UP but has no SSIDS") | ||
540 | 155 | return true // ap is up with no ssids | ||
541 | 156 | } | ||
542 | 157 | return false | ||
546 | 158 | } | 134 | } |
547 | 159 | 135 | ||
551 | 160 | // ManagementServerUp starts the management server if it is | 136 | // startManagementServer starts the management server if it is not running |
552 | 161 | // not running | 137 | func (s *Service) startManagementServer() { |
550 | 162 | func (c *Client) ManagementServerUp() { | ||
553 | 163 | if server.Current != server.Management && server.State == server.Stopped { | 138 | if server.Current != server.Management && server.State == server.Stopped { |
555 | 164 | err = server.StartManagementServer() | 139 | err := server.StartManagementServer(s.chUserEvs, s.config, &s.ssidLs) |
556 | 165 | if err != nil { | 140 | if err != nil { |
558 | 166 | log.Printf("Error start Mamagement portal: %v", err) | 141 | log.Printf("Error starting Management portal: %v", err) |
559 | 167 | } | 142 | } |
560 | 168 | // init mDNS | 143 | // init mDNS |
562 | 169 | avahi.InitMDNS() | 144 | //avahi.InitMDNS() |
563 | 170 | } | 145 | } |
564 | 171 | } | 146 | } |
565 | 172 | 147 | ||
571 | 173 | // ManagementServerDown stops the management server if it is running | 148 | // stopManagementServer stops the management server if it is running |
572 | 174 | // also remove the wait flag file, thus resetting proper State | 149 | func (s *Service) stopManagementServer() { |
573 | 175 | func (c *Client) ManagementServerDown() { | 150 | if server.Current == server.Management && server.State == server.Running { |
574 | 176 | if server.Current == server.Management && (server.State == server.Running || server.State == server.Starting) { | 151 | err := server.ShutdownManagementServer() |
570 | 177 | err = server.ShutdownManagementServer() | ||
575 | 178 | if err != nil { | 152 | if err != nil { |
577 | 179 | log.Printf("Error stopping the Management portal: %v", err) | 153 | log.Printf("Error stopping Management portal: %v", err) |
578 | 180 | } | 154 | } |
579 | 181 | //remove flag fie so daemon resumes normal control | ||
580 | 182 | utils.RemoveFlagFile(os.Getenv("SNAP_COMMON") + "/startingApConnect") | ||
581 | 183 | } | 155 | } |
582 | 184 | } | 156 | } |
583 | 185 | 157 | ||
587 | 186 | // OperationalServerUp starts the operational server if it is | 158 | // startOperationalServer starts the operational server if it is not running |
588 | 187 | // not running | 159 | func (s *Service) startOperationalServer() { |
589 | 188 | func (c *Client) OperationalServerUp() { | 160 | if s.config.Portal.Operational == false { |
590 | 161 | return | ||
591 | 162 | } | ||
592 | 163 | log.Print("Starting operational server") | ||
593 | 189 | if server.Current != server.Operational && server.State == server.Stopped { | 164 | if server.Current != server.Operational && server.State == server.Stopped { |
595 | 190 | err = server.StartOperationalServer() | 165 | err := server.StartOperationalServer(s.chUserEvs, s.config) |
596 | 191 | if err != nil { | 166 | if err != nil { |
598 | 192 | log.Printf("Error starting the Operational portal: %v", err) | 167 | log.Printf("Error starting Operational portal: %v", err) |
599 | 193 | } | 168 | } |
600 | 194 | // init mDNS | 169 | // init mDNS |
602 | 195 | avahi.InitMDNS() | 170 | //avahi.InitMDNS() |
603 | 196 | } | 171 | } |
604 | 197 | } | 172 | } |
605 | 198 | 173 | ||
610 | 199 | // OperationalServerDown stops the operational server if it is running | 174 | // stopOperationalServer stops the operational server if it is running |
611 | 200 | func (c *Client) OperationalServerDown() { | 175 | func (s *Service) stopOperationalServer() { |
612 | 201 | if server.Current == server.Operational && (server.State == server.Running || server.State == server.Starting) { | 176 | if server.Current == server.Operational && server.State == server.Running { |
613 | 202 | err = server.ShutdownOperationalServer() | 177 | err := server.ShutdownOperationalServer() |
614 | 203 | if err != nil { | 178 | if err != nil { |
615 | 204 | log.Printf("Error stopping Operational portal: %v", err) | 179 | log.Printf("Error stopping Operational portal: %v", err) |
616 | 205 | } | 180 | } |
617 | 206 | } | 181 | } |
618 | 207 | } | 182 | } |
619 | 208 | 183 | ||
625 | 209 | // LoadPreConfig returns a PreConfig based on the pre-config.json, if present, and an error to indicate | 184 | func (s *Service) connectToAp(ssid, pwd string) error { |
626 | 210 | // possible json unmarshal failure | 185 | s.exitManagingMode() |
627 | 211 | func LoadPreConfig() (*PreConfig, error) { | 186 | |
628 | 212 | config := &PreConfig{} | 187 | log.Printf("Trying to connect") |
629 | 213 | content, err := ioutil.ReadFile(PreConfigFile) | 188 | _, _, err := s.cnetman.ConnectToAp(ssid, pwd, s.devicePath) |
630 | 189 | if err != nil { | ||
631 | 190 | log.Printf("Failed connecting to %v: %v", ssid, err) | ||
632 | 191 | s.enterManagingMode() | ||
633 | 192 | } | ||
634 | 193 | return err | ||
635 | 194 | } | ||
636 | 195 | |||
637 | 196 | func (s *Service) doWifiScan() { | ||
638 | 197 | s.exitManagingMode() | ||
639 | 198 | |||
640 | 199 | // wait a bit so we get ssids when scanning | ||
641 | 200 | // TODO with a modern enough NM we could Rescan and wait for the | ||
642 | 201 | // LastScan property to be signalled. But that requires NM >= 1.12. | ||
643 | 202 | time.Sleep(waitBeforeScanSec * time.Second) | ||
644 | 203 | |||
645 | 204 | // Do some retries, just in case | ||
646 | 205 | for i := 0; i < 5; i++ { | ||
647 | 206 | ssids, _, _ := s.cnetman.Ssids(s.devicePath) | ||
648 | 207 | if len(ssids) == 0 { | ||
649 | 208 | log.Print("no ssids!!") | ||
650 | 209 | time.Sleep(waitBeforeScanSec * time.Second) | ||
651 | 210 | continue | ||
652 | 211 | } | ||
653 | 212 | |||
654 | 213 | ssidNames := make([]string, len(ssids)) | ||
655 | 214 | for i, ssid := range ssids { | ||
656 | 215 | ssidNames[i] = ssid.Ssid | ||
657 | 216 | } | ||
658 | 217 | s.ssidLs.SetSsidList(ssidNames) | ||
659 | 218 | break | ||
660 | 219 | } | ||
661 | 220 | |||
662 | 221 | s.enterManagingMode() | ||
663 | 222 | } | ||
664 | 223 | |||
665 | 224 | func (s *Service) enterOperatingMode() { | ||
666 | 225 | s.startOperationalServer() | ||
667 | 226 | } | ||
668 | 227 | |||
669 | 228 | func (s *Service) exitOperatingMode() { | ||
670 | 229 | s.stopOperationalServer() | ||
671 | 230 | } | ||
672 | 231 | |||
673 | 232 | func (s *Service) enterManagingMode() { | ||
674 | 233 | log.Print("Entering MANAGEMENT mode") | ||
675 | 234 | |||
676 | 235 | // Remove all connections to APs that could re-connect | ||
677 | 236 | s.cnetman.DeleteWifiConnections(netman.StaWifiConn) | ||
678 | 237 | if err := s.cnetman.DisconnectDevice(s.devicePath); err != nil { | ||
679 | 238 | log.Printf("Error when disconnecting device: %v", err) | ||
680 | 239 | } | ||
681 | 240 | |||
682 | 241 | // wait a bit so we get ssids when scanning | ||
683 | 242 | // TODO with a modern enough NM we could Rescan and wait for the | ||
684 | 243 | // LastScan property to be signalled. But that requires NM >= 1.12. | ||
685 | 244 | time.Sleep(waitBeforeScanSec * time.Second) | ||
686 | 245 | |||
687 | 246 | // Do an scan of currently available SSIDs | ||
688 | 247 | ssids, _, _ := s.cnetman.Ssids(s.devicePath) | ||
689 | 248 | if len(ssids) == 0 { | ||
690 | 249 | log.Print("No SSIDs found when scanning") | ||
691 | 250 | } | ||
692 | 251 | ssidNames := make([]string, len(ssids)) | ||
693 | 252 | for i, ssid := range ssids { | ||
694 | 253 | ssidNames[i] = ssid.Ssid | ||
695 | 254 | } | ||
696 | 255 | s.ssidLs.SetSsidList(ssidNames) | ||
697 | 256 | |||
698 | 257 | ip, subnet, err := net.ParseCIDR(s.config.Wifi.Subnet) | ||
699 | 258 | if err != nil { | ||
700 | 259 | log.Printf("Error while parsing %s: %v", s.config.Wifi.Subnet, err) | ||
701 | 260 | return | ||
702 | 261 | } | ||
703 | 262 | if ip.To4() == nil { | ||
704 | 263 | log.Printf("Error: %v is not an IPv4 subnet", s.config.Wifi.Subnet) | ||
705 | 264 | return | ||
706 | 265 | } | ||
707 | 266 | maskedBits, _ := subnet.Mask.Size() | ||
708 | 267 | if maskedBits > 28 { | ||
709 | 268 | log.Printf("Error: %v needs a mask smaller than 29", s.config.Wifi.Subnet) | ||
710 | 269 | return | ||
711 | 270 | } | ||
712 | 271 | apIP := ip.Mask(subnet.Mask) | ||
713 | 272 | apIP[3] |= 1 | ||
714 | 273 | ipStr := apIP.String() | ||
715 | 274 | |||
716 | 275 | log.Printf("starting AP with name %s, subnet %s/%d", | ||
717 | 276 | s.config.Wifi.Ssid, ipStr, maskedBits) | ||
718 | 277 | s.apSettingsPath, s.apActiveConnPath, err = | ||
719 | 278 | s.cnetman.CreateAccessPoint(s.config.Wifi.Ssid, | ||
720 | 279 | s.config.Wifi.Passphrase, s.config.Wifi.Interface, | ||
721 | 280 | ipStr, uint(maskedBits)) | ||
722 | 281 | if err != nil { | ||
723 | 282 | log.Printf("Cannot create AP: %v", err) | ||
724 | 283 | return | ||
725 | 284 | } | ||
726 | 285 | |||
727 | 286 | s.startManagementServer() | ||
728 | 287 | |||
729 | 288 | // DHCP range is X.X.X.10 - X.X.X.15 | ||
730 | 289 | dhcpFirst := make(net.IP, net.IPv4len) | ||
731 | 290 | dhcpLast := make(net.IP, net.IPv4len) | ||
732 | 291 | copy(dhcpFirst, subnet.IP) | ||
733 | 292 | copy(dhcpLast, subnet.IP) | ||
734 | 293 | dhcpFirst[3] = 10 | ||
735 | 294 | dhcpLast[3] = 15 | ||
736 | 295 | dhcpRange := dhcpFirst.String() + "," + dhcpLast.String() | ||
737 | 296 | // Run DNS and DHCP server. The DNS will always return the interface | ||
738 | 297 | // address for any query, redirecting all request to our server. | ||
739 | 298 | // With "--user=root --group=" we get the right code path so we avoid | ||
740 | 299 | // problems when changing user/group from the snap. | ||
741 | 300 | s.dnsmasqCmd = | ||
742 | 301 | exec.Command("dnsmasq", "--conf-file", "--no-hosts", | ||
743 | 302 | "--keep-in-foreground", "--bind-interfaces", | ||
744 | 303 | "--except-interface=lo", "--clear-on-reload", | ||
745 | 304 | "--strict-order", "--listen-address="+ipStr, | ||
746 | 305 | "--dhcp-range="+dhcpRange+",60m", "--dhcp-lease-max=50", | ||
747 | 306 | "--address=/#/"+ipStr, | ||
748 | 307 | "--pid-file=/tmp/dnsmasq-wlp3s0.pid", | ||
749 | 308 | "--user=root", "--group=") | ||
750 | 309 | log.Printf("Running %v", s.dnsmasqCmd) | ||
751 | 310 | err = s.dnsmasqCmd.Start() | ||
752 | 214 | if err != nil { | 311 | if err != nil { |
754 | 215 | return config, err | 312 | log.Fatalf("Cannot run %v: %v", s.dnsmasqCmd, err) |
755 | 216 | } | 313 | } |
757 | 217 | err = json.Unmarshal(content, config) | 314 | |
758 | 315 | // Redirect all http to our server | ||
759 | 316 | s.iptblForwArgs = []string{"PREROUTING", "-p", "tcp", "-m", "tcp", | ||
760 | 317 | "-s", subnet.String(), | ||
761 | 318 | "--dport", "80", "-j", "DNAT", | ||
762 | 319 | "--to-destination", | ||
763 | 320 | ipStr + ":" + strconv.Itoa(s.config.Portal.Port)} | ||
764 | 321 | iptblArgs := append([]string{"-t", "nat", "-A"}, s.iptblForwArgs...) | ||
765 | 322 | iptblCmd := exec.Command("iptables", iptblArgs...) | ||
766 | 323 | log.Printf("Running %v", iptblCmd) | ||
767 | 324 | err = iptblCmd.Run() | ||
768 | 218 | if err != nil { | 325 | if err != nil { |
770 | 219 | return config, err | 326 | log.Printf("Error running %v: %v", iptblCmd, err) |
771 | 220 | } | 327 | } |
772 | 221 | return config, nil | ||
773 | 222 | } | 328 | } |
774 | 223 | 329 | ||
778 | 224 | // SetDefaults creates the run time configuration based on wifi-ap and the pre-config.json | 330 | func (s *Service) exitManagingMode() { |
779 | 225 | // configuration file, if any. The configuration is returned with an error. | 331 | log.Printf("Exiting MANAGEMENT mode") |
780 | 226 | func (c *Client) SetDefaults(cw wifiap.Operations, config *PreConfig) error { | 332 | |
781 | 333 | if s.dnsmasqCmd != nil { | ||
782 | 334 | log.Printf("Stopping dnsmasq") | ||
783 | 335 | s.dnsmasqCmd.Process.Signal(syscall.SIGTERM) | ||
784 | 336 | s.dnsmasqCmd.Process.Wait() | ||
785 | 337 | s.dnsmasqCmd = nil | ||
786 | 338 | } | ||
787 | 339 | |||
788 | 340 | // Remove http redirection | ||
789 | 341 | iptblArgs := append([]string{"-t", "nat", "-D"}, s.iptblForwArgs...) | ||
790 | 342 | iptblCmd := exec.Command("iptables", iptblArgs...) | ||
791 | 343 | log.Printf("Running %v", iptblCmd) | ||
792 | 344 | err := iptblCmd.Run() | ||
793 | 227 | if err != nil { | 345 | if err != nil { |
809 | 228 | log.Printf("SetDefaults: preconfig unmarshall errorr: %v", err) | 346 | log.Printf("Error running %v: %v", iptblCmd, err) |
795 | 229 | } | ||
796 | 230 | ap, errShow := cw.Show() | ||
797 | 231 | if errShow != nil { | ||
798 | 232 | log.Printf("SetDefaults: wifi-ap.Show err: %v", errShow) | ||
799 | 233 | } | ||
800 | 234 | if ap["wifi.security-passphrase"] != config.Passphrase { | ||
801 | 235 | if len(config.Passphrase) > 0 { | ||
802 | 236 | err = cw.SetPassphrase(config.Passphrase) | ||
803 | 237 | log.Print("SetDefaults wifi-ap passphrase being set") | ||
804 | 238 | if err != nil { | ||
805 | 239 | log.Printf("SetDefaults: passphrase err: %v", err) | ||
806 | 240 | return err | ||
807 | 241 | } | ||
808 | 242 | } | ||
810 | 243 | } | 347 | } |
817 | 244 | if len(config.Password) > 0 { | 348 | |
818 | 245 | fmt.Println("== wifi-connect/SetDefaults portal password being set") | 349 | s.cnetman.DeleteWifiConnections(netman.ApWifiConn) |
819 | 246 | _, err = utils.HashIt(config.Password) | 350 | if err := s.cnetman.DisconnectDevice(s.devicePath); err != nil { |
820 | 247 | if err != nil { | 351 | log.Printf("Error when disconnecting device: %v", err) |
821 | 248 | log.Printf("SetDefaults: password err: %v", err) | 352 | } |
822 | 249 | return err | 353 | |
823 | 354 | s.stopManagementServer() | ||
824 | 355 | } | ||
825 | 356 | |||
826 | 357 | // event: NM is up event received | ||
827 | 358 | func (s *Service) netManUpEv(current serviceState) serviceState { | ||
828 | 359 | log.Printf("NetworkManager is up") | ||
829 | 360 | if current != stStarting { | ||
830 | 361 | // Spurious signal? | ||
831 | 362 | return current | ||
832 | 363 | } | ||
833 | 364 | |||
834 | 365 | err := s.setDefaults() | ||
835 | 366 | if err != nil { | ||
836 | 367 | log.Printf("setDefaults error: %v", err) | ||
837 | 368 | } | ||
838 | 369 | s.cleanUpPrevRuns() | ||
839 | 370 | |||
840 | 371 | var state serviceState | ||
841 | 372 | if s.config.ConfigWifiOnce && | ||
842 | 373 | utils.FlagFileExists(utils.WifiConfigDoneFlagFile) { | ||
843 | 374 | state = stOperating | ||
844 | 375 | s.enterOperatingMode() | ||
845 | 376 | } else { | ||
846 | 377 | // Wait some time on first run to let the wifi connect | ||
847 | 378 | // TODO wait for DBus signal instead | ||
848 | 379 | time.Sleep(10 * time.Second) | ||
849 | 380 | if s.isConnectedToAnAP() { | ||
850 | 381 | state = stOperating | ||
851 | 382 | s.enterOperatingMode() | ||
852 | 383 | } else { | ||
853 | 384 | state = stManaging | ||
854 | 385 | s.enterManagingMode() | ||
855 | 250 | } | 386 | } |
856 | 251 | } | 387 | } |
859 | 252 | if config.Operational { | 388 | return state |
860 | 253 | log.Print("SetDefaults: operational portal is enabled") | 389 | } |
861 | 390 | |||
862 | 391 | func (s *Service) stopServices(state serviceState) { | ||
863 | 392 | switch state { | ||
864 | 393 | case stManaging: | ||
865 | 394 | s.exitManagingMode() | ||
866 | 395 | case stOperating: | ||
867 | 396 | s.exitOperatingMode() | ||
868 | 397 | } | ||
869 | 398 | } | ||
870 | 399 | |||
871 | 400 | // event: NM is down event received | ||
872 | 401 | func (s *Service) netManDownEv(current serviceState) serviceState { | ||
873 | 402 | log.Printf("NetworkManager is down") | ||
874 | 403 | s.stopServices(current) | ||
875 | 404 | return stStarting | ||
876 | 405 | } | ||
877 | 406 | |||
878 | 407 | // event: user sends command to connect | ||
879 | 408 | func (s *Service) userConnectEv(current serviceState, | ||
880 | 409 | ssid, password string) serviceState { | ||
881 | 410 | |||
882 | 411 | if current != stManaging { | ||
883 | 412 | log.Printf("Connect event, but not in managing mode") | ||
884 | 413 | return current | ||
885 | 414 | } | ||
886 | 415 | |||
887 | 416 | // connectToAp exits managing mode on success | ||
888 | 417 | err := s.connectToAp(ssid, password) | ||
889 | 418 | if err != nil { | ||
890 | 419 | log.Printf("Failed to connect: %v", err) | ||
891 | 420 | return current | ||
892 | 254 | } | 421 | } |
895 | 255 | if config.NoResetCreds { | 422 | |
896 | 256 | log.Print("SetDefaults: reset creds requirement is disabled") | 423 | utils.WriteFlagFile(utils.WifiConfigDoneFlagFile) |
897 | 424 | s.enterOperatingMode() | ||
898 | 425 | |||
899 | 426 | return stOperating | ||
900 | 427 | } | ||
901 | 428 | |||
902 | 429 | // event: user sends command to refresh wifi list | ||
903 | 430 | func (s *Service) userRefreshEv(current serviceState) { | ||
904 | 431 | if current != stManaging { | ||
905 | 432 | log.Printf("Refresh event, but not in managing mode") | ||
906 | 257 | } | 433 | } |
908 | 258 | return nil | 434 | |
909 | 435 | s.doWifiScan() | ||
910 | 436 | } | ||
911 | 437 | |||
912 | 438 | // event: user sends command to disconnect | ||
913 | 439 | func (s *Service) userDisconnectEv(current serviceState) serviceState { | ||
914 | 440 | if current != stOperating { | ||
915 | 441 | log.Printf("Disconnect event, but not in operating mode") | ||
916 | 442 | return current | ||
917 | 443 | } | ||
918 | 444 | |||
919 | 445 | s.exitOperatingMode() | ||
920 | 446 | utils.RemoveFlagFile(utils.WifiConfigDoneFlagFile) | ||
921 | 447 | s.enterManagingMode() | ||
922 | 448 | |||
923 | 449 | return stManaging | ||
924 | 450 | } | ||
925 | 451 | |||
926 | 452 | // Start state machine | ||
927 | 453 | func (s *Service) Start() { | ||
928 | 454 | |||
929 | 455 | chDbus := make(chan *dbus.Signal, 30) | ||
930 | 456 | defer close(chDbus) | ||
931 | 457 | if err := s.cnetman.RegisterForNameOwnerChanged(chDbus); err != nil { | ||
932 | 458 | log.Printf("Error: cannot register for NameOwnerChanged: %v", err) | ||
933 | 459 | os.Exit(1) | ||
934 | 460 | } | ||
935 | 461 | defer s.cnetman.UnregisterForNameOwnerChanged(chDbus) | ||
936 | 462 | |||
937 | 463 | state := stStarting | ||
938 | 464 | nmUp, err := s.cnetman.CheckNetworkManagerRunning() | ||
939 | 465 | if nmUp == false { | ||
940 | 466 | log.Printf("Waiting for NetworkManager service: %v", err) | ||
941 | 467 | } else { | ||
942 | 468 | state = s.netManUpEv(state) | ||
943 | 469 | } | ||
944 | 470 | spuriousSignal := false | ||
945 | 471 | |||
946 | 472 | MainLoop: | ||
947 | 473 | for { | ||
948 | 474 | if spuriousSignal == false { | ||
949 | 475 | log.Printf("State is %v", state) | ||
950 | 476 | } else { | ||
951 | 477 | spuriousSignal = false | ||
952 | 478 | } | ||
953 | 479 | select { | ||
954 | 480 | case uEv := <-s.chUserEvs: | ||
955 | 481 | log.Print("Event received: ", uEv.EvType) | ||
956 | 482 | switch uEv.EvType { | ||
957 | 483 | case server.UserEventConnect: | ||
958 | 484 | state = s.userConnectEv(state, | ||
959 | 485 | uEv.Params[server.UeConnSsid], | ||
960 | 486 | uEv.Params[server.UeConnPassword]) | ||
961 | 487 | case server.UserEventRefresh: | ||
962 | 488 | s.userRefreshEv(state) | ||
963 | 489 | case server.UserEventDisconnect: | ||
964 | 490 | state = s.userDisconnectEv(state) | ||
965 | 491 | } | ||
966 | 492 | case sig := <-s.chSigs: | ||
967 | 493 | log.Printf("Exiting on %v signal", sig) | ||
968 | 494 | break MainLoop | ||
969 | 495 | case dbusSig := <-chDbus: | ||
970 | 496 | if dbusSig.Name == "org.freedesktop.DBus.NameOwnerChanged" && | ||
971 | 497 | dbusSig.Body[0].(string) == "org.freedesktop.NetworkManager" { | ||
972 | 498 | // Check if there is a current owner | ||
973 | 499 | if dbusSig.Body[2].(string) == "" { | ||
974 | 500 | state = s.netManDownEv(state) | ||
975 | 501 | } else { | ||
976 | 502 | state = s.netManUpEv(state) | ||
977 | 503 | } | ||
978 | 504 | } else { | ||
979 | 505 | // We do not want to print the state for signals | ||
980 | 506 | // that we are not interested in... | ||
981 | 507 | spuriousSignal = true | ||
982 | 508 | } | ||
983 | 509 | } | ||
984 | 510 | } | ||
985 | 511 | |||
986 | 512 | s.stopServices(state) | ||
987 | 259 | } | 513 | } |
988 | diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go | |||
989 | index 07fe109..2af53e8 100644 | |||
990 | --- a/daemon/daemon_test.go | |||
991 | +++ b/daemon/daemon_test.go | |||
992 | @@ -18,225 +18,13 @@ | |||
993 | 18 | package daemon | 18 | package daemon |
994 | 19 | 19 | ||
995 | 20 | import ( | 20 | import ( |
996 | 21 | "fmt" | ||
997 | 22 | "io/ioutil" | ||
998 | 23 | "net/http" | ||
999 | 24 | "os" | ||
1000 | 25 | "strings" | ||
1001 | 26 | "testing" | 21 | "testing" |
1002 | 27 | |||
1003 | 28 | "launchpad.net/wifi-connect/utils" | ||
1004 | 29 | ) | 22 | ) |
1005 | 30 | 23 | ||
1217 | 31 | func TestManualFlagPath(t *testing.T) { | 24 | func TestGetService(t *testing.T) { |
1218 | 32 | mfpInit := "mfp" | 25 | // cfg := utils.Config{Wifi: utils.WifiConfig{Ssid: "myssid"}, ConfigWifiOnce: false} |
1219 | 33 | client := GetClient() | 26 | // service := GetService(&cfg) |
1220 | 34 | client.SetManualFlagPath(mfpInit) | 27 | // if service == nil { |
1221 | 35 | mfp := client.GetManualFlagPath() | 28 | // t.Error("Cannot get service") |
1222 | 36 | if mfp != mfpInit { | 29 | // } |
1012 | 37 | t.Errorf("ManualFlag path should be %s but is %s\n", mfpInit, mfp) | ||
1013 | 38 | } | ||
1014 | 39 | } | ||
1015 | 40 | |||
1016 | 41 | func TestWaitFlagPath(t *testing.T) { | ||
1017 | 42 | wfpInit := "wfp" | ||
1018 | 43 | client := GetClient() | ||
1019 | 44 | client.SetWaitFlagPath(wfpInit) | ||
1020 | 45 | wfp := client.GetWaitFlagPath() | ||
1021 | 46 | if wfp != wfpInit { | ||
1022 | 47 | t.Errorf("WaitFlag path should be %s but is %s", wfp, wfpInit) | ||
1023 | 48 | } | ||
1024 | 49 | } | ||
1025 | 50 | |||
1026 | 51 | func TestState(t *testing.T) { | ||
1027 | 52 | client := GetClient() | ||
1028 | 53 | client.SetState(MANAGING) | ||
1029 | 54 | client.SetPreviousState(STARTING) | ||
1030 | 55 | ps := client.GetPreviousState() | ||
1031 | 56 | if ps != STARTING { | ||
1032 | 57 | t.Errorf("Previous state should be %d but is %d", STARTING, ps) | ||
1033 | 58 | } | ||
1034 | 59 | s := client.GetState() | ||
1035 | 60 | if s != MANAGING { | ||
1036 | 61 | t.Errorf("State should be %d but is %d", MANAGING, s) | ||
1037 | 62 | } | ||
1038 | 63 | } | ||
1039 | 64 | |||
1040 | 65 | func TestCheckWaitApConnect(t *testing.T) { | ||
1041 | 66 | client := GetClient() | ||
1042 | 67 | wfp := "thispathnevershouldexist" | ||
1043 | 68 | client.SetWaitFlagPath(wfp) | ||
1044 | 69 | if client.CheckWaitApConnect() { | ||
1045 | 70 | t.Errorf("CheckWaitApConnect returns true but should return false") | ||
1046 | 71 | } | ||
1047 | 72 | wfp = "../static/tests/waitFile" | ||
1048 | 73 | client.SetWaitFlagPath(wfp) | ||
1049 | 74 | if !client.CheckWaitApConnect() { | ||
1050 | 75 | t.Errorf("CheckWaitApConnect returns false but should return true") | ||
1051 | 76 | } | ||
1052 | 77 | } | ||
1053 | 78 | |||
1054 | 79 | func TestManualMode(t *testing.T) { | ||
1055 | 80 | mfp := "thisfileshouldneverexist" | ||
1056 | 81 | client := GetClient() | ||
1057 | 82 | client.SetManualFlagPath(mfp) | ||
1058 | 83 | client.SetState(MANUAL) | ||
1059 | 84 | if client.ManualMode() { | ||
1060 | 85 | t.Errorf("ManualMode returns true but should return false") | ||
1061 | 86 | } | ||
1062 | 87 | if client.GetState() != STARTING { | ||
1063 | 88 | t.Errorf("ManualMode should set state to STARTING when not in manual mode but does not") | ||
1064 | 89 | } | ||
1065 | 90 | mfp = "../static/tests/manualMode" | ||
1066 | 91 | client.SetManualFlagPath(mfp) | ||
1067 | 92 | client.SetState(STARTING) | ||
1068 | 93 | if !client.ManualMode() { | ||
1069 | 94 | t.Errorf("ManualMode returns false but should return true") | ||
1070 | 95 | } | ||
1071 | 96 | if client.GetState() != MANUAL { | ||
1072 | 97 | t.Errorf("ManualMode should set state to MANUAL when in manual mode but does not") | ||
1073 | 98 | } | ||
1074 | 99 | } | ||
1075 | 100 | |||
1076 | 101 | type mockWifiap struct{} | ||
1077 | 102 | |||
1078 | 103 | func (mock *mockWifiap) Do(req *http.Request) (*http.Response, error) { | ||
1079 | 104 | fmt.Println("==== MY do called") | ||
1080 | 105 | url := req.URL.String() | ||
1081 | 106 | if url != "http://unix/v1/configuration" { | ||
1082 | 107 | return nil, fmt.Errorf("Not valid request URL: %v", url) | ||
1083 | 108 | } | ||
1084 | 109 | |||
1085 | 110 | if req.Method != "GET" { | ||
1086 | 111 | return nil, fmt.Errorf("Method is not valid. Expected GET, got %v", req.Method) | ||
1087 | 112 | } | ||
1088 | 113 | |||
1089 | 114 | rawBody := `{"result":{ | ||
1090 | 115 | "debug":false, | ||
1091 | 116 | "dhcp.lease-time": "12h", | ||
1092 | 117 | "dhcp.range-start": "10.0.60.2", | ||
1093 | 118 | "dhcp.range-stop": "10.0.60.199", | ||
1094 | 119 | "disabled": true, | ||
1095 | 120 | "share.disabled": false, | ||
1096 | 121 | "share-network-interface": "tun0", | ||
1097 | 122 | "wifi-address": "10.0.60.1", | ||
1098 | 123 | "wifi.channel": "6", | ||
1099 | 124 | "wifi.hostapd-driver": "nl80211", | ||
1100 | 125 | "wifi.interface": "wlan0", | ||
1101 | 126 | "wifi.interface-mode": "direct", | ||
1102 | 127 | "wifi.netmask": "255.255.255.0", | ||
1103 | 128 | "wifi.operation-mode": "g", | ||
1104 | 129 | "wifi.security": " | ||
1105 | 130 | "wifi.security-passphrase": "passphrase123", | ||
1106 | 131 | "wifi.ssid": "AP"},"status":"OK","status-code":200,""sync"}` | ||
1107 | 132 | |||
1108 | 133 | response := http.Response{ | ||
1109 | 134 | StatusCode: 200, | ||
1110 | 135 | Status: "200 OK", | ||
1111 | 136 | Body: ioutil.NopCloser(strings.NewReader(rawBody)), | ||
1112 | 137 | } | ||
1113 | 138 | |||
1114 | 139 | return &response, nil | ||
1115 | 140 | } | ||
1116 | 141 | |||
1117 | 142 | func (mock *mockWifiap) Show() (map[string]interface{}, error) { | ||
1118 | 143 | wifiAp := make(map[string]interface{}) | ||
1119 | 144 | wifiAp["wifi.security-passphrase"] = "randompassphrase" | ||
1120 | 145 | return wifiAp, nil | ||
1121 | 146 | } | ||
1122 | 147 | |||
1123 | 148 | func (mock *mockWifiap) Enabled() (bool, error) { | ||
1124 | 149 | return true, nil | ||
1125 | 150 | } | ||
1126 | 151 | |||
1127 | 152 | func (mock *mockWifiap) Enable() error { | ||
1128 | 153 | return nil | ||
1129 | 154 | } | ||
1130 | 155 | func (mock *mockWifiap) Disable() error { | ||
1131 | 156 | return nil | ||
1132 | 157 | } | ||
1133 | 158 | func (mock *mockWifiap) SetSsid(s string) error { | ||
1134 | 159 | return nil | ||
1135 | 160 | } | ||
1136 | 161 | |||
1137 | 162 | func (mock *mockWifiap) SetPassphrase(p string) error { | ||
1138 | 163 | return nil | ||
1139 | 164 | } | ||
1140 | 165 | |||
1141 | 166 | func (mock *mockWifiap) Set(map[string]interface{}) error { | ||
1142 | 167 | return nil | ||
1143 | 168 | } | ||
1144 | 169 | |||
1145 | 170 | func TestLoadPreConfig(t *testing.T) { | ||
1146 | 171 | PreConfigFile = "../static/tests/pre-config0.json" | ||
1147 | 172 | config, err := LoadPreConfig() | ||
1148 | 173 | if err != nil { | ||
1149 | 174 | t.Errorf("Unexpected error using LoadPreConfig: %s", err) | ||
1150 | 175 | } | ||
1151 | 176 | if config.Passphrase != "abcdefghijklmnop" { | ||
1152 | 177 | t.Errorf("Passphrase of %s expected but got %s:", "abcdefghijklmnop", config.Passphrase) | ||
1153 | 178 | } | ||
1154 | 179 | if !config.Operational { | ||
1155 | 180 | t.Errorf("portal.operational was set to false but the loaded config is %t", config.Operational) | ||
1156 | 181 | } | ||
1157 | 182 | if !config.NoResetCreds { | ||
1158 | 183 | t.Errorf("portal.no-reset-creds was set to true but the loaded config is %t", config.NoResetCreds) | ||
1159 | 184 | } | ||
1160 | 185 | |||
1161 | 186 | } | ||
1162 | 187 | |||
1163 | 188 | func TestSetDefaults(t *testing.T) { | ||
1164 | 189 | client := GetClient() | ||
1165 | 190 | PreConfigFile = "../static/tests/pre-config0.json" | ||
1166 | 191 | hfp := "/tmp/hash" | ||
1167 | 192 | if _, err := os.Stat(hfp); err == nil { | ||
1168 | 193 | err = os.Remove(hfp) | ||
1169 | 194 | if err != nil { | ||
1170 | 195 | t.Errorf("Could not remove previous file version") | ||
1171 | 196 | } | ||
1172 | 197 | } | ||
1173 | 198 | config, _ := LoadPreConfig() | ||
1174 | 199 | utils.SetHashFile(hfp) | ||
1175 | 200 | client.SetDefaults(&mockWifiap{}, config) | ||
1176 | 201 | expectedPassphrase := "abcdefghijklmnop" | ||
1177 | 202 | expectedPassword := "qwerzxcv" | ||
1178 | 203 | if config.Passphrase != expectedPassphrase { | ||
1179 | 204 | t.Errorf("SetDefaults: Preconfig passphrase should be %s but is %s", expectedPassphrase, config.Passphrase) | ||
1180 | 205 | } | ||
1181 | 206 | if os.IsNotExist(err) { | ||
1182 | 207 | t.Errorf("SetDefaults should have created %s but did not", hfp) | ||
1183 | 208 | } | ||
1184 | 209 | res, _ := utils.MatchingHash(expectedPassword) | ||
1185 | 210 | if !res { | ||
1186 | 211 | t.Errorf("SetDefaults: Preconfig password hash did not match actual") | ||
1187 | 212 | } | ||
1188 | 213 | if !config.Operational { | ||
1189 | 214 | t.Errorf("SetDefaults: Preconfig portal.operational should be true (set) but is %t", config.Operational) | ||
1190 | 215 | } | ||
1191 | 216 | if !config.NoResetCreds { | ||
1192 | 217 | t.Errorf("SetDefaults: Preconfig portal.no-reset-creds should be true (set) but is %t", config.NoResetCreds) | ||
1193 | 218 | } | ||
1194 | 219 | |||
1195 | 220 | if _, err := os.Stat(hfp); err == nil { | ||
1196 | 221 | err = os.Remove(hfp) | ||
1197 | 222 | if err != nil { | ||
1198 | 223 | t.Errorf("Could not remove previous file version") | ||
1199 | 224 | } | ||
1200 | 225 | } | ||
1201 | 226 | PreConfigFile = "../static/tests/pre-config1.json" | ||
1202 | 227 | config, _ = LoadPreConfig() | ||
1203 | 228 | client.SetDefaults(&mockWifiap{}, config) | ||
1204 | 229 | if len(config.Passphrase) > 0 { | ||
1205 | 230 | t.Errorf("SetDefaults: Preconfig passphrase was not set but is %s", config.Passphrase) | ||
1206 | 231 | } | ||
1207 | 232 | res2, _ := utils.MatchingHash(expectedPassword) | ||
1208 | 233 | if res2 { | ||
1209 | 234 | t.Errorf("SetDefaults: Preconfig password was not set, but the hash matched") | ||
1210 | 235 | } | ||
1211 | 236 | if config.Operational { | ||
1212 | 237 | t.Errorf("SetDefaults: Preconfig portal.no-operational should be false (unset) but is %t", config.Operational) | ||
1213 | 238 | } | ||
1214 | 239 | if config.NoResetCreds { | ||
1215 | 240 | t.Errorf("SetDefaults: Preconfig portal.no-reset-creds should be false (unnset) but is %t", config.NoResetCreds) | ||
1216 | 241 | } | ||
1223 | 242 | } | 30 | } |
1224 | diff --git a/dependencies.tsv b/dependencies.tsv | |||
1225 | index e5f04d8..e9c02fa 100644 | |||
1226 | --- a/dependencies.tsv | |||
1227 | +++ b/dependencies.tsv | |||
1228 | @@ -1,7 +1,7 @@ | |||
1233 | 1 | github.com/go-yaml/yaml git cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b 2017-04-07T18:21:22Z | 1 | github.com/godbus/dbus git 06fc4b473149e499166adbb9e31c7365a8ea146f 2020-02-14T23:16:04Z |
1234 | 2 | github.com/godbus/dbus git fe0e1d54eaeda11a9979659a8d32f459e88bee75 2017-03-03T19:03:06Z | 2 | github.com/gorilla/mux git 75dcda0896e109a2a22c9315bca3bb21b87b2ba5 2020-01-12T19:17:43Z |
1235 | 3 | github.com/gorilla/context git 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 2016-08-17T18:46:32Z | 3 | golang.org/x/crypto git 8b5121be2f68d8fc40bb06467003bdde1040a094 2020-01-24T22:56:46Z |
1236 | 4 | github.com/gorilla/mux git 757bef944d0f21880861c2dd9c871ca543023cba 2016-09-20T23:08:13Z | 4 | golang.org/x/net git 13f9640d40b9cc418fb53703dfbd177679788ceb 2019-10-04T11:05:52Z |
1237 | 5 | golang.org/x/text git 342b2e1fbaa52c93f31447ad2c6abc048c63e475 2019-04-25T21:42:06Z | ||
1238 | 5 | github.com/presotto/go-mdns-sd git 343772046ec1b3840b8591799a7bbcc68ea47b4b 2015-11-03T06:16:58Z | 6 | github.com/presotto/go-mdns-sd git 343772046ec1b3840b8591799a7bbcc68ea47b4b 2015-11-03T06:16:58Z |
1241 | 6 | github.com/reiver/go-oi git 431c83978379297f04f85f6eb94f129f25ab741d 2016-03-25T06:16:15Z | 7 | gopkg.in/check.v1 git 4f90aeace3a26ad7021961c297b22c42160c7b25 2016-01-05T16:49:36Z |
1240 | 7 | github.com/reiver/go-telnet git 6b696f32801a8f8dd07947f1e1fdb1a7dc4766ff 2016-03-30T05:09:16Z | ||
1242 | diff --git a/hooks/configure.go b/hooks/configure.go | |||
1243 | index 80cfe91..7775eab 100644 | |||
1244 | --- a/hooks/configure.go | |||
1245 | +++ b/hooks/configure.go | |||
1246 | @@ -20,15 +20,14 @@ | |||
1247 | 20 | package main | 20 | package main |
1248 | 21 | 21 | ||
1249 | 22 | import ( | 22 | import ( |
1250 | 23 | "encoding/json" | ||
1251 | 24 | "io/ioutil" | ||
1252 | 25 | "os" | 23 | "os" |
1253 | 26 | "os/exec" | 24 | "os/exec" |
1254 | 27 | "path/filepath" | 25 | "path/filepath" |
1255 | 26 | "strconv" | ||
1256 | 28 | "strings" | 27 | "strings" |
1257 | 29 | "time" | 28 | "time" |
1258 | 30 | 29 | ||
1260 | 31 | "launchpad.net/wifi-connect/daemon" | 30 | "launchpad.net/wifi-connect/utils" |
1261 | 32 | ) | 31 | ) |
1262 | 33 | 32 | ||
1263 | 34 | // logFile is the file into which log msgs go. Needed because snap hook stdout | 33 | // logFile is the file into which log msgs go. Needed because snap hook stdout |
1264 | @@ -65,7 +64,6 @@ func (g *Get) SnapGet(key string) (string, error) { | |||
1265 | 65 | return "", err | 64 | return "", err |
1266 | 66 | } | 65 | } |
1267 | 67 | return strings.TrimSpace(string(out)), nil | 66 | return strings.TrimSpace(string(out)), nil |
1268 | 68 | |||
1269 | 69 | } | 67 | } |
1270 | 70 | 68 | ||
1271 | 71 | // snapGetStr wraps SnapGet for string types and verifies the snap var is valid | 69 | // snapGetStr wraps SnapGet for string types and verifies the snap var is valid |
1272 | @@ -75,12 +73,28 @@ func (c *Client) snapGetStr(key string, target *string) { | |||
1273 | 75 | return | 73 | return |
1274 | 76 | } | 74 | } |
1275 | 77 | if len(val) == 0 { | 75 | if len(val) == 0 { |
1277 | 78 | log("configure error: key %s exists but has zero length" + key) | 76 | log("configure error: key " + key + " exists but has zero length") |
1278 | 79 | return | 77 | return |
1279 | 80 | } | 78 | } |
1280 | 81 | *target = val | 79 | *target = val |
1281 | 82 | } | 80 | } |
1282 | 83 | 81 | ||
1283 | 82 | func (c *Client) snapGetInt(key string, target *int) { | ||
1284 | 83 | val, err := c.getter.SnapGet(key) | ||
1285 | 84 | if err != nil { | ||
1286 | 85 | return | ||
1287 | 86 | } | ||
1288 | 87 | if len(val) == 0 { | ||
1289 | 88 | log("configure error: key " + key + " exists but has zero length") | ||
1290 | 89 | return | ||
1291 | 90 | } | ||
1292 | 91 | *target, err = strconv.Atoi(val) | ||
1293 | 92 | if err != nil { | ||
1294 | 93 | log("bad integer: " + err.Error()) | ||
1295 | 94 | *target = 0 | ||
1296 | 95 | } | ||
1297 | 96 | } | ||
1298 | 97 | |||
1299 | 84 | // snapGetBool wraps SnapGet for bool types and verifies the snap var is valid | 98 | // snapGetBool wraps SnapGet for bool types and verifies the snap var is valid |
1300 | 85 | func (c *Client) snapGetBool(key string, target *bool) { | 99 | func (c *Client) snapGetBool(key string, target *bool) { |
1301 | 86 | val, err := c.getter.SnapGet(key) | 100 | val, err := c.getter.SnapGet(key) |
1302 | @@ -88,7 +102,7 @@ func (c *Client) snapGetBool(key string, target *bool) { | |||
1303 | 88 | return | 102 | return |
1304 | 89 | } | 103 | } |
1305 | 90 | if len(val) == 0 { | 104 | if len(val) == 0 { |
1307 | 91 | log("configure error: key %s exists but has zero length" + key) | 105 | log("configure error: key " + key + " exists but has zero length") |
1308 | 92 | return | 106 | return |
1309 | 93 | } | 107 | } |
1310 | 94 | 108 | ||
1311 | @@ -109,20 +123,28 @@ func log(msg string) { | |||
1312 | 109 | func main() { | 123 | func main() { |
1313 | 110 | log("Configure hook running") | 124 | log("Configure hook running") |
1314 | 111 | client := GetClient() | 125 | client := GetClient() |
1315 | 112 | preConfig := &daemon.PreConfig{} | ||
1316 | 113 | client.snapGetStr("wifi.security-passphrase", &preConfig.Passphrase) | ||
1317 | 114 | client.snapGetStr("portal.password", &preConfig.Password) | ||
1318 | 115 | client.snapGetBool("portal.operational", &preConfig.Operational) | ||
1319 | 116 | client.snapGetBool("portal.no-reset-creds", &preConfig.NoResetCreds) | ||
1320 | 117 | 126 | ||
1326 | 118 | b, err := json.Marshal(preConfig) | 127 | config, _ := utils.LoadConfig() |
1327 | 119 | if err != nil { | 128 | |
1328 | 120 | log("Marshall error: " + err.Error()) | 129 | client.snapGetBool("config-wifi-once", &config.ConfigWifiOnce) |
1329 | 121 | return | 130 | |
1330 | 122 | } | 131 | client.snapGetStr("wifi.ssid", &config.Wifi.Ssid) |
1331 | 132 | client.snapGetStr("wifi.security-passphrase", &config.Wifi.Passphrase) | ||
1332 | 133 | client.snapGetStr("wifi.interface", &config.Wifi.Interface) | ||
1333 | 134 | client.snapGetStr("wifi.country-code", &config.Wifi.CountryCode) | ||
1334 | 135 | client.snapGetInt("wifi.channel", &config.Wifi.Channel) | ||
1335 | 136 | client.snapGetStr("wifi.operation-mode", &config.Wifi.OperationMode) | ||
1336 | 137 | client.snapGetStr("wifi.subnet", &config.Wifi.Subnet) | ||
1337 | 138 | |||
1338 | 139 | client.snapGetStr("portal.password", &config.Portal.Password) | ||
1339 | 140 | client.snapGetInt("portal.port", &config.Portal.Port) | ||
1340 | 141 | client.snapGetBool("portal.no-reset-creds", &config.Portal.NoResetCredentials) | ||
1341 | 142 | client.snapGetBool("portal.operational", &config.Portal.Operational) | ||
1342 | 143 | |||
1343 | 144 | utils.WriteConfigFile(config) | ||
1344 | 123 | 145 | ||
1346 | 124 | err = ioutil.WriteFile(daemon.PreConfigFile, b, 0644) | 146 | err := exec.Command("snapctl", "restart", os.Getenv("SNAP_NAME")).Run() |
1347 | 125 | if err != nil { | 147 | if err != nil { |
1349 | 126 | log("configure hook: " + err.Error()) | 148 | log("Error while re-starting the service: " + err.Error()) |
1350 | 127 | } | 149 | } |
1351 | 128 | } | 150 | } |
1352 | diff --git a/netman/dbus.go b/netman/dbus.go | |||
1353 | index 55564bd..b6ce186 100644 | |||
1354 | --- a/netman/dbus.go | |||
1355 | +++ b/netman/dbus.go | |||
1356 | @@ -20,31 +20,45 @@ package netman | |||
1357 | 20 | import ( | 20 | import ( |
1358 | 21 | "errors" | 21 | "errors" |
1359 | 22 | "fmt" | 22 | "fmt" |
1360 | 23 | "io" | ||
1361 | 24 | "log" | 23 | "log" |
1362 | 25 | "os" | ||
1363 | 26 | "strings" | 24 | "strings" |
1364 | 27 | "time" | 25 | "time" |
1365 | 28 | 26 | ||
1366 | 29 | "github.com/godbus/dbus" | 27 | "github.com/godbus/dbus" |
1367 | 30 | ) | 28 | ) |
1368 | 31 | 29 | ||
1369 | 30 | // WifiConnType enum for types of wifi connections | ||
1370 | 31 | type WifiConnType int | ||
1371 | 32 | |||
1372 | 33 | // All wifi conns, just STA or just AP | ||
1373 | 34 | const ( | ||
1374 | 35 | AllWifiConn WifiConnType = iota | ||
1375 | 36 | StaWifiConn | ||
1376 | 37 | ApWifiConn | ||
1377 | 38 | ) | ||
1378 | 39 | |||
1379 | 32 | // Operations defines operations for this package client | 40 | // Operations defines operations for this package client |
1380 | 33 | type Operations interface { | 41 | type Operations interface { |
1381 | 34 | GetDevices() []string | 42 | GetDevices() []string |
1383 | 35 | GetWifiDevices(devices []string) []string | 43 | GetWifiDevices() []string |
1384 | 44 | GetIfaceFromDevicePath(connSettPath string) (string, error) | ||
1385 | 45 | GetDevicePathFromIface(iface string) (string, error) | ||
1386 | 36 | GetAccessPoints(devices []string, ap2device map[string]string) []string | 46 | GetAccessPoints(devices []string, ap2device map[string]string) []string |
1389 | 37 | ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error | 47 | ConnectToAp(ssid, password, device string) (string, string, error) |
1390 | 38 | Ssids() ([]SSID, map[string]string, map[string]string) | 48 | Ssids(devicePath string) ([]SSID, map[string]string, map[string]string) |
1391 | 39 | Connected(devices []string) bool | 49 | Connected(devices []string) bool |
1394 | 40 | ConnectedWifi(wifiDevices []string) bool | 50 | ConnectedToAnAP(wifiDevice string) bool |
1395 | 41 | DeleteWifiConnections() | 51 | DisconnectDevice(device string) error |
1396 | 52 | DeleteWifiConnection(connSett string) error | ||
1397 | 53 | DeleteWifiConnections(wifiType int) | ||
1398 | 42 | DisconnectWifi(wifiDevices []string) int | 54 | DisconnectWifi(wifiDevices []string) int |
1399 | 43 | SetIfaceManaged(iface string, state bool, devices []string) string | 55 | SetIfaceManaged(iface string, state bool, devices []string) string |
1400 | 44 | WifisManaged(wifiDevices []string) (map[string]string, error) | 56 | WifisManaged(wifiDevices []string) (map[string]string, error) |
1404 | 45 | Unmanage() error | 57 | Unmanage(wifiIface string) error |
1405 | 46 | Manage() error | 58 | Manage(wifiIface string) error |
1406 | 47 | ScanAndWriteSsidsToFile(filepath string) bool | 59 | CreateAccessPoint(ssid string, password string, wifiPath string, |
1407 | 60 | ipAddress string, prefix uint) (string, string, error) | ||
1408 | 61 | ActivateConnection(devicePath string, connPath string) (string, error) | ||
1409 | 48 | } | 62 | } |
1410 | 49 | 63 | ||
1411 | 50 | // Client type to support unit test mock and runtime execution | 64 | // Client type to support unit test mock and runtime execution |
1412 | @@ -64,16 +78,20 @@ type Objecter interface { | |||
1413 | 64 | Object(dest string, path dbus.ObjectPath) dbus.BusObject | 78 | Object(dest string, path dbus.ObjectPath) dbus.BusObject |
1414 | 65 | } | 79 | } |
1415 | 66 | 80 | ||
1420 | 67 | // Object is the mock implementation of the godbus Object function | 81 | func getSystemBus() *dbus.Conn { |
1421 | 68 | func (d *DbusClient) Object(dest string, path dbus.ObjectPath) dbus.BusObject { | 82 | conn, err := dbus.SystemBus() |
1422 | 69 | obj := d.Connection.Object(dest, path) | 83 | if err != nil { |
1423 | 70 | return obj | 84 | log.Printf("Error: Failed to connect to system bus: %v", err) |
1424 | 85 | panic(1) | ||
1425 | 86 | } | ||
1426 | 87 | return conn | ||
1427 | 71 | } | 88 | } |
1428 | 72 | 89 | ||
1429 | 73 | // DefaultClient is the runtime client object | 90 | // DefaultClient is the runtime client object |
1430 | 74 | func DefaultClient() *Client { | 91 | func DefaultClient() *Client { |
1431 | 75 | conn := getSystemBus() | 92 | conn := getSystemBus() |
1433 | 76 | obj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") | 93 | obj := conn.Object("org.freedesktop.NetworkManager", |
1434 | 94 | "/org/freedesktop/NetworkManager") | ||
1435 | 77 | return &Client{ | 95 | return &Client{ |
1436 | 78 | dbusClient: DbusClient{ | 96 | dbusClient: DbusClient{ |
1437 | 79 | test: false, | 97 | test: false, |
1438 | @@ -102,13 +120,10 @@ func setObject(c *Client, iface string, path dbus.ObjectPath) { | |||
1439 | 102 | 120 | ||
1440 | 103 | // GetDevices returns NetMan (NetworkManager) devices | 121 | // GetDevices returns NetMan (NetworkManager) devices |
1441 | 104 | func (c *Client) GetDevices() []string { | 122 | func (c *Client) GetDevices() []string { |
1442 | 105 | if !c.dbusClient.test { | ||
1443 | 106 | c.dbusClient.Connection = getSystemBus() | ||
1444 | 107 | } | ||
1445 | 108 | c.dbusClient.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") | ||
1446 | 109 | setObject(c, "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") | 123 | setObject(c, "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") |
1447 | 110 | var devices []string | 124 | var devices []string |
1449 | 111 | err := c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.GetAllDevices", 0).Store(&devices) | 125 | err := c.dbusClient.BusObj.Call( |
1450 | 126 | "org.freedesktop.NetworkManager.GetAllDevices", 0).Store(&devices) | ||
1451 | 112 | if err != nil { | 127 | if err != nil { |
1452 | 113 | log.Printf("Error getting devices: %v", err) | 128 | log.Printf("Error getting devices: %v", err) |
1453 | 114 | } | 129 | } |
1454 | @@ -116,22 +131,20 @@ func (c *Client) GetDevices() []string { | |||
1455 | 116 | } | 131 | } |
1456 | 117 | 132 | ||
1457 | 118 | // GetWifiDevices returns wifi NetMan devices | 133 | // GetWifiDevices returns wifi NetMan devices |
1459 | 119 | func (c *Client) GetWifiDevices(devices []string) []string { | 134 | func (c *Client) GetWifiDevices() []string { |
1460 | 135 | devices := c.GetDevices() | ||
1461 | 120 | var wifiDevices []string | 136 | var wifiDevices []string |
1462 | 121 | for _, d := range devices { | 137 | for _, d := range devices { |
1463 | 122 | objPath := dbus.ObjectPath(d) | 138 | objPath := dbus.ObjectPath(d) |
1464 | 123 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | ||
1465 | 124 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 139 | setObject(c, "org.freedesktop.NetworkManager", objPath) |
1469 | 125 | deviceType, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.DeviceType") | 140 | deviceType, err := c.dbusClient.BusObj.GetProperty( |
1470 | 126 | if err2 != nil { | 141 | "org.freedesktop.NetworkManager.Device.DeviceType") |
1471 | 127 | log.Printf("Error getting wifi devices: %v", err2) | 142 | if err != nil { |
1472 | 143 | log.Printf("Error getting DeviceType: %v", err) | ||
1473 | 128 | continue | 144 | continue |
1474 | 129 | } | 145 | } |
1475 | 130 | var wifiType uint32 | 146 | var wifiType uint32 |
1476 | 131 | wifiType = 2 | 147 | wifiType = 2 |
1477 | 132 | if deviceType.Value() == nil { | ||
1478 | 133 | break | ||
1479 | 134 | } | ||
1480 | 135 | if deviceType.Value() != wifiType { | 148 | if deviceType.Value() != wifiType { |
1481 | 136 | continue | 149 | continue |
1482 | 137 | } | 150 | } |
1483 | @@ -140,15 +153,52 @@ func (c *Client) GetWifiDevices(devices []string) []string { | |||
1484 | 140 | return wifiDevices | 153 | return wifiDevices |
1485 | 141 | } | 154 | } |
1486 | 142 | 155 | ||
1488 | 143 | //GetAccessPoints returns NetMan known external APs | 156 | // GetIfaceFromDevicePath gets interface name from NM's dbus device path |
1489 | 157 | func (c *Client) GetIfaceFromDevicePath(connSettPath string) (string, error) { | ||
1490 | 158 | setObject(c, "org.freedesktop.NetworkManager", | ||
1491 | 159 | dbus.ObjectPath(connSettPath)) | ||
1492 | 160 | iface, err := c.dbusClient.BusObj.GetProperty( | ||
1493 | 161 | "org.freedesktop.NetworkManager.Device.Interface") | ||
1494 | 162 | if err != nil { | ||
1495 | 163 | return "", err | ||
1496 | 164 | } | ||
1497 | 165 | return iface.Value().(string), err | ||
1498 | 166 | } | ||
1499 | 167 | |||
1500 | 168 | // GetDevicePathFromIface gets NM's DBus object path for a device from the interface name | ||
1501 | 169 | func (c *Client) GetDevicePathFromIface(iface string) (string, error) { | ||
1502 | 170 | devPaths := c.GetDevices() | ||
1503 | 171 | matchDevPath := "" | ||
1504 | 172 | for _, devPath := range devPaths { | ||
1505 | 173 | setObject(c, "org.freedesktop.NetworkManager", | ||
1506 | 174 | dbus.ObjectPath(devPath)) | ||
1507 | 175 | devIface, err := c.dbusClient.BusObj.GetProperty( | ||
1508 | 176 | "org.freedesktop.NetworkManager.Device.Interface") | ||
1509 | 177 | if err != nil { | ||
1510 | 178 | log.Printf("Error getting wifi devices: %v", err) | ||
1511 | 179 | continue | ||
1512 | 180 | } | ||
1513 | 181 | if devIface.Value().(string) == iface { | ||
1514 | 182 | matchDevPath = devPath | ||
1515 | 183 | break | ||
1516 | 184 | } | ||
1517 | 185 | } | ||
1518 | 186 | if matchDevPath == "" { | ||
1519 | 187 | return "", errors.New("Interface not found") | ||
1520 | 188 | } | ||
1521 | 189 | return matchDevPath, nil | ||
1522 | 190 | } | ||
1523 | 191 | |||
1524 | 192 | // GetAccessPoints returns NetMan known external APs | ||
1525 | 144 | func (c *Client) GetAccessPoints(devices []string, ap2device map[string]string) []string { | 193 | func (c *Client) GetAccessPoints(devices []string, ap2device map[string]string) []string { |
1526 | 145 | var APs []string | 194 | var APs []string |
1527 | 146 | for _, d := range devices { | 195 | for _, d := range devices { |
1528 | 147 | var aps []string | 196 | var aps []string |
1529 | 148 | objPath := dbus.ObjectPath(d) | 197 | objPath := dbus.ObjectPath(d) |
1530 | 149 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | ||
1531 | 150 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 198 | setObject(c, "org.freedesktop.NetworkManager", objPath) |
1533 | 151 | err := c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Device.Wireless.GetAllAccessPoints", 0).Store(&aps) | 199 | err := c.dbusClient.BusObj.Call("org.freedesktop."+ |
1534 | 200 | "NetworkManager.Device.Wireless.GetAllAccessPoints", | ||
1535 | 201 | 0).Store(&aps) | ||
1536 | 152 | if err != nil { | 202 | if err != nil { |
1537 | 153 | log.Printf("Error getting accesspoints: %v", err) | 203 | log.Printf("Error getting accesspoints: %v", err) |
1538 | 154 | continue | 204 | continue |
1539 | @@ -175,9 +225,9 @@ func (c *Client) getSsids(APs []string, ssid2ap map[string]string) []SSID { | |||
1540 | 175 | var SSIDs []SSID | 225 | var SSIDs []SSID |
1541 | 176 | for _, ap := range APs { | 226 | for _, ap := range APs { |
1542 | 177 | objPath := dbus.ObjectPath(ap) | 227 | objPath := dbus.ObjectPath(ap) |
1543 | 178 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | ||
1544 | 179 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 228 | setObject(c, "org.freedesktop.NetworkManager", objPath) |
1546 | 180 | ssid, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.AccessPoint.Ssid") | 229 | ssid, err := c.dbusClient.BusObj.GetProperty( |
1547 | 230 | "org.freedesktop.NetworkManager.AccessPoint.Ssid") | ||
1548 | 181 | if err != nil { | 231 | if err != nil { |
1549 | 182 | log.Printf("Error getting accesspoint's ssids: %v", err) | 232 | log.Printf("Error getting accesspoint's ssids: %v", err) |
1550 | 183 | continue | 233 | continue |
1551 | @@ -196,60 +246,43 @@ func (c *Client) getSsids(APs []string, ssid2ap map[string]string) []SSID { | |||
1552 | 196 | Ssid := SSID{Ssid: ssidStr, ApPath: ap} | 246 | Ssid := SSID{Ssid: ssidStr, ApPath: ap} |
1553 | 197 | SSIDs = append(SSIDs, Ssid) | 247 | SSIDs = append(SSIDs, Ssid) |
1554 | 198 | ssid2ap[strings.TrimSpace(ssidStr)] = ap | 248 | ssid2ap[strings.TrimSpace(ssidStr)] = ap |
1555 | 199 | //TODO: exclude ssid of device's own AP (the wifi-ap one) | ||
1556 | 200 | } | 249 | } |
1557 | 201 | return SSIDs | 250 | return SSIDs |
1558 | 202 | } | 251 | } |
1559 | 203 | 252 | ||
1568 | 204 | // ConnectAp attempts to Connect to an external AP | 253 | // ConnectToAp attempts to connect to an external AP |
1569 | 205 | func (c *Client) ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error { | 254 | func (c *Client) ConnectToAp(ssid, password, device string) (string, string, error) { |
1562 | 206 | inner1 := make(map[string]dbus.Variant) | ||
1563 | 207 | inner1["security"] = dbus.MakeVariant("802-11-wireless-security") | ||
1564 | 208 | |||
1565 | 209 | inner2 := make(map[string]dbus.Variant) | ||
1566 | 210 | inner2["key-mgmt"] = dbus.MakeVariant("wpa-psk") | ||
1567 | 211 | inner2["psk"] = dbus.MakeVariant(p) | ||
1570 | 212 | 255 | ||
1574 | 213 | outer := make(map[string]map[string]dbus.Variant) | 256 | wirelessSett := make(map[string]dbus.Variant) |
1575 | 214 | outer["802-11-wireless"] = inner1 | 257 | wirelessSett["ssid"] = dbus.MakeVariant([]byte(ssid)) |
1573 | 215 | outer["802-11-wireless-security"] = inner2 | ||
1576 | 216 | 258 | ||
1580 | 217 | c.dbusClient.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") | 259 | wirelessSecSett := make(map[string]dbus.Variant) |
1581 | 218 | setObject(c, "org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager")) | 260 | wirelessSecSett["key-mgmt"] = dbus.MakeVariant("wpa-psk") |
1582 | 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) |
1583 | 220 | 262 | ||
1599 | 221 | // loop until connected or until max loops | 263 | settings := make(map[string]map[string]dbus.Variant) |
1600 | 222 | trying := true | 264 | settings["802-11-wireless"] = wirelessSett |
1601 | 223 | idx := -1 | 265 | settings["802-11-wireless-security"] = wirelessSecSett |
1587 | 224 | for trying { | ||
1588 | 225 | idx++ | ||
1589 | 226 | time.Sleep(1000 * time.Millisecond) | ||
1590 | 227 | if c.Connected(c.GetWifiDevices(c.GetDevices())) { | ||
1591 | 228 | return nil | ||
1592 | 229 | } | ||
1593 | 230 | if idx == 19 { | ||
1594 | 231 | return errors.New("wifi-connect: cannot connect to AP") | ||
1595 | 232 | } | ||
1596 | 233 | } | ||
1597 | 234 | return nil | ||
1598 | 235 | } | ||
1602 | 236 | 266 | ||
1605 | 237 | func getSystemBus() *dbus.Conn { | 267 | setObject(c, "org.freedesktop.NetworkManager", |
1606 | 238 | conn, err := dbus.SystemBus() | 268 | dbus.ObjectPath("/org/freedesktop/NetworkManager")) |
1607 | 269 | var connPath, actConnPath string | ||
1608 | 270 | err := c.dbusClient.BusObj.Call( | ||
1609 | 271 | "org.freedesktop.NetworkManager.AddAndActivateConnection", | ||
1610 | 272 | 0, settings, dbus.ObjectPath(device), | ||
1611 | 273 | dbus.ObjectPath("/")).Store(&connPath, &actConnPath) | ||
1612 | 239 | if err != nil { | 274 | if err != nil { |
1615 | 240 | log.Printf("Error: Failed to connect to system bus: %v", err) | 275 | return "", "", err |
1614 | 241 | panic(1) | ||
1616 | 242 | } | 276 | } |
1618 | 243 | return conn | 277 | |
1619 | 278 | return connPath, actConnPath, c.WaitConnectionActivated(actConnPath) | ||
1620 | 244 | } | 279 | } |
1621 | 245 | 280 | ||
1622 | 246 | // Ssids returns known SSIDs | 281 | // Ssids returns known SSIDs |
1624 | 247 | func (c *Client) Ssids() ([]SSID, map[string]string, map[string]string) { | 282 | func (c *Client) Ssids(devicePath string) ([]SSID, map[string]string, map[string]string) { |
1625 | 248 | ap2device := make(map[string]string) | 283 | ap2device := make(map[string]string) |
1626 | 249 | ssid2ap := make(map[string]string) | 284 | ssid2ap := make(map[string]string) |
1630 | 250 | devices := c.GetDevices() | 285 | APs := c.GetAccessPoints([]string{devicePath}, ap2device) |
1628 | 251 | wifiDevices := c.GetWifiDevices(devices) | ||
1629 | 252 | APs := c.GetAccessPoints(wifiDevices, ap2device) | ||
1631 | 253 | SSIDs := c.getSsids(APs, ssid2ap) | 286 | SSIDs := c.getSsids(APs, ssid2ap) |
1632 | 254 | return SSIDs, ap2device, ssid2ap | 287 | return SSIDs, ap2device, ssid2ap |
1633 | 255 | } | 288 | } |
1634 | @@ -258,20 +291,22 @@ func (c *Client) Ssids() ([]SSID, map[string]string, map[string]string) { | |||
1635 | 258 | func (c *Client) Connected(devices []string) bool { | 291 | func (c *Client) Connected(devices []string) bool { |
1636 | 259 | for _, d := range devices { | 292 | for _, d := range devices { |
1637 | 260 | objPath := dbus.ObjectPath(d) | 293 | objPath := dbus.ObjectPath(d) |
1638 | 261 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | ||
1639 | 262 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 294 | setObject(c, "org.freedesktop.NetworkManager", objPath) |
1641 | 263 | dType, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.DeviceType") | 295 | dType, err := c.dbusClient.BusObj. |
1642 | 296 | GetProperty("org.freedesktop.NetworkManager.Device.DeviceType") | ||
1643 | 264 | if err != nil { | 297 | if err != nil { |
1644 | 265 | log.Printf("Error getting device type: %v", err) | 298 | log.Printf("Error getting device type: %v", err) |
1645 | 266 | continue | 299 | continue |
1646 | 267 | } | 300 | } |
1648 | 268 | state, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.State") | 301 | state, err2 := c.dbusClient.BusObj. |
1649 | 302 | GetProperty("org.freedesktop.NetworkManager.Device.State") | ||
1650 | 269 | if err2 != nil { | 303 | if err2 != nil { |
1651 | 270 | log.Printf("Error getting device state: %v", err2) | 304 | log.Printf("Error getting device state: %v", err2) |
1652 | 271 | continue | 305 | continue |
1653 | 272 | } | 306 | } |
1654 | 273 | // only handle eth and wifi device type | 307 | // only handle eth and wifi device type |
1656 | 274 | if dbus.Variant.Value(dType) != uint32(1) && dbus.Variant.Value(dType) != uint32(2) { | 308 | if dbus.Variant.Value(dType) != uint32(1) && |
1657 | 309 | dbus.Variant.Value(dType) != uint32(2) { | ||
1658 | 275 | continue | 310 | continue |
1659 | 276 | } | 311 | } |
1660 | 277 | if dbus.Variant.Value(state) == uint32(100) { | 312 | if dbus.Variant.Value(state) == uint32(100) { |
1661 | @@ -281,75 +316,180 @@ func (c *Client) Connected(devices []string) bool { | |||
1662 | 281 | return false | 316 | return false |
1663 | 282 | } | 317 | } |
1664 | 283 | 318 | ||
1679 | 284 | // ConnectedWifi checks if any passed wifi devices are connected | 319 | // ConnectedToAnAP checks if the passed device is connected to an AP. If the |
1680 | 285 | func (c *Client) ConnectedWifi(wifiDevices []string) bool { | 320 | // device is itself an AP, we do not consider that a connection. |
1681 | 286 | for _, d := range wifiDevices { | 321 | func (c *Client) ConnectedToAnAP(wifiDevice string) bool { |
1682 | 287 | objPath := dbus.ObjectPath(d) | 322 | objPath := dbus.ObjectPath(wifiDevice) |
1683 | 288 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | 323 | setObject(c, "org.freedesktop.NetworkManager", objPath) |
1684 | 289 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 324 | |
1685 | 290 | state, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.State") | 325 | // GetAppliedConnection will return the current active |
1686 | 291 | if err != nil { | 326 | // connection for the device, or nothing if the device is not |
1687 | 292 | log.Printf("Error getting device state: %v", err) | 327 | // active. Checking for existence of "802-11-wireless" will |
1688 | 293 | continue | 328 | // rule out both non-wifi and non-active wifi devices. |
1689 | 294 | } | 329 | settings := make(map[string]interface{}) |
1690 | 295 | if dbus.Variant.Value(state) == uint32(100) { | 330 | var connID uint64 |
1691 | 296 | return true | 331 | c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager."+ |
1692 | 297 | } | 332 | "Device.GetAppliedConnection", 0, uint32(0)). |
1693 | 333 | Store(&settings, &connID) | ||
1694 | 334 | wifiSett, ok := settings["802-11-wireless"].(map[string]interface{}) | ||
1695 | 335 | if !ok { | ||
1696 | 336 | return false | ||
1697 | 298 | } | 337 | } |
1699 | 299 | return false | 338 | // If mode is not set, it is infrastructure mode |
1700 | 339 | mode, ok := wifiSett["mode"].(string) | ||
1701 | 340 | if !ok { | ||
1702 | 341 | return true | ||
1703 | 342 | } | ||
1704 | 343 | if mode == "ap" { | ||
1705 | 344 | // We ARE an AP, so not connected to one | ||
1706 | 345 | return false | ||
1707 | 346 | } | ||
1708 | 347 | return true | ||
1709 | 348 | } | ||
1710 | 349 | |||
1711 | 350 | // DeleteWifiConnection deletes a wifi connection using the object path to the | ||
1712 | 351 | // connetion settings TODO wait for Removed signal? | ||
1713 | 352 | func (c *Client) DeleteWifiConnection(connSett string) error { | ||
1714 | 353 | setObject(c, "org.freedesktop.NetworkManager", | ||
1715 | 354 | dbus.ObjectPath(connSett)) | ||
1716 | 355 | |||
1717 | 356 | call := c.dbusClient.BusObj.Call( | ||
1718 | 357 | "org.freedesktop.NetworkManager.Settings.Connection.Delete", 0) | ||
1719 | 358 | return call.Err | ||
1720 | 300 | } | 359 | } |
1721 | 301 | 360 | ||
1724 | 302 | // DeleteWifiConnections deletes all wifi connections (type 802-11-wireless) | 361 | // DeleteWifiConnections deletes all wifi connections for a given type |
1725 | 303 | func (c *Client) DeleteWifiConnections() { | 362 | func (c *Client) DeleteWifiConnections(wifiType WifiConnType) { |
1726 | 304 | objPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings") | 363 | objPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings") |
1727 | 305 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | ||
1728 | 306 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 364 | setObject(c, "org.freedesktop.NetworkManager", objPath) |
1729 | 307 | 365 | ||
1731 | 308 | //connect all connections | 366 | // get all connections |
1732 | 309 | var conns []string | 367 | var conns []string |
1734 | 310 | err := c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Settings.ListConnections", 0).Store(&conns) | 368 | err := c.dbusClient.BusObj.Call( |
1735 | 369 | "org.freedesktop.NetworkManager.Settings.ListConnections", 0). | ||
1736 | 370 | Store(&conns) | ||
1737 | 311 | if err != nil { | 371 | if err != nil { |
1738 | 312 | log.Printf("Error getting connections: %v", err) | 372 | log.Printf("Error getting connections: %v", err) |
1739 | 313 | } | 373 | } |
1740 | 314 | 374 | ||
1742 | 315 | //Delete all 802-11-wireless connections to clean netman cruft | 375 | // Delete 802-11-wireless connections for the given type |
1743 | 316 | for _, conn := range conns { | 376 | for _, conn := range conns { |
1744 | 317 | objPath := dbus.ObjectPath(conn) | 377 | objPath := dbus.ObjectPath(conn) |
1745 | 318 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | ||
1746 | 319 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 378 | setObject(c, "org.freedesktop.NetworkManager", objPath) |
1747 | 320 | settings := make(map[string]interface{}) | 379 | settings := make(map[string]interface{}) |
1755 | 321 | c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&settings) | 380 | c.dbusClient.BusObj.Call("org.freedesktop."+ |
1756 | 322 | _, ok := settings["802-11-wireless"] | 381 | "NetworkManager.Settings.Connection.GetSettings", 0). |
1757 | 323 | if ok { | 382 | Store(&settings) |
1758 | 324 | objPath := dbus.ObjectPath(conn) | 383 | wifiSett, ok := settings["802-11-wireless"].(map[string]interface{}) |
1759 | 325 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | 384 | if !ok { |
1760 | 326 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 385 | continue |
1761 | 327 | c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Delete", 0) | 386 | } |
1762 | 387 | isAp := false | ||
1763 | 388 | mode, ok := wifiSett["mode"].(string) | ||
1764 | 389 | if ok && mode == "ap" { | ||
1765 | 390 | isAp = true | ||
1766 | 391 | } | ||
1767 | 392 | switch wifiType { | ||
1768 | 393 | case StaWifiConn: | ||
1769 | 394 | if isAp == true { | ||
1770 | 395 | continue | ||
1771 | 396 | } | ||
1772 | 397 | case ApWifiConn: | ||
1773 | 398 | if isAp == false { | ||
1774 | 399 | continue | ||
1775 | 400 | } | ||
1776 | 401 | } | ||
1777 | 402 | err = c.DeleteWifiConnection(conn) | ||
1778 | 403 | if err != nil { | ||
1779 | 404 | log.Printf("Error while deleting %s: %v", conn, err) | ||
1780 | 328 | } | 405 | } |
1781 | 329 | } | 406 | } |
1782 | 330 | } | 407 | } |
1783 | 331 | 408 | ||
1794 | 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 |
1795 | 333 | func (c *Client) DisconnectWifi(wifiDevices []string) int { | 410 | func (c *Client) DisconnectDevice(device string) error { |
1796 | 334 | c.DeleteWifiConnections() | 411 | objPath := dbus.ObjectPath(device) |
1797 | 335 | ran := 0 | 412 | setObject(c, "org.freedesktop.NetworkManager", objPath) |
1798 | 336 | for _, d := range wifiDevices { | 413 | err := c.dbusClient.BusObj.Call( |
1799 | 337 | objPath := dbus.ObjectPath(d) | 414 | "org.freedesktop.NetworkManager.Device.Disconnect", 0).Err |
1800 | 338 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | 415 | if err != nil { |
1801 | 339 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 416 | if err.Error() == "This device is not active" { |
1802 | 340 | c.dbusClient.BusObj.Call("org.freedesktop.NetworkManager.Device.Disconnect", 0) | 417 | return nil |
1803 | 341 | ran++ | 418 | } |
1804 | 419 | return err | ||
1805 | 342 | } | 420 | } |
1807 | 343 | return ran | 421 | |
1808 | 422 | return c.waitDeviceDisconnect(device) | ||
1809 | 344 | } | 423 | } |
1810 | 345 | 424 | ||
1812 | 346 | // SetIfaceManaged sets passed device to be managed/unmanaged by network manager, return iface set, if any | 425 | func (c *Client) waitDeviceDisconnect(device string) error { |
1813 | 426 | rule := "type='signal',sender='org.freedesktop.NetworkManager'," + | ||
1814 | 427 | "path='" + device + "'," + | ||
1815 | 428 | "interface='org.freedesktop.NetworkManager.Device'," + | ||
1816 | 429 | "member='StateChanged'" | ||
1817 | 430 | setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus") | ||
1818 | 431 | err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Err | ||
1819 | 432 | if err != nil { | ||
1820 | 433 | return err | ||
1821 | 434 | } | ||
1822 | 435 | defer func() { | ||
1823 | 436 | setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus") | ||
1824 | 437 | err := c.dbusClient.BusObj.Call( | ||
1825 | 438 | "org.freedesktop.DBus.RemoveMatch", 0, rule).Err | ||
1826 | 439 | if err != nil { | ||
1827 | 440 | log.Printf("Error when calling RemoveMatch: %v", err) | ||
1828 | 441 | } | ||
1829 | 442 | }() | ||
1830 | 443 | |||
1831 | 444 | chSig := make(chan *dbus.Signal, 10) | ||
1832 | 445 | defer close(chSig) | ||
1833 | 446 | c.dbusClient.Connection.Signal(chSig) | ||
1834 | 447 | defer c.dbusClient.Connection.RemoveSignal(chSig) | ||
1835 | 448 | |||
1836 | 449 | // Get current state, as we might have missed the signal while we were | ||
1837 | 450 | // registering for it (the connection already existed). | ||
1838 | 451 | devPath := dbus.ObjectPath(device) | ||
1839 | 452 | setObject(c, "org.freedesktop.NetworkManager", devPath) | ||
1840 | 453 | state, err := c.dbusClient.BusObj.GetProperty( | ||
1841 | 454 | "org.freedesktop.NetworkManager.Device.State") | ||
1842 | 455 | if err != nil { | ||
1843 | 456 | return err | ||
1844 | 457 | } | ||
1845 | 458 | // NM_DEVICE_STATE_DISCONNECTED = 30 | ||
1846 | 459 | if state.Value() == 30 { | ||
1847 | 460 | return nil | ||
1848 | 461 | } | ||
1849 | 462 | |||
1850 | 463 | SignalLoop: | ||
1851 | 464 | for { | ||
1852 | 465 | select { | ||
1853 | 466 | case sig := <-chSig: | ||
1854 | 467 | // Note that we need to make sure the signal comes from | ||
1855 | 468 | // the right object, as we will receive here any signal | ||
1856 | 469 | // to which the process has registered. | ||
1857 | 470 | // NM_DEVICE_STATE_DISCONNECTED = 30 | ||
1858 | 471 | if sig.Path == devPath && | ||
1859 | 472 | sig.Name == | ||
1860 | 473 | "org.freedesktop.NetworkManager.Device.StateChanged" && | ||
1861 | 474 | sig.Body[0].(uint32) == 30 { | ||
1862 | 475 | break SignalLoop | ||
1863 | 476 | } | ||
1864 | 477 | case <-time.After(20 * time.Second): | ||
1865 | 478 | return fmt.Errorf("Timeout while waiting for disconnection") | ||
1866 | 479 | } | ||
1867 | 480 | } | ||
1868 | 481 | |||
1869 | 482 | return nil | ||
1870 | 483 | } | ||
1871 | 484 | |||
1872 | 485 | // SetIfaceManaged sets passed device to be managed/unmanaged by network | ||
1873 | 486 | // manager, return iface set, if any | ||
1874 | 347 | func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) string { | 487 | func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) string { |
1875 | 348 | for _, d := range devices { | 488 | for _, d := range devices { |
1876 | 349 | objPath := dbus.ObjectPath(d) | 489 | objPath := dbus.ObjectPath(d) |
1877 | 350 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | ||
1878 | 351 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 490 | setObject(c, "org.freedesktop.NetworkManager", objPath) |
1880 | 352 | intface, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Interface") | 491 | intface, err2 := c.dbusClient.BusObj.GetProperty( |
1881 | 492 | "org.freedesktop.NetworkManager.Device.Interface") | ||
1882 | 353 | if err2 != nil { | 493 | if err2 != nil { |
1883 | 354 | log.Printf("Error in SetIfaceManaged() geting interface: %v", err2) | 494 | log.Printf("Error in SetIfaceManaged() geting interface: %v", err2) |
1884 | 355 | return "" | 495 | return "" |
1885 | @@ -357,9 +497,11 @@ func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) str | |||
1886 | 357 | if iface != intface.Value().(string) { | 497 | if iface != intface.Value().(string) { |
1887 | 358 | continue | 498 | continue |
1888 | 359 | } | 499 | } |
1890 | 360 | managed, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Managed") | 500 | managed, err := c.dbusClient.BusObj.GetProperty( |
1891 | 501 | "org.freedesktop.NetworkManager.Device.Managed") | ||
1892 | 361 | if err != nil { | 502 | if err != nil { |
1894 | 362 | log.Printf("Error in SetIfaceManaged() fetching device managed: %v", err) | 503 | log.Printf("Error in SetIfaceManaged() fetching device managed: %v", |
1895 | 504 | err) | ||
1896 | 363 | return "" | 505 | return "" |
1897 | 364 | } | 506 | } |
1898 | 365 | switch state { | 507 | switch state { |
1899 | @@ -373,21 +515,26 @@ func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) str | |||
1900 | 373 | } | 515 | } |
1901 | 374 | } | 516 | } |
1902 | 375 | 517 | ||
1904 | 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", |
1905 | 519 | 0, "org.freedesktop.NetworkManager.Device", | ||
1906 | 520 | "Managed", dbus.MakeVariant(state)) | ||
1907 | 377 | // loop until interface is in desired managed state or max iters reached | 521 | // loop until interface is in desired managed state or max iters reached |
1908 | 378 | idx := -1 | 522 | idx := -1 |
1909 | 379 | for { | 523 | for { |
1910 | 380 | idx++ | 524 | idx++ |
1911 | 381 | time.Sleep(1000 * time.Millisecond) | 525 | time.Sleep(1000 * time.Millisecond) |
1913 | 382 | managedState, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.State") | 526 | managedState, err := c.dbusClient.BusObj.GetProperty( |
1914 | 527 | "org.freedesktop.NetworkManager.Device.State") | ||
1915 | 383 | if err == nil { | 528 | if err == nil { |
1916 | 384 | switch state { | 529 | switch state { |
1917 | 385 | case true: | 530 | case true: |
1919 | 386 | if managedState.Value() == uint32(30) { //NM_DEVICE_STATE_DISCONNECTED | 531 | if managedState.Value() == uint32(30) { |
1920 | 532 | //NM_DEVICE_STATE_DISCONNECTED | ||
1921 | 387 | return iface | 533 | return iface |
1922 | 388 | } | 534 | } |
1923 | 389 | case false: | 535 | case false: |
1925 | 390 | if managedState.Value() == uint32(10) { //NM_DEVICE_STATE_UNMANAGED | 536 | if managedState.Value() == uint32(10) { |
1926 | 537 | //NM_DEVICE_STATE_UNMANAGED | ||
1927 | 391 | return iface | 538 | return iface |
1928 | 392 | } | 539 | } |
1929 | 393 | } | 540 | } |
1930 | @@ -400,21 +547,25 @@ func (c *Client) SetIfaceManaged(iface string, state bool, devices []string) str | |||
1931 | 400 | return "" //no iface state changed | 547 | return "" //no iface state changed |
1932 | 401 | } | 548 | } |
1933 | 402 | 549 | ||
1935 | 403 | // WifisManaged returns map[iface]device of wifi iterfaces that are managed by network manager | 550 | // WifisManaged returns map[iface]device of wifi interfaces that are |
1936 | 551 | // managed by network manager | ||
1937 | 404 | func (c *Client) WifisManaged(wifiDevices []string) (map[string]string, error) { | 552 | func (c *Client) WifisManaged(wifiDevices []string) (map[string]string, error) { |
1938 | 405 | ifaces := make(map[string]string) | 553 | ifaces := make(map[string]string) |
1939 | 406 | for _, d := range wifiDevices { | 554 | for _, d := range wifiDevices { |
1940 | 407 | objPath := dbus.ObjectPath(d) | 555 | objPath := dbus.ObjectPath(d) |
1941 | 408 | c.dbusClient.Object("org.freedesktop.NetworkManager", objPath) | ||
1942 | 409 | setObject(c, "org.freedesktop.NetworkManager", objPath) | 556 | setObject(c, "org.freedesktop.NetworkManager", objPath) |
1944 | 410 | managed, err := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Managed") | 557 | managed, err := c.dbusClient.BusObj.GetProperty( |
1945 | 558 | "org.freedesktop.NetworkManager.Device.Managed") | ||
1946 | 411 | if err != nil { | 559 | if err != nil { |
1948 | 412 | log.Printf("Error in WifisManaged() getting device managed: %v", err) | 560 | log.Printf("Error in WifisManaged() getting device managed: %v", |
1949 | 561 | err) | ||
1950 | 413 | return ifaces, err | 562 | return ifaces, err |
1951 | 414 | } | 563 | } |
1953 | 415 | iface, err2 := c.dbusClient.BusObj.GetProperty("org.freedesktop.NetworkManager.Device.Interface") | 564 | iface, err2 := c.dbusClient.BusObj.GetProperty( |
1954 | 565 | "org.freedesktop.NetworkManager.Device.Interface") | ||
1955 | 416 | if err2 != nil { | 566 | if err2 != nil { |
1957 | 417 | log.Printf("Error in WifisManaged() getting device interface: %v", err) | 567 | log.Printf("Error in WifisManaged() getting device interface: %v", |
1958 | 568 | err) | ||
1959 | 418 | return ifaces, err2 | 569 | return ifaces, err2 |
1960 | 419 | } | 570 | } |
1961 | 420 | if managed.Value().(bool) == true { | 571 | if managed.Value().(bool) == true { |
1962 | @@ -424,75 +575,208 @@ func (c *Client) WifisManaged(wifiDevices []string) (map[string]string, error) { | |||
1963 | 424 | return ifaces, nil | 575 | return ifaces, nil |
1964 | 425 | } | 576 | } |
1965 | 426 | 577 | ||
1967 | 427 | // Unmanage sets wlan0 to be Unmanaged by network manager if it | 578 | // Unmanage sets an interface to be Unmanaged by network manager if it |
1968 | 428 | // is managed | 579 | // is managed |
1971 | 429 | func (c *Client) Unmanage() error { | 580 | func (c *Client) Unmanage(wifiIface string) error { |
1972 | 430 | ifaces, err := c.WifisManaged(c.GetWifiDevices(c.GetDevices())) | 581 | ifaces, err := c.WifisManaged(c.GetWifiDevices()) |
1973 | 431 | if err != nil { | 582 | if err != nil { |
1974 | 432 | return fmt.Errorf("Error getting managed wifis: %v", err) | 583 | return fmt.Errorf("Error getting managed wifis: %v", err) |
1975 | 433 | } | 584 | } |
1978 | 434 | if _, ok := ifaces["wlan0"]; ok { | 585 | if _, ok := ifaces[wifiIface]; ok { |
1979 | 435 | if c.SetIfaceManaged("wlan0", false, c.GetWifiDevices(c.GetDevices())) == "" { | 586 | if c.SetIfaceManaged(wifiIface, false, |
1980 | 587 | c.GetWifiDevices()) == "" { | ||
1981 | 436 | return fmt.Errorf("No interface was set to unmanaged") | 588 | return fmt.Errorf("No interface was set to unmanaged") |
1982 | 437 | } | 589 | } |
1983 | 438 | } | 590 | } |
1984 | 439 | return nil | 591 | return nil |
1985 | 440 | } | 592 | } |
1986 | 441 | 593 | ||
1990 | 442 | // Manage sets wlan0 to not managed by network manager | 594 | // Manage sets an interface to not managed by network manager |
1991 | 443 | func (c *Client) Manage() error { | 595 | func (c *Client) Manage(wifiIface string) error { |
1992 | 444 | if c.SetIfaceManaged("wlan0", true, c.GetWifiDevices(c.GetDevices())) == "" { | 596 | if c.SetIfaceManaged(wifiIface, true, |
1993 | 597 | c.GetWifiDevices()) == "" { | ||
1994 | 445 | return fmt.Errorf("No interface was set to managed") | 598 | return fmt.Errorf("No interface was set to managed") |
1995 | 446 | } | 599 | } |
1996 | 447 | return nil | 600 | return nil |
1997 | 448 | } | 601 | } |
1998 | 449 | 602 | ||
2001 | 450 | // ScanAndWriteSsidsToFile scans for ssids and writes to file | 603 | // CreateAccessPoint creates an AP with given SSID and password, for the wifi |
2002 | 451 | func (c *Client) ScanAndWriteSsidsToFile(filepath string) bool { | 604 | // interface specified. Returns the path to the connection settings and the |
2003 | 605 | // active connection. | ||
2004 | 606 | func (c *Client) CreateAccessPoint(ssid string, password string, | ||
2005 | 607 | iface string, ipAddress string, prefix uint) (string, string, error) { | ||
2006 | 452 | 608 | ||
2015 | 453 | var err error | 609 | wifiPath, err := c.GetDevicePathFromIface(iface) |
2016 | 454 | var file *os.File | 610 | if err != nil { |
2017 | 455 | if _, err = os.Stat(filepath); os.IsNotExist(err) { | 611 | return "", "", err |
2010 | 456 | file, err = os.Create(filepath) | ||
2011 | 457 | if err != nil { | ||
2012 | 458 | log.Printf("Error touching ssids file: %v", err) | ||
2013 | 459 | return false | ||
2014 | 460 | } | ||
2018 | 461 | } | 612 | } |
2019 | 462 | 613 | ||
2021 | 463 | file, err = os.OpenFile(filepath, os.O_RDWR, 0644) | 614 | setObject(c, "org.freedesktop.NetworkManager", |
2022 | 615 | "/org/freedesktop/NetworkManager") | ||
2023 | 616 | |||
2024 | 617 | connectSett := make(map[string]dbus.Variant) | ||
2025 | 618 | connectSett["id"] = dbus.MakeVariant(ssid) | ||
2026 | 619 | connectSett["autoconnect"] = dbus.MakeVariant(false) | ||
2027 | 620 | |||
2028 | 621 | wirelessSett := make(map[string]dbus.Variant) | ||
2029 | 622 | wirelessSett["ssid"] = dbus.MakeVariant([]byte(ssid)) | ||
2030 | 623 | wirelessSett["mode"] = dbus.MakeVariant("ap") | ||
2031 | 624 | |||
2032 | 625 | securitySett := make(map[string]dbus.Variant) | ||
2033 | 626 | securitySett["key-mgmt"] = dbus.MakeVariant("wpa-psk") | ||
2034 | 627 | securitySett["psk"] = dbus.MakeVariant(password) | ||
2035 | 628 | securitySett["proto"] = dbus.MakeVariant([]string{"rsn"}) | ||
2036 | 629 | securitySett["pairwise"] = dbus.MakeVariant([]string{"ccmp"}) | ||
2037 | 630 | securitySett["group"] = dbus.MakeVariant([]string{"ccmp"}) | ||
2038 | 631 | |||
2039 | 632 | ipv4Sett := make(map[string]dbus.Variant) | ||
2040 | 633 | ipv4Sett["method"] = dbus.MakeVariant("manual") | ||
2041 | 634 | ipv4Address := make(map[string]dbus.Variant) | ||
2042 | 635 | ipv4Address["address"] = dbus.MakeVariant(ipAddress) | ||
2043 | 636 | ipv4Address["prefix"] = dbus.MakeVariant(uint32(prefix)) | ||
2044 | 637 | ipv4AddressData := make([]map[string]dbus.Variant, 1) | ||
2045 | 638 | ipv4AddressData[0] = ipv4Address | ||
2046 | 639 | ipv4Sett["address-data"] = dbus.MakeVariant(ipv4AddressData) | ||
2047 | 640 | |||
2048 | 641 | ipv6Sett := make(map[string]dbus.Variant) | ||
2049 | 642 | ipv6Sett["method"] = dbus.MakeVariant("ignore") | ||
2050 | 643 | |||
2051 | 644 | settings := make(map[string]map[string]dbus.Variant) | ||
2052 | 645 | settings["connection"] = connectSett | ||
2053 | 646 | settings["802-11-wireless"] = wirelessSett | ||
2054 | 647 | settings["802-11-wireless-security"] = securitySett | ||
2055 | 648 | settings["ipv4"] = ipv4Sett | ||
2056 | 649 | settings["ipv6"] = ipv6Sett | ||
2057 | 650 | |||
2058 | 651 | var connPath, actConnPath string | ||
2059 | 652 | err = c.dbusClient.BusObj.Call( | ||
2060 | 653 | "org.freedesktop.NetworkManager.AddAndActivateConnection", 0, | ||
2061 | 654 | settings, dbus.ObjectPath(wifiPath), dbus.ObjectPath("/")). | ||
2062 | 655 | Store(&connPath, &actConnPath) | ||
2063 | 464 | if err != nil { | 656 | if err != nil { |
2066 | 465 | log.Printf("Error opening ssids file: %v", err) | 657 | return "", "", err |
2065 | 466 | return false | ||
2067 | 467 | } | 658 | } |
2068 | 468 | 659 | ||
2070 | 469 | defer file.Close() | 660 | log.Printf("Connection setting in %s", connPath) |
2071 | 661 | log.Printf("Active connection config in %s", actConnPath) | ||
2072 | 662 | return connPath, actConnPath, c.WaitConnectionActivated(actConnPath) | ||
2073 | 663 | } | ||
2074 | 664 | |||
2075 | 665 | // ActivateConnection starts a connection on a device | ||
2076 | 666 | func (c *Client) ActivateConnection(devicePath string, | ||
2077 | 667 | connPath string) (string, error) { | ||
2078 | 470 | 668 | ||
2080 | 471 | return c.scanSsids(file) | 669 | setObject(c, "org.freedesktop.NetworkManager", |
2081 | 670 | "/org/freedesktop/NetworkManager") | ||
2082 | 472 | 671 | ||
2083 | 672 | var activeConnPath string | ||
2084 | 673 | err := c.dbusClient.BusObj.Call( | ||
2085 | 674 | "org.freedesktop.NetworkManager.ActivateConnection", 0, | ||
2086 | 675 | dbus.ObjectPath(connPath), dbus.ObjectPath(devicePath), | ||
2087 | 676 | dbus.ObjectPath("/")).Store(&activeConnPath) | ||
2088 | 677 | if err != nil { | ||
2089 | 678 | return "", err | ||
2090 | 679 | } | ||
2091 | 680 | return activeConnPath, c.WaitConnectionActivated(activeConnPath) | ||
2092 | 473 | } | 681 | } |
2093 | 474 | 682 | ||
2108 | 475 | // ScanSsids sets wlan0 to be managed and then scans | 683 | // WaitConnectionActivated waits until a connection has been activated |
2109 | 476 | // for ssids. If found, write the ssids (comma separated) | 684 | func (c *Client) WaitConnectionActivated(actConnPath string) error { |
2110 | 477 | // to writer and return true, else return false. | 685 | rule := "type='signal',sender='org.freedesktop.NetworkManager'," + |
2111 | 478 | func (c *Client) scanSsids(writer io.Writer) bool { | 686 | "path='" + actConnPath + "'," + |
2112 | 479 | c.Manage() | 687 | "interface='org.freedesktop.NetworkManager.Connection.Active'," + |
2113 | 480 | SSIDs, _, _ := c.Ssids() | 688 | "member='StateChanged'" |
2114 | 481 | //only write SSIDs when found | 689 | setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus") |
2115 | 482 | if len(SSIDs) > 0 { | 690 | err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Err |
2116 | 483 | var out string | 691 | if err != nil { |
2117 | 484 | for _, ssid := range SSIDs { | 692 | return err |
2118 | 485 | out += strings.TrimSpace(ssid.Ssid) + "," | 693 | } |
2119 | 486 | } | 694 | defer func() { |
2120 | 487 | out = out[:len(out)-1] | 695 | setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus") |
2121 | 488 | _, err := writer.Write([]byte(out)) | 696 | err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.RemoveMatch", 0, rule).Err |
2122 | 489 | if err != nil { | 697 | if err != nil { |
2127 | 490 | log.Printf("Error writing SSID(s): %v ", err) | 698 | log.Printf("Error when calling RemoveMatch: %v", err) |
2124 | 491 | } else { | ||
2125 | 492 | log.Print("SSID(s) obtained") | ||
2126 | 493 | return true | ||
2128 | 494 | } | 699 | } |
2129 | 700 | }() | ||
2130 | 701 | |||
2131 | 702 | chSig := make(chan *dbus.Signal, 10) | ||
2132 | 703 | defer close(chSig) | ||
2133 | 704 | c.dbusClient.Connection.Signal(chSig) | ||
2134 | 705 | defer c.dbusClient.Connection.RemoveSignal(chSig) | ||
2135 | 706 | |||
2136 | 707 | // Get current state, as we might have missed the signal while we were | ||
2137 | 708 | // registering for it (the connection already existed). | ||
2138 | 709 | setObject(c, "org.freedesktop.NetworkManager", dbus.ObjectPath(actConnPath)) | ||
2139 | 710 | state, err := c.dbusClient.BusObj.GetProperty( | ||
2140 | 711 | "org.freedesktop.NetworkManager.Connection.Active.State") | ||
2141 | 712 | if err != nil { | ||
2142 | 713 | return err | ||
2143 | 495 | } | 714 | } |
2146 | 496 | log.Print("No SSID found") | 715 | // NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2 |
2147 | 497 | return false | 716 | if state.Value() == 2 { |
2148 | 717 | return nil | ||
2149 | 718 | } | ||
2150 | 719 | |||
2151 | 720 | SignalLoop: | ||
2152 | 721 | for { | ||
2153 | 722 | select { | ||
2154 | 723 | case sig := <-chSig: | ||
2155 | 724 | // Note that we need to make sure the signal comes from | ||
2156 | 725 | // the right object, as we will receive here any signal | ||
2157 | 726 | // to which the process has registered. | ||
2158 | 727 | // NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2 | ||
2159 | 728 | if sig.Path == dbus.ObjectPath(actConnPath) && | ||
2160 | 729 | sig.Body[0].(uint32) == 2 { | ||
2161 | 730 | break SignalLoop | ||
2162 | 731 | } | ||
2163 | 732 | case <-time.After(20 * time.Second): | ||
2164 | 733 | return fmt.Errorf("Timeout while waiting for conn activation") | ||
2165 | 734 | } | ||
2166 | 735 | } | ||
2167 | 736 | |||
2168 | 737 | return nil | ||
2169 | 738 | } | ||
2170 | 739 | |||
2171 | 740 | // CheckNetworkManagerRunning finds out whether NM is running or not | ||
2172 | 741 | func (c *Client) CheckNetworkManagerRunning() (bool, error) { | ||
2173 | 742 | setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus") | ||
2174 | 743 | err := c.dbusClient.BusObj.Call( | ||
2175 | 744 | "org.freedesktop.DBus.GetNameOwner", 0, | ||
2176 | 745 | "org.freedesktop.NetworkManager").Err | ||
2177 | 746 | if err != nil { | ||
2178 | 747 | return false, err | ||
2179 | 748 | } | ||
2180 | 749 | return true, nil | ||
2181 | 750 | } | ||
2182 | 751 | |||
2183 | 752 | // RegisterForNameOwnerChanged registers for changes in DBus names | ||
2184 | 753 | func (c *Client) RegisterForNameOwnerChanged(chSig chan<- *dbus.Signal) error { | ||
2185 | 754 | rule := "type='signal',sender='org.freedesktop.DBus'," + | ||
2186 | 755 | "path='/org/freedesktop/DBus'," + | ||
2187 | 756 | "interface='org.freedesktop.DBus'," + | ||
2188 | 757 | "member='NameOwnerChanged'" | ||
2189 | 758 | setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus") | ||
2190 | 759 | err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Err | ||
2191 | 760 | if err != nil { | ||
2192 | 761 | return err | ||
2193 | 762 | } | ||
2194 | 763 | |||
2195 | 764 | c.dbusClient.Connection.Signal(chSig) | ||
2196 | 765 | return nil | ||
2197 | 766 | } | ||
2198 | 767 | |||
2199 | 768 | // UnregisterForNameOwnerChanged unregisters for changes in DBus names | ||
2200 | 769 | func (c *Client) UnregisterForNameOwnerChanged(chSig chan<- *dbus.Signal) error { | ||
2201 | 770 | rule := "type='signal',sender='org.freedesktop.DBus'," + | ||
2202 | 771 | "path='/org/freedesktop/DBus'," + | ||
2203 | 772 | "interface='org.freedesktop.DBus'," + | ||
2204 | 773 | "member='NameOwnerChanged'" | ||
2205 | 774 | setObject(c, "org.freedesktop.DBus", "/org/freedesktop/DBus") | ||
2206 | 775 | err := c.dbusClient.BusObj.Call("org.freedesktop.DBus.RemoveMatch", 0, rule).Err | ||
2207 | 776 | if err != nil { | ||
2208 | 777 | log.Printf("Error unregistering for NameOwnerChanged: %v", err) | ||
2209 | 778 | } | ||
2210 | 779 | |||
2211 | 780 | c.dbusClient.Connection.RemoveSignal(chSig) | ||
2212 | 781 | return err | ||
2213 | 498 | } | 782 | } |
2214 | diff --git a/netman/dbus_test.go b/netman/dbus_test.go | |||
2215 | index c27a2f6..3110d1e 100644 | |||
2216 | --- a/netman/dbus_test.go | |||
2217 | +++ b/netman/dbus_test.go | |||
2218 | @@ -18,6 +18,7 @@ | |||
2219 | 18 | package netman | 18 | package netman |
2220 | 19 | 19 | ||
2221 | 20 | import ( | 20 | import ( |
2222 | 21 | "context" | ||
2223 | 21 | "errors" | 22 | "errors" |
2224 | 22 | "fmt" | 23 | "fmt" |
2225 | 23 | "strconv" | 24 | "strconv" |
2226 | @@ -77,16 +78,23 @@ func (mock *mockObj) Call(method string, flags dbus.Flags, args ...interface{}) | |||
2227 | 77 | return call | 78 | return call |
2228 | 78 | } | 79 | } |
2229 | 79 | 80 | ||
2231 | 80 | func (mock *mockObj) Go(method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call { | 81 | func (mock *mockObj) Go(method string, flags dbus.Flags, ch chan *dbus.Call, |
2232 | 82 | args ...interface{}) *dbus.Call { | ||
2233 | 81 | call := makeCall() | 83 | call := makeCall() |
2234 | 82 | return call | 84 | return call |
2235 | 83 | } | 85 | } |
2236 | 84 | 86 | ||
2237 | 87 | func (mock *mockObj) GoWithContext(ctx context.Context, method string, | ||
2238 | 88 | flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call { | ||
2239 | 89 | return nil | ||
2240 | 90 | } | ||
2241 | 91 | |||
2242 | 85 | func (mock *mockObj) GetProperty(p string) (dbus.Variant, error) { | 92 | func (mock *mockObj) GetProperty(p string) (dbus.Variant, error) { |
2243 | 86 | switch p { | 93 | switch p { |
2244 | 87 | case "org.freedesktop.NetworkManager.Device.DeviceType": | 94 | case "org.freedesktop.NetworkManager.Device.DeviceType": |
2245 | 88 | if len(mock.wifiDevices) < 2 { // 2 of three devices are wifi | 95 | if len(mock.wifiDevices) < 2 { // 2 of three devices are wifi |
2247 | 89 | mock.wifiDevices = append(mock.wifiDevices, "wifi"+strconv.Itoa(len(mock.wifiDevices))) | 96 | mock.wifiDevices = append(mock.wifiDevices, |
2248 | 97 | "wifi"+strconv.Itoa(len(mock.wifiDevices))) | ||
2249 | 90 | return dbus.MakeVariant(uint32(2)), nil | 98 | return dbus.MakeVariant(uint32(2)), nil |
2250 | 91 | } | 99 | } |
2251 | 92 | return dbus.MakeVariant(uint32(1)), nil | 100 | return dbus.MakeVariant(uint32(1)), nil |
2252 | @@ -123,6 +131,25 @@ func (mock *mockObj) GetProperty(p string) (dbus.Variant, error) { | |||
2253 | 123 | return dbus.MakeVariant("GetProperty error"), errors.New("no such property found") | 131 | return dbus.MakeVariant("GetProperty error"), errors.New("no such property found") |
2254 | 124 | } | 132 | } |
2255 | 125 | 133 | ||
2256 | 134 | func (mock *mockObj) AddMatchSignal(iface, member string, | ||
2257 | 135 | options ...dbus.MatchOption) *dbus.Call { | ||
2258 | 136 | return nil | ||
2259 | 137 | } | ||
2260 | 138 | |||
2261 | 139 | func (mock *mockObj) RemoveMatchSignal(iface, member string, | ||
2262 | 140 | options ...dbus.MatchOption) *dbus.Call { | ||
2263 | 141 | return nil | ||
2264 | 142 | } | ||
2265 | 143 | |||
2266 | 144 | func (mock *mockObj) CallWithContext(ctx context.Context, method string, | ||
2267 | 145 | flags dbus.Flags, args ...interface{}) *dbus.Call { | ||
2268 | 146 | return nil | ||
2269 | 147 | } | ||
2270 | 148 | |||
2271 | 149 | func (mock *mockObj) SetProperty(p string, v interface{}) error { | ||
2272 | 150 | return nil | ||
2273 | 151 | } | ||
2274 | 152 | |||
2275 | 126 | func (mock *mockObj) Destination() string { | 153 | func (mock *mockObj) Destination() string { |
2276 | 127 | return "destination" | 154 | return "destination" |
2277 | 128 | } | 155 | } |
2278 | @@ -160,11 +187,10 @@ func TestGetDevices(t *testing.T) { | |||
2279 | 160 | 187 | ||
2280 | 161 | func TestGetWifiDevices(t *testing.T) { | 188 | func TestGetWifiDevices(t *testing.T) { |
2281 | 162 | client := NewClient(&mockObj{}) | 189 | client := NewClient(&mockObj{}) |
2284 | 163 | devices := client.GetDevices() | 190 | wifiDevices := client.GetWifiDevices() |
2283 | 164 | wifiDevices := client.GetWifiDevices(devices) | ||
2285 | 165 | found1 := false | 191 | found1 := false |
2286 | 166 | found2 := false | 192 | found2 := false |
2288 | 167 | for _, v := range devices { | 193 | for _, v := range wifiDevices { |
2289 | 168 | switch v { | 194 | switch v { |
2290 | 169 | case "/d/1": | 195 | case "/d/1": |
2291 | 170 | found1 = true | 196 | found1 = true |
2292 | @@ -176,16 +202,16 @@ func TestGetWifiDevices(t *testing.T) { | |||
2293 | 176 | t.Errorf("An expected device was not found") | 202 | t.Errorf("An expected device was not found") |
2294 | 177 | } | 203 | } |
2295 | 178 | if len(wifiDevices) != 2 { | 204 | if len(wifiDevices) != 2 { |
2297 | 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", |
2298 | 206 | len(wifiDevices)) | ||
2299 | 180 | } | 207 | } |
2300 | 181 | fmt.Printf("===== Found wifi devices: %v\n", wifiDevices) | 208 | fmt.Printf("===== Found wifi devices: %v\n", wifiDevices) |
2301 | 182 | } | 209 | } |
2302 | 183 | 210 | ||
2303 | 184 | func TestGetAPs(t *testing.T) { | 211 | func TestGetAPs(t *testing.T) { |
2304 | 185 | client := NewClient(&mockObj{}) | 212 | client := NewClient(&mockObj{}) |
2305 | 186 | devices := client.GetDevices() | ||
2306 | 187 | ap2device := make(map[string]string) | 213 | ap2device := make(map[string]string) |
2308 | 188 | wifiDevices := client.GetWifiDevices(devices) | 214 | wifiDevices := client.GetWifiDevices() |
2309 | 189 | aps := client.GetAccessPoints(wifiDevices, ap2device) | 215 | aps := client.GetAccessPoints(wifiDevices, ap2device) |
2310 | 190 | if len(aps) != 4 { | 216 | if len(aps) != 4 { |
2311 | 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)) |
2312 | @@ -195,8 +221,7 @@ func TestGetAPs(t *testing.T) { | |||
2313 | 195 | 221 | ||
2314 | 196 | func TestGetSsids(t *testing.T) { | 222 | func TestGetSsids(t *testing.T) { |
2315 | 197 | client := NewClient(&mockObj{}) | 223 | client := NewClient(&mockObj{}) |
2318 | 198 | devices := client.GetDevices() | 224 | wifiDevices := client.GetWifiDevices() |
2317 | 199 | wifiDevices := client.GetWifiDevices(devices) | ||
2319 | 200 | ap2device := make(map[string]string) | 225 | ap2device := make(map[string]string) |
2320 | 201 | ssid2ap := make(map[string]string) | 226 | ssid2ap := make(map[string]string) |
2321 | 202 | aps := client.GetAccessPoints(wifiDevices, ap2device) | 227 | aps := client.GetAccessPoints(wifiDevices, ap2device) |
2322 | @@ -209,8 +234,8 @@ func TestGetSsids(t *testing.T) { | |||
2323 | 209 | 234 | ||
2324 | 210 | func TestSsids(t *testing.T) { | 235 | func TestSsids(t *testing.T) { |
2325 | 211 | client := NewClient(&mockObj{}) | 236 | client := NewClient(&mockObj{}) |
2328 | 212 | ssids, _, _ := client.Ssids() | 237 | ssids, _, _ := client.Ssids("/d/3") |
2329 | 213 | if len(ssids) != 4 { | 238 | if len(ssids) != 2 { |
2330 | 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)) |
2331 | 215 | } | 240 | } |
2332 | 216 | fmt.Printf("===== Ssids() (ssid/ap): %v\n", ssids) | 241 | fmt.Printf("===== Ssids() (ssid/ap): %v\n", ssids) |
2333 | @@ -232,26 +257,14 @@ func TestConnectedWifi(t *testing.T) { | |||
2334 | 232 | mock := &mockObj{} | 257 | mock := &mockObj{} |
2335 | 233 | mock.connect = true | 258 | mock.connect = true |
2336 | 234 | client := NewClient(mock) | 259 | client := NewClient(mock) |
2338 | 235 | if !client.ConnectedWifi([]string{"d1"}) { | 260 | if !client.Connected([]string{"d1"}) { |
2339 | 236 | t.Errorf("Should have found Wificonnected state, but did not") | 261 | t.Errorf("Should have found Wificonnected state, but did not") |
2340 | 237 | } | 262 | } |
2342 | 238 | if client.ConnectedWifi([]string{}) { | 263 | if client.Connected([]string{}) { |
2343 | 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") |
2344 | 240 | } | 265 | } |
2345 | 241 | } | 266 | } |
2346 | 242 | 267 | ||
2347 | 243 | func TestDiscconnectWifi(t *testing.T) { | ||
2348 | 244 | client := NewClient(&mockObj{}) | ||
2349 | 245 | res := client.DisconnectWifi([]string{}) | ||
2350 | 246 | if res != 0 { | ||
2351 | 247 | t.Errorf("0 Disconnect call expected, but found: %d", res) | ||
2352 | 248 | } | ||
2353 | 249 | res = client.DisconnectWifi([]string{"d1"}) | ||
2354 | 250 | if res != 1 { | ||
2355 | 251 | t.Errorf("1 Disconnect call expected, but found: %d", res) | ||
2356 | 252 | } | ||
2357 | 253 | } | ||
2358 | 254 | |||
2359 | 255 | func TestSetIfaceManaged(t *testing.T) { | 268 | func TestSetIfaceManaged(t *testing.T) { |
2360 | 256 | mock := &mockObj{} | 269 | mock := &mockObj{} |
2361 | 257 | client := NewClient(mock) | 270 | client := NewClient(mock) |
2362 | diff --git a/run-checks b/run-checks | |||
2363 | index 562c14d..d922caf 100755 | |||
2364 | --- a/run-checks | |||
2365 | +++ b/run-checks | |||
2366 | @@ -85,7 +85,7 @@ if [ ! -z "$STATIC" ]; then | |||
2367 | 85 | 85 | ||
2368 | 86 | # golint | 86 | # golint |
2369 | 87 | echo Install golint | 87 | echo Install golint |
2371 | 88 | go get github.com/golang/lint/golint | 88 | go get -u golang.org/x/lint/golint |
2372 | 89 | export PATH=$PATH:$GOPATH/bin | 89 | export PATH=$PATH:$GOPATH/bin |
2373 | 90 | 90 | ||
2374 | 91 | echo Running lint | 91 | echo Running lint |
2375 | diff --git a/scriptlets/country-codes b/scriptlets/country-codes | |||
2376 | index d4e8a97..16c718a 100644 | |||
2377 | --- a/scriptlets/country-codes | |||
2378 | +++ b/scriptlets/country-codes | |||
2379 | @@ -1,243 +1,250 @@ | |||
2623 | 1 | <html><head><title>ISO-3166-1 Country Names</title></head> | 1 | Name,Code |
2624 | 2 | <body bgcolor=#ffffff> | 2 | Afghanistan,AF |
2625 | 3 | <h2>ISO-3166-1 Country Names</h2> | 3 | Åland Islands,AX |
2626 | 4 | <a href="/iso3166/">Reproduced with permission from ISO</a> | 4 | Albania,AL |
2627 | 5 | <p> | 5 | Algeria,DZ |
2628 | 6 | Follow links for ISO-3166-2 subdivision codes | 6 | American Samoa,AS |
2629 | 7 | <p> | 7 | Andorra,AD |
2630 | 8 | <a href=iso.AD.html>AD</a> : ANDORRA<br> | 8 | Angola,AO |
2631 | 9 | <a href=iso.AE.html>AE</a> : UNITED ARAB EMIRATES<br> | 9 | Anguilla,AI |
2632 | 10 | <a href=iso.AF.html>AF</a> : AFGHANISTAN<br> | 10 | Antarctica,AQ |
2633 | 11 | <a href=iso.AG.html>AG</a> : ANTIGUA AND BARBUDA<br> | 11 | Antigua and Barbuda,AG |
2634 | 12 | <a href=iso.AI.html>AI</a> : ANGUILLA<br> | 12 | Argentina,AR |
2635 | 13 | <a href=iso.AL.html>AL</a> : ALBANIA<br> | 13 | Armenia,AM |
2636 | 14 | <a href=iso.AM.html>AM</a> : ARMENIA<br> | 14 | Aruba,AW |
2637 | 15 | <a href=iso.AN.html>AN</a> : NETHERLANDS ANTILLES<br> | 15 | Australia,AU |
2638 | 16 | <a href=iso.AO.html>AO</a> : ANGOLA<br> | 16 | Austria,AT |
2639 | 17 | <a href=iso.AQ.html>AQ</a> : ANTARCTICA<br> | 17 | Azerbaijan,AZ |
2640 | 18 | <a href=iso.AR.html>AR</a> : ARGENTINA<br> | 18 | Bahamas,BS |
2641 | 19 | <a href=iso.AS.html>AS</a> : AMERICAN SAMOA<br> | 19 | Bahrain,BH |
2642 | 20 | <a href=iso.AT.html>AT</a> : AUSTRIA<br> | 20 | Bangladesh,BD |
2643 | 21 | <a href=iso.AU.html>AU</a> : AUSTRALIA<br> | 21 | Barbados,BB |
2644 | 22 | <a href=iso.AW.html>AW</a> : ARUBA<br> | 22 | Belarus,BY |
2645 | 23 | <a href=iso.AZ.html>AZ</a> : AZERBAIJAN<br> | 23 | Belgium,BE |
2646 | 24 | <a href=iso.BA.html>BA</a> : BOSNIA AND HERZEGOVINA<br> | 24 | Belize,BZ |
2647 | 25 | <a href=iso.BB.html>BB</a> : BARBADOS<br> | 25 | Benin,BJ |
2648 | 26 | <a href=iso.BD.html>BD</a> : BANGLADESH<br> | 26 | Bermuda,BM |
2649 | 27 | <a href=iso.BE.html>BE</a> : BELGIUM<br> | 27 | Bhutan,BT |
2650 | 28 | <a href=iso.BF.html>BF</a> : BURKINA FASO<br> | 28 | "Bolivia, Plurinational State of",BO |
2651 | 29 | <a href=iso.BG.html>BG</a> : BULGARIA<br> | 29 | "Bonaire, Sint Eustatius and Saba",BQ |
2652 | 30 | <a href=iso.BH.html>BH</a> : BAHRAIN<br> | 30 | Bosnia and Herzegovina,BA |
2653 | 31 | <a href=iso.BI.html>BI</a> : BURUNDI<br> | 31 | Botswana,BW |
2654 | 32 | <a href=iso.BJ.html>BJ</a> : BENIN<br> | 32 | Bouvet Island,BV |
2655 | 33 | <a href=iso.BM.html>BM</a> : BERMUDA<br> | 33 | Brazil,BR |
2656 | 34 | <a href=iso.BN.html>BN</a> : BRUNEI DARUSSALAM<br> | 34 | British Indian Ocean Territory,IO |
2657 | 35 | <a href=iso.BO.html>BO</a> : BOLIVIA<br> | 35 | Brunei Darussalam,BN |
2658 | 36 | <a href=iso.BR.html>BR</a> : BRAZIL<br> | 36 | Bulgaria,BG |
2659 | 37 | <a href=iso.BS.html>BS</a> : BAHAMAS<br> | 37 | Burkina Faso,BF |
2660 | 38 | <a href=iso.BT.html>BT</a> : BHUTAN<br> | 38 | Burundi,BI |
2661 | 39 | <a href=iso.BV.html>BV</a> : BOUVET ISLAND<br> | 39 | Cambodia,KH |
2662 | 40 | <a href=iso.BW.html>BW</a> : BOTSWANA<br> | 40 | Cameroon,CM |
2663 | 41 | <a href=iso.BY.html>BY</a> : BELARUS<br> | 41 | Canada,CA |
2664 | 42 | <a href=iso.BZ.html>BZ</a> : BELIZE<br> | 42 | Cape Verde,CV |
2665 | 43 | <a href=iso.CA.html>CA</a> : CANADA<br> | 43 | Cayman Islands,KY |
2666 | 44 | <a href=iso.CC.html>CC</a> : COCOS (KEELING) ISLANDS<br> | 44 | Central African Republic,CF |
2667 | 45 | <a href=iso.CD.html>CD</a> : CONGO, THE DEMOCRATIC REPUBLIC OF THE<br> | 45 | Chad,TD |
2668 | 46 | <a href=iso.CF.html>CF</a> : CENTRAL AFRICAN REPUBLIC<br> | 46 | Chile,CL |
2669 | 47 | <a href=iso.CG.html>CG</a> : CONGO<br> | 47 | China,CN |
2670 | 48 | <a href=iso.CH.html>CH</a> : SWITZERLAND<br> | 48 | Christmas Island,CX |
2671 | 49 | <a href=iso.CI.html>CI</a> : C�TE D'IVOIRE<br> | 49 | Cocos (Keeling) Islands,CC |
2672 | 50 | <a href=iso.CK.html>CK</a> : COOK ISLANDS<br> | 50 | Colombia,CO |
2673 | 51 | <a href=iso.CL.html>CL</a> : CHILE<br> | 51 | Comoros,KM |
2674 | 52 | <a href=iso.CM.html>CM</a> : CAMEROON<br> | 52 | Congo,CG |
2675 | 53 | <a href=iso.CN.html>CN</a> : CHINA<br> | 53 | "Congo, the Democratic Republic of the",CD |
2676 | 54 | <a href=iso.CO.html>CO</a> : COLOMBIA<br> | 54 | Cook Islands,CK |
2677 | 55 | <a href=iso.CR.html>CR</a> : COSTA RICA<br> | 55 | Costa Rica,CR |
2678 | 56 | <a href=iso.CU.html>CU</a> : CUBA<br> | 56 | Côte d'Ivoire,CI |
2679 | 57 | <a href=iso.CV.html>CV</a> : CAPE VERDE<br> | 57 | Croatia,HR |
2680 | 58 | <a href=iso.CX.html>CX</a> : CHRISTMAS ISLAND<br> | 58 | Cuba,CU |
2681 | 59 | <a href=iso.CY.html>CY</a> : CYPRUS<br> | 59 | Curaçao,CW |
2682 | 60 | <a href=iso.CZ.html>CZ</a> : CZECH REPUBLIC<br> | 60 | Cyprus,CY |
2683 | 61 | <a href=iso.DE.html>DE</a> : GERMANY<br> | 61 | Czech Republic,CZ |
2684 | 62 | <a href=iso.DJ.html>DJ</a> : DJIBOUTI<br> | 62 | Denmark,DK |
2685 | 63 | <a href=iso.DK.html>DK</a> : DENMARK<br> | 63 | Djibouti,DJ |
2686 | 64 | <a href=iso.DM.html>DM</a> : DOMINICA<br> | 64 | Dominica,DM |
2687 | 65 | <a href=iso.DO.html>DO</a> : DOMINICAN REPUBLIC<br> | 65 | Dominican Republic,DO |
2688 | 66 | <a href=iso.DZ.html>DZ</a> : ALGERIA<br> | 66 | Ecuador,EC |
2689 | 67 | <a href=iso.EC.html>EC</a> : ECUADOR<br> | 67 | Egypt,EG |
2690 | 68 | <a href=iso.EE.html>EE</a> : ESTONIA<br> | 68 | El Salvador,SV |
2691 | 69 | <a href=iso.EG.html>EG</a> : EGYPT<br> | 69 | Equatorial Guinea,GQ |
2692 | 70 | <a href=iso.EH.html>EH</a> : WESTERN SARARA<br> | 70 | Eritrea,ER |
2693 | 71 | <a href=iso.ER.html>ER</a> : ERITREA<br> | 71 | Estonia,EE |
2694 | 72 | <a href=iso.ES.html>ES</a> : SPAIN<br> | 72 | Ethiopia,ET |
2695 | 73 | <a href=iso.ET.html>ET</a> : ETHIOPIA<br> | 73 | Falkland Islands (Malvinas),FK |
2696 | 74 | <a href=iso.FI.html>FI</a> : FINLAND<br> | 74 | Faroe Islands,FO |
2697 | 75 | <a href=iso.FJ.html>FJ</a> : FIJI<br> | 75 | Fiji,FJ |
2698 | 76 | <a href=iso.FK.html>FK</a> : FALKLAND ISLANDS (MALVINAS)<br> | 76 | Finland,FI |
2699 | 77 | <a href=iso.FM.html>FM</a> : MICRONESIA, FEDERATED STATES OF<br> | 77 | France,FR |
2700 | 78 | <a href=iso.FO.html>FO</a> : FAROE ISLANDS<br> | 78 | French Guiana,GF |
2701 | 79 | <a href=iso.FR.html>FR</a> : FRANCE<br> | 79 | French Polynesia,PF |
2702 | 80 | <a href=iso.GA.html>GA</a> : GABON<br> | 80 | French Southern Territories,TF |
2703 | 81 | <a href=iso.GB.html>GB</a> : UNITED KINGDOM<br> | 81 | Gabon,GA |
2704 | 82 | <a href=iso.GD.html>GD</a> : GRENADA<br> | 82 | Gambia,GM |
2705 | 83 | <a href=iso.GE.html>GE</a> : GEORGIA<br> | 83 | Georgia,GE |
2706 | 84 | <a href=iso.GF.html>GF</a> : FRENCH GUIANA<br> | 84 | Germany,DE |
2707 | 85 | <a href=iso.GH.html>GH</a> : GHANA<br> | 85 | Ghana,GH |
2708 | 86 | <a href=iso.GI.html>GI</a> : GIBRALTAR<br> | 86 | Gibraltar,GI |
2709 | 87 | <a href=iso.GL.html>GL</a> : GREENLAND<br> | 87 | Greece,GR |
2710 | 88 | <a href=iso.GM.html>GM</a> : GAMBIA<br> | 88 | Greenland,GL |
2711 | 89 | <a href=iso.GN.html>GN</a> : GUINEA<br> | 89 | Grenada,GD |
2712 | 90 | <a href=iso.GP.html>GP</a> : GUADELOUPE<br> | 90 | Guadeloupe,GP |
2713 | 91 | <a href=iso.GQ.html>GQ</a> : EQUATORIAL GUINEA<br> | 91 | Guam,GU |
2714 | 92 | <a href=iso.GR.html>GR</a> : GREECE<br> | 92 | Guatemala,GT |
2715 | 93 | <a href=iso.GS.html>GS</a> : SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS<br> | 93 | Guernsey,GG |
2716 | 94 | <a href=iso.GT.html>GT</a> : GUATEMALA<br> | 94 | Guinea,GN |
2717 | 95 | <a href=iso.GU.html>GU</a> : GUAM<br> | 95 | Guinea-Bissau,GW |
2718 | 96 | <a href=iso.GW.html>GW</a> : GUINEA-BISSAU<br> | 96 | Guyana,GY |
2719 | 97 | <a href=iso.GY.html>GY</a> : GUYANA<br> | 97 | Haiti,HT |
2720 | 98 | <a href=iso.HK.html>HK</a> : HONG KONG<br> | 98 | Heard Island and McDonald Islands,HM |
2721 | 99 | <a href=iso.HM.html>HM</a> : HEARD ISLAND AND MCDONALD ISLANDS<br> | 99 | Holy See (Vatican City State),VA |
2722 | 100 | <a href=iso.HN.html>HN</a> : HONDURAS<br> | 100 | Honduras,HN |
2723 | 101 | <a href=iso.HR.html>HR</a> : CROATIA<br> | 101 | Hong Kong,HK |
2724 | 102 | <a href=iso.HT.html>HT</a> : HAITI<br> | 102 | Hungary,HU |
2725 | 103 | <a href=iso.HU.html>HU</a> : HUNGARY<br> | 103 | Iceland,IS |
2726 | 104 | <a href=iso.ID.html>ID</a> : INDONESIA<br> | 104 | India,IN |
2727 | 105 | <a href=iso.IE.html>IE</a> : IRELAND<br> | 105 | Indonesia,ID |
2728 | 106 | <a href=iso.IL.html>IL</a> : ISRAEL<br> | 106 | "Iran, Islamic Republic of",IR |
2729 | 107 | <a href=iso.IN.html>IN</a> : INDIA<br> | 107 | Iraq,IQ |
2730 | 108 | <a href=iso.IO.html>IO</a> : BRITISH INDIAN OCEAN TERRITORY<br> | 108 | Ireland,IE |
2731 | 109 | <a href=iso.IQ.html>IQ</a> : IRAQ<br> | 109 | Isle of Man,IM |
2732 | 110 | <a href=iso.IR.html>IR</a> : IRAN, ISLAMIC REPUBLIC OF<br> | 110 | Israel,IL |
2733 | 111 | <a href=iso.IS.html>IS</a> : ICELAND<br> | 111 | Italy,IT |
2734 | 112 | <a href=iso.IT.html>IT</a> : ITALY<br> | 112 | Jamaica,JM |
2735 | 113 | <a href=iso.JM.html>JM</a> : JAMAICA<br> | 113 | Japan,JP |
2736 | 114 | <a href=iso.JO.html>JO</a> : JORDAN<br> | 114 | Jersey,JE |
2737 | 115 | <a href=iso.JP.html>JP</a> : JAPAN<br> | 115 | Jordan,JO |
2738 | 116 | <a href=iso.KE.html>KE</a> : KENYA<br> | 116 | Kazakhstan,KZ |
2739 | 117 | <a href=iso.KG.html>KG</a> : KYRGYZSTAN<br> | 117 | Kenya,KE |
2740 | 118 | <a href=iso.KH.html>KH</a> : CAMBODIA<br> | 118 | Kiribati,KI |
2741 | 119 | <a href=iso.KI.html>KI</a> : KIRIBATI<br> | 119 | "Korea, Democratic People's Republic of",KP |
2742 | 120 | <a href=iso.KM.html>KM</a> : COMOROS<br> | 120 | "Korea, Republic of",KR |
2743 | 121 | <a href=iso.KN.html>KN</a> : SAINT KITTS AND NEVIS<br> | 121 | Kuwait,KW |
2744 | 122 | <a href=iso.KP.html>KP</a> : KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF<br> | 122 | Kyrgyzstan,KG |
2745 | 123 | <a href=iso.KR.html>KR</a> : KOREA, REPUBLIC OF<br> | 123 | Lao People's Democratic Republic,LA |
2746 | 124 | <a href=iso.KW.html>KW</a> : KUWAIT<br> | 124 | Latvia,LV |
2747 | 125 | <a href=iso.KY.html>KY</a> : CAYMAN ISLANDS<br> | 125 | Lebanon,LB |
2748 | 126 | <a href=iso.KZ.html>KZ</a> : KAZAKHSTAN<br> | 126 | Lesotho,LS |
2749 | 127 | <a href=iso.LA.html>LA</a> : LAO PEOPLE'S DEMOCRATIC REPUBLIC<br> | 127 | Liberia,LR |
2750 | 128 | <a href=iso.LB.html>LB</a> : LEBANON<br> | 128 | Libya,LY |
2751 | 129 | <a href=iso.LC.html>LC</a> : SAINT LUCIA<br> | 129 | Liechtenstein,LI |
2752 | 130 | <a href=iso.LI.html>LI</a> : LIECHTENSTEIN<br> | 130 | Lithuania,LT |
2753 | 131 | <a href=iso.LK.html>LK</a> : SRI LANKA<br> | 131 | Luxembourg,LU |
2754 | 132 | <a href=iso.LR.html>LR</a> : LIBERIA<br> | 132 | Macao,MO |
2755 | 133 | <a href=iso.LS.html>LS</a> : LESOTHO<br> | 133 | "Macedonia, the Former Yugoslav Republic of",MK |
2756 | 134 | <a href=iso.LT.html>LT</a> : LITHUANIA<br> | 134 | Madagascar,MG |
2757 | 135 | <a href=iso.LU.html>LU</a> : LUXEMBOURG<br> | 135 | Malawi,MW |
2758 | 136 | <a href=iso.LV.html>LV</a> : LATVIA<br> | 136 | Malaysia,MY |
2759 | 137 | <a href=iso.LY.html>LY</a> : LIBYAN ARAB JAMABIRIYA<br> | 137 | Maldives,MV |
2760 | 138 | <a href=iso.MA.html>MA</a> : MOROCCO<br> | 138 | Mali,ML |
2761 | 139 | <a href=iso.MC.html>MC</a> : MONACO<br> | 139 | Malta,MT |
2762 | 140 | <a href=iso.MD.html>MD</a> : MOLDOVA, REPUBLIC OF<br> | 140 | Marshall Islands,MH |
2763 | 141 | <a href=iso.MG.html>MG</a> : MADAGASCAR<br> | 141 | Martinique,MQ |
2764 | 142 | <a href=iso.MH.html>MH</a> : MARSHALL ISLANDS<br> | 142 | Mauritania,MR |
2765 | 143 | <a href=iso.MK.html>MK</a> : MACEDONIA, THE FORMER YUGOSLAV REPU8LIC OF<br> | 143 | Mauritius,MU |
2766 | 144 | <a href=iso.ML.html>ML</a> : MALI<br> | 144 | Mayotte,YT |
2767 | 145 | <a href=iso.MM.html>MM</a> : MYANMAR<br> | 145 | Mexico,MX |
2768 | 146 | <a href=iso.MN.html>MN</a> : MONGOLIA<br> | 146 | "Micronesia, Federated States of",FM |
2769 | 147 | <a href=iso.MO.html>MO</a> : MACAU<br> | 147 | "Moldova, Republic of",MD |
2770 | 148 | <a href=iso.MP.html>MP</a> : NORTHERN MARIANA ISLANDS<br> | 148 | Monaco,MC |
2771 | 149 | <a href=iso.MQ.html>MQ</a> : MARTINIQUE<br> | 149 | Mongolia,MN |
2772 | 150 | <a href=iso.MR.html>MR</a> : MAURITANIA<br> | 150 | Montenegro,ME |
2773 | 151 | <a href=iso.MS.html>MS</a> : MONTSERRAT<br> | 151 | Montserrat,MS |
2774 | 152 | <a href=iso.MT.html>MT</a> : MALTA<br> | 152 | Morocco,MA |
2775 | 153 | <a href=iso.MU.html>MU</a> : MAURITIUS<br> | 153 | Mozambique,MZ |
2776 | 154 | <a href=iso.MV.html>MV</a> : MALDIVES<br> | 154 | Myanmar,MM |
2777 | 155 | <a href=iso.MW.html>MW</a> : MALAWI<br> | 155 | Namibia,NA |
2778 | 156 | <a href=iso.MX.html>MX</a> : MEXICO<br> | 156 | Nauru,NR |
2779 | 157 | <a href=iso.MY.html>MY</a> : MALAYSIA<br> | 157 | Nepal,NP |
2780 | 158 | <a href=iso.MZ.html>MZ</a> : MOZAMBIQUE<br> | 158 | Netherlands,NL |
2781 | 159 | <a href=iso.NA.html>NA</a> : NAMIBIA<br> | 159 | New Caledonia,NC |
2782 | 160 | <a href=iso.NC.html>NC</a> : NEW CALEDONIA<br> | 160 | New Zealand,NZ |
2783 | 161 | <a href=iso.NE.html>NE</a> : NIGER<br> | 161 | Nicaragua,NI |
2784 | 162 | <a href=iso.NF.html>NF</a> : NORFOLK ISLAND<br> | 162 | Niger,NE |
2785 | 163 | <a href=iso.NG.html>NG</a> : NIGERIA<br> | 163 | Nigeria,NG |
2786 | 164 | <a href=iso.NI.html>NI</a> : NICARAGUA<br> | 164 | Niue,NU |
2787 | 165 | <a href=iso.NL.html>NL</a> : NETHERLANDS<br> | 165 | Norfolk Island,NF |
2788 | 166 | <a href=iso.NO.html>NO</a> : NORWAY<br> | 166 | Northern Mariana Islands,MP |
2789 | 167 | <a href=iso.NP.html>NP</a> : NEPAL<br> | 167 | Norway,NO |
2790 | 168 | <a href=iso.NU.html>NU</a> : NIUE<br> | 168 | Oman,OM |
2791 | 169 | <a href=iso.NZ.html>NZ</a> : NEW ZEALAND<br> | 169 | Pakistan,PK |
2792 | 170 | <a href=iso.OM.html>OM</a> : OMAN<br> | 170 | Palau,PW |
2793 | 171 | <a href=iso.PA.html>PA</a> : PANAMA<br> | 171 | "Palestine, State of",PS |
2794 | 172 | <a href=iso.PE.html>PE</a> : PERU<br> | 172 | Panama,PA |
2795 | 173 | <a href=iso.PF.html>PF</a> : FRENCH POLYNESIA<br> | 173 | Papua New Guinea,PG |
2796 | 174 | <a href=iso.PG.html>PG</a> : PAPUA NEW GUINEA<br> | 174 | Paraguay,PY |
2797 | 175 | <a href=iso.PH.html>PH</a> : PHILIPPINES<br> | 175 | Peru,PE |
2798 | 176 | <a href=iso.PK.html>PK</a> : PAKISTAN<br> | 176 | Philippines,PH |
2799 | 177 | <a href=iso.PL.html>PL</a> : POLAND<br> | 177 | Pitcairn,PN |
2800 | 178 | <a href=iso.PM.html>PM</a> : SAINT PIERRE AND MIQUELON<br> | 178 | Poland,PL |
2801 | 179 | <a href=iso.PN.html>PN</a> : PITCAIRN<br> | 179 | Portugal,PT |
2802 | 180 | <a href=iso.PR.html>PR</a> : PUERTO RICO<br> | 180 | Puerto Rico,PR |
2803 | 181 | <a href=iso.PT.html>PT</a> : PORTUGAL<br> | 181 | Qatar,QA |
2804 | 182 | <a href=iso.PW.html>PW</a> : PALAU<br> | 182 | Réunion,RE |
2805 | 183 | <a href=iso.PY.html>PY</a> : PARAGUAY<br> | 183 | Romania,RO |
2806 | 184 | <a href=iso.QA.html>QA</a> : QATAR<br> | 184 | Russian Federation,RU |
2807 | 185 | <a href=iso.RE.html>RE</a> : R�UNION<br> | 185 | Rwanda,RW |
2808 | 186 | <a href=iso.RO.html>RO</a> : ROMANIA<br> | 186 | Saint Barthélemy,BL |
2809 | 187 | <a href=iso.RU.html>RU</a> : RUSSIAN FEDERATION<br> | 187 | "Saint Helena, Ascension and Tristan da Cunha",SH |
2810 | 188 | <a href=iso.RW.html>RW</a> : RWANDA<br> | 188 | Saint Kitts and Nevis,KN |
2811 | 189 | <a href=iso.SA.html>SA</a> : SAUDI ARABIA<br> | 189 | Saint Lucia,LC |
2812 | 190 | <a href=iso.SB.html>SB</a> : SOLOMON ISLANDS<br> | 190 | Saint Martin (French part),MF |
2813 | 191 | <a href=iso.SC.html>SC</a> : SEYCHELLES<br> | 191 | Saint Pierre and Miquelon,PM |
2814 | 192 | <a href=iso.SD.html>SD</a> : SUDAN<br> | 192 | Saint Vincent and the Grenadines,VC |
2815 | 193 | <a href=iso.SE.html>SE</a> : SWEDEN<br> | 193 | Samoa,WS |
2816 | 194 | <a href=iso.SG.html>SG</a> : SINGAPORE<br> | 194 | San Marino,SM |
2817 | 195 | <a href=iso.SH.html>SH</a> : SAINT HELENA<br> | 195 | Sao Tome and Principe,ST |
2818 | 196 | <a href=iso.SI.html>SI</a> : SLOVENIA<br> | 196 | Saudi Arabia,SA |
2819 | 197 | <a href=iso.SJ.html>SJ</a> : SVALBARD AND JAN MAYEN<br> | 197 | Senegal,SN |
2820 | 198 | <a href=iso.SK.html>SK</a> : SLOVAKIA<br> | 198 | Serbia,RS |
2821 | 199 | <a href=iso.SL.html>SL</a> : SIERRA LEONE<br> | 199 | Seychelles,SC |
2822 | 200 | <a href=iso.SM.html>SM</a> : SAN MARINO<br> | 200 | Sierra Leone,SL |
2823 | 201 | <a href=iso.SN.html>SN</a> : SENEGAL<br> | 201 | Singapore,SG |
2824 | 202 | <a href=iso.SO.html>SO</a> : SOMALIA<br> | 202 | Sint Maarten (Dutch part),SX |
2825 | 203 | <a href=iso.SR.html>SR</a> : SURINAME<br> | 203 | Slovakia,SK |
2826 | 204 | <a href=iso.ST.html>ST</a> : SAO TOME AND PRINCIPE<br> | 204 | Slovenia,SI |
2827 | 205 | <a href=iso.SV.html>SV</a> : EL SALVADOR<br> | 205 | Solomon Islands,SB |
2828 | 206 | <a href=iso.SY.html>SY</a> : SYRIAN ARAB REPUBLIC<br> | 206 | Somalia,SO |
2829 | 207 | <a href=iso.SZ.html>SZ</a> : SWAZILAND<br> | 207 | South Africa,ZA |
2830 | 208 | <a href=iso.TC.html>TC</a> : TURKS AND CAICOS ISLANDS<br> | 208 | South Georgia and the South Sandwich Islands,GS |
2831 | 209 | <a href=iso.TD.html>TD</a> : CHAD<br> | 209 | South Sudan,SS |
2832 | 210 | <a href=iso.TF.html>TF</a> : FRENCH SOUTHERN TERRITORIES<br> | 210 | Spain,ES |
2833 | 211 | <a href=iso.TG.html>TG</a> : TOGO<br> | 211 | Sri Lanka,LK |
2834 | 212 | <a href=iso.TH.html>TH</a> : THAILAND<br> | 212 | Sudan,SD |
2835 | 213 | <a href=iso.TJ.html>TJ</a> : TAJIKISTAN<br> | 213 | Suriname,SR |
2836 | 214 | <a href=iso.TK.html>TK</a> : TOKELAU<br> | 214 | Svalbard and Jan Mayen,SJ |
2837 | 215 | <a href=iso.TM.html>TM</a> : TURKMENISTAN<br> | 215 | Swaziland,SZ |
2838 | 216 | <a href=iso.TN.html>TN</a> : TUNISIA<br> | 216 | Sweden,SE |
2839 | 217 | <a href=iso.TO.html>TO</a> : TONGA<br> | 217 | Switzerland,CH |
2840 | 218 | <a href=iso.TP.html>TP</a> : EAST TIMOR<br> | 218 | Syrian Arab Republic,SY |
2841 | 219 | <a href=iso.TR.html>TR</a> : TURKEY<br> | 219 | "Taiwan, Province of China",TW |
2842 | 220 | <a href=iso.TT.html>TT</a> : TRINIDAD AND TOBAGO<br> | 220 | Tajikistan,TJ |
2843 | 221 | <a href=iso.TV.html>TV</a> : TUVALU<br> | 221 | "Tanzania, United Republic of",TZ |
2844 | 222 | <a href=iso.TW.html>TW</a> : TAIWAN, PROVINCE OF CHINA<br> | 222 | Thailand,TH |
2845 | 223 | <a href=iso.TZ.html>TZ</a> : TANZANIA, UNITED REPUBLIC OF<br> | 223 | Timor-Leste,TL |
2846 | 224 | <a href=iso.UA.html>UA</a> : UKRAINE<br> | 224 | Togo,TG |
2847 | 225 | <a href=iso.UG.html>UG</a> : UGANDA<br> | 225 | Tokelau,TK |
2848 | 226 | <a href=iso.UM.html>UM</a> : UNITED STATES MINOR OUTLYING ISLANDS<br> | 226 | Tonga,TO |
2849 | 227 | <a href=iso.US.html>US</a> : UNITED STATES<br> | 227 | Trinidad and Tobago,TT |
2850 | 228 | <a href=iso.UY.html>UY</a> : URUGUAY<br> | 228 | Tunisia,TN |
2851 | 229 | <a href=iso.UZ.html>UZ</a> : UZBEKISTAN<br> | 229 | Turkey,TR |
2852 | 230 | <a href=iso.VE.html>VE</a> : VENEZUELA<br> | 230 | Turkmenistan,TM |
2853 | 231 | <a href=iso.VG.html>VG</a> : VIRGIN ISLANDS, BRITISH<br> | 231 | Turks and Caicos Islands,TC |
2854 | 232 | <a href=iso.VI.html>VI</a> : VIRGIN ISLANDS, U.S.<br> | 232 | Tuvalu,TV |
2855 | 233 | <a href=iso.VN.html>VN</a> : VIET NAM<br> | 233 | Uganda,UG |
2856 | 234 | <a href=iso.VU.html>VU</a> : VANUATU<br> | 234 | Ukraine,UA |
2857 | 235 | <a href=iso.WF.html>WF</a> : WALLIS AND FUTUNA<br> | 235 | United Arab Emirates,AE |
2858 | 236 | <a href=iso.WS.html>WS</a> : SAMOA<br> | 236 | United Kingdom,GB |
2859 | 237 | <a href=iso.YE.html>YE</a> : YEMEN<br> | 237 | United States,US |
2860 | 238 | <a href=iso.YT.html>YT</a> : MAYOTTE<br> | 238 | United States Minor Outlying Islands,UM |
2861 | 239 | <a href=iso.YU.html>YU</a> : YUGOSLAVIA<br> | 239 | Uruguay,UY |
2862 | 240 | <a href=iso.ZA.html>ZA</a> : SOUTH AFRICA<br> | 240 | Uzbekistan,UZ |
2863 | 241 | <a href=iso.ZM.html>ZM</a> : ZAMBIA<br> | 241 | Vanuatu,VU |
2864 | 242 | <a href=iso.ZW.html>ZW</a> : ZIMBABWE<br> | 242 | "Venezuela, Bolivarian Republic of",VE |
2865 | 243 | </body></html> | 243 | Viet Nam,VN |
2866 | 244 | "Virgin Islands, British",VG | ||
2867 | 245 | "Virgin Islands, U.S.",VI | ||
2868 | 246 | Wallis and Futuna,WF | ||
2869 | 247 | Western Sahara,EH | ||
2870 | 248 | Yemen,YE | ||
2871 | 249 | Zambia,ZM | ||
2872 | 250 | Zimbabwe,ZW | ||
2873 | diff --git a/scriptlets/fetch_country_codes.sh b/scriptlets/fetch_country_codes.sh | |||
2874 | index 8dae525..0850b77 100755 | |||
2875 | --- a/scriptlets/fetch_country_codes.sh | |||
2876 | +++ b/scriptlets/fetch_country_codes.sh | |||
2877 | @@ -3,8 +3,8 @@ | |||
2878 | 3 | # store them locally to be used in compilation time. | 3 | # store them locally to be used in compilation time. |
2879 | 4 | # This operation must be executed by hand when wanted to update | 4 | # This operation must be executed by hand when wanted to update |
2880 | 5 | # current ones shown in config page | 5 | # current ones shown in config page |
2882 | 6 | set -e | 6 | set -ex |
2883 | 7 | 7 | ||
2884 | 8 | cd "$(dirname "$0")" | 8 | cd "$(dirname "$0")" |
2885 | 9 | 9 | ||
2886 | 10 | curl -X GET http://geotags.com/iso3166/countries.html > country-codes | ||
2887 | 11 | \ No newline at end of file | 10 | \ No newline at end of file |
2888 | 11 | curl -X GET https://raw.githubusercontent.com/umpirsky/country-list/master/data/en_US/country.csv > country-codes | ||
2889 | diff --git a/scriptlets/fill_country_codes.sh b/scriptlets/fill_country_codes.sh | |||
2890 | index b8babe4..028b77a 100755 | |||
2891 | --- a/scriptlets/fill_country_codes.sh | |||
2892 | +++ b/scriptlets/fill_country_codes.sh | |||
2893 | @@ -4,9 +4,9 @@ | |||
2894 | 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 |
2895 | 5 | # html page were they will be used | 5 | # html page were they will be used |
2896 | 6 | 6 | ||
2898 | 7 | set -e | 7 | set -e |
2899 | 8 | 8 | ||
2901 | 9 | COUNTRY_CODES=../../../scriptlets/country-codes | 9 | COUNTRY_CODES=./scriptlets/country-codes |
2902 | 10 | 10 | ||
2903 | 11 | if [ ! -e $COUNTRY_CODES ]; then | 11 | if [ ! -e $COUNTRY_CODES ]; then |
2904 | 12 | echo "=======================================================" | 12 | echo "=======================================================" |
2905 | @@ -19,21 +19,16 @@ if [ ! -e $COUNTRY_CODES ]; then | |||
2906 | 19 | exit 1 | 19 | exit 1 |
2907 | 20 | fi | 20 | fi |
2908 | 21 | 21 | ||
2919 | 22 | # think that this is a scriptlet, executed in parts/<the_part>/build folder | 22 | # remove first line and create html |
2920 | 23 | cp $COUNTRY_CODES . | 23 | tail -n +2 $COUNTRY_CODES | sed 's/\r$//' | |
2921 | 24 | 24 | while read -r ln; do | |
2922 | 25 | # remove non processable lines | 25 | printf '<option value="%s">%s</option>\n' "${ln##*,}" "${ln%,*}" |
2923 | 26 | sed -i '/^<a href=iso/!d' country-codes | 26 | done > country-codes |
2914 | 27 | |||
2915 | 28 | # process lines to change its format to be html select options | ||
2916 | 29 | sed -i '/<a href=iso/ s/<a href=iso.*html>/<option value="/ | ||
2917 | 30 | s/<\/a>\ :\ /">/ | ||
2918 | 31 | s/<br>/<\/option>/' country-codes | ||
2924 | 32 | 27 | ||
2925 | 33 | # add world wide default option at beginning | 28 | # add world wide default option at beginning |
2926 | 34 | sed -i '1s;^;<option value="XX">\-WORLD WIDE\-<\/option>\n;' country-codes | 29 | sed -i '1s;^;<option value="XX">\-WORLD WIDE\-<\/option>\n;' country-codes |
2927 | 35 | 30 | ||
2928 | 36 | # replace mark in management.html template with real country-codes | 31 | # replace mark in management.html template with real country-codes |
2930 | 37 | sed -e '/\[COUNTRY_CODE_OPTIONS\]/ {' -e 'r country-codes' -e 'd' -e '}' -i static/templates/management.html | 32 | sed -e '/\[COUNTRY_CODE_OPTIONS\]/ {' -e 'r country-codes' -e 'd' -e '}' -i static/templates/management.html |
2931 | 38 | 33 | ||
2932 | 39 | echo "country codes filled ok" | 34 | echo "country codes filled ok" |
2933 | diff --git a/server/handlers.go b/server/handlers.go | |||
2934 | index 45ec7fe..b7e71ff 100644 | |||
2935 | --- a/server/handlers.go | |||
2936 | +++ b/server/handlers.go | |||
2937 | @@ -24,10 +24,8 @@ import ( | |||
2938 | 24 | "net/http" | 24 | "net/http" |
2939 | 25 | "os" | 25 | "os" |
2940 | 26 | "path/filepath" | 26 | "path/filepath" |
2941 | 27 | "text/template" | ||
2942 | 28 | "time" | ||
2943 | 29 | |||
2944 | 30 | "strconv" | 27 | "strconv" |
2945 | 28 | "text/template" | ||
2946 | 31 | 29 | ||
2947 | 32 | "launchpad.net/wifi-connect/utils" | 30 | "launchpad.net/wifi-connect/utils" |
2948 | 33 | ) | 31 | ) |
2949 | @@ -42,9 +40,6 @@ const ( | |||
2950 | 42 | // ResourcesPath absolute path to web static resources | 40 | // ResourcesPath absolute path to web static resources |
2951 | 43 | var ResourcesPath = filepath.Join(os.Getenv("SNAP"), "static") | 41 | var ResourcesPath = filepath.Join(os.Getenv("SNAP"), "static") |
2952 | 44 | 42 | ||
2953 | 45 | // first time management portal is accessed this file is created. | ||
2954 | 46 | var firstConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), ".first_config") | ||
2955 | 47 | |||
2956 | 48 | var cw interface{} | 43 | var cw interface{} |
2957 | 49 | 44 | ||
2958 | 50 | // Data interface representing any data included in a template | 45 | // Data interface representing any data included in a template |
2959 | @@ -66,7 +61,36 @@ type ConnectingData struct { | |||
2960 | 66 | Ssid string | 61 | Ssid string |
2961 | 67 | } | 62 | } |
2962 | 68 | 63 | ||
2964 | 69 | type noData struct { | 64 | // UserEvent stores data from events generated by the user, that is |
2965 | 65 | // sent back to the service main loop | ||
2966 | 66 | type UserEvent struct { | ||
2967 | 67 | EvType int | ||
2968 | 68 | Params map[string]string | ||
2969 | 69 | } | ||
2970 | 70 | |||
2971 | 71 | // Event types for UserEvent.evType | ||
2972 | 72 | const ( | ||
2973 | 73 | UserEventConnect = 0 + iota | ||
2974 | 74 | UserEventRefresh | ||
2975 | 75 | UserEventDisconnect | ||
2976 | 76 | ) | ||
2977 | 77 | |||
2978 | 78 | // Parameters for UserEventConnect | ||
2979 | 79 | const ( | ||
2980 | 80 | UeConnSsid = "ssid" | ||
2981 | 81 | UeConnPassword = "password" | ||
2982 | 82 | ) | ||
2983 | 83 | |||
2984 | 84 | // ManagementServer models the management server | ||
2985 | 85 | type ManagementServer struct { | ||
2986 | 86 | chUserEvs chan<- UserEvent | ||
2987 | 87 | config utils.Config | ||
2988 | 88 | ssidLs *utils.SsidList | ||
2989 | 89 | } | ||
2990 | 90 | |||
2991 | 91 | // OperationalServer models the operational server | ||
2992 | 92 | type OperationalServer struct { | ||
2993 | 93 | chUserEvs chan<- UserEvent | ||
2994 | 70 | } | 94 | } |
2995 | 71 | 95 | ||
2996 | 72 | func execTemplate(w http.ResponseWriter, templatePath string, data Data) { | 96 | func execTemplate(w http.ResponseWriter, templatePath string, data Data) { |
2997 | @@ -81,7 +105,7 @@ func execTemplate(w http.ResponseWriter, templatePath string, data Data) { | |||
2998 | 81 | 105 | ||
2999 | 82 | err = t.Execute(w, data) | 106 | err = t.Execute(w, data) |
3000 | 83 | if err != nil { | 107 | if err != nil { |
3002 | 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) |
3003 | 85 | log.Print(msg) | 109 | log.Print(msg) |
3004 | 86 | http.Error(w, msg, http.StatusInternalServerError) | 110 | http.Error(w, msg, http.StatusInternalServerError) |
3005 | 87 | return | 111 | return |
3006 | @@ -89,53 +113,36 @@ func execTemplate(w http.ResponseWriter, templatePath string, data Data) { | |||
3007 | 89 | } | 113 | } |
3008 | 90 | 114 | ||
3009 | 91 | // ManagementHandler handles management portal | 115 | // ManagementHandler handles management portal |
3011 | 92 | func ManagementHandler(w http.ResponseWriter, r *http.Request) { | 116 | func (ms *ManagementServer) ManagementHandler(w http.ResponseWriter, r *http.Request) { |
3012 | 93 | 117 | ||
3013 | 94 | if utils.MustSetConfig() { | 118 | if utils.MustSetConfig() { |
3014 | 95 | 119 | ||
3025 | 96 | config, err := utils.ReadConfig() | 120 | if !ms.config.Portal.NoResetCredentials { |
3026 | 97 | if err != nil { | 121 | execTemplate(w, managementTemplatePath, |
3027 | 98 | msg := fmt.Sprintf("Error reading configuration: %v", err) | 122 | ManagementData{Config: &ms.config, Page: "config"}) |
3018 | 99 | log.Println(msg) | ||
3019 | 100 | http.Error(w, msg, http.StatusInternalServerError) | ||
3020 | 101 | return | ||
3021 | 102 | } | ||
3022 | 103 | |||
3023 | 104 | if !config.Portal.NoResetCredentials { | ||
3024 | 105 | execTemplate(w, managementTemplatePath, ManagementData{Config: config, Page: "config"}) | ||
3028 | 106 | return | 123 | return |
3029 | 107 | } | 124 | } |
3030 | 108 | 125 | ||
3031 | 109 | } | 126 | } |
3032 | 110 | 127 | ||
3041 | 111 | ssids, err := utils.ReadSsidsFile() | 128 | execTemplate(w, managementTemplatePath, |
3042 | 112 | if err != nil { | 129 | ManagementData{Ssids: ms.ssidLs.GetSsidList(), Page: "ssids"}) |
3035 | 113 | log.Printf("Error reading SSIDs file: %v", err) | ||
3036 | 114 | http.Error(w, err.Error(), http.StatusInternalServerError) | ||
3037 | 115 | return | ||
3038 | 116 | } | ||
3039 | 117 | |||
3040 | 118 | execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"}) | ||
3043 | 119 | } | 130 | } |
3044 | 120 | 131 | ||
3045 | 121 | // SaveConfigHandler saves config received as form post parameters | 132 | // SaveConfigHandler saves config received as form post parameters |
3055 | 122 | func SaveConfigHandler(w http.ResponseWriter, r *http.Request) { | 133 | func (ms *ManagementServer) SaveConfigHandler(w http.ResponseWriter, r *http.Request) { |
3056 | 123 | // read previous config | 134 | // start from previous config |
3057 | 124 | config, err := utils.ReadConfig() | 135 | config := ms.config |
3049 | 125 | if err != nil { | ||
3050 | 126 | msg := fmt.Sprintf("Error reading previous stored config: %v", err) | ||
3051 | 127 | log.Println(msg) | ||
3052 | 128 | http.Error(w, msg, http.StatusInternalServerError) | ||
3053 | 129 | return | ||
3054 | 130 | } | ||
3058 | 131 | 136 | ||
3059 | 132 | r.ParseForm() | 137 | r.ParseForm() |
3060 | 133 | 138 | ||
3061 | 139 | var err error | ||
3062 | 134 | config.Wifi.Ssid = utils.ParseFormParamSingleValue(r.Form, "Ssid") | 140 | config.Wifi.Ssid = utils.ParseFormParamSingleValue(r.Form, "Ssid") |
3063 | 135 | config.Wifi.Passphrase = utils.ParseFormParamSingleValue(r.Form, "Passphrase") | 141 | config.Wifi.Passphrase = utils.ParseFormParamSingleValue(r.Form, "Passphrase") |
3064 | 136 | config.Wifi.Interface = utils.ParseFormParamSingleValue(r.Form, "Interface") | 142 | config.Wifi.Interface = utils.ParseFormParamSingleValue(r.Form, "Interface") |
3065 | 137 | config.Wifi.CountryCode = utils.ParseFormParamSingleValue(r.Form, "CountryCode") | 143 | config.Wifi.CountryCode = utils.ParseFormParamSingleValue(r.Form, "CountryCode") |
3067 | 138 | config.Wifi.Channel, err = strconv.Atoi(utils.ParseFormParamSingleValue(r.Form, "Channel")) | 144 | config.Wifi.Channel, err = strconv.Atoi( |
3068 | 145 | utils.ParseFormParamSingleValue(r.Form, "Channel")) | ||
3069 | 139 | if err != nil { | 146 | if err != nil { |
3070 | 140 | msg := fmt.Sprintf("Error parsing channel form value: %v", err) | 147 | msg := fmt.Sprintf("Error parsing channel form value: %v", err) |
3071 | 141 | log.Println(msg) | 148 | log.Println(msg) |
3072 | @@ -145,7 +152,7 @@ func SaveConfigHandler(w http.ResponseWriter, r *http.Request) { | |||
3073 | 145 | config.Wifi.OperationMode = utils.ParseFormParamSingleValue(r.Form, "OperationMode") | 152 | config.Wifi.OperationMode = utils.ParseFormParamSingleValue(r.Form, "OperationMode") |
3074 | 146 | config.Portal.Password = utils.ParseFormParamSingleValue(r.Form, "PortalPassword") | 153 | config.Portal.Password = utils.ParseFormParamSingleValue(r.Form, "PortalPassword") |
3075 | 147 | 154 | ||
3077 | 148 | err = utils.WriteConfig(config) | 155 | err = utils.WriteConfig(&config) |
3078 | 149 | if err != nil { | 156 | if err != nil { |
3079 | 150 | msg := fmt.Sprintf("Error saving config: %v", err) | 157 | msg := fmt.Sprintf("Error saving config: %v", err) |
3080 | 151 | log.Println(msg) | 158 | log.Println(msg) |
3081 | @@ -153,20 +160,12 @@ func SaveConfigHandler(w http.ResponseWriter, r *http.Request) { | |||
3082 | 153 | return | 160 | return |
3083 | 154 | } | 161 | } |
3084 | 155 | 162 | ||
3095 | 156 | //after saving config, redirect to management portal, showing available ssids | 163 | execTemplate(w, managementTemplatePath, |
3096 | 157 | ssids, err := utils.ReadSsidsFile() | 164 | ManagementData{Ssids: ms.ssidLs.GetSsidList(), Page: "ssids"}) |
3087 | 158 | if err != nil { | ||
3088 | 159 | msg := fmt.Sprintf("== wifi-connect/handler: Error reading SSIDs file: %v\n", err) | ||
3089 | 160 | log.Println(msg) | ||
3090 | 161 | http.Error(w, msg, http.StatusInternalServerError) | ||
3091 | 162 | return | ||
3092 | 163 | } | ||
3093 | 164 | |||
3094 | 165 | execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"}) | ||
3097 | 166 | } | 165 | } |
3098 | 167 | 166 | ||
3099 | 168 | // ConnectHandler reads form got ssid and password and tries to connect to that network | 167 | // ConnectHandler reads form got ssid and password and tries to connect to that network |
3101 | 169 | func ConnectHandler(w http.ResponseWriter, r *http.Request) { | 168 | func (ms *ManagementServer) ConnectHandler(w http.ResponseWriter, r *http.Request) { |
3102 | 170 | r.ParseForm() | 169 | r.ParseForm() |
3103 | 171 | 170 | ||
3104 | 172 | pwd := "" | 171 | pwd := "" |
3105 | @@ -184,55 +183,15 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) { | |||
3106 | 184 | } | 183 | } |
3107 | 185 | ssid := ssids[0] | 184 | ssid := ssids[0] |
3108 | 186 | 185 | ||
3151 | 187 | go func() { | 186 | ms.chUserEvs <- UserEvent{EvType: UserEventConnect, |
3152 | 188 | log.Printf("Connecting to %v", ssid) | 187 | Params: map[string]string{UeConnSsid: ssid, UeConnPassword: pwd}} |
3111 | 189 | |||
3112 | 190 | // While the connecting is attempted, the flag file must be | ||
3113 | 191 | // present so the service loops harmlessly | ||
3114 | 192 | waitPath := os.Getenv("SNAP_COMMON") + "/startingApConnect" | ||
3115 | 193 | err1 := utils.WriteFlagFile(waitPath) | ||
3116 | 194 | if err1 != nil { | ||
3117 | 195 | log.Print("Error writing flag file") | ||
3118 | 196 | return | ||
3119 | 197 | } | ||
3120 | 198 | |||
3121 | 199 | err := wifiapClient.Disable() | ||
3122 | 200 | if err != nil { | ||
3123 | 201 | msg := fmt.Sprintf("Error disabling AP: %v", err) | ||
3124 | 202 | log.Print(msg) | ||
3125 | 203 | http.Error(w, msg, http.StatusInternalServerError) | ||
3126 | 204 | return | ||
3127 | 205 | } | ||
3128 | 206 | |||
3129 | 207 | // manage iface by netman | ||
3130 | 208 | netmanClient.SetIfaceManaged("wlan0", true, netmanClient.GetWifiDevices(netmanClient.GetDevices())) | ||
3131 | 209 | _, ap2device, ssid2ap := netmanClient.Ssids() | ||
3132 | 210 | |||
3133 | 211 | // attempt to connect to external AP | ||
3134 | 212 | err = netmanClient.ConnectAp(ssid, pwd, ap2device, ssid2ap) | ||
3135 | 213 | //TODO signal user in portal on failure to connect | ||
3136 | 214 | if err != nil { | ||
3137 | 215 | msg := fmt.Sprintf("Failed connecting to %v", ssid) | ||
3138 | 216 | log.Println(msg) | ||
3139 | 217 | netmanClient.DeleteWifiConnections() | ||
3140 | 218 | //note that this call takes time, perhaps a couple minutes, so | ||
3141 | 219 | //deleting the wait file happens after, which means it takes a couple | ||
3142 | 220 | //minutes for the wifi-connect service to restor system to Management | ||
3143 | 221 | //mode | ||
3144 | 222 | http.Error(w, msg, http.StatusInternalServerError) | ||
3145 | 223 | //remove WAIT flag file so that daemon starts checking state | ||
3146 | 224 | //and takes control again | ||
3147 | 225 | waitPath := os.Getenv("SNAP_COMMON") + "/startingApConnect" | ||
3148 | 226 | utils.RemoveFlagFile(waitPath) | ||
3149 | 227 | } | ||
3150 | 228 | }() | ||
3153 | 229 | } | 188 | } |
3154 | 230 | 189 | ||
3155 | 231 | type disconnectData struct { | 190 | type disconnectData struct { |
3156 | 232 | } | 191 | } |
3157 | 233 | 192 | ||
3158 | 234 | // OperationalHandler display Opertational mode page | 193 | // OperationalHandler display Opertational mode page |
3160 | 235 | func OperationalHandler(w http.ResponseWriter, r *http.Request) { | 194 | func (os *OperationalServer) OperationalHandler(w http.ResponseWriter, r *http.Request) { |
3161 | 236 | data := disconnectData{} | 195 | data := disconnectData{} |
3162 | 237 | execTemplate(w, operationalTemplatePath, data) | 196 | execTemplate(w, operationalTemplatePath, data) |
3163 | 238 | } | 197 | } |
3164 | @@ -242,8 +201,8 @@ type hashResponse struct { | |||
3165 | 242 | HashMatch bool | 201 | HashMatch bool |
3166 | 243 | } | 202 | } |
3167 | 244 | 203 | ||
3170 | 245 | // HashItHandler returns a hash of the password as json | 204 | // hashItHandler returns a hash of the password as json |
3171 | 246 | func HashItHandler(w http.ResponseWriter, r *http.Request) { | 205 | func hashItHandler(w http.ResponseWriter, r *http.Request) { |
3172 | 247 | r.ParseForm() | 206 | r.ParseForm() |
3173 | 248 | hashMe := r.Form["Hash"] | 207 | hashMe := r.Form["Hash"] |
3174 | 249 | hashed, errH := utils.MatchingHash(hashMe[0]) | 208 | hashed, errH := utils.MatchingHash(hashMe[0]) |
3175 | @@ -262,52 +221,31 @@ func HashItHandler(w http.ResponseWriter, r *http.Request) { | |||
3176 | 262 | w.Write(b) | 221 | w.Write(b) |
3177 | 263 | } | 222 | } |
3178 | 264 | 223 | ||
3179 | 224 | // HashItHandler hashes the password | ||
3180 | 225 | func (os *OperationalServer) HashItHandler(w http.ResponseWriter, r *http.Request) { | ||
3181 | 226 | hashItHandler(w, r) | ||
3182 | 227 | } | ||
3183 | 228 | |||
3184 | 229 | // HashItHandler hashes the password | ||
3185 | 230 | func (ms *ManagementServer) HashItHandler(w http.ResponseWriter, r *http.Request) { | ||
3186 | 231 | hashItHandler(w, r) | ||
3187 | 232 | } | ||
3188 | 233 | |||
3189 | 265 | // DisconnectHandler allows user to disconnect from external AP | 234 | // DisconnectHandler allows user to disconnect from external AP |
3192 | 266 | func DisconnectHandler(w http.ResponseWriter, r *http.Request) { | 235 | func (os *OperationalServer) DisconnectHandler(w http.ResponseWriter, r *http.Request) { |
3193 | 267 | netmanClient.DisconnectWifi(netmanClient.GetWifiDevices(netmanClient.GetDevices())) | 236 | os.chUserEvs <- UserEvent{EvType: UserEventDisconnect} |
3194 | 268 | } | 237 | } |
3195 | 269 | 238 | ||
3196 | 270 | // RefreshHandler handles ssids refreshment | 239 | // RefreshHandler handles ssids refreshment |
3198 | 271 | func RefreshHandler(w http.ResponseWriter, r *http.Request) { | 240 | func (ms *ManagementServer) RefreshHandler(w http.ResponseWriter, r *http.Request) { |
3199 | 272 | 241 | ||
3200 | 273 | // show same page. After refresh operation, management page should show a refresh alert | 242 | // show same page. After refresh operation, management page should show a refresh alert |
3222 | 274 | ManagementHandler(w, r) | 243 | ms.ManagementHandler(w, r) |
3202 | 275 | |||
3203 | 276 | go func() { | ||
3204 | 277 | if err := netmanClient.Unmanage(); err != nil { | ||
3205 | 278 | fmt.Println(err) | ||
3206 | 279 | return | ||
3207 | 280 | } | ||
3208 | 281 | |||
3209 | 282 | apUp, err := wifiapClient.Enabled() | ||
3210 | 283 | if err != nil { | ||
3211 | 284 | fmt.Println(Sprintf("An error happened while requesting current AP status: %v\n", err)) | ||
3212 | 285 | return | ||
3213 | 286 | } | ||
3214 | 287 | |||
3215 | 288 | if apUp { | ||
3216 | 289 | err := wifiapClient.Disable() | ||
3217 | 290 | if err != nil { | ||
3218 | 291 | fmt.Println(Sprintf("An error happened while bringing AP down: %v\n", err)) | ||
3219 | 292 | return | ||
3220 | 293 | } | ||
3221 | 294 | } | ||
3223 | 295 | 244 | ||
3238 | 296 | for found := netmanClient.ScanAndWriteSsidsToFile(utils.SsidsFile); !found; found = netmanClient.ScanAndWriteSsidsToFile(utils.SsidsFile) { | 245 | ms.chUserEvs <- UserEvent{EvType: UserEventRefresh} |
3239 | 297 | time.Sleep(5 * time.Second) | 246 | } |
3226 | 298 | } | ||
3227 | 299 | |||
3228 | 300 | if err := netmanClient.Unmanage(); err != nil { | ||
3229 | 301 | fmt.Println(err) | ||
3230 | 302 | return | ||
3231 | 303 | } | ||
3232 | 304 | |||
3233 | 305 | err = wifiapClient.Enable() | ||
3234 | 306 | if err != nil { | ||
3235 | 307 | fmt.Println(Sprintf("An error happened while bringing AP up: %v\n", err)) | ||
3236 | 308 | return | ||
3237 | 309 | } | ||
3240 | 310 | 247 | ||
3243 | 311 | fmt.Println("== wifi-connect/RefreshHandler: starting wifi-ap") | 248 | // RedirectHandler redirects to our main URL |
3244 | 312 | }() | 249 | func (ms *ManagementServer) RedirectHandler(w http.ResponseWriter, r *http.Request) { |
3245 | 250 | http.Redirect(w, r, "http://device-wifi-connect.com", http.StatusFound) | ||
3246 | 313 | } | 251 | } |
3247 | diff --git a/server/handlers_test.go b/server/handlers_test.go | |||
3248 | index f4278b7..27a81e3 100644 | |||
3249 | --- a/server/handlers_test.go | |||
3250 | +++ b/server/handlers_test.go | |||
3251 | @@ -26,124 +26,19 @@ import ( | |||
3252 | 26 | "strings" | 26 | "strings" |
3253 | 27 | "testing" | 27 | "testing" |
3254 | 28 | 28 | ||
3255 | 29 | "launchpad.net/wifi-connect/netman" | ||
3256 | 30 | "launchpad.net/wifi-connect/utils" | 29 | "launchpad.net/wifi-connect/utils" |
3257 | 31 | ) | 30 | ) |
3258 | 32 | 31 | ||
3259 | 33 | type wifiapClientMock struct{} | ||
3260 | 34 | |||
3261 | 35 | func (c *wifiapClientMock) Show() (map[string]interface{}, error) { | ||
3262 | 36 | return nil, nil | ||
3263 | 37 | } | ||
3264 | 38 | |||
3265 | 39 | func (c *wifiapClientMock) Enable() error { | ||
3266 | 40 | return nil | ||
3267 | 41 | } | ||
3268 | 42 | |||
3269 | 43 | func (c *wifiapClientMock) Disable() error { | ||
3270 | 44 | return nil | ||
3271 | 45 | } | ||
3272 | 46 | |||
3273 | 47 | func (c *wifiapClientMock) Enabled() (bool, error) { | ||
3274 | 48 | return true, nil | ||
3275 | 49 | } | ||
3276 | 50 | |||
3277 | 51 | func (c *wifiapClientMock) SetSsid(string) error { | ||
3278 | 52 | return nil | ||
3279 | 53 | } | ||
3280 | 54 | |||
3281 | 55 | func (c *wifiapClientMock) SetPassphrase(string) error { | ||
3282 | 56 | return nil | ||
3283 | 57 | } | ||
3284 | 58 | |||
3285 | 59 | func (c *wifiapClientMock) Set(map[string]interface{}) error { | ||
3286 | 60 | return nil | ||
3287 | 61 | } | ||
3288 | 62 | |||
3289 | 63 | type netmanClientMock struct{} | ||
3290 | 64 | |||
3291 | 65 | func (c *netmanClientMock) GetDevices() []string { | ||
3292 | 66 | return []string{"/d/1"} | ||
3293 | 67 | } | ||
3294 | 68 | |||
3295 | 69 | func (c *netmanClientMock) GetWifiDevices(devices []string) []string { | ||
3296 | 70 | return []string{"/d/1"} | ||
3297 | 71 | } | ||
3298 | 72 | |||
3299 | 73 | func (c *netmanClientMock) GetAccessPoints(devices []string, ap2device map[string]string) []string { | ||
3300 | 74 | return []string{"/ap/1"} | ||
3301 | 75 | } | ||
3302 | 76 | |||
3303 | 77 | func (c *netmanClientMock) ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error { | ||
3304 | 78 | return nil | ||
3305 | 79 | } | ||
3306 | 80 | |||
3307 | 81 | func (c *netmanClientMock) Ssids() ([]netman.SSID, map[string]string, map[string]string) { | ||
3308 | 82 | myssid := netman.SSID{Ssid: "myssid", ApPath: "/ap/1"} | ||
3309 | 83 | return []netman.SSID{myssid}, map[string]string{"/ap/1": "/d/1"}, map[string]string{"myssid": "/ap/1"} | ||
3310 | 84 | } | ||
3311 | 85 | |||
3312 | 86 | func (c *netmanClientMock) Connected(devices []string) bool { | ||
3313 | 87 | return false | ||
3314 | 88 | } | ||
3315 | 89 | |||
3316 | 90 | func (c *netmanClientMock) ConnectedWifi(wifiDevices []string) bool { | ||
3317 | 91 | return false | ||
3318 | 92 | } | ||
3319 | 93 | |||
3320 | 94 | func (c *netmanClientMock) DisconnectWifi(wifiDevices []string) int { | ||
3321 | 95 | return 0 | ||
3322 | 96 | } | ||
3323 | 97 | |||
3324 | 98 | func (c *netmanClientMock) SetIfaceManaged(iface string, state bool, devices []string) string { | ||
3325 | 99 | return "wlan0" | ||
3326 | 100 | } | ||
3327 | 101 | |||
3328 | 102 | func (c *netmanClientMock) WifisManaged(wifiDevices []string) (map[string]string, error) { | ||
3329 | 103 | return map[string]string{"wlan0": "/d/1"}, nil | ||
3330 | 104 | } | ||
3331 | 105 | func (c *netmanClientMock) Unmanage() error { | ||
3332 | 106 | return nil | ||
3333 | 107 | } | ||
3334 | 108 | func (c *netmanClientMock) Manage() error { | ||
3335 | 109 | return nil | ||
3336 | 110 | } | ||
3337 | 111 | |||
3338 | 112 | func (c *netmanClientMock) ScanAndWriteSsidsToFile(filepath string) bool { | ||
3339 | 113 | return true | ||
3340 | 114 | } | ||
3341 | 115 | |||
3342 | 116 | func (c *netmanClientMock) DeleteWifiConnections() {} | ||
3343 | 117 | |||
3344 | 118 | func TestManagementHandler(t *testing.T) { | 32 | func TestManagementHandler(t *testing.T) { |
3365 | 119 | 33 | chUserEvs := make(chan UserEvent) | |
3366 | 120 | // mock settings to not to ask for saving a first configuration | 34 | ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{}, |
3367 | 121 | utils.ReadConfig = func() (*utils.Config, error) { | 35 | ssidLs: &utils.SsidList{}} |
3348 | 122 | return &utils.Config{ | ||
3349 | 123 | Wifi: &utils.WifiConfig{ | ||
3350 | 124 | Ssid: "Ubuntu", | ||
3351 | 125 | Passphrase: "17Soj8/Sxh14lcpD", | ||
3352 | 126 | Interface: "wlp2s0", | ||
3353 | 127 | CountryCode: "0x31", | ||
3354 | 128 | Channel: 6, | ||
3355 | 129 | OperationMode: "g", | ||
3356 | 130 | }, | ||
3357 | 131 | Portal: &utils.PortalConfig{ | ||
3358 | 132 | Password: "the_password", | ||
3359 | 133 | NoResetCredentials: true, | ||
3360 | 134 | NoOperational: false, | ||
3361 | 135 | }, | ||
3362 | 136 | }, nil | ||
3363 | 137 | } | ||
3364 | 138 | utils.MustSetConfig = func() bool { return false } | ||
3368 | 139 | 36 | ||
3369 | 140 | ResourcesPath = "../static" | 37 | ResourcesPath = "../static" |
3370 | 141 | SsidsFile := "../static/tests/ssids" | ||
3371 | 142 | utils.SetSsidsFile(SsidsFile) | ||
3372 | 143 | 38 | ||
3373 | 144 | w := httptest.NewRecorder() | 39 | w := httptest.NewRecorder() |
3374 | 145 | r, _ := http.NewRequest("GET", "/", nil) | 40 | r, _ := http.NewRequest("GET", "/", nil) |
3376 | 146 | http.HandlerFunc(ManagementHandler).ServeHTTP(w, r) | 41 | http.HandlerFunc(ms.ManagementHandler).ServeHTTP(w, r) |
3377 | 147 | 42 | ||
3378 | 148 | if w.Code != http.StatusOK { | 43 | if w.Code != http.StatusOK { |
3379 | 149 | t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code) | 44 | t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code) |
3380 | @@ -155,9 +50,9 @@ func TestManagementHandler(t *testing.T) { | |||
3381 | 155 | } | 50 | } |
3382 | 156 | 51 | ||
3383 | 157 | func TestConnectHandler(t *testing.T) { | 52 | func TestConnectHandler(t *testing.T) { |
3387 | 158 | 53 | chUserEvs := make(chan UserEvent) | |
3388 | 159 | wifiapClient = &wifiapClientMock{} | 54 | ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{}, |
3389 | 160 | netmanClient = &netmanClientMock{} | 55 | ssidLs: &utils.SsidList{}} |
3390 | 161 | 56 | ||
3391 | 162 | ResourcesPath = "../static" | 57 | ResourcesPath = "../static" |
3392 | 163 | 58 | ||
3393 | @@ -169,7 +64,21 @@ func TestConnectHandler(t *testing.T) { | |||
3394 | 169 | r, _ := http.NewRequest("POST", "/connect", bytes.NewBufferString(form.Encode())) | 64 | r, _ := http.NewRequest("POST", "/connect", bytes.NewBufferString(form.Encode())) |
3395 | 170 | r.Header.Add("Content-Type", "application/x-www-form-urlencoded") | 65 | r.Header.Add("Content-Type", "application/x-www-form-urlencoded") |
3396 | 171 | r.Header.Add("Content-Length", strconv.Itoa(len(form.Encode()))) | 66 | r.Header.Add("Content-Length", strconv.Itoa(len(form.Encode()))) |
3398 | 172 | http.HandlerFunc(ConnectHandler).ServeHTTP(w, r) | 67 | |
3399 | 68 | go func() { | ||
3400 | 69 | uEv := <-chUserEvs | ||
3401 | 70 | if uEv.EvType != UserEventConnect { | ||
3402 | 71 | t.Errorf("Bad event %v", uEv.EvType) | ||
3403 | 72 | } | ||
3404 | 73 | if uEv.Params[UeConnSsid] != "myssid" { | ||
3405 | 74 | t.Errorf("Bad ssid: %s", uEv.Params[UeConnSsid]) | ||
3406 | 75 | } | ||
3407 | 76 | if uEv.Params[UeConnPassword] != "mypassphrase" { | ||
3408 | 77 | t.Errorf("Bad password: %s", uEv.Params[UeConnPassword]) | ||
3409 | 78 | } | ||
3410 | 79 | }() | ||
3411 | 80 | |||
3412 | 81 | http.HandlerFunc(ms.ConnectHandler).ServeHTTP(w, r) | ||
3413 | 173 | 82 | ||
3414 | 174 | if w.Code != http.StatusOK { | 83 | if w.Code != http.StatusOK { |
3415 | 175 | t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code) | 84 | t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code) |
3416 | @@ -177,14 +86,21 @@ func TestConnectHandler(t *testing.T) { | |||
3417 | 177 | } | 86 | } |
3418 | 178 | 87 | ||
3419 | 179 | func TestDisconnectHandler(t *testing.T) { | 88 | func TestDisconnectHandler(t *testing.T) { |
3423 | 180 | 89 | chUserEvs := make(chan UserEvent) | |
3424 | 181 | netmanClient = &netmanClientMock{} | 90 | os := &OperationalServer{chUserEvs: chUserEvs} |
3422 | 182 | |||
3425 | 183 | ResourcesPath = "../static" | 91 | ResourcesPath = "../static" |
3426 | 184 | 92 | ||
3427 | 185 | w := httptest.NewRecorder() | 93 | w := httptest.NewRecorder() |
3428 | 186 | r, _ := http.NewRequest("GET", "/disconnect", nil) | 94 | r, _ := http.NewRequest("GET", "/disconnect", nil) |
3430 | 187 | http.HandlerFunc(DisconnectHandler).ServeHTTP(w, r) | 95 | |
3431 | 96 | go func() { | ||
3432 | 97 | uEv := <-chUserEvs | ||
3433 | 98 | if uEv.EvType != UserEventDisconnect { | ||
3434 | 99 | t.Errorf("Bad event %v", uEv.EvType) | ||
3435 | 100 | } | ||
3436 | 101 | }() | ||
3437 | 102 | |||
3438 | 103 | http.HandlerFunc(os.DisconnectHandler).ServeHTTP(w, r) | ||
3439 | 188 | 104 | ||
3440 | 189 | if w.Code != http.StatusOK { | 105 | if w.Code != http.StatusOK { |
3441 | 190 | t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code) | 106 | t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code) |
3442 | @@ -192,17 +108,22 @@ func TestDisconnectHandler(t *testing.T) { | |||
3443 | 192 | } | 108 | } |
3444 | 193 | 109 | ||
3445 | 194 | func TestRefreshHandler(t *testing.T) { | 110 | func TestRefreshHandler(t *testing.T) { |
3450 | 195 | 111 | chUserEvs := make(chan UserEvent) | |
3451 | 196 | wifiapClient = &wifiapClientMock{} | 112 | ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{}, |
3452 | 197 | netmanClient = &netmanClientMock{} | 113 | ssidLs: &utils.SsidList{}} |
3449 | 198 | |||
3453 | 199 | ResourcesPath = "../static" | 114 | ResourcesPath = "../static" |
3454 | 200 | SsidsFile := "../static/tests/ssids" | ||
3455 | 201 | utils.SetSsidsFile(SsidsFile) | ||
3456 | 202 | 115 | ||
3457 | 203 | w := httptest.NewRecorder() | 116 | w := httptest.NewRecorder() |
3458 | 204 | r, _ := http.NewRequest("GET", "/refresh", nil) | 117 | r, _ := http.NewRequest("GET", "/refresh", nil) |
3460 | 205 | http.HandlerFunc(RefreshHandler).ServeHTTP(w, r) | 118 | |
3461 | 119 | go func() { | ||
3462 | 120 | uEv := <-chUserEvs | ||
3463 | 121 | if uEv.EvType != UserEventRefresh { | ||
3464 | 122 | t.Errorf("Bad event %v", uEv.EvType) | ||
3465 | 123 | } | ||
3466 | 124 | }() | ||
3467 | 125 | |||
3468 | 126 | http.HandlerFunc(ms.RefreshHandler).ServeHTTP(w, r) | ||
3469 | 206 | 127 | ||
3470 | 207 | if w.Code != http.StatusOK { | 128 | if w.Code != http.StatusOK { |
3471 | 208 | t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code) | 129 | t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code) |
3472 | @@ -214,11 +135,13 @@ func TestRefreshHandler(t *testing.T) { | |||
3473 | 214 | } | 135 | } |
3474 | 215 | 136 | ||
3475 | 216 | func TestOperationalHandler(t *testing.T) { | 137 | func TestOperationalHandler(t *testing.T) { |
3476 | 138 | chUserEvs := make(chan UserEvent) | ||
3477 | 139 | os := &OperationalServer{chUserEvs: chUserEvs} | ||
3478 | 217 | ResourcesPath = "../static" | 140 | ResourcesPath = "../static" |
3479 | 218 | 141 | ||
3480 | 219 | w := httptest.NewRecorder() | 142 | w := httptest.NewRecorder() |
3481 | 220 | r, _ := http.NewRequest("GET", "/", nil) | 143 | r, _ := http.NewRequest("GET", "/", nil) |
3483 | 221 | http.HandlerFunc(OperationalHandler).ServeHTTP(w, r) | 144 | http.HandlerFunc(os.OperationalHandler).ServeHTTP(w, r) |
3484 | 222 | 145 | ||
3485 | 223 | if w.Code != http.StatusOK { | 146 | if w.Code != http.StatusOK { |
3486 | 224 | t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code) | 147 | t.Errorf("Expected status %d, got: %d", http.StatusOK, w.Code) |
3487 | @@ -230,83 +153,17 @@ func TestOperationalHandler(t *testing.T) { | |||
3488 | 230 | } | 153 | } |
3489 | 231 | 154 | ||
3490 | 232 | func TestInvalidTemplateHandler(t *testing.T) { | 155 | func TestInvalidTemplateHandler(t *testing.T) { |
3491 | 156 | chUserEvs := make(chan UserEvent) | ||
3492 | 157 | ms := &ManagementServer{chUserEvs: chUserEvs, config: utils.Config{}, | ||
3493 | 158 | ssidLs: &utils.SsidList{}} | ||
3494 | 233 | 159 | ||
3495 | 234 | ResourcesPath = "/invalidpath" | 160 | ResourcesPath = "/invalidpath" |
3496 | 235 | 161 | ||
3497 | 236 | w := httptest.NewRecorder() | 162 | w := httptest.NewRecorder() |
3498 | 237 | r, _ := http.NewRequest("GET", "/", nil) | 163 | r, _ := http.NewRequest("GET", "/", nil) |
3500 | 238 | http.HandlerFunc(ManagementHandler).ServeHTTP(w, r) | 164 | http.HandlerFunc(ms.ManagementHandler).ServeHTTP(w, r) |
3501 | 239 | 165 | ||
3502 | 240 | if w.Code != http.StatusInternalServerError { | 166 | if w.Code != http.StatusInternalServerError { |
3503 | 241 | t.Errorf("Expected status %d, got: %d", http.StatusInternalServerError, w.Code) | 167 | t.Errorf("Expected status %d, got: %d", http.StatusInternalServerError, w.Code) |
3504 | 242 | } | 168 | } |
3505 | 243 | } | 169 | } |
3506 | 244 | |||
3507 | 245 | func TestReadSsidsFile(t *testing.T) { | ||
3508 | 246 | |||
3509 | 247 | SsidsFile := "../static/tests/ssids" | ||
3510 | 248 | utils.SetSsidsFile(SsidsFile) | ||
3511 | 249 | ssids, err := utils.ReadSsidsFile() | ||
3512 | 250 | if err != nil { | ||
3513 | 251 | t.Errorf("Unexpected error reading ssids file: %v", err) | ||
3514 | 252 | } | ||
3515 | 253 | |||
3516 | 254 | if len(ssids) != 4 { | ||
3517 | 255 | t.Error("Expected 4 elements in csv record") | ||
3518 | 256 | } | ||
3519 | 257 | |||
3520 | 258 | set := make(map[string]bool) | ||
3521 | 259 | for _, v := range ssids { | ||
3522 | 260 | set[v] = true | ||
3523 | 261 | } | ||
3524 | 262 | |||
3525 | 263 | if !set["mynetwork"] { | ||
3526 | 264 | t.Error("mynetwork value not found") | ||
3527 | 265 | } | ||
3528 | 266 | if !set["yournetwork"] { | ||
3529 | 267 | t.Error("yournetwork value not found") | ||
3530 | 268 | } | ||
3531 | 269 | if !set["hernetwork"] { | ||
3532 | 270 | t.Error("hernetwork value not found") | ||
3533 | 271 | } | ||
3534 | 272 | if !set["hisnetwork"] { | ||
3535 | 273 | t.Error("hisnetwork value not found") | ||
3536 | 274 | } | ||
3537 | 275 | } | ||
3538 | 276 | |||
3539 | 277 | func TestReadSsidsFileWithOnlyOne(t *testing.T) { | ||
3540 | 278 | |||
3541 | 279 | SsidsFile := "../static/tests/ssids_onlyonessid" | ||
3542 | 280 | utils.SetSsidsFile(SsidsFile) | ||
3543 | 281 | ssids, err := utils.ReadSsidsFile() | ||
3544 | 282 | if err != nil { | ||
3545 | 283 | t.Errorf("Unexpected error reading ssids file: %v", err) | ||
3546 | 284 | } | ||
3547 | 285 | |||
3548 | 286 | if len(ssids) != 1 { | ||
3549 | 287 | t.Error("Expected 1 elements in csv record") | ||
3550 | 288 | } | ||
3551 | 289 | |||
3552 | 290 | set := make(map[string]bool) | ||
3553 | 291 | for _, v := range ssids { | ||
3554 | 292 | set[v] = true | ||
3555 | 293 | } | ||
3556 | 294 | |||
3557 | 295 | if !set["mynetwork"] { | ||
3558 | 296 | t.Error("mynetwork value not found") | ||
3559 | 297 | } | ||
3560 | 298 | } | ||
3561 | 299 | |||
3562 | 300 | func TestReadEmptySsidsFile(t *testing.T) { | ||
3563 | 301 | |||
3564 | 302 | SsidsFile := "../static/tests/ssids_empty" | ||
3565 | 303 | utils.SetSsidsFile(SsidsFile) | ||
3566 | 304 | ssids, err := utils.ReadSsidsFile() | ||
3567 | 305 | if err != nil { | ||
3568 | 306 | t.Errorf("Unexpected error reading ssids file: %v", err) | ||
3569 | 307 | } | ||
3570 | 308 | |||
3571 | 309 | if len(ssids) != 0 { | ||
3572 | 310 | t.Error("Expected 0 elements in csv record") | ||
3573 | 311 | } | ||
3574 | 312 | } | ||
3575 | diff --git a/server/launcher.go b/server/launcher.go | |||
3576 | index 7317df0..3615781 100644 | |||
3577 | --- a/server/launcher.go | |||
3578 | +++ b/server/launcher.go | |||
3579 | @@ -19,118 +19,50 @@ package server | |||
3580 | 19 | 19 | ||
3581 | 20 | import ( | 20 | import ( |
3582 | 21 | "log" | 21 | "log" |
3583 | 22 | "net" | ||
3584 | 23 | "net/http" | 22 | "net/http" |
3588 | 24 | "time" | 23 | "strconv" |
3586 | 25 | |||
3587 | 26 | "launchpad.net/wifi-connect/utils" | ||
3589 | 27 | ) | 24 | ) |
3590 | 28 | 25 | ||
3591 | 26 | // RunningState enum defining which server is up and running | ||
3592 | 27 | type RunningState int | ||
3593 | 28 | |||
3594 | 29 | // Server running state | 29 | // Server running state |
3595 | 30 | const ( | 30 | const ( |
3598 | 31 | Stopped RunningState = 0 + iota | 31 | Stopped RunningState = iota |
3597 | 32 | Starting | ||
3599 | 33 | Running | 32 | Running |
3600 | 34 | Stopping | ||
3601 | 35 | ) | 33 | ) |
3602 | 36 | 34 | ||
3603 | 37 | const ( | ||
3604 | 38 | // TestingAddress listening point for testing servers | ||
3605 | 39 | TestingAddress = ":8081" | ||
3606 | 40 | ) | ||
3607 | 41 | |||
3608 | 42 | // RunningState enum defining which server is up and running | ||
3609 | 43 | type RunningState int | ||
3610 | 44 | |||
3611 | 45 | // Address where server is listening | ||
3612 | 46 | var Address = ":8080" | ||
3613 | 47 | |||
3614 | 48 | // State holds current server state | 35 | // State holds current server state |
3615 | 49 | var State = Stopped | 36 | var State = Stopped |
3616 | 50 | 37 | ||
3618 | 51 | var listener net.Listener | 38 | var server *http.Server |
3619 | 52 | var done chan bool | 39 | var done chan bool |
3620 | 53 | 40 | ||
3648 | 54 | type tcpKeepAliveListener struct { | 41 | func listenAndServe(port int, handler http.Handler) error { |
3622 | 55 | *net.TCPListener | ||
3623 | 56 | } | ||
3624 | 57 | |||
3625 | 58 | // WaitForState waits for server reach certain state | ||
3626 | 59 | func WaitForState(state RunningState) bool { | ||
3627 | 60 | retries := 10 | ||
3628 | 61 | idle := 10 * time.Millisecond | ||
3629 | 62 | for ; retries > 0 && State != state; retries-- { | ||
3630 | 63 | time.Sleep(idle) | ||
3631 | 64 | idle *= 2 | ||
3632 | 65 | } | ||
3633 | 66 | return State == state | ||
3634 | 67 | } | ||
3635 | 68 | |||
3636 | 69 | // Accept accepts incoming tcp connections | ||
3637 | 70 | func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { | ||
3638 | 71 | tc, err := ln.AcceptTCP() | ||
3639 | 72 | if err != nil { | ||
3640 | 73 | return tc, err | ||
3641 | 74 | } | ||
3642 | 75 | tc.SetKeepAlive(true) | ||
3643 | 76 | tc.SetKeepAlivePeriod(3 * time.Minute) | ||
3644 | 77 | return tc, nil | ||
3645 | 78 | } | ||
3646 | 79 | |||
3647 | 80 | func listenAndServe(addr string, handler http.Handler) error { | ||
3649 | 81 | 42 | ||
3650 | 82 | if State != Stopped { | 43 | if State != Stopped { |
3656 | 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") |
3652 | 84 | } | ||
3653 | 85 | |||
3654 | 86 | if utils.RunningOn(addr) { | ||
3655 | 87 | return Errorf("Another instance is running in same address %v", addr) | ||
3657 | 88 | } | 45 | } |
3658 | 89 | 46 | ||
3664 | 90 | State = Starting | 47 | server = &http.Server{Addr: ":" + strconv.Itoa(port), Handler: handler} |
3665 | 91 | 48 | // channel needed to communicate real server shutdown, as after calling | |
3666 | 92 | srv := &http.Server{Addr: addr, Handler: handler} | 49 | // server.Close() it can take several milliseconds to really stop the |
3667 | 93 | // channel needed to communicate real server shutdown, as after calling listener.Close() | 50 | // listening. |
3663 | 94 | // it can take several milliseconds to really stop the listening. | ||
3668 | 95 | done = make(chan bool) | 51 | done = make(chan bool) |
3669 | 96 | 52 | ||
3670 | 97 | var err error | ||
3671 | 98 | listener, err = net.Listen("tcp", addr) | ||
3672 | 99 | if err != nil { | ||
3673 | 100 | return err | ||
3674 | 101 | } | ||
3675 | 102 | |||
3676 | 103 | // launch goroutine to check server state changes after startup is triggered | ||
3677 | 104 | go func() { | ||
3678 | 105 | retries := 10 | ||
3679 | 106 | idle := 10 * time.Millisecond | ||
3680 | 107 | for ; !utils.RunningOn(addr) && retries > 0; retries-- { | ||
3681 | 108 | time.Sleep(idle) | ||
3682 | 109 | idle *= 2 | ||
3683 | 110 | } | ||
3684 | 111 | |||
3685 | 112 | if retries == 0 { | ||
3686 | 113 | log.Print("Server could not be started") | ||
3687 | 114 | return | ||
3688 | 115 | } | ||
3689 | 116 | |||
3690 | 117 | State = Running | ||
3691 | 118 | }() | ||
3692 | 119 | |||
3693 | 120 | // launching server in a goroutine for not blocking | 53 | // launching server in a goroutine for not blocking |
3702 | 121 | go func() { | 54 | go func(server *http.Server) { |
3703 | 122 | if listener != nil { | 55 | err := server.ListenAndServe() |
3704 | 123 | err := srv.Serve(tcpKeepAliveListener{listener.(*net.TCPListener)}) | 56 | if err != nil { |
3705 | 124 | if err != nil { | 57 | log.Printf("HTTP Server closing - %v", err) |
3698 | 125 | log.Printf("HTTP Server closing - %v", err) | ||
3699 | 126 | } | ||
3700 | 127 | // notify server real stop | ||
3701 | 128 | done <- true | ||
3706 | 129 | } | 58 | } |
3707 | 59 | // notify server real stop | ||
3708 | 60 | done <- true | ||
3709 | 130 | 61 | ||
3710 | 131 | close(done) | 62 | close(done) |
3712 | 132 | }() | 63 | }(server) |
3713 | 133 | 64 | ||
3714 | 65 | State = Running | ||
3715 | 134 | return nil | 66 | return nil |
3716 | 135 | } | 67 | } |
3717 | 136 | 68 | ||
3718 | @@ -140,18 +72,16 @@ func stop() error { | |||
3719 | 140 | return Errorf("Already stopped") | 72 | return Errorf("Already stopped") |
3720 | 141 | } | 73 | } |
3721 | 142 | 74 | ||
3723 | 143 | if listener == nil { | 75 | if server == nil { |
3724 | 144 | State = Stopped | 76 | State = Stopped |
3725 | 145 | return Errorf("Already closed") | 77 | return Errorf("Already closed") |
3726 | 146 | } | 78 | } |
3727 | 147 | 79 | ||
3731 | 148 | State = Stopping | 80 | err := server.Close() |
3729 | 149 | |||
3730 | 150 | err := listener.Close() | ||
3732 | 151 | if err != nil { | 81 | if err != nil { |
3733 | 152 | return err | 82 | return err |
3734 | 153 | } | 83 | } |
3736 | 154 | listener = nil | 84 | server = nil |
3737 | 155 | 85 | ||
3738 | 156 | // wait for server real shutdown confirmation | 86 | // wait for server real shutdown confirmation |
3739 | 157 | <-done | 87 | <-done |
3740 | diff --git a/server/launcher_test.go b/server/launcher_test.go | |||
3741 | index c377c79..eb252d3 100644 | |||
3742 | --- a/server/launcher_test.go | |||
3743 | +++ b/server/launcher_test.go | |||
3744 | @@ -19,77 +19,24 @@ package server | |||
3745 | 19 | 19 | ||
3746 | 20 | import ( | 20 | import ( |
3747 | 21 | "testing" | 21 | "testing" |
3748 | 22 | "time" | ||
3749 | 22 | 23 | ||
3751 | 23 | telnet "github.com/reiver/go-telnet" | 24 | "launchpad.net/wifi-connect/utils" |
3752 | 24 | ) | 25 | ) |
3753 | 25 | 26 | ||
3754 | 26 | func TestLaunchAndStop(t *testing.T) { | 27 | func TestLaunchAndStop(t *testing.T) { |
3755 | 28 | thePort := 8444 | ||
3756 | 27 | 29 | ||
3792 | 28 | thePort := ":14444" | 30 | chUserEvs := make(chan UserEvent) |
3793 | 29 | 31 | err := listenAndServe(thePort, managementHandler(chUserEvs, | |
3794 | 30 | err := listenAndServe(thePort, nil) | 32 | &utils.Config{}, &utils.SsidList{})) |
3760 | 31 | if err != nil { | ||
3761 | 32 | t.Errorf("Start server failed: %v", err) | ||
3762 | 33 | } | ||
3763 | 34 | |||
3764 | 35 | // telnet to check server is alive | ||
3765 | 36 | caller := telnet.StandardCaller | ||
3766 | 37 | err = telnet.DialToAndCall("localhost"+thePort, caller) | ||
3767 | 38 | if err != nil { | ||
3768 | 39 | t.Errorf("Failed to telnet localhost server at port %v: %v", thePort, err) | ||
3769 | 40 | } | ||
3770 | 41 | |||
3771 | 42 | err = stop() | ||
3772 | 43 | if err != nil { | ||
3773 | 44 | t.Errorf("Stop server error: %v", err) | ||
3774 | 45 | } | ||
3775 | 46 | } | ||
3776 | 47 | |||
3777 | 48 | // This test is being skipped because of a problem with jenkins where it fails. | ||
3778 | 49 | // We cannot reproduce the failure outside of jenkins. Additionally, we are | ||
3779 | 50 | // changing the http server and will not track state in the same way in the future. | ||
3780 | 51 | // These tests will change. | ||
3781 | 52 | func SkipTestStates(t *testing.T) { | ||
3782 | 53 | |||
3783 | 54 | WaitForState(Stopped) | ||
3784 | 55 | |||
3785 | 56 | if State != Stopped { | ||
3786 | 57 | t.Error("Not in initial state") | ||
3787 | 58 | } | ||
3788 | 59 | |||
3789 | 60 | thePort := ":14444" | ||
3790 | 61 | |||
3791 | 62 | err := listenAndServe(thePort, nil) | ||
3795 | 63 | if err != nil { | 33 | if err != nil { |
3796 | 64 | t.Errorf("Start server failed: %v", err) | 34 | t.Errorf("Start server failed: %v", err) |
3797 | 65 | } | 35 | } |
3798 | 66 | 36 | ||
3811 | 67 | if State != Starting && State != Running { | 37 | time.Sleep(1 * time.Second) |
3800 | 68 | t.Error("Not in proper start(ing) state") | ||
3801 | 69 | } | ||
3802 | 70 | |||
3803 | 71 | WaitForState(Running) | ||
3804 | 72 | |||
3805 | 73 | // try a bad transition | ||
3806 | 74 | err = listenAndServe(thePort, nil) | ||
3807 | 75 | if err == nil { | ||
3808 | 76 | t.Error("An error should be thrown when trying to start an already running instance") | ||
3809 | 77 | } | ||
3810 | 78 | |||
3812 | 79 | err = stop() | 38 | err = stop() |
3813 | 80 | if err != nil { | 39 | if err != nil { |
3814 | 81 | t.Errorf("Stop server error: %v", err) | 40 | t.Errorf("Stop server error: %v", err) |
3815 | 82 | } | 41 | } |
3816 | 83 | |||
3817 | 84 | if State != Stopping && State != Stopped { | ||
3818 | 85 | t.Error("Not in proper stop(ing) state") | ||
3819 | 86 | } | ||
3820 | 87 | |||
3821 | 88 | WaitForState(Stopped) | ||
3822 | 89 | |||
3823 | 90 | // try bad transitions | ||
3824 | 91 | err = stop() | ||
3825 | 92 | if err == nil { | ||
3826 | 93 | t.Error("An error should be thrown when trying to stop a stopped instance") | ||
3827 | 94 | } | ||
3828 | 95 | } | 42 | } |
3829 | diff --git a/server/manager.go b/server/manager.go | |||
3830 | index 6b11c23..c668974 100644 | |||
3831 | --- a/server/manager.go | |||
3832 | +++ b/server/manager.go | |||
3833 | @@ -18,8 +18,6 @@ | |||
3834 | 18 | package server | 18 | package server |
3835 | 19 | 19 | ||
3836 | 20 | import ( | 20 | import ( |
3837 | 21 | "os" | ||
3838 | 22 | |||
3839 | 23 | "launchpad.net/wifi-connect/utils" | 21 | "launchpad.net/wifi-connect/utils" |
3840 | 24 | ) | 22 | ) |
3841 | 25 | 23 | ||
3842 | @@ -37,23 +35,20 @@ type Server int | |||
3843 | 37 | var Current = None | 35 | var Current = None |
3844 | 38 | 36 | ||
3845 | 39 | // StartManagementServer starts server in management mode | 37 | // StartManagementServer starts server in management mode |
3847 | 40 | func StartManagementServer() error { | 38 | func StartManagementServer(chUserEvs chan<- UserEvent, config *utils.Config, |
3848 | 39 | ssidLs *utils.SsidList) error { | ||
3849 | 40 | |||
3850 | 41 | if Current != None { | 41 | if Current != None { |
3851 | 42 | Current = None | 42 | Current = None |
3853 | 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") |
3854 | 44 | } | 44 | } |
3855 | 45 | 45 | ||
3856 | 46 | // change current instance asap we manage this server | 46 | // change current instance asap we manage this server |
3857 | 47 | Current = Management | 47 | Current = Management |
3858 | 48 | 48 | ||
3867 | 49 | waitPath := os.Getenv("SNAP_COMMON") + "/startingApConnect" | 49 | // Pass around program config to handlers |
3868 | 50 | err := utils.WriteFlagFile(waitPath) | 50 | err := listenAndServe(config.Portal.Port, |
3869 | 51 | if err != nil { | 51 | managementHandler(chUserEvs, config, ssidLs)) |
3862 | 52 | Current = None | ||
3863 | 53 | return err | ||
3864 | 54 | } | ||
3865 | 55 | |||
3866 | 56 | err = listenAndServe(Address, managementHandler()) | ||
3870 | 57 | if err != nil { | 52 | if err != nil { |
3871 | 58 | Current = None | 53 | Current = None |
3872 | 59 | return err | 54 | return err |
3873 | @@ -63,15 +58,15 @@ func StartManagementServer() error { | |||
3874 | 63 | } | 58 | } |
3875 | 64 | 59 | ||
3876 | 65 | // StartOperationalServer starts server in operational mode | 60 | // StartOperationalServer starts server in operational mode |
3878 | 66 | func StartOperationalServer() error { | 61 | func StartOperationalServer(chUserEvs chan<- UserEvent, config *utils.Config) error { |
3879 | 67 | if Current != None { | 62 | if Current != None { |
3881 | 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") |
3882 | 69 | } | 64 | } |
3883 | 70 | 65 | ||
3884 | 71 | // change current instance asap we manage this server | 66 | // change current instance asap we manage this server |
3885 | 72 | Current = Operational | 67 | Current = Operational |
3886 | 73 | 68 | ||
3888 | 74 | err := listenAndServe(Address, operationalHandler()) | 69 | err := listenAndServe(config.Portal.Port, operationalHandler(chUserEvs)) |
3889 | 75 | if err != nil { | 70 | if err != nil { |
3890 | 76 | Current = None | 71 | Current = None |
3891 | 77 | return err | 72 | return err |
3892 | @@ -80,9 +75,10 @@ func StartOperationalServer() error { | |||
3893 | 80 | return nil | 75 | return nil |
3894 | 81 | } | 76 | } |
3895 | 82 | 77 | ||
3897 | 83 | // ShutdownManagementServer shutdown server management mode. If management server is not up, returns error | 78 | // ShutdownManagementServer shutdown server management mode. If management |
3898 | 79 | // server is not up, returns error | ||
3899 | 84 | func ShutdownManagementServer() error { | 80 | func ShutdownManagementServer() error { |
3901 | 85 | if Current != Management || (State != Running && State != Starting) { | 81 | if Current != Management || State != Running { |
3902 | 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") |
3903 | 87 | } | 83 | } |
3904 | 88 | 84 | ||
3905 | @@ -95,9 +91,10 @@ func ShutdownManagementServer() error { | |||
3906 | 95 | return nil | 91 | return nil |
3907 | 96 | } | 92 | } |
3908 | 97 | 93 | ||
3910 | 98 | // ShutdownOperationalServer shutdown server operational mode. If operational server is not up, returns error | 94 | // ShutdownOperationalServer shutdown server operational mode. If operational |
3911 | 95 | // server is not up, returns error | ||
3912 | 99 | func ShutdownOperationalServer() error { | 96 | func ShutdownOperationalServer() error { |
3914 | 100 | if Current != Operational || (State != Running && State != Starting) { | 97 | if Current != Operational || State != Running { |
3915 | 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") |
3916 | 102 | } | 99 | } |
3917 | 103 | 100 | ||
3918 | diff --git a/server/manager_test.go b/server/manager_test.go | |||
3919 | index ea0661e..dbb38ae 100644 | |||
3920 | --- a/server/manager_test.go | |||
3921 | +++ b/server/manager_test.go | |||
3922 | @@ -1,29 +1,28 @@ | |||
3923 | 1 | package server | 1 | package server |
3924 | 2 | 2 | ||
3925 | 3 | import ( | 3 | import ( |
3926 | 4 | "os" | ||
3927 | 5 | "testing" | 4 | "testing" |
3928 | 5 | "time" | ||
3929 | 6 | |||
3930 | 7 | "launchpad.net/wifi-connect/utils" | ||
3931 | 6 | ) | 8 | ) |
3932 | 7 | 9 | ||
3933 | 8 | func TestBasicServerTransitionStates(t *testing.T) { | 10 | func TestBasicServerTransitionStates(t *testing.T) { |
3934 | 9 | 11 | ||
3935 | 10 | Address = ":14444" | ||
3936 | 11 | |||
3937 | 12 | os.Setenv("SNAP_COMMON", os.TempDir()) | ||
3938 | 13 | |||
3939 | 14 | if Current != None || State != Stopped { | 12 | if Current != None || State != Stopped { |
3940 | 15 | t.Errorf("Server is not in initial state") | 13 | t.Errorf("Server is not in initial state") |
3941 | 16 | } | 14 | } |
3942 | 17 | 15 | ||
3944 | 18 | if err := StartManagementServer(); err != nil { | 16 | chUserEvs := make(chan UserEvent) |
3945 | 17 | cfg := utils.Config{Portal: utils.PortalConfig{Port: 14444}} | ||
3946 | 18 | if err := StartManagementServer(chUserEvs, &cfg, &utils.SsidList{}); err != nil { | ||
3947 | 19 | t.Errorf("Error starting management server %v", err) | 19 | t.Errorf("Error starting management server %v", err) |
3948 | 20 | } | 20 | } |
3950 | 21 | if Current != Management || (State != Starting && State != Running) { | 21 | if Current != Management || State != Running { |
3951 | 22 | t.Errorf("Server is not in starting or in management status") | 22 | t.Errorf("Server is not in starting or in management status") |
3952 | 23 | } | 23 | } |
3953 | 24 | 24 | ||
3956 | 25 | WaitForState(Running) | 25 | time.Sleep(1 * time.Second) |
3955 | 26 | |||
3957 | 27 | if err := ShutdownManagementServer(); err != nil { | 26 | if err := ShutdownManagementServer(); err != nil { |
3958 | 28 | t.Errorf("Error stopping management server %v", err) | 27 | t.Errorf("Error stopping management server %v", err) |
3959 | 29 | } | 28 | } |
3960 | @@ -32,17 +31,14 @@ func TestBasicServerTransitionStates(t *testing.T) { | |||
3961 | 32 | t.Errorf("Current server is not None") | 31 | t.Errorf("Current server is not None") |
3962 | 33 | } | 32 | } |
3963 | 34 | 33 | ||
3967 | 35 | WaitForState(Stopped) | 34 | if err := StartOperationalServer(chUserEvs, &cfg); err != nil { |
3965 | 36 | |||
3966 | 37 | if err := StartOperationalServer(); err != nil { | ||
3968 | 38 | t.Errorf("Error starting operational server %v", err) | 35 | t.Errorf("Error starting operational server %v", err) |
3969 | 39 | } | 36 | } |
3971 | 40 | if Current != Operational || (State != Starting && State != Running) { | 37 | if Current != Operational || State != Running { |
3972 | 41 | t.Errorf("Server is not in starting or in operational status") | 38 | t.Errorf("Server is not in starting or in operational status") |
3973 | 42 | } | 39 | } |
3974 | 43 | 40 | ||
3977 | 44 | WaitForState(Running) | 41 | time.Sleep(1 * time.Second) |
3976 | 45 | |||
3978 | 46 | if err := ShutdownOperationalServer(); err != nil { | 42 | if err := ShutdownOperationalServer(); err != nil { |
3979 | 47 | t.Errorf("Error stopping operational server %v", err) | 43 | t.Errorf("Error stopping operational server %v", err) |
3980 | 48 | } | 44 | } |
3981 | @@ -52,31 +48,26 @@ func TestBasicServerTransitionStates(t *testing.T) { | |||
3982 | 52 | } | 48 | } |
3983 | 53 | 49 | ||
3984 | 54 | func TestEdgeServerTransitionStates(t *testing.T) { | 50 | func TestEdgeServerTransitionStates(t *testing.T) { |
3985 | 55 | |||
3986 | 56 | Address = ":14444" | ||
3987 | 57 | |||
3988 | 58 | os.Setenv("SNAP_COMMON", os.TempDir()) | ||
3989 | 59 | |||
3990 | 60 | if Current != None { | 51 | if Current != None { |
3991 | 61 | t.Errorf("Server is not in initial state") | 52 | t.Errorf("Server is not in initial state") |
3992 | 62 | } | 53 | } |
3993 | 63 | 54 | ||
3995 | 64 | if err := StartManagementServer(); err != nil { | 55 | chUserEvs := make(chan UserEvent) |
3996 | 56 | cfg := utils.Config{Portal: utils.PortalConfig{Port: 14444}} | ||
3997 | 57 | if err := StartManagementServer(chUserEvs, &cfg, &utils.SsidList{}); err != nil { | ||
3998 | 65 | t.Errorf("Error starting management server %v", err) | 58 | t.Errorf("Error starting management server %v", err) |
3999 | 66 | } | 59 | } |
4001 | 67 | if Current != Management || (State != Starting && State != Running) { | 60 | if Current != Management || State != Running { |
4002 | 68 | t.Errorf("Server is not in starting or in management status") | 61 | t.Errorf("Server is not in starting or in management status") |
4003 | 69 | } | 62 | } |
4004 | 70 | 63 | ||
4005 | 71 | WaitForState(Running) | ||
4006 | 72 | |||
4007 | 73 | // start operational server without stopping management must throw an error | 64 | // start operational server without stopping management must throw an error |
4008 | 74 | if err := StartOperationalServer; err == nil { | 65 | if err := StartOperationalServer; err == nil { |
4011 | 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 " + |
4012 | 76 | the other active`) | 67 | "the other active") |
4013 | 77 | } | 68 | } |
4014 | 78 | if Current != Management { | 69 | if Current != Management { |
4016 | 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") |
4017 | 80 | } | 71 | } |
4018 | 81 | 72 | ||
4019 | 82 | // stop wrong server must throw an error | 73 | // stop wrong server must throw an error |
4020 | @@ -84,9 +75,10 @@ func TestEdgeServerTransitionStates(t *testing.T) { | |||
4021 | 84 | t.Errorf("Expected an error when trying to shutdown wrong server") | 75 | t.Errorf("Expected an error when trying to shutdown wrong server") |
4022 | 85 | } | 76 | } |
4023 | 86 | if Current != Management { | 77 | if Current != Management { |
4025 | 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") |
4026 | 88 | } | 79 | } |
4027 | 89 | 80 | ||
4028 | 81 | time.Sleep(1 * time.Second) | ||
4029 | 90 | if err := ShutdownManagementServer(); err != nil { | 82 | if err := ShutdownManagementServer(); err != nil { |
4030 | 91 | t.Errorf("Error stopping management server %v", err) | 83 | t.Errorf("Error stopping management server %v", err) |
4031 | 92 | } | 84 | } |
4032 | @@ -94,25 +86,21 @@ func TestEdgeServerTransitionStates(t *testing.T) { | |||
4033 | 94 | t.Errorf("Server is not in None status") | 86 | t.Errorf("Server is not in None status") |
4034 | 95 | } | 87 | } |
4035 | 96 | 88 | ||
4036 | 97 | WaitForState(Stopped) | ||
4037 | 98 | |||
4038 | 99 | // analog tests with operational server | 89 | // analog tests with operational server |
4040 | 100 | if err := StartOperationalServer(); err != nil { | 90 | if err := StartOperationalServer(chUserEvs, &cfg); err != nil { |
4041 | 101 | t.Errorf("Error starting operational server %v", err) | 91 | t.Errorf("Error starting operational server %v", err) |
4042 | 102 | } | 92 | } |
4044 | 103 | if Current != Operational || (State != Starting && State != Running) { | 93 | if Current != Operational || State != Running { |
4045 | 104 | t.Errorf("Server is not in starting or in operational status") | 94 | t.Errorf("Server is not in starting or in operational status") |
4046 | 105 | } | 95 | } |
4047 | 106 | 96 | ||
4048 | 107 | WaitForState(Running) | ||
4049 | 108 | |||
4050 | 109 | // start management server without stopping operational must throw an error | 97 | // start management server without stopping operational must throw an error |
4051 | 110 | if err := StartManagementServer; err == nil { | 98 | if err := StartManagementServer; err == nil { |
4054 | 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 " + |
4055 | 112 | the other active`) | 100 | "instance having the other active") |
4056 | 113 | } | 101 | } |
4057 | 114 | if Current != Operational { | 102 | if Current != Operational { |
4059 | 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") |
4060 | 116 | } | 104 | } |
4061 | 117 | 105 | ||
4062 | 118 | // stop wrong server must throw an error | 106 | // stop wrong server must throw an error |
4063 | @@ -120,9 +108,10 @@ func TestEdgeServerTransitionStates(t *testing.T) { | |||
4064 | 120 | t.Errorf("Expected an error when trying to shutdown wrong server") | 108 | t.Errorf("Expected an error when trying to shutdown wrong server") |
4065 | 121 | } | 109 | } |
4066 | 122 | if Current != Operational { | 110 | if Current != Operational { |
4068 | 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") |
4069 | 124 | } | 112 | } |
4070 | 125 | 113 | ||
4071 | 114 | time.Sleep(1 * time.Second) | ||
4072 | 126 | if err := ShutdownOperationalServer(); err != nil { | 115 | if err := ShutdownOperationalServer(); err != nil { |
4073 | 127 | t.Errorf("Error stopping operational server %v", err) | 116 | t.Errorf("Error stopping operational server %v", err) |
4074 | 128 | } | 117 | } |
4075 | diff --git a/server/router.go b/server/router.go | |||
4076 | index 20a3716..3af467e 100644 | |||
4077 | --- a/server/router.go | |||
4078 | +++ b/server/router.go | |||
4079 | @@ -21,18 +21,31 @@ import ( | |||
4080 | 21 | "net/http" | 21 | "net/http" |
4081 | 22 | 22 | ||
4082 | 23 | "github.com/gorilla/mux" | 23 | "github.com/gorilla/mux" |
4083 | 24 | "launchpad.net/wifi-connect/utils" | ||
4084 | 24 | ) | 25 | ) |
4085 | 25 | 26 | ||
4086 | 26 | // managementHandler handles requests for web UI when AP is up | 27 | // managementHandler handles requests for web UI when AP is up |
4088 | 27 | func managementHandler() *mux.Router { | 28 | func managementHandler(chUserEvs chan<- UserEvent, config *utils.Config, |
4089 | 29 | ssidLs *utils.SsidList) *mux.Router { | ||
4090 | 30 | |||
4091 | 28 | router := mux.NewRouter() | 31 | router := mux.NewRouter() |
4092 | 32 | ms := &ManagementServer{chUserEvs: chUserEvs, config: *config, ssidLs: ssidLs} | ||
4093 | 29 | 33 | ||
4094 | 30 | // Pages routes | 34 | // Pages routes |
4100 | 31 | router.Handle("/", Middleware(http.HandlerFunc(ManagementHandler))).Methods("GET") | 35 | router.HandleFunc("/", ms.ManagementHandler).Methods("GET") |
4101 | 32 | router.Handle("/config", Middleware(http.HandlerFunc(SaveConfigHandler))).Methods("POST") | 36 | router.HandleFunc("/config", ms.SaveConfigHandler).Methods("POST") |
4102 | 33 | router.Handle("/connect", Middleware(http.HandlerFunc(ConnectHandler))).Methods("POST") | 37 | router.HandleFunc("/connect", ms.ConnectHandler).Methods("POST") |
4103 | 34 | router.HandleFunc("/hashit", HashItHandler).Methods("POST") | 38 | router.HandleFunc("/hashit", ms.HashItHandler).Methods("POST") |
4104 | 35 | router.Handle("/refresh", Middleware(http.HandlerFunc(RefreshHandler))).Methods("GET") | 39 | router.HandleFunc("/refresh", ms.RefreshHandler).Methods("GET") |
4105 | 40 | // To allow detection of captive portal | ||
4106 | 41 | // Android/ChromeOS | ||
4107 | 42 | router.HandleFunc("/generate_204", ms.RedirectHandler).Methods("GET") | ||
4108 | 43 | // iOS/Mac | ||
4109 | 44 | router.HandleFunc("/hotspot-detect.html", ms.RedirectHandler).Methods("GET") | ||
4110 | 45 | router.HandleFunc("/library/test/success.html", ms.RedirectHandler).Methods("GET") | ||
4111 | 46 | // Windows | ||
4112 | 47 | router.HandleFunc("/connecttest.txt", ms.RedirectHandler).Methods("GET") | ||
4113 | 48 | router.HandleFunc("/redirect", ms.RedirectHandler).Methods("GET") | ||
4114 | 36 | 49 | ||
4115 | 37 | // Resources path | 50 | // Resources path |
4116 | 38 | fs := http.StripPrefix("/static/", http.FileServer(http.Dir(ResourcesPath))) | 51 | fs := http.StripPrefix("/static/", http.FileServer(http.Dir(ResourcesPath))) |
4117 | @@ -42,15 +55,17 @@ func managementHandler() *mux.Router { | |||
4118 | 42 | } | 55 | } |
4119 | 43 | 56 | ||
4120 | 44 | // operationalHandler handles request for web UI when connected to external Wi-Fi | 57 | // operationalHandler handles request for web UI when connected to external Wi-Fi |
4122 | 45 | func operationalHandler() *mux.Router { | 58 | func operationalHandler(chUserEvs chan<- UserEvent) *mux.Router { |
4123 | 46 | router := mux.NewRouter() | 59 | router := mux.NewRouter() |
4124 | 60 | os := &OperationalServer{chUserEvs: chUserEvs} | ||
4125 | 47 | 61 | ||
4129 | 48 | router.HandleFunc("/", OperationalHandler).Methods("GET") | 62 | router.HandleFunc("/", os.OperationalHandler).Methods("GET") |
4130 | 49 | router.Handle("/disconnect", Middleware(http.HandlerFunc(DisconnectHandler))).Methods("GET") | 63 | router.HandleFunc("/disconnect", os.DisconnectHandler).Methods("GET") |
4131 | 50 | router.HandleFunc("/hashit", HashItHandler).Methods("POST") | 64 | router.HandleFunc("/hashit", os.HashItHandler).Methods("POST") |
4132 | 51 | 65 | ||
4133 | 52 | // Resources path | 66 | // Resources path |
4134 | 53 | fs := http.StripPrefix("/static/", http.FileServer(http.Dir(ResourcesPath))) | 67 | fs := http.StripPrefix("/static/", http.FileServer(http.Dir(ResourcesPath))) |
4135 | 54 | router.PathPrefix("/static/").Handler(fs) | 68 | router.PathPrefix("/static/").Handler(fs) |
4136 | 69 | |||
4137 | 55 | return router | 70 | return router |
4138 | 56 | } | 71 | } |
4139 | diff --git a/service/service.go b/service/service.go | |||
4140 | index 3ea136c..3d30bc8 100644 | |||
4141 | --- a/service/service.go | |||
4142 | +++ b/service/service.go | |||
4143 | @@ -18,157 +18,21 @@ | |||
4144 | 18 | package main | 18 | package main |
4145 | 19 | 19 | ||
4146 | 20 | import ( | 20 | import ( |
4147 | 21 | "fmt" | ||
4148 | 22 | "log" | 21 | "log" |
4149 | 23 | "os" | ||
4150 | 24 | "time" | ||
4151 | 25 | 22 | ||
4152 | 26 | "launchpad.net/wifi-connect/daemon" | 23 | "launchpad.net/wifi-connect/daemon" |
4153 | 27 | "launchpad.net/wifi-connect/netman" | ||
4154 | 28 | "launchpad.net/wifi-connect/utils" | 24 | "launchpad.net/wifi-connect/utils" |
4155 | 29 | "launchpad.net/wifi-connect/wifiap" | ||
4156 | 30 | ) | 25 | ) |
4157 | 31 | 26 | ||
4158 | 32 | var prevConnected bool | ||
4159 | 33 | |||
4160 | 34 | func main() { | 27 | func main() { |
4163 | 35 | 28 | log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile) | |
4162 | 36 | log.SetFlags(log.Lshortfile) | ||
4164 | 37 | log.SetPrefix("== wifi-connect: ") | 29 | log.SetPrefix("== wifi-connect: ") |
4165 | 38 | 30 | ||
4175 | 39 | c := netman.DefaultClient() | 31 | config, err := utils.LoadConfig() |
4167 | 40 | cw := wifiap.DefaultClient() | ||
4168 | 41 | client := daemon.GetClient() | ||
4169 | 42 | |||
4170 | 43 | config, err := daemon.LoadPreConfig() | ||
4171 | 44 | if err != nil { | ||
4172 | 45 | log.Printf("Empty preconfiguration: %v", err) | ||
4173 | 46 | } | ||
4174 | 47 | err = client.SetDefaults(cw, config) | ||
4176 | 48 | if err != nil { | 32 | if err != nil { |
4178 | 49 | log.Printf("SetDetaults error: %v", err) | 33 | log.Printf("Empty configuration: %v", err) |
4179 | 50 | } | 34 | } |
4180 | 51 | first := true | ||
4181 | 52 | client.SetWaitFlagPath(os.Getenv("SNAP_COMMON") + "/startingApConnect") | ||
4182 | 53 | client.SetManualFlagPath(os.Getenv("SNAP_COMMON") + "/manualMode") | ||
4183 | 54 | |||
4184 | 55 | client.ManagementServerDown() | ||
4185 | 56 | client.OperationalServerDown() | ||
4186 | 57 | |||
4187 | 58 | connected := false | ||
4188 | 59 | prevConnected = false | ||
4189 | 60 | |||
4190 | 61 | for { | ||
4191 | 62 | if first { | ||
4192 | 63 | log.Print("daemon STARTING") | ||
4193 | 64 | client.SetPreviousState(daemon.STARTING) | ||
4194 | 65 | client.SetState(daemon.STARTING) | ||
4195 | 66 | first = false | ||
4196 | 67 | //clean start require wifi AP down so we can get SSIDs | ||
4197 | 68 | cw.Disable() | ||
4198 | 69 | //remove previous State flags | ||
4199 | 70 | utils.RemoveFlagFile(client.GetWaitFlagPath()) | ||
4200 | 71 | utils.RemoveFlagFile(client.GetManualFlagPath()) | ||
4201 | 72 | //TODO only wait if wlan0 is managed | ||
4202 | 73 | //wait time period (TBD) on first run to allow wifi connections | ||
4203 | 74 | time.Sleep(10000 * time.Millisecond) | ||
4204 | 75 | } | ||
4205 | 76 | |||
4206 | 77 | // wait 5 seconds on each iter | ||
4207 | 78 | time.Sleep(5000 * time.Millisecond) | ||
4208 | 79 | 35 | ||
4303 | 80 | config, err = daemon.LoadPreConfig() | 36 | service := daemon.GetService(config) |
4304 | 81 | if err != nil { | 37 | service.Start() |
4211 | 82 | log.Printf("Empty preconfiguration: %v", err) | ||
4212 | 83 | } | ||
4213 | 84 | // loop without action if in manual mode | ||
4214 | 85 | if client.ManualMode() { | ||
4215 | 86 | continue | ||
4216 | 87 | } | ||
4217 | 88 | |||
4218 | 89 | // start clean on exiting manual mode | ||
4219 | 90 | if client.GetPreviousState() == daemon.MANUAL { | ||
4220 | 91 | first = true | ||
4221 | 92 | continue | ||
4222 | 93 | } | ||
4223 | 94 | // the AP should not be up without SSIDS | ||
4224 | 95 | if client.IsApUpWithoutSSIDs(cw) { | ||
4225 | 96 | cw.Disable() | ||
4226 | 97 | continue | ||
4227 | 98 | } | ||
4228 | 99 | |||
4229 | 100 | // log connected state if different | ||
4230 | 101 | prevConnected = connected | ||
4231 | 102 | connected = c.ConnectedWifi(c.GetWifiDevices(c.GetDevices())) | ||
4232 | 103 | if first || prevConnected != connected { | ||
4233 | 104 | if connected { | ||
4234 | 105 | log.Print("WIFI CONNECTED") | ||
4235 | 106 | } else { | ||
4236 | 107 | log.Print("WIFI NOT CONNECTED") | ||
4237 | 108 | } | ||
4238 | 109 | } | ||
4239 | 110 | |||
4240 | 111 | // if an external wifi connection, we are in Operational mode | ||
4241 | 112 | // and we stay here until there is an external wifi connection | ||
4242 | 113 | if connected { | ||
4243 | 114 | //log.Print("operational config:", config.Operational) | ||
4244 | 115 | client.SetState(daemon.OPERATING) | ||
4245 | 116 | if client.GetPreviousState() != daemon.OPERATING { | ||
4246 | 117 | log.Print("entering OPERATIONAL mode") | ||
4247 | 118 | client.ManagementServerDown() | ||
4248 | 119 | if config.Operational { | ||
4249 | 120 | //log.Print("about to put up oper server") | ||
4250 | 121 | client.OperationalServerUp() | ||
4251 | 122 | } | ||
4252 | 123 | } | ||
4253 | 124 | continue | ||
4254 | 125 | } | ||
4255 | 126 | |||
4256 | 127 | // wait/loop until wait flag file is gone | ||
4257 | 128 | // this stops daemon State changing until the management portal | ||
4258 | 129 | // is done, either stopped or the user has attempted to connect to | ||
4259 | 130 | // an external AP | ||
4260 | 131 | if client.CheckWaitApConnect() { | ||
4261 | 132 | continue | ||
4262 | 133 | } | ||
4263 | 134 | |||
4264 | 135 | client.SetState(daemon.MANAGING) | ||
4265 | 136 | if client.GetPreviousState() != daemon.MANAGING || !client.CheckWaitApConnect() { | ||
4266 | 137 | // if wlan0 managed, set Unmanaged so that we can bring up wifi-ap | ||
4267 | 138 | // properly if needed | ||
4268 | 139 | if err := c.Unmanage(); err != nil { | ||
4269 | 140 | log.Print(err) | ||
4270 | 141 | continue | ||
4271 | 142 | } | ||
4272 | 143 | |||
4273 | 144 | //wifi-ap UP? | ||
4274 | 145 | wifiUp, err := cw.Enabled() | ||
4275 | 146 | if err != nil { | ||
4276 | 147 | log.Printf("Error checking wifi-ap.Enabled(): %v", err) | ||
4277 | 148 | continue // try again since no better course of action | ||
4278 | 149 | } | ||
4279 | 150 | |||
4280 | 151 | if !wifiUp { | ||
4281 | 152 | log.Print("entering MANAGEMENT mode") | ||
4282 | 153 | found := c.ScanAndWriteSsidsToFile(utils.SsidsFile) | ||
4283 | 154 | if err := c.Unmanage(); err != nil { | ||
4284 | 155 | fmt.Println(err) | ||
4285 | 156 | continue | ||
4286 | 157 | } | ||
4287 | 158 | if !found { | ||
4288 | 159 | log.Print("No SSIDs found. Continuing to scan for SSIDS...") | ||
4289 | 160 | continue | ||
4290 | 161 | } | ||
4291 | 162 | log.Printf("starting wifi-ap") | ||
4292 | 163 | if err := cw.Enable(); err != nil { | ||
4293 | 164 | log.Print(err) | ||
4294 | 165 | continue | ||
4295 | 166 | } | ||
4296 | 167 | //if client.GetPreviousState() == daemon.OPERATING { | ||
4297 | 168 | client.OperationalServerDown() | ||
4298 | 169 | //} | ||
4299 | 170 | client.ManagementServerUp() | ||
4300 | 171 | } | ||
4301 | 172 | } | ||
4302 | 173 | } | ||
4305 | 174 | } | 38 | } |
4306 | diff --git a/service/service_test.go b/service/service_test.go | |||
4307 | 175 | new file mode 100644 | 39 | new file mode 100644 |
4308 | index 0000000..795dc6a | |||
4309 | --- /dev/null | |||
4310 | +++ b/service/service_test.go | |||
4311 | @@ -0,0 +1,26 @@ | |||
4312 | 1 | /* | ||
4313 | 2 | * Copyright (C) 2017 Canonical Ltd | ||
4314 | 3 | * | ||
4315 | 4 | * This program is free software: you can redistribute it and/or modify | ||
4316 | 5 | * it under the terms of the GNU General Public License version 3 as | ||
4317 | 6 | * published by the Free Software Foundation. | ||
4318 | 7 | * | ||
4319 | 8 | * This program is distributed in the hope that it will be useful, | ||
4320 | 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4321 | 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4322 | 11 | * GNU General Public License for more details. | ||
4323 | 12 | * | ||
4324 | 13 | * You should have received a copy of the GNU General Public License | ||
4325 | 14 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
4326 | 15 | * | ||
4327 | 16 | */ | ||
4328 | 17 | |||
4329 | 18 | package main | ||
4330 | 19 | |||
4331 | 20 | import ( | ||
4332 | 21 | "testing" | ||
4333 | 22 | ) | ||
4334 | 23 | |||
4335 | 24 | func TestService(t *testing.T) { | ||
4336 | 25 | // TODO Fill it | ||
4337 | 26 | } | ||
4338 | diff --git a/snapcraft.yaml b/snapcraft.yaml | |||
4339 | index a2af4b2..b7e3be7 100644 | |||
4340 | --- a/snapcraft.yaml | |||
4341 | +++ b/snapcraft.yaml | |||
4342 | @@ -1,5 +1,5 @@ | |||
4343 | 1 | name: wifi-connect | 1 | name: wifi-connect |
4345 | 2 | version: 0.11 | 2 | version: '0.2' |
4346 | 3 | summary: Connect your device to external wifi over temp wifi AP | 3 | summary: Connect your device to external wifi over temp wifi AP |
4347 | 4 | description: | | 4 | description: | |
4348 | 5 | A solution to enable your device to connect to an external | 5 | A solution to enable your device to connect to an external |
4349 | @@ -11,51 +11,73 @@ description: | | |||
4350 | 11 | https://code.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect | 11 | https://code.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect |
4351 | 12 | grade: stable | 12 | grade: stable |
4352 | 13 | confinement: strict | 13 | confinement: strict |
4353 | 14 | base: core18 | ||
4354 | 14 | 15 | ||
4355 | 15 | apps: | 16 | apps: |
4356 | 16 | wifi-connect: | ||
4357 | 17 | command: cmd | ||
4358 | 18 | plugs: [network, network-bind, network-manager, control] | ||
4359 | 19 | daemon: | 17 | daemon: |
4361 | 20 | command: service | 18 | command: wificonnectd |
4362 | 21 | daemon: simple | 19 | daemon: simple |
4364 | 22 | plugs: [network-manager, control, network-bind] | 20 | plugs: [network-manager, network-bind, firewall-control] |
4365 | 23 | 21 | ||
4366 | 24 | hooks: | 22 | hooks: |
4376 | 25 | configure: | 23 | configure: |
4377 | 26 | plugs: [network] | 24 | plugs: [network] |
4378 | 27 | 25 | ||
4379 | 28 | plugs: | 26 | # For dnsmasq, dir where leases are stored |
4380 | 29 | control: | 27 | layout: |
4381 | 30 | interface: content | 28 | /var/lib/misc: |
4382 | 31 | content: socket-directory | 29 | bind: $SNAP_DATA |
4374 | 32 | target: $SNAP_COMMON | ||
4375 | 33 | default-provider: wifi-ap | ||
4383 | 34 | 30 | ||
4384 | 35 | parts: | 31 | parts: |
4393 | 36 | go: | 32 | |
4394 | 37 | plugin: go | 33 | go-binaries: |
4395 | 38 | source: . | 34 | plugin: dump |
4396 | 39 | go-importpath: launchpad.net/wifi-connect | 35 | source: . |
4397 | 40 | build-packages: | 36 | source-type: local |
4398 | 41 | # needed by go get | 37 | build-snaps: [ "go" ] |
4399 | 42 | - bzr | 38 | build-packages: [ "bzr", "git", "python", "build-essential" ] |
4400 | 43 | install: | | 39 | stage-packages: [ "dnsmasq" ] |
4401 | 40 | override-build: | | ||
4402 | 41 | set -ex | ||
4403 | 42 | export GOPATH=$(mktemp -d) | ||
4404 | 43 | ( | ||
4405 | 44 | src_path="$GOPATH"/src/launchpad.net/wifi-connect | ||
4406 | 45 | mkdir -p "$src_path" | ||
4407 | 46 | cp -a avahi daemon hooks netman server service utils static \ | ||
4408 | 47 | dependencies.tsv mdlint.py README.md run-checks "$src_path" | ||
4409 | 48 | cd "$src_path" | ||
4410 | 49 | go get launchpad.net/godeps | ||
4411 | 50 | export PATH=$PATH:$GOPATH/bin | ||
4412 | 51 | godeps -u dependencies.tsv | ||
4413 | 52 | mkdir -p "$SNAPCRAFT_PART_INSTALL"/bin/ | ||
4414 | 53 | cd "$GOPATH"/src/launchpad.net/wifi-connect/ | ||
4415 | 54 | go build -o "$SNAPCRAFT_PART_INSTALL"/bin/wificonnectd service/service.go | ||
4416 | 55 | # configure hook | ||
4417 | 56 | mkdir -p "$SNAPCRAFT_PART_INSTALL"/snap/hooks | ||
4418 | 57 | go build -o "$SNAPCRAFT_PART_INSTALL"/snap/hooks/configure hooks/configure.go | ||
4419 | 58 | ) | ||
4420 | 44 | # set environment var SKIP_TESTS to 'y' or 'yes' if you want not to execute | 59 | # set environment var SKIP_TESTS to 'y' or 'yes' if you want not to execute |
4421 | 45 | # this part unit tests in your next compilation. | 60 | # this part unit tests in your next compilation. |
4422 | 61 | SKIP_TESTS=no | ||
4423 | 46 | if [ "$SKIP_TESTS" = "yes" ] || [ "$SKIP_TESTS" = "y" ]; then | 62 | if [ "$SKIP_TESTS" = "yes" ] || [ "$SKIP_TESTS" = "y" ]; then |
4425 | 47 | echo "skipping unit tests" | 63 | echo "skipping unit tests" |
4426 | 48 | else | 64 | else |
4430 | 49 | export GOPATH=$PWD/../go | 65 | cd "$GOPATH"/src/launchpad.net/wifi-connect |
4431 | 50 | cd $GOPATH/src/launchpad.net/wifi-connect | 66 | ./run-checks all |
4429 | 51 | ./run-checks all | ||
4432 | 52 | fi | 67 | fi |
4436 | 53 | # configure hook | 68 | cd |
4437 | 54 | mkdir -p $SNAPCRAFT_PART_INSTALL/snap/hooks | 69 | rm -rf "$GOPATH" |
4438 | 55 | mv $SNAPCRAFT_PART_INSTALL/bin/hooks $SNAPCRAFT_PART_INSTALL/snap/hooks/configure | 70 | stage: |
4439 | 71 | - bin/wificonnectd | ||
4440 | 72 | - snap/hooks/configure | ||
4441 | 73 | - usr/sbin/dnsmasq | ||
4442 | 74 | |||
4443 | 56 | assets: | 75 | assets: |
4444 | 57 | plugin: dump | 76 | plugin: dump |
4445 | 58 | source: . | 77 | source: . |
4447 | 59 | prepare: ../../../scriptlets/fill_country_codes.sh | 78 | override-build: | |
4448 | 79 | "$SNAPCRAFT_PROJECT_DIR"/scriptlets/fill_country_codes.sh | ||
4449 | 80 | snapcraftctl build | ||
4450 | 60 | stage: | 81 | stage: |
4451 | 61 | - static | 82 | - static |
4452 | 83 | - -static/tests | ||
4453 | diff --git a/static/js/jquery.min.js b/static/js/jquery.min.js | |||
4454 | 62 | new file mode 100644 | 84 | new file mode 100644 |
4455 | index 0000000..644d35e | |||
4456 | --- /dev/null | |||
4457 | +++ b/static/js/jquery.min.js | |||
4458 | @@ -0,0 +1,4 @@ | |||
4459 | 1 | /*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ | ||
4460 | 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), | ||
4461 | 3 | a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},U=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function V(){this.expando=r.expando+V.uid++}V.uid=1,V.prototype={cache:function(a){var b=a[this.expando];return b||(b={},U(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){Array.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(L)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var W=new V,X=new V,Y=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Z=/[A-Z]/g;function $(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:Y.test(a)?JSON.parse(a):a)}function _(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Z,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=$(c)}catch(e){}X.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return X.hasData(a)||W.hasData(a)},data:function(a,b,c){return X.access(a,b,c)},removeData:function(a,b){X.remove(a,b)},_data:function(a,b,c){return W.access(a,b,c)},_removeData:function(a,b){W.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=X.get(f),1===f.nodeType&&!W.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),_(f,d,e[d])));W.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){X.set(this,a)}):T(this,function(b){var c;if(f&&void 0===b){if(c=X.get(f,a),void 0!==c)return c;if(c=_(f,a),void 0!==c)return c}else this.each(function(){X.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=W.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var aa=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ba=new RegExp("^(?:([+-])=|)("+aa+")([a-z%]*)$","i"),ca=["Top","Right","Bottom","Left"],da=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},ea=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function fa(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&ba.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var ga={};function ha(a){var b,c=a.ownerDocument,d=a.nodeName,e=ga[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),ga[d]=e,e)}function ia(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=W.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&da(d)&&(e[f]=ha(d))):"none"!==c&&(e[f]="none",W.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ia(this,!0)},hide:function(){return ia(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){da(this)?r(this).show():r(this).hide()})}});var ja=/^(?:checkbox|radio)$/i,ka=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c<d;c++)W.set(a[c],"globalEval",!b||W.get(b[c],"globalEval"))}var pa=/<|&#?\w+;/;function qa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(pa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ka.exec(f)||["",""])[1].toLowerCase(),i=ma[h]||ma._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==xa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===xa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&B(this,"input"))return this.click(),!1},_default:function(a){return B(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?va:wa,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:wa,isPropagationStopped:wa,isImmediatePropagationStopped:wa,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=va,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=va,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=va,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&sa.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&ta.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return ya(this,a,b,c,d)},one:function(a,b,c,d){return ya(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=wa),this.each(function(){r.event.remove(this,a,c,b)})}});var za=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/<script|<style|<link/i,Ba=/checked\s*(?:[^=]|=\s*.checked.)/i,Ca=/^true\/(.*)/,Da=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}X.hasData(a)&&(h=X.access(a),i=r.extend({},h),X.set(b,i))}}function Ia(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ja.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ja(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,na(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Ga),l=0;l<i;l++)j=h[l],la.test(j.type||"")&&!W.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Da,""),k))}return a}function Ka(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(na(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&oa(na(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(za,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d<e;d++)Ia(f[d],g[d]);if(b)if(c)for(f=f||na(a),g=g||na(h),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);else Ha(a,h);return g=na(h,"script"),g.length>0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(na(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ja(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(na(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var La=/^margin/,Ma=new RegExp("^("+aa+")(?!px)[a-z%]+$","i"),Na=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",ra.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,ra.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Oa(a,b,c){var d,e,f,g,h=a.style;return c=c||Na(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&Ma.test(g)&&La.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Pa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Qa=/^(none|table(?!-c[ea]).+)/,Ra=/^--/,Sa={position:"absolute",visibility:"hidden",display:"block"},Ta={letterSpacing:"0",fontWeight:"400"},Ua=["Webkit","Moz","ms"],Va=d.createElement("div").style;function Wa(a){if(a in Va)return a;var b=a[0].toUpperCase()+a.slice(1),c=Ua.length;while(c--)if(a=Ua[c]+b,a in Va)return a}function Xa(a){var b=r.cssProps[a];return b||(b=r.cssProps[a]=Wa(a)||a),b}function Ya(a,b,c){var d=ba.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Za(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ca[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ca[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ca[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ca[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ca[f]+"Width",!0,e)));return g}function $a(a,b,c){var d,e=Na(a),f=Oa(a,b,e),g="border-box"===r.css(a,"boxSizing",!1,e);return Ma.test(f)?f:(d=g&&(o.boxSizingReliable()||f===a.style[b]),"auto"===f&&(f=a["offset"+b[0].toUpperCase()+b.slice(1)]),f=parseFloat(f)||0,f+Za(a,b,c||(g?"border":"content"),d,e)+"px")}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Oa(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=Ra.test(b),j=a.style;return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:j[b]:(f=typeof c,"string"===f&&(e=ba.exec(c))&&e[1]&&(c=fa(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(j[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i?j.setProperty(b,c):j[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b),i=Ra.test(b);return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Oa(a,b,d)),"normal"===e&&b in Ta&&(e=Ta[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Qa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?$a(a,b,d):ea(a,Sa,function(){return $a(a,b,d)})},set:function(a,c,d){var e,f=d&&Na(a),g=d&&Za(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=ba.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Ya(a,c,g)}}}),r.cssHooks.marginLeft=Pa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Oa(a,"marginLeft"))||a.getBoundingClientRect().left-ea(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ca[d]+b]=f[d]||f[d-2]||f[0];return e}},La.test(a)||(r.cssHooks[a+b].set=Ya)}),r.fn.extend({css:function(a,b){return T(this,function(a,b,c){var d,e,f={},g=0;if(Array.isArray(b)){for(d=Na(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function ib(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&da(a),q=W.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],cb.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=W.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ia([a],!0),j=a.style.display||j,k=r.css(a,"display"),ia([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=W.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ia([a],!0),m.done(function(){p||ia([a]),W.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=hb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function jb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],Array.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kb(a,b,c){var d,e,f=0,g=kb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=ab||fb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(i||h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:ab||fb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jb(k,j.opts.specialEasing);f<g;f++)if(d=kb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,hb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j}r.Animation=r.extend(kb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return fa(c.elem,a,ba.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(L);for(var c,d=0,e=a.length;d<e;d++)c=a[d],kb.tweeners[c]=kb.tweeners[c]||[],kb.tweeners[c].unshift(b)},prefilters:[ib],prefilter:function(a,b){b?kb.prefilters.unshift(a):kb.prefilters.push(a)}}),r.speed=function(a,b,c){var d=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off?d.duration=0:"number"!=typeof d.duration&&(d.duration in r.fx.speeds?d.duration=r.fx.speeds[d.duration]:d.duration=r.fx.speeds._default),null!=d.queue&&d.queue!==!0||(d.queue="fx"),d.old=d.complete,d.complete=function(){r.isFunction(d.old)&&d.old.call(this),d.queue&&r.dequeue(this,d.queue)},d},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(da).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=kb(this,r.extend({},a),f);(e||W.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=W.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&db.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=W.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gb(b,!0),a,d,e)}}),r.each({slideDown:gb("show"),slideUp:gb("hide"),slideToggle:gb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(ab=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),ab=void 0},r.fx.timer=function(a){r.timers.push(a),r.fx.start()},r.fx.interval=13,r.fx.start=function(){bb||(bb=!0,eb())},r.fx.stop=function(){bb=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var lb,mb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return T(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), | ||
4462 | 4 | null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!B(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Tb=[],Ub=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Tb.pop()||r.expando+"_"+ub++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Ub.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ub.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Ub,"$1"+e):b.jsonp!==!1&&(b.url+=(vb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Tb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=C.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=qa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=pb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length},r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),b=f.ownerDocument,c=b.documentElement,e=b.defaultView,{top:d.top+e.pageYOffset-c.clientTop,left:d.left+e.pageXOffset-c.clientLeft}):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),B(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||ra})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return T(this,function(a,d,e){var f;return r.isWindow(a)?f=a:9===a.nodeType&&(f=a.defaultView),void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Pa(o.pixelPosition,function(a,c){if(c)return c=Oa(a,b),Ma.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return T(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.holdReady=function(a){a?r.readyWait++:r.ready(!0)},r.isArray=Array.isArray,r.parseJSON=JSON.parse,r.nodeName=B,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Vb=a.jQuery,Wb=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Wb),b&&a.jQuery===r&&(a.jQuery=Vb),r},b||(a.jQuery=a.$=r),r}); | ||
4463 | diff --git a/static/templates/management.html b/static/templates/management.html | |||
4464 | index f11bb00..4d68f2b 100644 | |||
4465 | --- a/static/templates/management.html | |||
4466 | +++ b/static/templates/management.html | |||
4467 | @@ -3,7 +3,7 @@ | |||
4468 | 3 | <head> | 3 | <head> |
4469 | 4 | <meta charset="utf-8" /> | 4 | <meta charset="utf-8" /> |
4470 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
4472 | 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> |
4473 | 7 | <title>Select Wi-Fi</title> | 7 | <title>Select Wi-Fi</title> |
4474 | 8 | 8 | ||
4475 | 9 | <link rel="stylesheet" href="/static/css/application.css" /> | 9 | <link rel="stylesheet" href="/static/css/application.css" /> |
4476 | diff --git a/static/templates/operational.html b/static/templates/operational.html | |||
4477 | index 27d4f94..9d2cd89 100644 | |||
4478 | --- a/static/templates/operational.html | |||
4479 | +++ b/static/templates/operational.html | |||
4480 | @@ -5,7 +5,7 @@ | |||
4481 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
4482 | 6 | <title>Wifi Connected</title> | 6 | <title>Wifi Connected</title> |
4483 | 7 | <link rel="stylesheet" href="/static/css/application.css" /> | 7 | <link rel="stylesheet" href="/static/css/application.css" /> |
4485 | 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> |
4486 | 9 | </head> | 9 | </head> |
4487 | 10 | 10 | ||
4488 | 11 | <body> | 11 | <body> |
4489 | diff --git a/utils/config.go b/utils/config.go | |||
4490 | index bd23686..2cba34c 100644 | |||
4491 | --- a/utils/config.go | |||
4492 | +++ b/utils/config.go | |||
4493 | @@ -4,41 +4,47 @@ import ( | |||
4494 | 4 | "encoding/json" | 4 | "encoding/json" |
4495 | 5 | "fmt" | 5 | "fmt" |
4496 | 6 | "io/ioutil" | 6 | "io/ioutil" |
4497 | 7 | "log" | ||
4498 | 8 | "os" | 7 | "os" |
4499 | 9 | "path/filepath" | 8 | "path/filepath" |
4500 | 10 | "strconv" | 9 | "strconv" |
4501 | 11 | "strings" | 10 | "strings" |
4502 | 12 | |||
4503 | 13 | "launchpad.net/wifi-connect/wifiap" | ||
4504 | 14 | ) | 11 | ) |
4505 | 15 | 12 | ||
4507 | 16 | var configFile = filepath.Join(os.Getenv("SNAP_COMMON"), "pre-config.json") | 13 | var configFile = filepath.Join(os.Getenv("SNAP_COMMON"), "config.json") |
4508 | 17 | var mustConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), "config_done.flag") | 14 | var mustConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), "config_done.flag") |
4509 | 18 | 15 | ||
4510 | 19 | var wifiapClient wifiap.Operations = wifiap.DefaultClient() | ||
4511 | 20 | |||
4512 | 21 | // Config this project config got from wifi-ap + custom wifi-connect params | 16 | // Config this project config got from wifi-ap + custom wifi-connect params |
4513 | 22 | type Config struct { | 17 | type Config struct { |
4516 | 23 | Wifi *WifiConfig | 18 | Wifi WifiConfig `json:"wifi"` |
4517 | 24 | Portal *PortalConfig | 19 | Portal PortalConfig `json:"portal"` |
4518 | 20 | // If set, once we have connected to an AP once, we do not enter | ||
4519 | 21 | // managing mode when the service is re-started, even if we are | ||
4520 | 22 | // not able to reconnect to said AP. To change that, we would | ||
4521 | 23 | // have to use the operational portal. | ||
4522 | 24 | ConfigWifiOnce bool `json:"config-wifi-once"` | ||
4523 | 25 | } | 25 | } |
4524 | 26 | 26 | ||
4525 | 27 | // WifiConfig config specific parameters for wifi configuration | 27 | // WifiConfig config specific parameters for wifi configuration |
4526 | 28 | type WifiConfig struct { | 28 | type WifiConfig struct { |
4533 | 29 | Ssid string `json:"wifi.ssid"` | 29 | Ssid string `json:"ssid"` |
4534 | 30 | Passphrase string `json:"wifi.security-passphrase"` | 30 | Passphrase string `json:"security-passphrase"` |
4535 | 31 | Interface string `json:"wifi.interface"` | 31 | Interface string `json:"interface"` |
4536 | 32 | CountryCode string `json:"wifi.country-code"` | 32 | CountryCode string `json:"country-code"` |
4537 | 33 | Channel int `json:"wifi.channel"` | 33 | Channel int `json:"channel"` |
4538 | 34 | OperationMode string `json:"wifi.operation-mode"` | 34 | OperationMode string `json:"operation-mode"` |
4539 | 35 | // Subnet for our AP, in CIDR format | ||
4540 | 36 | Subnet string `json:"subnet"` | ||
4541 | 35 | } | 37 | } |
4542 | 36 | 38 | ||
4543 | 37 | // PortalConfig config specific parameters for portals configuration | 39 | // PortalConfig config specific parameters for portals configuration |
4544 | 38 | type PortalConfig struct { | 40 | type PortalConfig struct { |
4548 | 39 | Password string //`json:"portal.password"` | 41 | Password string `json:"password"` |
4549 | 40 | NoResetCredentials bool `json:"portal.no-reset-creds"` | 42 | // Port where we will be listening |
4550 | 41 | NoOperational bool `json:"portal.no-operational"` | 43 | Port int `json:"port"` |
4551 | 44 | // whether user must reset passphrase and password on first use of mgmt portal | ||
4552 | 45 | NoResetCredentials bool `json:"no-reset-creds"` | ||
4553 | 46 | // whether to show the operational portal | ||
4554 | 47 | Operational bool `json:"operational"` | ||
4555 | 42 | } | 48 | } |
4556 | 43 | 49 | ||
4557 | 44 | func (c *Config) String() string { | 50 | func (c *Config) String() string { |
4558 | @@ -52,8 +58,11 @@ func (c *Config) String() string { | |||
4559 | 52 | func (c *PortalConfig) String() string { | 58 | func (c *PortalConfig) String() string { |
4560 | 53 | s := []string{ | 59 | s := []string{ |
4561 | 54 | strings.Join([]string{"Password: ", c.Password}, " "), | 60 | strings.Join([]string{"Password: ", c.Password}, " "), |
4564 | 55 | strings.Join([]string{"NoResetCredentials:", strconv.FormatBool(c.NoResetCredentials)}, " "), | 61 | strings.Join([]string{"Port:", strconv.Itoa(c.Port)}, " "), |
4565 | 56 | strings.Join([]string{"NoOperational:", strconv.FormatBool(c.NoOperational)}, " "), | 62 | strings.Join([]string{"NoResetCredentials:", |
4566 | 63 | strconv.FormatBool(c.NoResetCredentials)}, " "), | ||
4567 | 64 | strings.Join([]string{"Operational:", | ||
4568 | 65 | strconv.FormatBool(c.Operational)}, " "), | ||
4569 | 57 | } | 66 | } |
4570 | 58 | return fmt.Sprintf(strings.Join(s, "\n")) | 67 | return fmt.Sprintf(strings.Join(s, "\n")) |
4571 | 59 | } | 68 | } |
4572 | @@ -66,161 +75,67 @@ func (c *WifiConfig) String() string { | |||
4573 | 66 | strings.Join([]string{"CountryCode:", c.CountryCode}, " "), | 75 | strings.Join([]string{"CountryCode:", c.CountryCode}, " "), |
4574 | 67 | strings.Join([]string{"Channel:", strconv.Itoa(c.Channel)}, " "), | 76 | strings.Join([]string{"Channel:", strconv.Itoa(c.Channel)}, " "), |
4575 | 68 | strings.Join([]string{"OperationMode:", c.OperationMode}, " "), | 77 | strings.Join([]string{"OperationMode:", c.OperationMode}, " "), |
4576 | 78 | strings.Join([]string{"Subnet:", c.Subnet}, " "), | ||
4577 | 69 | } | 79 | } |
4578 | 70 | return fmt.Sprintf(strings.Join(s, "\n")) | 80 | return fmt.Sprintf(strings.Join(s, "\n")) |
4579 | 71 | } | 81 | } |
4580 | 72 | 82 | ||
4597 | 73 | func defaultPortalConfig() *PortalConfig { | 83 | // LoadConfig loads configuration file |
4598 | 74 | return &PortalConfig{ | 84 | func LoadConfig() (*Config, error) { |
4599 | 75 | Password: "", | 85 | config := &Config{} |
4600 | 76 | NoResetCredentials: false, | 86 | // Set some defaults |
4601 | 77 | NoOperational: false, | 87 | config.Portal.Port = 8085 |
4602 | 78 | } | 88 | config.Wifi.Subnet = "10.25.52.1/24" |
4587 | 79 | } | ||
4588 | 80 | |||
4589 | 81 | // config currently stored in local json file is completely storable in PortalConfig | ||
4590 | 82 | // If needed to scale, we could rewrite this method to support a more generic type | ||
4591 | 83 | func readLocalConfig() (*PortalConfig, error) { | ||
4592 | 84 | if _, err := os.Stat(configFile); os.IsNotExist(err) { | ||
4593 | 85 | log.Printf("Warn: not found local config file at %v\n", configFile) | ||
4594 | 86 | // in case there is no local config file, return a null pointer | ||
4595 | 87 | return nil, nil | ||
4596 | 88 | } | ||
4603 | 89 | 89 | ||
4605 | 90 | fileContents, err := ioutil.ReadFile(configFile) | 90 | content, err := ioutil.ReadFile(configFile) |
4606 | 91 | if err != nil { | 91 | if err != nil { |
4608 | 92 | return nil, fmt.Errorf("Error reading json config file: %v", err) | 92 | return config, err |
4609 | 93 | } | 93 | } |
4624 | 94 | 94 | err = json.Unmarshal(content, config) | |
4611 | 95 | // parameters not available in config file will be se to default value | ||
4612 | 96 | portalConfig := defaultPortalConfig() | ||
4613 | 97 | err = json.Unmarshal(fileContents, portalConfig) | ||
4614 | 98 | if err != nil { | ||
4615 | 99 | return nil, fmt.Errorf("Error unmarshalling json config file contents: %v", err) | ||
4616 | 100 | } | ||
4617 | 101 | |||
4618 | 102 | return portalConfig, nil | ||
4619 | 103 | } | ||
4620 | 104 | |||
4621 | 105 | func writeLocalConfig(p *PortalConfig) error { | ||
4622 | 106 | // the only writable local config param is the password, stored as a hash | ||
4623 | 107 | _, err := HashIt(p.Password) | ||
4625 | 108 | if err != nil { | 95 | if err != nil { |
4627 | 109 | return fmt.Errorf("Could not hash portal password to file: %v", err) | 96 | return config, err |
4628 | 110 | } | 97 | } |
4631 | 111 | 98 | return config, nil | |
4630 | 112 | return nil | ||
4632 | 113 | } | 99 | } |
4633 | 114 | 100 | ||
4646 | 115 | func readRemoteParam(m map[string]interface{}, key string, defaultValue interface{}) interface{} { | 101 | // WriteConfigFile writes configuration file |
4647 | 116 | val, ok := m[key] | 102 | func WriteConfigFile(config *Config) error { |
4648 | 117 | if !ok { | 103 | b, err := json.Marshal(config) |
4637 | 118 | val = defaultValue | ||
4638 | 119 | log.Printf("Warning: %v key was not found in remote config", key) | ||
4639 | 120 | } | ||
4640 | 121 | |||
4641 | 122 | return val | ||
4642 | 123 | } | ||
4643 | 124 | |||
4644 | 125 | func readRemoteConfig() (*WifiConfig, error) { | ||
4645 | 126 | settings, err := wifiapClient.Show() | ||
4649 | 127 | if err != nil { | 104 | if err != nil { |
4662 | 128 | return nil, fmt.Errorf("Error reading wifi-ap remote configuration: %v", err) | 105 | return err |
4651 | 129 | } | ||
4652 | 130 | |||
4653 | 131 | // NOTE: Preprocessing for the case of wifi.channel. | ||
4654 | 132 | // In the case of the channel, it is returned as string from rest api, but we have to convert it | ||
4655 | 133 | // as it is handled as int internally. | ||
4656 | 134 | // In case wifi-ap provides this in future as int, this could be replaced by | ||
4657 | 135 | // | ||
4658 | 136 | // readRemoteParam(settings, "wifi.channel", 0).(int) | ||
4659 | 137 | channel, err := strconv.Atoi(readRemoteParam(settings, "wifi.channel", "0").(string)) | ||
4660 | 138 | if err != nil { | ||
4661 | 139 | return nil, fmt.Errorf("Could not parse wifi.channel parameter: %v", err) | ||
4663 | 140 | } | 106 | } |
4664 | 141 | 107 | ||
4685 | 142 | return &WifiConfig{ | 108 | err = ioutil.WriteFile(configFile, b, 0644) |
4666 | 143 | Ssid: readRemoteParam(settings, "wifi.ssid", "").(string), | ||
4667 | 144 | Passphrase: readRemoteParam(settings, "wifi.security-passphrase", "").(string), | ||
4668 | 145 | Interface: readRemoteParam(settings, "wifi.interface", "").(string), | ||
4669 | 146 | CountryCode: readRemoteParam(settings, "wifi.country-code", "").(string), | ||
4670 | 147 | Channel: channel, | ||
4671 | 148 | OperationMode: readRemoteParam(settings, "wifi.operation-mode", "").(string), | ||
4672 | 149 | }, nil | ||
4673 | 150 | } | ||
4674 | 151 | |||
4675 | 152 | func writeRemoteConfig(wc *WifiConfig) error { | ||
4676 | 153 | params := make(map[string]interface{}) | ||
4677 | 154 | params["wifi.ssid"] = wc.Ssid | ||
4678 | 155 | params["wifi.security-passphrase"] = wc.Passphrase | ||
4679 | 156 | params["wifi.interface"] = wc.Interface | ||
4680 | 157 | params["wifi.country-code"] = wc.CountryCode | ||
4681 | 158 | params["wifi.channel"] = wc.Channel | ||
4682 | 159 | params["wifi.operation-mode"] = wc.OperationMode | ||
4683 | 160 | |||
4684 | 161 | err := wifiapClient.Set(params) | ||
4686 | 162 | if err != nil { | 109 | if err != nil { |
4688 | 163 | return fmt.Errorf("Error writing remote configuration: %v", err) | 110 | return err |
4689 | 164 | } | 111 | } |
4690 | 165 | 112 | ||
4691 | 166 | return nil | 113 | return nil |
4692 | 167 | } | 114 | } |
4693 | 168 | 115 | ||
4702 | 169 | // ReadConfig reads all config, remote and local, at the same time | 116 | func writeConfig(p *PortalConfig) error { |
4703 | 170 | var ReadConfig = func() (*Config, error) { | 117 | // Just take password, stored as a hash |
4704 | 171 | wifiConfig, err := readRemoteConfig() | 118 | _, err := HashIt(p.Password) |
4697 | 172 | if err != nil { | ||
4698 | 173 | return nil, err | ||
4699 | 174 | } | ||
4700 | 175 | |||
4701 | 176 | portalConfig, err := readLocalConfig() | ||
4705 | 177 | if err != nil { | 119 | if err != nil { |
4712 | 178 | return nil, err | 120 | return fmt.Errorf("Could not hash portal password to file: %v", err) |
4707 | 179 | } | ||
4708 | 180 | |||
4709 | 181 | // if local config is nil, fill returning object with default values | ||
4710 | 182 | if portalConfig == nil { | ||
4711 | 183 | portalConfig = defaultPortalConfig() | ||
4713 | 184 | } | 121 | } |
4714 | 185 | 122 | ||
4716 | 186 | return &Config{Wifi: wifiConfig, Portal: portalConfig}, nil | 123 | return nil |
4717 | 187 | } | 124 | } |
4718 | 188 | 125 | ||
4720 | 189 | // WriteConfig writes all remote and local config at the same time | 126 | // WriteConfig writes configuration |
4721 | 190 | var WriteConfig = func(c *Config) error { | 127 | var WriteConfig = func(c *Config) error { |
4738 | 191 | previousRemoteConfig, err := readRemoteConfig() | 128 | err := writeConfig(&c.Portal) |
4723 | 192 | if err != nil { | ||
4724 | 193 | return fmt.Errorf("Error reading current remote config before applying new one: %v", err) | ||
4725 | 194 | } | ||
4726 | 195 | |||
4727 | 196 | // only write remote config if it's different from current | ||
4728 | 197 | if *previousRemoteConfig != *c.Wifi { | ||
4729 | 198 | err = writeRemoteConfig(c.Wifi) | ||
4730 | 199 | if err != nil { | ||
4731 | 200 | // if an error happens writing remote config there is no need to restore | ||
4732 | 201 | // backup, as nothing shouldn't have been written | ||
4733 | 202 | return err | ||
4734 | 203 | } | ||
4735 | 204 | } | ||
4736 | 205 | |||
4737 | 206 | err = writeLocalConfig(c.Portal) | ||
4739 | 207 | if err != nil { | 129 | if err != nil { |
4740 | 208 | // rollback | ||
4741 | 209 | if previousRemoteConfig != nil { | ||
4742 | 210 | backupErr := writeRemoteConfig(previousRemoteConfig) | ||
4743 | 211 | if backupErr != nil { | ||
4744 | 212 | return fmt.Errorf("Could not restore previous remote configuration: %v\n after error: %v", backupErr, err) | ||
4745 | 213 | } | ||
4746 | 214 | } | ||
4747 | 215 | return err | 130 | return err |
4748 | 216 | } | 131 | } |
4749 | 217 | 132 | ||
4756 | 218 | // write flag file for not asking more times for configuring snap before first use | 133 | // write flag file for not asking more times for configuring snap before |
4757 | 219 | if MustSetConfig() { | 134 | // first use |
4758 | 220 | err = WriteFlagFile(mustConfigFlagFile) | 135 | err = WriteFlagFile(mustConfigFlagFile) |
4759 | 221 | if err != nil { | 136 | if err != nil { |
4760 | 222 | return fmt.Errorf("Error writing flag file after configuring for a first time") | 137 | return fmt.Errorf("Error writing flag file after " + |
4761 | 223 | } | 138 | "configuring for a first time") |
4762 | 224 | } | 139 | } |
4763 | 225 | 140 | ||
4764 | 226 | return nil | 141 | return nil |
4765 | @@ -228,8 +143,11 @@ var WriteConfig = func(c *Config) error { | |||
4766 | 228 | 143 | ||
4767 | 229 | // MustSetConfig true if one needs to configure snap before continuing | 144 | // MustSetConfig true if one needs to configure snap before continuing |
4768 | 230 | var MustSetConfig = func() bool { | 145 | var MustSetConfig = func() bool { |
4772 | 231 | if _, err := os.Stat(mustConfigFlagFile); os.IsNotExist(err) { | 146 | // TODO Do not request portal configuration for the moment. It is not |
4773 | 232 | return true | 147 | // clear if it makes sense as we will be using wifi-connect only once |
4774 | 233 | } | 148 | // to select an AP. |
4775 | 149 | // if _, err := os.Stat(mustConfigFlagFile); os.IsNotExist(err) { | ||
4776 | 150 | // return true | ||
4777 | 151 | // } | ||
4778 | 234 | return false | 152 | return false |
4779 | 235 | } | 153 | } |
4780 | diff --git a/utils/config_test.go b/utils/config_test.go | |||
4781 | index 1802a60..b4339ed 100644 | |||
4782 | --- a/utils/config_test.go | |||
4783 | +++ b/utils/config_test.go | |||
4784 | @@ -4,61 +4,37 @@ import ( | |||
4785 | 4 | "fmt" | 4 | "fmt" |
4786 | 5 | "io/ioutil" | 5 | "io/ioutil" |
4787 | 6 | "os" | 6 | "os" |
4788 | 7 | "path/filepath" | ||
4789 | 8 | "strconv" | ||
4790 | 9 | "sync" | ||
4791 | 10 | "testing" | 7 | "testing" |
4792 | 11 | "time" | ||
4793 | 12 | 8 | ||
4794 | 13 | "gopkg.in/check.v1" | 9 | "gopkg.in/check.v1" |
4795 | 14 | ) | 10 | ) |
4796 | 15 | 11 | ||
4801 | 16 | const testLocalConfig = ` | 12 | const testConfig1 = `{ |
4802 | 17 | { | 13 | "wifi": { |
4803 | 18 | "portal.no-reset-creds": true, | 14 | "ssid": "confconn", |
4804 | 19 | "portal.no-operational": false | 15 | "security-passphrase": "mypassphrase", |
4805 | 16 | "interface": "wlp1s1", | ||
4806 | 17 | "country-code": "ES", | ||
4807 | 18 | "channel": 6 , | ||
4808 | 19 | "operation-mode": "g", | ||
4809 | 20 | "subnet": "23.23.23.23/16" | ||
4810 | 21 | }, | ||
4811 | 22 | "portal": { | ||
4812 | 23 | "password": "mypassword", | ||
4813 | 24 | "port": 9009, | ||
4814 | 25 | "no-reset-creds": false, | ||
4815 | 26 | "operational": true | ||
4816 | 27 | }, | ||
4817 | 28 | "config-wifi-once": true | ||
4818 | 20 | } | 29 | } |
4819 | 21 | ` | 30 | ` |
4820 | 22 | 31 | ||
4840 | 23 | const testLocalConfigBadEntry = ` | 32 | // Hook up gocheck into the "go test" runner. |
4822 | 24 | { | ||
4823 | 25 | "portal.password": "the_password", | ||
4824 | 26 | "portal.no-reset-creds": true, | ||
4825 | 27 | "bad.parameter": "bad.value", | ||
4826 | 28 | "portal.no-operational": false | ||
4827 | 29 | } | ||
4828 | 30 | ` | ||
4829 | 31 | |||
4830 | 32 | const testLocalEmptyConfig = ` | ||
4831 | 33 | { | ||
4832 | 34 | } | ||
4833 | 35 | ` | ||
4834 | 36 | |||
4835 | 37 | var testPortalConfig = &PortalConfig{"the_password", true, false} | ||
4836 | 38 | |||
4837 | 39 | var rand uint32 | ||
4838 | 40 | var randmu sync.Mutex | ||
4839 | 41 | |||
4841 | 42 | func Test(t *testing.T) { check.TestingT(t) } | 33 | func Test(t *testing.T) { check.TestingT(t) } |
4842 | 43 | 34 | ||
4846 | 44 | type S struct{} | 35 | type CfgSuite struct{} |
4844 | 45 | |||
4845 | 46 | var _ = check.Suite(&S{}) | ||
4847 | 47 | 36 | ||
4862 | 48 | // #################### | 37 | var _ = check.Suite(&CfgSuite{}) |
4849 | 49 | // Testing local config | ||
4850 | 50 | // #################### | ||
4851 | 51 | func randomName() string { | ||
4852 | 52 | randmu.Lock() | ||
4853 | 53 | r := rand | ||
4854 | 54 | if r == 0 { | ||
4855 | 55 | r = uint32(time.Now().UnixNano() + int64(os.Getpid())) | ||
4856 | 56 | } | ||
4857 | 57 | r = r*1664525 + 1013904223 // constants from Numerical Recipes | ||
4858 | 58 | rand = r | ||
4859 | 59 | randmu.Unlock() | ||
4860 | 60 | return strconv.Itoa(int(1e9 + r%1e9))[1:] | ||
4861 | 61 | } | ||
4863 | 62 | 38 | ||
4864 | 63 | func createTempFile(content string) (*os.File, error) { | 39 | func createTempFile(content string) (*os.File, error) { |
4865 | 64 | contentAsBytes := []byte(content) | 40 | contentAsBytes := []byte(content) |
4866 | @@ -79,465 +55,52 @@ func createTempFile(content string) (*os.File, error) { | |||
4867 | 79 | return tmpfile, nil | 55 | return tmpfile, nil |
4868 | 80 | } | 56 | } |
4869 | 81 | 57 | ||
4882 | 82 | func verifyLocalConfig(c *check.C, cfg *PortalConfig, expectedPwd string, expectedNoResetCredentials bool, expectedNoOperational bool) { | 58 | func (s *CfgSuite) TestReadConfigDefaults(c *check.C) { |
4883 | 83 | c.Assert(cfg.Password, check.Equals, expectedPwd) | 59 | f, err := createTempFile("{}") |
4872 | 84 | c.Assert(cfg.NoResetCredentials, check.Equals, expectedNoResetCredentials) | ||
4873 | 85 | c.Assert(cfg.NoOperational, check.Equals, expectedNoOperational) | ||
4874 | 86 | } | ||
4875 | 87 | |||
4876 | 88 | func verifyDefaultLocalConfig(c *check.C, cfg *PortalConfig) { | ||
4877 | 89 | verifyLocalConfig(c, cfg, "", false, false) | ||
4878 | 90 | } | ||
4879 | 91 | |||
4880 | 92 | func (s *S) TestReadLocalConfig(c *check.C) { | ||
4881 | 93 | f, err := createTempFile(testLocalConfig) | ||
4884 | 94 | c.Assert(err, check.IsNil) | 60 | c.Assert(err, check.IsNil) |
4885 | 95 | 61 | ||
4886 | 96 | defer os.Remove(f.Name()) | ||
4887 | 97 | configFile = f.Name() | 62 | configFile = f.Name() |
4888 | 98 | 63 | ||
4894 | 99 | cfg, err := readLocalConfig() | 64 | cfg, err := LoadConfig() |
4890 | 100 | c.Assert(err, check.IsNil) | ||
4891 | 101 | |||
4892 | 102 | verifyLocalConfig(c, cfg, "", true, false) | ||
4893 | 103 | } | ||
4895 | 104 | 65 | ||
4896 | 105 | func (s *S) TestReadLocalConfigBadEntry(c *check.C) { | ||
4897 | 106 | // No matter if there are additional not recognized params, only known should be marshalled | ||
4898 | 107 | f, err := createTempFile(testLocalConfigBadEntry) | ||
4899 | 108 | c.Assert(err, check.IsNil) | 66 | c.Assert(err, check.IsNil) |
4900 | 67 | c.Assert(cfg.Wifi.Ssid, check.Equals, "") | ||
4901 | 68 | c.Assert(cfg.Wifi.Passphrase, check.Equals, "") | ||
4902 | 69 | c.Assert(cfg.Wifi.Interface, check.Equals, "") | ||
4903 | 70 | c.Assert(cfg.Wifi.CountryCode, check.Equals, "") | ||
4904 | 71 | c.Assert(cfg.Wifi.Channel, check.Equals, 0) | ||
4905 | 72 | c.Assert(cfg.Wifi.OperationMode, check.Equals, "") | ||
4906 | 73 | c.Assert(cfg.Wifi.Subnet, check.Equals, "10.25.52.1/24") | ||
4907 | 109 | 74 | ||
4913 | 110 | defer os.Remove(f.Name()) | 75 | c.Assert(cfg.Portal.Password, check.Equals, "") |
4914 | 111 | configFile = f.Name() | 76 | c.Assert(cfg.Portal.Port, check.Equals, 8085) |
4915 | 112 | 77 | c.Assert(cfg.Portal.NoResetCredentials, check.Equals, false) | |
4916 | 113 | cfg, err := readLocalConfig() | 78 | c.Assert(cfg.Portal.Operational, check.Equals, false) |
4912 | 114 | c.Assert(err, check.IsNil) | ||
4917 | 115 | 79 | ||
4919 | 116 | verifyLocalConfig(c, cfg, "", true, false) | 80 | c.Assert(cfg.ConfigWifiOnce, check.Equals, false) |
4920 | 117 | } | 81 | } |
4921 | 118 | 82 | ||
4925 | 119 | func (s *S) TestReadLocalEmptyConfig(c *check.C) { | 83 | func (s *CfgSuite) TestReadConfig(c *check.C) { |
4926 | 120 | // No matter if there are additional not recognized params, only known should be marshalled | 84 | f, err := createTempFile(testConfig1) |
4924 | 121 | f, err := createTempFile(testLocalEmptyConfig) | ||
4927 | 122 | c.Assert(err, check.IsNil) | 85 | c.Assert(err, check.IsNil) |
4928 | 123 | 86 | ||
4929 | 124 | defer os.Remove(f.Name()) | ||
4930 | 125 | configFile = f.Name() | 87 | configFile = f.Name() |
4931 | 126 | 88 | ||
4951 | 127 | cfg, err := readLocalConfig() | 89 | cfg, err := LoadConfig() |
4933 | 128 | c.Assert(err, check.IsNil) | ||
4934 | 129 | |||
4935 | 130 | verifyDefaultLocalConfig(c, cfg) | ||
4936 | 131 | } | ||
4937 | 132 | |||
4938 | 133 | func (s *S) TestReadLocalNotExistingConfig(c *check.C) { | ||
4939 | 134 | configFile = "does/not/exists/config.json" | ||
4940 | 135 | |||
4941 | 136 | cfg, err := readLocalConfig() | ||
4942 | 137 | c.Assert(err, check.IsNil) | ||
4943 | 138 | c.Assert(cfg, check.IsNil) | ||
4944 | 139 | } | ||
4945 | 140 | |||
4946 | 141 | func (s *S) TestWriteLocalConfigFileDoesNotExists(c *check.C) { | ||
4947 | 142 | mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName()) | ||
4948 | 143 | defer os.Remove(mustConfigFlagFile) | ||
4949 | 144 | HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName()) | ||
4950 | 145 | defer os.Remove(HashFile) | ||
4952 | 146 | 90 | ||
4953 | 147 | err := writeLocalConfig(testPortalConfig) | ||
4954 | 148 | c.Assert(err, check.IsNil) | 91 | c.Assert(err, check.IsNil) |
4955 | 92 | c.Assert(cfg.Wifi.Ssid, check.Equals, "confconn") | ||
4956 | 93 | c.Assert(cfg.Wifi.Passphrase, check.Equals, "mypassphrase") | ||
4957 | 94 | c.Assert(cfg.Wifi.Interface, check.Equals, "wlp1s1") | ||
4958 | 95 | c.Assert(cfg.Wifi.CountryCode, check.Equals, "ES") | ||
4959 | 96 | c.Assert(cfg.Wifi.Channel, check.Equals, 6) | ||
4960 | 97 | c.Assert(cfg.Wifi.OperationMode, check.Equals, "g") | ||
4961 | 98 | c.Assert(cfg.Wifi.Subnet, check.Equals, "23.23.23.23/16") | ||
4962 | 149 | 99 | ||
4963 | 150 | ok, err := MatchingHash(testPortalConfig.Password) | ||
4964 | 151 | c.Assert(err, check.IsNil) | ||
4965 | 152 | c.Assert(ok, check.Equals, true) | ||
4966 | 153 | } | ||
4967 | 154 | |||
4968 | 155 | func (s *S) TestWriteLocalConfigFiletExists(c *check.C) { | ||
4969 | 156 | mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName()) | ||
4970 | 157 | defer os.Remove(mustConfigFlagFile) | ||
4971 | 158 | |||
4972 | 159 | f, err := createTempFile("whateverbadpasswordhash") | ||
4973 | 160 | c.Assert(err, check.IsNil) | ||
4974 | 161 | |||
4975 | 162 | defer os.Remove(f.Name()) | ||
4976 | 163 | HashFile = f.Name() | ||
4977 | 164 | |||
4978 | 165 | err = writeLocalConfig(testPortalConfig) | ||
4979 | 166 | c.Assert(err, check.IsNil) | ||
4980 | 167 | |||
4981 | 168 | ok, err := MatchingHash(testPortalConfig.Password) | ||
4982 | 169 | c.Assert(err, check.IsNil) | ||
4983 | 170 | c.Assert(ok, check.Equals, true) | ||
4984 | 171 | } | ||
4985 | 172 | |||
4986 | 173 | // ##################### | ||
4987 | 174 | // Testing remote config | ||
4988 | 175 | // ##################### | ||
4989 | 176 | type wifiapClientMock struct { | ||
4990 | 177 | m map[string]interface{} | ||
4991 | 178 | } | ||
4992 | 179 | |||
4993 | 180 | func (c *wifiapClientMock) Show() (map[string]interface{}, error) { | ||
4994 | 181 | return c.m, nil | ||
4995 | 182 | } | ||
4996 | 183 | |||
4997 | 184 | func (c *wifiapClientMock) Enable() error { | ||
4998 | 185 | return nil | ||
4999 | 186 | } | ||
5000 | 187 |
PASSED: Successfully build documentation, rev: 223cd235a400650 79971211ab2a3a7 2749b3d730
Generated documentation is available at https:/ /jenkins. canonical. com/system- enablement/ job/snappy- hwe-snaps- snap-docs/ 1305/