Merge ~teknoraver/snappy-hwe-snaps/+git/wifi-ap:master into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master
- Git
- lp:~teknoraver/snappy-hwe-snaps/+git/wifi-ap
- master
- Merge into master
Status: | Superseded |
---|---|
Proposed branch: | ~teknoraver/snappy-hwe-snaps/+git/wifi-ap:master |
Merge into: | ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master |
Diff against target: |
2417 lines (+1506/-604) 27 files modified
MAINTAINERS (+1/-0) README.md (+35/-0) bin/ap.sh (+31/-9) bin/first-config.sh (+25/-0) bin/helper.sh (+1/-2) cmd/client/client.go (+5/-0) cmd/client/cmd_config.go (+4/-12) cmd/client/cmd_status.go (+58/-0) cmd/client/utils.go (+33/-0) cmd/service/api.go (+176/-0) cmd/service/api_test.go (+344/-0) cmd/service/background_process.go (+138/-0) cmd/service/background_process_test.go (+36/-0) cmd/service/config.go (+140/-0) cmd/service/config_test.go (+66/-0) cmd/service/main.go (+11/-266) cmd/service/response.go (+73/-0) cmd/service/response_test.go (+47/-0) cmd/service/service.go (+159/-0) dev/null (+0/-298) snapcraft.yaml (+14/-8) tests/lib/prepare-all.sh (+7/-0) tests/lib/restore-each.sh (+0/-3) tests/main/background-process-control/task.yaml (+46/-0) tests/main/default-conf-brings-up-ap/task.yaml (+0/-3) tests/main/stress-ap-status-control/task.yaml (+49/-0) tests/main/utf8-ssid/task.yaml (+7/-3) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
System Enablement Bot | continuous-integration | Approve | |
Review via email: mp+312061@code.launchpad.net |
This proposal has been superseded by a proposal from 2016-11-29.
Commit message
Description of the change
run wizaard of first boot
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:93e735e85a5
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Unmerged commits
- 93e735e... by Matteo Croce
-
run wizard --auto on first boot
- 0871e0a... by Simon Fels
-
Set SSID before enabling the access point
- 5f4d419... by Simon Fels
-
Use higher number of iterations for stress test an check process count
- 19c64ab... by Simon Fels
-
Kill the whole process group instead of just the main process
- 31520f4... by Simon Fels
-
Mark stress test as manual only for now
The test isn't stable yet and requires some more changes in the
process management infrastructure. - 60a3f50... by Simon Fels
-
Add stress test for new background process infrastructure
- f5f7fbf... by Simon Fels
-
Don't rebuild wifi-ap snap when already build locally
- 649c860... by Simon Fels
-
Respect review comments
- c90122c... by Simon Fels
-
Add missing absolute path to wifi-ap command
- 25734ea... by Simon Fels
-
Fix UTF8 SSID test case
Preview Diff
1 | diff --git a/MAINTAINERS b/MAINTAINERS |
2 | index 420159f..1fed46c 100644 |
3 | --- a/MAINTAINERS |
4 | +++ b/MAINTAINERS |
5 | @@ -1 +1,2 @@ |
6 | Simon Fels <simon.fels@canonical.com> |
7 | +Matteo Croce <matteo.croce@canonical.com> |
8 | diff --git a/README.md b/README.md |
9 | index 3c89bcd..1a49a98 100644 |
10 | --- a/README.md |
11 | +++ b/README.md |
12 | @@ -5,6 +5,41 @@ This snap provided WiFi AP functionality out of the box. |
13 | Documentation is currently available at |
14 | https://docs.google.com/document/d/1vNu3fBqpOkBkjv_Vs9NZyTv50vOEfugrQqgxD0_f0rE/edit# |
15 | |
16 | +## Development |
17 | + |
18 | +To modify any of the included services written in Go you need setup |
19 | +your build environment first. |
20 | + |
21 | +``` |
22 | + $ snapcraft clean |
23 | + $ snapcraft |
24 | +``` |
25 | + |
26 | +Now we need to export the GOPATH and point it to the directory |
27 | +snapcraft already created for us. |
28 | + |
29 | +``` |
30 | + $ export GOPATH=$PWD/parts/management-service/go |
31 | +``` |
32 | + |
33 | +Now you can build the management-service by running |
34 | + |
35 | +``` |
36 | + $ cd cmd/service |
37 | + $ go build -o management-service *.go |
38 | +``` |
39 | + |
40 | +If you want to start it afterwards outside of a snap environment you |
41 | +need to setup the right environment variables. |
42 | + |
43 | +``` |
44 | + # Needs to be the top source dir which contains the snapcraft.yaml |
45 | + $ export SNAP=`pwd` |
46 | + $ mkdir tmp-data |
47 | + $ export SNAP_DATA=$PWD/tmp-data |
48 | + $ cmd/service/management-service |
49 | +``` |
50 | + |
51 | ## Running tests |
52 | |
53 | We have a set of spread (https://github.com/snapcore/spread) tests which |
54 | diff --git a/bin/ap.sh b/bin/ap.sh |
55 | index 54c12ac..be8dac4 100755 |
56 | --- a/bin/ap.sh |
57 | +++ b/bin/ap.sh |
58 | @@ -39,10 +39,18 @@ if ! ifconfig $WIFI_INTERFACE ; then |
59 | fi |
60 | |
61 | cleanup_on_exit() { |
62 | + read HOSTAPD_PID <$SNAP_DATA/hostapd.pid |
63 | + if [ -n "$HOSTAPD_PID" ] ; then |
64 | + kill -TERM $HOSTAPD_PID || true |
65 | + wait $HOSTAPD_PID |
66 | + fi |
67 | + |
68 | read DNSMASQ_PID <$SNAP_DATA/dnsmasq.pid |
69 | - # If dnsmasq is already gone don't error out here |
70 | - kill -TERM $DNSMASQ_PID || true |
71 | - wait $DNSMASQ_PID |
72 | + if [ -n "$DNSMASQ_PID" ] ; then |
73 | + # If dnsmasq is already gone don't error out here |
74 | + kill -TERM $DNSMASQ_PID || true |
75 | + wait $DNSMASQ_PID |
76 | + fi |
77 | |
78 | iface=$WIFI_INTERFACE |
79 | if [ "$WIFI_INTERFACE_MODE" = "virtual" ] ; then |
80 | @@ -56,18 +64,22 @@ cleanup_on_exit() { |
81 | sysctl -w net.ipv4.ip_forward=0 |
82 | fi |
83 | |
84 | - if [ "$WIFI_INTERFACE_MODE" = "virtual" ] && does_interface_exist $iface ; then |
85 | - $SNAP/bin/iw dev $iface del |
86 | - fi |
87 | - |
88 | if is_nm_running ; then |
89 | # Hand interface back to network-manager. This will also trigger the |
90 | # auto connection process inside network-manager to get connected |
91 | # with the previous network. |
92 | $SNAP/bin/nmcli d set $iface managed yes |
93 | fi |
94 | + |
95 | + if [ "$WIFI_INTERFACE_MODE" = "virtual" ] ; then |
96 | + $SNAP/bin/iw dev $iface del |
97 | + fi |
98 | } |
99 | |
100 | +# We need to install this right before we do anything to |
101 | +# ensure that we cleanup everything again when we termiante. |
102 | +trap cleanup_on_exit TERM |
103 | + |
104 | iface=$WIFI_INTERFACE |
105 | if [ "$WIFI_INTERFACE_MODE" = "virtual" ] ; then |
106 | iface=$DEFAULT_ACCESS_POINT_INTERFACE |
107 | @@ -139,7 +151,13 @@ if [ $SHARE_DISABLED -eq 0 ] ; then |
108 | fi |
109 | |
110 | generate_dnsmasq_config $SNAP_DATA/dnsmasq.conf |
111 | -$SNAP/bin/dnsmasq -k -C $SNAP_DATA/dnsmasq.conf -l $SNAP_DATA/dnsmasq.leases -x $SNAP_DATA/dnsmasq.pid & |
112 | +$SNAP/bin/dnsmasq \ |
113 | + -k \ |
114 | + -C $SNAP_DATA/dnsmasq.conf \ |
115 | + -l $SNAP_DATA/dnsmasq.leases \ |
116 | + -x $SNAP_DATA/dnsmasq.pid \ |
117 | + -u root -g root \ |
118 | + & |
119 | |
120 | driver=$WIFI_HOSTAPD_DRIVER |
121 | if [ "$driver" = "rtl8188" ] ; then |
122 | @@ -219,6 +237,10 @@ case "$WIFI_HOSTAPD_DRIVER" in |
123 | esac |
124 | |
125 | # Startup hostapd with the configuration we've put in place |
126 | -$hostapd $EXTRA_ARGS $SNAP_DATA/hostapd.conf |
127 | +$hostapd $EXTRA_ARGS $SNAP_DATA/hostapd.conf & |
128 | +hostapd_pid=$! |
129 | +echo $hostapd_pid > $SNAP_DATA/hostapd.pid |
130 | +wait $hostapd_pid |
131 | + |
132 | cleanup_on_exit |
133 | exit 0 |
134 | diff --git a/bin/first-config.sh b/bin/first-config.sh |
135 | new file mode 100755 |
136 | index 0000000..fd1665b |
137 | --- /dev/null |
138 | +++ b/bin/first-config.sh |
139 | @@ -0,0 +1,25 @@ |
140 | +#!/bin/sh |
141 | +# |
142 | +# Copyright (C) 2015, 2016 Canonical Ltd |
143 | +# |
144 | +# This program is free software: you can redistribute it and/or modify |
145 | +# it under the terms of the GNU General Public License version 3 as |
146 | +# published by the Free Software Foundation. |
147 | +# |
148 | +# This program is distributed in the hope that it will be useful, |
149 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
150 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
151 | +# GNU General Public License for more details. |
152 | +# |
153 | +# You should have received a copy of the GNU General Public License |
154 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
155 | + |
156 | +set -x |
157 | + |
158 | +[ -f "$SNAP_DATA/conf/config" ] && exit 0 |
159 | + |
160 | +while ! /snap/bin/wifi-ap.status; do |
161 | + sleep .5 |
162 | +done |
163 | + |
164 | +exec /snap/bin/wifi-ap.setup-wizard wizard --auto |
165 | diff --git a/bin/helper.sh b/bin/helper.sh |
166 | index 718a2d6..4a557d6 100644 |
167 | --- a/bin/helper.sh |
168 | +++ b/bin/helper.sh |
169 | @@ -56,6 +56,5 @@ generate_dnsmasq_config() { |
170 | |
171 | is_nm_running() { |
172 | nm_status=`$SNAP/bin/nmcli -t -f RUNNING general` |
173 | - [ "$nm_status" = "running" ] && return 1 |
174 | - return 0 |
175 | + [ "$nm_status" = "running" ] |
176 | } |
177 | diff --git a/cmd/client/client.go b/cmd/client/client.go |
178 | index 0a770d7..ec8bb76 100644 |
179 | --- a/cmd/client/client.go |
180 | +++ b/cmd/client/client.go |
181 | @@ -25,6 +25,7 @@ import ( |
182 | const ( |
183 | servicePort = 5005 |
184 | configurationV1Uri = "/v1/configuration" |
185 | + statusV1Uri = "/v1/status" |
186 | ) |
187 | |
188 | type serviceResponse struct { |
189 | @@ -38,6 +39,10 @@ func getServiceConfigurationURI() string { |
190 | return fmt.Sprintf("http://localhost:%d%s", servicePort, configurationV1Uri) |
191 | } |
192 | |
193 | +func getServiceStatusURI() string { |
194 | + return fmt.Sprintf("http://localhost:%d%s", servicePort, statusV1Uri) |
195 | +} |
196 | + |
197 | type doer interface { |
198 | Do(*http.Request) (*http.Response, error) |
199 | } |
200 | diff --git a/cmd/client/cmd_config.go b/cmd/client/cmd_config.go |
201 | index 8a82f4a..f0d6b4c 100644 |
202 | --- a/cmd/client/cmd_config.go |
203 | +++ b/cmd/client/cmd_config.go |
204 | @@ -20,7 +20,6 @@ import ( |
205 | "encoding/json" |
206 | "fmt" |
207 | "os" |
208 | - "sort" |
209 | ) |
210 | |
211 | type setCommand struct{} |
212 | @@ -58,14 +57,7 @@ func (cmd *getCommand) Execute(args []string) error { |
213 | return fmt.Errorf("Config item '%s' does not exist", wantedKey) |
214 | } |
215 | } else { |
216 | - sortedKeys := make([]string, 0, len(response.Result)) |
217 | - for key, _ := range response.Result { |
218 | - sortedKeys = append(sortedKeys, key) |
219 | - } |
220 | - sort.Strings(sortedKeys) |
221 | - for n := range sortedKeys { |
222 | - fmt.Fprintf(os.Stdout, "%s: %s\n", sortedKeys[n], response.Result[sortedKeys[n]]) |
223 | - } |
224 | + printMapSorted(response.Result) |
225 | } |
226 | |
227 | return nil |
228 | @@ -78,7 +70,7 @@ func (cmd *configCommand) Execute(args []string) error { |
229 | } |
230 | |
231 | func init() { |
232 | - cmdConfig, _ := addCommand("config", "Adjust the service configuration", "", &configCommand{}) |
233 | - cmdConfig.AddCommand("set", "", "", &setCommand{}) |
234 | - cmdConfig.AddCommand("get", "", "", &getCommand{}) |
235 | + cmd, _ := addCommand("config", "Adjust the service configuration", "", &configCommand{}) |
236 | + cmd.AddCommand("set", "", "", &setCommand{}) |
237 | + cmd.AddCommand("get", "", "", &getCommand{}) |
238 | } |
239 | diff --git a/cmd/client/cmd_status.go b/cmd/client/cmd_status.go |
240 | new file mode 100644 |
241 | index 0000000..e120e47 |
242 | --- /dev/null |
243 | +++ b/cmd/client/cmd_status.go |
244 | @@ -0,0 +1,58 @@ |
245 | +// |
246 | +// Copyright (C) 2016 Canonical Ltd |
247 | +// |
248 | +// This program is free software: you can redistribute it and/or modify |
249 | +// it under the terms of the GNU General Public License version 3 as |
250 | +// published by the Free Software Foundation. |
251 | +// |
252 | +// This program is distributed in the hope that it will be useful, |
253 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
254 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
255 | +// GNU General Public License for more details. |
256 | +// |
257 | +// You should have received a copy of the GNU General Public License |
258 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
259 | + |
260 | +package main |
261 | + |
262 | +import ( |
263 | + "bytes" |
264 | + "encoding/json" |
265 | +) |
266 | + |
267 | +type restartCommand struct{} |
268 | + |
269 | +func (cmd *restartCommand) Execute(args []string) error { |
270 | + req := make(map[string]string) |
271 | + req["action"] = "restart-ap" |
272 | + |
273 | + b, err := json.Marshal(req) |
274 | + if err != nil { |
275 | + return err |
276 | + } |
277 | + |
278 | + _, err = sendHTTPRequest(getServiceStatusURI(), "POST", bytes.NewReader(b)) |
279 | + if err != nil { |
280 | + return err |
281 | + } |
282 | + |
283 | + return nil |
284 | +} |
285 | + |
286 | +type statusCommand struct{} |
287 | + |
288 | +func (cmd *statusCommand) Execute(args []string) error { |
289 | + response, err := sendHTTPRequest(getServiceStatusURI(), "GET", nil) |
290 | + if err != nil { |
291 | + return err |
292 | + } |
293 | + printMapSorted(response.Result) |
294 | + return nil |
295 | +} |
296 | + |
297 | +func init() { |
298 | + cmd, _ := addCommand("status", "Show various status information about the access point", "", &statusCommand{}) |
299 | + cmd.SubcommandsOptional = true |
300 | + |
301 | + cmd.AddCommand("restart-ap", "Restart access point", "", &restartCommand{}) |
302 | +} |
303 | diff --git a/cmd/client/utils.go b/cmd/client/utils.go |
304 | new file mode 100644 |
305 | index 0000000..f5c55a3 |
306 | --- /dev/null |
307 | +++ b/cmd/client/utils.go |
308 | @@ -0,0 +1,33 @@ |
309 | +// |
310 | +// Copyright (C) 2016 Canonical Ltd |
311 | +// |
312 | +// This program is free software: you can redistribute it and/or modify |
313 | +// it under the terms of the GNU General Public License version 3 as |
314 | +// published by the Free Software Foundation. |
315 | +// |
316 | +// This program is distributed in the hope that it will be useful, |
317 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
318 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
319 | +// GNU General Public License for more details. |
320 | +// |
321 | +// You should have received a copy of the GNU General Public License |
322 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
323 | + |
324 | +package main |
325 | + |
326 | +import ( |
327 | + "fmt" |
328 | + "os" |
329 | + "sort" |
330 | +) |
331 | + |
332 | +func printMapSorted(m map[string]string) { |
333 | + sortedKeys := make([]string, 0, len(m)) |
334 | + for key, _ := range m { |
335 | + sortedKeys = append(sortedKeys, key) |
336 | + } |
337 | + sort.Strings(sortedKeys) |
338 | + for _, k := range sortedKeys { |
339 | + fmt.Fprintf(os.Stdout, "%s: %s\n", k, m[k]) |
340 | + } |
341 | +} |
342 | diff --git a/cmd/service/api.go b/cmd/service/api.go |
343 | new file mode 100644 |
344 | index 0000000..5c4c6fd |
345 | --- /dev/null |
346 | +++ b/cmd/service/api.go |
347 | @@ -0,0 +1,176 @@ |
348 | +// |
349 | +// Copyright (C) 2016 Canonical Ltd |
350 | +// |
351 | +// This program is free software: you can redistribute it and/or modify |
352 | +// it under the terms of the GNU General Public License version 3 as |
353 | +// published by the Free Software Foundation. |
354 | +// |
355 | +// This program is distributed in the hope that it will be useful, |
356 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
357 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
358 | +// GNU General Public License for more details. |
359 | +// |
360 | +// You should have received a copy of the GNU General Public License |
361 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
362 | + |
363 | +package main |
364 | + |
365 | +import ( |
366 | + "encoding/json" |
367 | + "fmt" |
368 | + "io/ioutil" |
369 | + "net/http" |
370 | + "os" |
371 | +) |
372 | + |
373 | +var api = []*serviceCommand{ |
374 | + configurationCmd, |
375 | + statusCmd, |
376 | +} |
377 | + |
378 | +var ( |
379 | + configurationCmd = &serviceCommand{ |
380 | + Path: "/v1/configuration", |
381 | + GET: getConfiguration, |
382 | + POST: postConfiguration, |
383 | + } |
384 | + statusCmd = &serviceCommand{ |
385 | + Path: "/v1/status", |
386 | + GET: getStatus, |
387 | + POST: postStatus, |
388 | + } |
389 | + validTokens map[string]bool |
390 | +) |
391 | + |
392 | +func getConfiguration(c *serviceCommand, writer http.ResponseWriter, request *http.Request) { |
393 | + config := make(map[string]string) |
394 | + if err := readConfiguration(configurationPaths, config); err == nil { |
395 | + sendHTTPResponse(writer, makeResponse(http.StatusOK, config)) |
396 | + } else { |
397 | + resp := makeErrorResponse(http.StatusInternalServerError, "Failed to read configuration data", "internal-error") |
398 | + sendHTTPResponse(writer, resp) |
399 | + } |
400 | +} |
401 | + |
402 | +func postConfiguration(c *serviceCommand, writer http.ResponseWriter, request *http.Request) { |
403 | + path := getConfigOnPath(os.Getenv("SNAP_DATA")) |
404 | + config := map[string]string{} |
405 | + if readConfiguration([]string{path}, config) != nil { |
406 | + resp := makeErrorResponse(http.StatusInternalServerError, |
407 | + "Failed to read existing configuration file", "internal-error") |
408 | + sendHTTPResponse(writer, resp) |
409 | + return |
410 | + } |
411 | + |
412 | + if validTokens == nil || len(validTokens) == 0 { |
413 | + errResponse := makeErrorResponse(http.StatusInternalServerError, "No default configuration file available", "internal-error") |
414 | + sendHTTPResponse(writer, errResponse) |
415 | + return |
416 | + } |
417 | + |
418 | + file, err := os.Create(path) |
419 | + if err != nil { |
420 | + resp := makeErrorResponse(http.StatusInternalServerError, "Can't write configuration file", "internal-error") |
421 | + sendHTTPResponse(writer, resp) |
422 | + return |
423 | + } |
424 | + defer file.Close() |
425 | + |
426 | + body, err := ioutil.ReadAll(request.Body) |
427 | + if err != nil { |
428 | + resp := makeErrorResponse(http.StatusInternalServerError, "Error reading the request body", "internal-error") |
429 | + sendHTTPResponse(writer, resp) |
430 | + return |
431 | + } |
432 | + |
433 | + var items map[string]string |
434 | + if err = json.Unmarshal(body, &items); err != nil { |
435 | + resp := makeErrorResponse(http.StatusInternalServerError, "Malformed request", "internal-error") |
436 | + sendHTTPResponse(writer, resp) |
437 | + return |
438 | + } |
439 | + |
440 | + // Add the items in the config, but only if all are in the whitelist |
441 | + for key, value := range items { |
442 | + if _, present := validTokens[key]; !present { |
443 | + errResponse := makeErrorResponse(http.StatusInternalServerError, `Invalid key "`+key+`"`, "internal-error") |
444 | + sendHTTPResponse(writer, errResponse) |
445 | + return |
446 | + } |
447 | + config[key] = value |
448 | + } |
449 | + |
450 | + for key, value := range config { |
451 | + key = convertKeyToStorageFormat(key) |
452 | + value = escapeTextForShell(value) |
453 | + file.WriteString(fmt.Sprintf("%s=%s\n", key, value)) |
454 | + } |
455 | + |
456 | + if err := restartAccessPoint(c); err != nil { |
457 | + resp := makeErrorResponse(http.StatusInternalServerError, "Failed to restart AP process", "internal-error") |
458 | + sendHTTPResponse(writer, resp) |
459 | + return |
460 | + } |
461 | + |
462 | + sendHTTPResponse(writer, makeResponse(http.StatusOK, nil)) |
463 | +} |
464 | + |
465 | +func restartAccessPoint(c *serviceCommand) error { |
466 | + if c.s.ap != nil { |
467 | + // Now that we have all configuration changes successfully applied |
468 | + // we can safely restart the service. |
469 | + if err := c.s.ap.Restart(); err != nil { |
470 | + return err |
471 | + } |
472 | + } |
473 | + return nil |
474 | +} |
475 | + |
476 | +func getStatus(c *serviceCommand, writer http.ResponseWriter, request *http.Request) { |
477 | + status := make(map[string]string) |
478 | + |
479 | + status["ap.active"] = "0" |
480 | + if c.s.ap != nil && c.s.ap.Running() { |
481 | + status["ap.active"] = "1" |
482 | + } |
483 | + |
484 | + sendHTTPResponse(writer, makeResponse(http.StatusOK, status)) |
485 | +} |
486 | + |
487 | +func postStatus(c *serviceCommand, writer http.ResponseWriter, request *http.Request) { |
488 | + body, err := ioutil.ReadAll(request.Body) |
489 | + if err != nil { |
490 | + resp := makeErrorResponse(http.StatusInternalServerError, "Error reading the request body", "internal-error") |
491 | + sendHTTPResponse(writer, resp) |
492 | + return |
493 | + } |
494 | + |
495 | + var items map[string]string |
496 | + if json.Unmarshal(body, &items) != nil { |
497 | + resp := makeErrorResponse(http.StatusInternalServerError, "Malformed request", "internal-error") |
498 | + sendHTTPResponse(writer, resp) |
499 | + return |
500 | + } |
501 | + |
502 | + action, ok := items["action"] |
503 | + if !ok { |
504 | + resp := makeErrorResponse(http.StatusInternalServerError, "Mailformed request", "internal-error") |
505 | + sendHTTPResponse(writer, resp) |
506 | + return |
507 | + } |
508 | + |
509 | + switch action { |
510 | + case "restart-ap": |
511 | + if err = restartAccessPoint(c); err != nil { |
512 | + resp := makeErrorResponse(http.StatusInternalServerError, "Failed to restart AP process", "internal-error") |
513 | + sendHTTPResponse(writer, resp) |
514 | + return |
515 | + } |
516 | + |
517 | + resp := makeResponse(http.StatusOK, nil) |
518 | + sendHTTPResponse(writer, resp) |
519 | + } |
520 | + |
521 | + resp := makeErrorResponse(http.StatusInternalServerError, "Invalid request", "internal-error") |
522 | + sendHTTPResponse(writer, resp) |
523 | +} |
524 | diff --git a/cmd/service/api_test.go b/cmd/service/api_test.go |
525 | new file mode 100644 |
526 | index 0000000..433843b |
527 | --- /dev/null |
528 | +++ b/cmd/service/api_test.go |
529 | @@ -0,0 +1,344 @@ |
530 | +// |
531 | +// Copyright (C) 2016 Canonical Ltd |
532 | +// |
533 | +// This program is free software: you can redistribute it and/or modify |
534 | +// it under the terms of the GNU General Public License version 3 as |
535 | +// published by the Free Software Foundation. |
536 | +// |
537 | +// This program is distributed in the hope that it will be useful, |
538 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
539 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
540 | +// GNU General Public License for more details. |
541 | +// |
542 | +// You should have received a copy of the GNU General Public License |
543 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
544 | + |
545 | +package main |
546 | + |
547 | +import ( |
548 | + "bytes" |
549 | + "encoding/json" |
550 | + "io/ioutil" |
551 | + "net/http" |
552 | + "net/http/httptest" |
553 | + "os" |
554 | + "path/filepath" |
555 | + "strings" |
556 | + "testing" |
557 | + |
558 | + "gopkg.in/check.v1" |
559 | +) |
560 | + |
561 | +// gopkg.in/check.v1 stuff |
562 | +func Test(t *testing.T) { check.TestingT(t) } |
563 | + |
564 | +type S struct{} |
565 | + |
566 | +var _ = check.Suite(&S{}) |
567 | + |
568 | +type mockBackgroundProcess struct { |
569 | + running bool |
570 | +} |
571 | + |
572 | +func (p *mockBackgroundProcess) Start() error { |
573 | + p.running = true |
574 | + return nil |
575 | +} |
576 | + |
577 | +func (p *mockBackgroundProcess) Stop() error { |
578 | + p.running = false |
579 | + return nil |
580 | +} |
581 | + |
582 | +func (p *mockBackgroundProcess) Restart() error { |
583 | + p.running = true |
584 | + return nil |
585 | +} |
586 | + |
587 | +func (p *mockBackgroundProcess) Running() bool { |
588 | + return p.running |
589 | +} |
590 | + |
591 | +func newMockServiceCommand() *serviceCommand { |
592 | + return &serviceCommand{ |
593 | + s: &service{ |
594 | + ap: &mockBackgroundProcess{}, |
595 | + }, |
596 | + } |
597 | +} |
598 | + |
599 | +func (s *S) TestGetConfiguration(c *check.C) { |
600 | + // Check it we get a valid JSON as configuration |
601 | + req, err := http.NewRequest(http.MethodGet, "/v1/configuration", nil) |
602 | + c.Assert(err, check.IsNil) |
603 | + |
604 | + rec := httptest.NewRecorder() |
605 | + |
606 | + cmd := newMockServiceCommand() |
607 | + getConfiguration(cmd, rec, req) |
608 | + |
609 | + body, err := ioutil.ReadAll(rec.Body) |
610 | + c.Assert(err, check.IsNil) |
611 | + |
612 | + // Parse the returned JSON |
613 | + var resp serviceResponse |
614 | + err = json.Unmarshal(body, &resp) |
615 | + c.Assert(err, check.IsNil) |
616 | + |
617 | + // Check for 200 status code |
618 | + c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK)) |
619 | + c.Assert(resp.StatusCode, check.Equals, http.StatusOK) |
620 | + c.Assert(resp.Type, check.Equals, "sync") |
621 | +} |
622 | + |
623 | +func (s *S) TestNoDefaultConfiguration(c *check.C) { |
624 | + oldsnap := os.Getenv("SNAP") |
625 | + os.Setenv("SNAP", "/nodir") |
626 | + os.Setenv("SNAP_DATA", "/tmp") |
627 | + |
628 | + req, err := http.NewRequest(http.MethodPost, "/v1/configuration", nil) |
629 | + c.Assert(err, check.IsNil) |
630 | + |
631 | + rec := httptest.NewRecorder() |
632 | + cmd := newMockServiceCommand() |
633 | + |
634 | + validTokens = nil |
635 | + |
636 | + postConfiguration(cmd, rec, req) |
637 | + |
638 | + c.Assert(rec.Code, check.Equals, http.StatusInternalServerError) |
639 | + |
640 | + body, err := ioutil.ReadAll(rec.Body) |
641 | + c.Assert(err, check.IsNil) |
642 | + |
643 | + // Parse the returned JSON |
644 | + var resp serviceResponse |
645 | + err = json.Unmarshal(body, &resp) |
646 | + c.Assert(err, check.IsNil) |
647 | + |
648 | + // Check for 500 status code and other error fields |
649 | + c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError)) |
650 | + c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError) |
651 | + c.Assert(resp.Type, check.Equals, "error") |
652 | + c.Assert(resp.Result["kind"], check.Equals, "internal-error") |
653 | + c.Assert(resp.Result["message"], check.Equals, "No default configuration file available") |
654 | + |
655 | + os.Setenv("SNAP", oldsnap) |
656 | +} |
657 | + |
658 | +func (s *S) TestWriteError(c *check.C) { |
659 | + // Test a non writable path: |
660 | + os.Setenv("SNAP_DATA", "/nodir") |
661 | + |
662 | + req, err := http.NewRequest(http.MethodPost, "/v1/configuration", nil) |
663 | + c.Assert(err, check.IsNil) |
664 | + |
665 | + rec := httptest.NewRecorder() |
666 | + cmd := newMockServiceCommand() |
667 | + |
668 | + validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config")) |
669 | + c.Assert(validTokens, check.NotNil) |
670 | + c.Assert(err, check.IsNil) |
671 | + |
672 | + postConfiguration(cmd, rec, req) |
673 | + |
674 | + c.Assert(rec.Code, check.Equals, http.StatusInternalServerError) |
675 | + |
676 | + body, err := ioutil.ReadAll(rec.Body) |
677 | + c.Assert(err, check.IsNil) |
678 | + |
679 | + // Parse the returned JSON |
680 | + var resp serviceResponse |
681 | + err = json.Unmarshal(body, &resp) |
682 | + c.Assert(err, check.IsNil) |
683 | + |
684 | + // Check for 500 status code and other error fields |
685 | + c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError)) |
686 | + c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError) |
687 | + c.Assert(resp.Type, check.Equals, "error") |
688 | + c.Assert(resp.Result["kind"], check.Equals, "internal-error") |
689 | + c.Assert(resp.Result["message"], check.Equals, "Can't write configuration file") |
690 | +} |
691 | + |
692 | +func (s *S) TestInvalidJSON(c *check.C) { |
693 | + // Test an invalid JSON |
694 | + os.Setenv("SNAP_DATA", "/tmp") |
695 | + req, err := http.NewRequest(http.MethodPost, "/v1/configuration", strings.NewReader("not a JSON content")) |
696 | + c.Assert(err, check.IsNil) |
697 | + |
698 | + rec := httptest.NewRecorder() |
699 | + cmd := newMockServiceCommand() |
700 | + |
701 | + postConfiguration(cmd, rec, req) |
702 | + |
703 | + c.Assert(rec.Code, check.Equals, http.StatusInternalServerError) |
704 | + |
705 | + body, err := ioutil.ReadAll(rec.Body) |
706 | + c.Assert(err, check.IsNil) |
707 | + |
708 | + // Parse the returned JSON |
709 | + resp := serviceResponse{} |
710 | + err = json.Unmarshal(body, &resp) |
711 | + c.Assert(err, check.IsNil) |
712 | + |
713 | + // Check for 500 status code and other error fields |
714 | + c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError)) |
715 | + c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError) |
716 | + c.Assert(resp.Type, check.Equals, "error") |
717 | + c.Assert(resp.Result["kind"], check.Equals, "internal-error") |
718 | + c.Assert(resp.Result["message"], check.Equals, "Malformed request") |
719 | +} |
720 | + |
721 | +func (s *S) TestInvalidToken(c *check.C) { |
722 | + // Test a succesful configuration set |
723 | + // Values to be used in the config |
724 | + values := map[string]string{ |
725 | + "wifi.security": "wpa2", |
726 | + "wifi.ssid": "UbuntuAP", |
727 | + "wifi.security-passphrase": "12345678", |
728 | + "bad.token": "xyz", |
729 | + } |
730 | + |
731 | + // Convert the map into JSON |
732 | + args, err := json.Marshal(values) |
733 | + c.Assert(err, check.IsNil) |
734 | + |
735 | + req, err := http.NewRequest(http.MethodPost, "/v1/configuration", bytes.NewReader(args)) |
736 | + c.Assert(err, check.IsNil) |
737 | + |
738 | + rec := httptest.NewRecorder() |
739 | + cmd := newMockServiceCommand() |
740 | + |
741 | + validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config")) |
742 | + c.Assert(validTokens, check.NotNil) |
743 | + c.Assert(err, check.IsNil) |
744 | + |
745 | + // Do the request |
746 | + postConfiguration(cmd, rec, req) |
747 | + |
748 | + c.Assert(rec.Code, check.Equals, http.StatusInternalServerError) |
749 | + |
750 | + // Read the result JSON |
751 | + body, err := ioutil.ReadAll(rec.Body) |
752 | + c.Assert(err, check.IsNil) |
753 | + |
754 | + // Parse the returned JSON |
755 | + resp := serviceResponse{} |
756 | + err = json.Unmarshal(body, &resp) |
757 | + c.Assert(err, check.IsNil) |
758 | + |
759 | + // Check for 500 status code and other succesful fields |
760 | + c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError)) |
761 | + c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError) |
762 | + c.Assert(resp.Type, check.Equals, "error") |
763 | +} |
764 | + |
765 | +func (s *S) TestChangeConfiguration(c *check.C) { |
766 | + // Values to be used in the config |
767 | + values := map[string]string{ |
768 | + "disabled": "0", |
769 | + "wifi.security": "wpa2", |
770 | + "wifi.ssid": "UbuntuAP", |
771 | + "wifi.security-passphrase": "12345678", |
772 | + } |
773 | + |
774 | + // Convert the map into JSON |
775 | + args, err := json.Marshal(values) |
776 | + c.Assert(err, check.IsNil) |
777 | + |
778 | + req, err := http.NewRequest(http.MethodPost, "/v1/configuration", bytes.NewReader(args)) |
779 | + c.Assert(err, check.IsNil) |
780 | + |
781 | + rec := httptest.NewRecorder() |
782 | + cmd := newMockServiceCommand() |
783 | + |
784 | + validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config")) |
785 | + c.Assert(validTokens, check.NotNil) |
786 | + c.Assert(err, check.IsNil) |
787 | + |
788 | + // Do the request |
789 | + postConfiguration(cmd, rec, req) |
790 | + |
791 | + c.Assert(rec.Code, check.Equals, http.StatusOK) |
792 | + |
793 | + // Read the result JSON |
794 | + body, err := ioutil.ReadAll(rec.Body) |
795 | + c.Assert(err, check.IsNil) |
796 | + |
797 | + // Parse the returned JSON |
798 | + resp := serviceResponse{} |
799 | + err = json.Unmarshal(body, &resp) |
800 | + c.Assert(err, check.IsNil) |
801 | + |
802 | + // Check for 200 status code and other succesful fields |
803 | + c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK)) |
804 | + c.Assert(resp.StatusCode, check.Equals, http.StatusOK) |
805 | + c.Assert(resp.Type, check.Equals, "sync") |
806 | + |
807 | + // Read the generated config and check that values were set |
808 | + config, err := ioutil.ReadFile(getConfigOnPath(os.Getenv("SNAP_DATA"))) |
809 | + c.Assert(err, check.IsNil) |
810 | + |
811 | + for key, value := range values { |
812 | + c.Assert(strings.Contains(string(config), |
813 | + convertKeyToStorageFormat(key)+"="+value+"\n"), |
814 | + check.Equals, true) |
815 | + } |
816 | + |
817 | + // As we've set 'disabled' to '0' above the AP should be active |
818 | + // now as the configuration post request will trigger an automatic |
819 | + // restart of the relevant background processes. |
820 | + c.Assert(cmd.s.ap.Running(), check.Equals, true) |
821 | + |
822 | + // Don't leave garbage in /tmp |
823 | + os.Remove(getConfigOnPath(os.Getenv("SNAP_DATA"))) |
824 | +} |
825 | + |
826 | +func (s *S) TestGetStatusDefaultOk(c *check.C) { |
827 | + req, err := http.NewRequest(http.MethodGet, "/v1/status", nil) |
828 | + c.Assert(err, check.IsNil) |
829 | + |
830 | + rec := httptest.NewRecorder() |
831 | + |
832 | + cmd := newMockServiceCommand() |
833 | + |
834 | + getStatus(cmd, rec, req) |
835 | + |
836 | + body, err := ioutil.ReadAll(rec.Body) |
837 | + c.Assert(err, check.IsNil) |
838 | + |
839 | + var resp serviceResponse |
840 | + err = json.Unmarshal(body, &resp) |
841 | + c.Assert(err, check.IsNil) |
842 | + |
843 | + c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK)) |
844 | + c.Assert(resp.StatusCode, check.Equals, http.StatusOK) |
845 | + c.Assert(resp.Type, check.Equals, "sync") |
846 | + |
847 | + c.Assert(resp.Result["ap.active"], check.Equals, "0") |
848 | +} |
849 | + |
850 | +func (s *S) TestGetStatusReturnsCorrectApStatus(c *check.C) { |
851 | + req, err := http.NewRequest(http.MethodGet, "/v1/status", nil) |
852 | + c.Assert(err, check.IsNil) |
853 | + |
854 | + rec := httptest.NewRecorder() |
855 | + |
856 | + cmd := newMockServiceCommand() |
857 | + cmd.s.ap.Start() |
858 | + |
859 | + getStatus(cmd, rec, req) |
860 | + |
861 | + body, err := ioutil.ReadAll(rec.Body) |
862 | + c.Assert(err, check.IsNil) |
863 | + |
864 | + var resp serviceResponse |
865 | + err = json.Unmarshal(body, &resp) |
866 | + c.Assert(err, check.IsNil) |
867 | + |
868 | + c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK)) |
869 | + c.Assert(resp.StatusCode, check.Equals, http.StatusOK) |
870 | + c.Assert(resp.Type, check.Equals, "sync") |
871 | + |
872 | + c.Assert(resp.Result["ap.active"], check.Equals, "1") |
873 | +} |
874 | diff --git a/cmd/service/background_process.go b/cmd/service/background_process.go |
875 | new file mode 100644 |
876 | index 0000000..4b1d350 |
877 | --- /dev/null |
878 | +++ b/cmd/service/background_process.go |
879 | @@ -0,0 +1,138 @@ |
880 | +// |
881 | +// Copyright (C) 2016 Canonical Ltd |
882 | +// |
883 | +// This program is free software: you can redistribute it and/or modify |
884 | +// it under the terms of the GNU General Public License version 3 as |
885 | +// published by the Free Software Foundation. |
886 | +// |
887 | +// This program is distributed in the hope that it will be useful, |
888 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
889 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
890 | +// GNU General Public License for more details. |
891 | +// |
892 | +// You should have received a copy of the GNU General Public License |
893 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
894 | + |
895 | +package main |
896 | + |
897 | +import ( |
898 | + "fmt" |
899 | + "os" |
900 | + "os/exec" |
901 | + "syscall" |
902 | + "time" |
903 | + |
904 | + "gopkg.in/tomb.v2" |
905 | +) |
906 | + |
907 | +type backgroundProcessImpl struct { |
908 | + path string |
909 | + args []string |
910 | + command *exec.Cmd |
911 | + tomb *tomb.Tomb |
912 | +} |
913 | + |
914 | +// BackgroundProcess provides control over a process running in the |
915 | +// background. |
916 | +type BackgroundProcess interface { |
917 | + Start() error |
918 | + Stop() error |
919 | + Restart() error |
920 | + Running() bool |
921 | +} |
922 | + |
923 | +func NewBackgroundProcess(path string, args ...string) (BackgroundProcess, error) { |
924 | + p := &backgroundProcessImpl{ |
925 | + path: path, |
926 | + args: args, |
927 | + command: nil, |
928 | + } |
929 | + if p == nil { |
930 | + return nil, fmt.Errorf("Failed to create background process") |
931 | + } |
932 | + |
933 | + return p, nil |
934 | +} |
935 | + |
936 | +func (p *backgroundProcessImpl) Start() error { |
937 | + if p.Running() { |
938 | + return fmt.Errorf("Background process is already running") |
939 | + } |
940 | + |
941 | + p.command = exec.Command(p.path, p.args...) |
942 | + if p.command == nil { |
943 | + return fmt.Errorf("Failed to create background process") |
944 | + } |
945 | + |
946 | + // Forward output to regular stdout/stderr |
947 | + p.command.Stdout = os.Stdout |
948 | + p.command.Stderr = os.Stderr |
949 | + |
950 | + // Create a new process group |
951 | + p.command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} |
952 | + |
953 | + // We need to recreate the tomb here everytime as otherwise |
954 | + // it will not cleanup its state from the last time. |
955 | + p.tomb = &tomb.Tomb{} |
956 | + |
957 | + c := make(chan int) |
958 | + p.tomb.Go(func() error { |
959 | + err := p.command.Start() |
960 | + if err != nil { |
961 | + fmt.Printf("Failed to execute process for binary '%s'", p.path) |
962 | + return err |
963 | + } |
964 | + c <- 1 |
965 | + p.command.Wait() |
966 | + p.command = nil |
967 | + return nil |
968 | + }) |
969 | + |
970 | + // Wait until the process is really started |
971 | + _ = <-c |
972 | + |
973 | + return nil |
974 | +} |
975 | + |
976 | +func (p *backgroundProcessImpl) Restart() error { |
977 | + if err := p.Stop(); err != nil { |
978 | + return err |
979 | + } |
980 | + if err := p.Start(); err != nil { |
981 | + return err |
982 | + } |
983 | + return nil |
984 | +} |
985 | + |
986 | +func (p *backgroundProcessImpl) killProcess(signal syscall.Signal) error { |
987 | + if p == nil || p.command == nil { |
988 | + return fmt.Errorf("Process is not running") |
989 | + } |
990 | + // We need to kill the whole process group as otherwise some |
991 | + // child processes are still around |
992 | + pgid, err := syscall.Getpgid(p.command.Process.Pid) |
993 | + if err == nil { |
994 | + syscall.Kill(-pgid, signal) |
995 | + } else { |
996 | + syscall.Kill(p.command.Process.Pid, signal) |
997 | + } |
998 | + return nil |
999 | +} |
1000 | + |
1001 | +func (p *backgroundProcessImpl) Stop() error { |
1002 | + if !p.Running() { |
1003 | + return nil |
1004 | + } |
1005 | + timer := time.AfterFunc(10*time.Second, func() { |
1006 | + p.killProcess(syscall.SIGKILL) |
1007 | + }) |
1008 | + p.killProcess(syscall.SIGTERM) |
1009 | + p.tomb.Kill(nil) |
1010 | + p.tomb.Wait() |
1011 | + timer.Stop() |
1012 | + return nil |
1013 | +} |
1014 | + |
1015 | +func (p *backgroundProcessImpl) Running() bool { |
1016 | + return p.command != nil |
1017 | +} |
1018 | diff --git a/cmd/service/background_process_test.go b/cmd/service/background_process_test.go |
1019 | new file mode 100644 |
1020 | index 0000000..5ae9cd7 |
1021 | --- /dev/null |
1022 | +++ b/cmd/service/background_process_test.go |
1023 | @@ -0,0 +1,36 @@ |
1024 | +// |
1025 | +// Copyright (C) 2016 Canonical Ltd |
1026 | +// |
1027 | +// This program is free software: you can redistribute it and/or modify |
1028 | +// it under the terms of the GNU General Public License version 3 as |
1029 | +// published by the Free Software Foundation. |
1030 | +// |
1031 | +// This program is distributed in the hope that it will be useful, |
1032 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
1033 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1034 | +// GNU General Public License for more details. |
1035 | +// |
1036 | +// You should have received a copy of the GNU General Public License |
1037 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
1038 | + |
1039 | +package main |
1040 | + |
1041 | +import ( |
1042 | + "fmt" |
1043 | + |
1044 | + "gopkg.in/check.v1" |
1045 | +) |
1046 | + |
1047 | +func (s *S) TestBackgroundProcessStartStop(c *check.C) { |
1048 | + p, err := NewBackgroundProcess("/bin/sleep", "1000") |
1049 | + c.Assert(err, check.IsNil) |
1050 | + c.Assert(p.Running(), check.Equals, false) |
1051 | + c.Assert(p.Start(), check.IsNil) |
1052 | + c.Assert(p.Running(), check.Equals, true) |
1053 | + c.Assert(p.Stop(), check.IsNil) |
1054 | + c.Assert(p.Running(), check.Equals, false) |
1055 | + c.Assert(p.Restart(), check.IsNil) |
1056 | + c.Assert(p.Running(), check.Equals, true) |
1057 | + c.Assert(p.Start(), check.DeepEquals, fmt.Errorf("Background process is already running")) |
1058 | + c.Assert(p.Running(), check.Equals, true) |
1059 | +} |
1060 | diff --git a/cmd/service/config.go b/cmd/service/config.go |
1061 | new file mode 100644 |
1062 | index 0000000..bbc047e |
1063 | --- /dev/null |
1064 | +++ b/cmd/service/config.go |
1065 | @@ -0,0 +1,140 @@ |
1066 | +// |
1067 | +// Copyright (C) 2016 Canonical Ltd |
1068 | +// |
1069 | +// This program is free software: you can redistribute it and/or modify |
1070 | +// it under the terms of the GNU General Public License version 3 as |
1071 | +// published by the Free Software Foundation. |
1072 | +// |
1073 | +// This program is distributed in the hope that it will be useful, |
1074 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
1075 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1076 | +// GNU General Public License for more details. |
1077 | +// |
1078 | +// You should have received a copy of the GNU General Public License |
1079 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
1080 | + |
1081 | +package main |
1082 | + |
1083 | +import ( |
1084 | + "bufio" |
1085 | + "fmt" |
1086 | + "os" |
1087 | + "path/filepath" |
1088 | + "regexp" |
1089 | + "strings" |
1090 | +) |
1091 | + |
1092 | +func getConfigOnPath(path string) string { |
1093 | + return filepath.Join(path, "config") |
1094 | +} |
1095 | + |
1096 | +// Array of paths where the config file can be found. |
1097 | +// The first one is readonly, the others are writable |
1098 | +// they are readed in order and the configuration is merged |
1099 | +var configurationPaths = []string{ |
1100 | + filepath.Join(os.Getenv("SNAP"), "conf", "default-config"), |
1101 | + getConfigOnPath(os.Getenv("SNAP_DATA")), |
1102 | + getConfigOnPath(os.Getenv("SNAP_USER_DATA"))} |
1103 | + |
1104 | +// Convert eg. WIFI_OPERATION_MODE to wifi.operation-mode |
1105 | +func convertKeyToRepresentationFormat(key string) string { |
1106 | + newKey := strings.ToLower(key) |
1107 | + newKey = strings.Replace(newKey, "_", ".", 1) |
1108 | + return strings.Replace(newKey, "_", "-", -1) |
1109 | +} |
1110 | + |
1111 | +func convertKeyToStorageFormat(key string) string { |
1112 | + // Convert eg. wifi.operation-mode to WIFI_OPERATION_MODE |
1113 | + newKey := strings.ToUpper(key) |
1114 | + newKey = strings.Replace(newKey, ".", "_", -1) |
1115 | + return strings.Replace(newKey, "-", "_", -1) |
1116 | +} |
1117 | + |
1118 | +func readConfigurationFile(filePath string, config map[string]string) (err error) { |
1119 | + file, err := os.Open(filePath) |
1120 | + if err != nil { |
1121 | + return nil |
1122 | + } |
1123 | + |
1124 | + defer file.Close() |
1125 | + |
1126 | + for scanner := bufio.NewScanner(file); scanner.Scan(); { |
1127 | + // Ignore all empty or commented lines |
1128 | + if line := scanner.Text(); len(line) != 0 && line[0] != '#' { |
1129 | + // Line must be in the KEY=VALUE format |
1130 | + if parts := strings.Split(line, "="); len(parts) == 2 { |
1131 | + value := unescapeTextByShell(parts[1]) |
1132 | + config[convertKeyToRepresentationFormat(parts[0])] = value |
1133 | + } |
1134 | + } |
1135 | + } |
1136 | + |
1137 | + return nil |
1138 | +} |
1139 | + |
1140 | +func readConfiguration(paths []string, config map[string]string) (err error) { |
1141 | + for _, location := range paths { |
1142 | + if readConfigurationFile(location, config) != nil { |
1143 | + return fmt.Errorf("Failed to read configuration file '%s'", location) |
1144 | + } |
1145 | + } |
1146 | + |
1147 | + return nil |
1148 | +} |
1149 | + |
1150 | +// Escape shell special characters, avoid injection |
1151 | +// eg. SSID set to "My AP$(nc -lp 2323 -e /bin/sh)" |
1152 | +// to get a root shell |
1153 | +func escapeTextForShell(input string) string { |
1154 | + if strings.ContainsAny(input, "\\\"'`$\n\t #") { |
1155 | + input = strings.Replace(input, `\`, `\\`, -1) |
1156 | + input = strings.Replace(input, `"`, `\"`, -1) |
1157 | + input = strings.Replace(input, "`", "\\`", -1) |
1158 | + input = strings.Replace(input, `$`, `\$`, -1) |
1159 | + |
1160 | + input = `"` + input + `"` |
1161 | + } |
1162 | + return input |
1163 | +} |
1164 | + |
1165 | +// Do the reverse of escapeTextForShell() here |
1166 | +// strip any \ followed by \$`" |
1167 | +func unescapeTextByShell(input string) string { |
1168 | + input = strings.Trim(input, `"'`) |
1169 | + if strings.ContainsAny(input, "\\") { |
1170 | + re := regexp.MustCompile("\\\\([\\\\$\\`\\\"])") |
1171 | + input = re.ReplaceAllString(input, "$1") |
1172 | + } |
1173 | + return input |
1174 | +} |
1175 | + |
1176 | +func loadValidTokens(path string) (map[string]bool, error) { |
1177 | + def, err := os.Open(path) |
1178 | + if err != nil { |
1179 | + return nil, err |
1180 | + } |
1181 | + defer def.Close() |
1182 | + |
1183 | + tokens := map[string]bool{} |
1184 | + |
1185 | + scanner := bufio.NewScanner(def) |
1186 | + for scanner.Scan() { |
1187 | + line := scanner.Text() |
1188 | + |
1189 | + // Skip empty lines and comments |
1190 | + if len(line) == 0 || line[0] == '#' { |
1191 | + continue |
1192 | + } |
1193 | + |
1194 | + // Get the substring before the '=' |
1195 | + if eq := strings.IndexRune(line, '='); eq > 0 { |
1196 | + // Add the token to the whitelist, converted in our format |
1197 | + tokens[convertKeyToRepresentationFormat(line[:eq])] = true |
1198 | + } |
1199 | + } |
1200 | + if err := scanner.Err(); err != nil { |
1201 | + return nil, err |
1202 | + } |
1203 | + |
1204 | + return tokens, nil |
1205 | +} |
1206 | diff --git a/cmd/service/config_test.go b/cmd/service/config_test.go |
1207 | new file mode 100644 |
1208 | index 0000000..9f59ea9 |
1209 | --- /dev/null |
1210 | +++ b/cmd/service/config_test.go |
1211 | @@ -0,0 +1,66 @@ |
1212 | +// |
1213 | +// Copyright (C) 2016 Canonical Ltd |
1214 | +// |
1215 | +// This program is free software: you can redistribute it and/or modify |
1216 | +// it under the terms of the GNU General Public License version 3 as |
1217 | +// published by the Free Software Foundation. |
1218 | +// |
1219 | +// This program is distributed in the hope that it will be useful, |
1220 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
1221 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1222 | +// GNU General Public License for more details. |
1223 | +// |
1224 | +// You should have received a copy of the GNU General Public License |
1225 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
1226 | + |
1227 | +package main |
1228 | + |
1229 | +import ( |
1230 | + "gopkg.in/check.v1" |
1231 | +) |
1232 | + |
1233 | +// Test the config file path append routine |
1234 | +func (s *S) TestPath(c *check.C) { |
1235 | + c.Assert(getConfigOnPath("/test"), check.Equals, "/test/config") |
1236 | +} |
1237 | + |
1238 | +// List of tokens to be translated |
1239 | +var cfgKeys = [...][2]string{ |
1240 | + {"DISABLED", "disabled"}, |
1241 | + {"WIFI_SSID", "wifi.ssid"}, |
1242 | + {"WIFI_INTERFACE", "wifi.interface"}, |
1243 | + {"WIFI_INTERFACE_MODE", "wifi.interface-mode"}, |
1244 | + {"DHCP_RANGE_START", "dhcp.range-start"}, |
1245 | + {"MYTOKEN", "mytoken"}, |
1246 | + {"CFG_TOKEN", "cfg.token"}, |
1247 | + {"MY_TOKEN$", "my.token$"}, |
1248 | +} |
1249 | + |
1250 | +// Test token conversion from internal format |
1251 | +func (s *S) TestConvertKeyToRepresentationFormat(c *check.C) { |
1252 | + for _, st := range cfgKeys { |
1253 | + c.Assert(convertKeyToRepresentationFormat(st[0]), check.Equals, st[1]) |
1254 | + } |
1255 | +} |
1256 | + |
1257 | +// Test token conversion to internal format |
1258 | +func (s *S) TestConvertKeyToStorageFormat(c *check.C) { |
1259 | + for _, st := range cfgKeys { |
1260 | + c.Assert(convertKeyToStorageFormat(st[1]), check.Equals, st[0]) |
1261 | + } |
1262 | +} |
1263 | + |
1264 | +// List of malicious tokens which needs to be escaped |
1265 | +func (s *S) TestEscapeShell(c *check.C) { |
1266 | + cmds := [...][2]string{ |
1267 | + {"my_ap", "my_ap"}, |
1268 | + {`my ap`, `"my ap"`}, |
1269 | + {`my "ap"`, `"my \"ap\""`}, |
1270 | + {`$(ps ax)`, `"\$(ps ax)"`}, |
1271 | + {"`ls /`", "\"\\`ls /\\`\""}, |
1272 | + {`c:\dir`, `"c:\\dir"`}, |
1273 | + } |
1274 | + for _, st := range cmds { |
1275 | + c.Assert(escapeTextForShell(st[0]), check.Equals, st[1]) |
1276 | + } |
1277 | +} |
1278 | diff --git a/cmd/service/main.go b/cmd/service/main.go |
1279 | index 0fd82e7..ae5e679 100644 |
1280 | --- a/cmd/service/main.go |
1281 | +++ b/cmd/service/main.go |
1282 | @@ -1,4 +1,3 @@ |
1283 | -// |
1284 | // Copyright (C) 2016 Canonical Ltd |
1285 | // |
1286 | // This program is free software: you can redistribute it and/or modify |
1287 | @@ -16,277 +15,23 @@ |
1288 | package main |
1289 | |
1290 | import ( |
1291 | - "bufio" |
1292 | - "encoding/json" |
1293 | - "fmt" |
1294 | - "io/ioutil" |
1295 | "log" |
1296 | - "net/http" |
1297 | "os" |
1298 | - "path/filepath" |
1299 | - "regexp" |
1300 | - "strconv" |
1301 | - "strings" |
1302 | - |
1303 | - "github.com/gorilla/mux" |
1304 | -) |
1305 | - |
1306 | -/* JSON message format, as described here: |
1307 | -{ |
1308 | - "result": { |
1309 | - "key" : "val" |
1310 | - }, |
1311 | - "status": "OK", |
1312 | - "status-code": 200, |
1313 | - "type": "sync" |
1314 | -} |
1315 | -*/ |
1316 | - |
1317 | -type serviceResponse struct { |
1318 | - Result map[string]string `json:"result"` |
1319 | - Status string `json:"status"` |
1320 | - StatusCode int `json:"status-code"` |
1321 | - Type string `json:"type"` |
1322 | -} |
1323 | - |
1324 | -func makeErrorResponse(code int, message, kind string) *serviceResponse { |
1325 | - return &serviceResponse{ |
1326 | - Type: "error", |
1327 | - Status: http.StatusText(code), |
1328 | - StatusCode: code, |
1329 | - Result: map[string]string{ |
1330 | - "message": message, |
1331 | - "kind": kind, |
1332 | - }, |
1333 | - } |
1334 | -} |
1335 | - |
1336 | -func makeResponse(status int, result map[string]string) *serviceResponse { |
1337 | - resp := &serviceResponse{ |
1338 | - Type: "sync", |
1339 | - Status: http.StatusText(status), |
1340 | - StatusCode: status, |
1341 | - Result: result, |
1342 | - } |
1343 | - |
1344 | - if resp.Result == nil { |
1345 | - resp.Result = make(map[string]string) |
1346 | - } |
1347 | - |
1348 | - return resp |
1349 | -} |
1350 | - |
1351 | -func sendHTTPResponse(writer http.ResponseWriter, response *serviceResponse) { |
1352 | - writer.WriteHeader(response.StatusCode) |
1353 | - data, _ := json.Marshal(response) |
1354 | - fmt.Fprintln(writer, string(data)) |
1355 | -} |
1356 | - |
1357 | -func getConfigOnPath(confPath string) string { |
1358 | - return filepath.Join(confPath, "config") |
1359 | -} |
1360 | - |
1361 | -// Array of paths where the config file can be found. |
1362 | -// The first one is readonly, the others are writable |
1363 | -// they are readed in order and the configuration is merged |
1364 | -var configurationPaths = []string{ |
1365 | - filepath.Join(os.Getenv("SNAP"), "conf", "default-config"), |
1366 | - getConfigOnPath(os.Getenv("SNAP_DATA")), |
1367 | - getConfigOnPath(os.Getenv("SNAP_USER_DATA"))} |
1368 | - |
1369 | -const ( |
1370 | - servicePort = 5005 |
1371 | - configurationV1Uri = "/v1/configuration" |
1372 | + "os/signal" |
1373 | + "syscall" |
1374 | ) |
1375 | |
1376 | -var validTokens map[string]bool |
1377 | - |
1378 | -func loadValidTokens(path string) (map[string]bool, error) { |
1379 | - def, err := os.Open(path) |
1380 | - if err != nil { |
1381 | - return nil, err |
1382 | - } |
1383 | - defer def.Close() |
1384 | - |
1385 | - tokens := map[string]bool{} |
1386 | - |
1387 | - scanner := bufio.NewScanner(def) |
1388 | - for scanner.Scan() { |
1389 | - line := scanner.Text() |
1390 | - |
1391 | - // Skip empty lines and comments |
1392 | - if len(line) == 0 || line[0] == '#' { |
1393 | - continue |
1394 | - } |
1395 | - |
1396 | - // Get the substring before the '=' |
1397 | - if eq := strings.IndexRune(line, '='); eq > 0 { |
1398 | - // Add the token to the whitelist, converted in our format |
1399 | - tokens[convertKeyToRepresentationFormat(line[:eq])] = true |
1400 | - } |
1401 | - } |
1402 | - if err := scanner.Err(); err != nil { |
1403 | - return nil, err |
1404 | - } |
1405 | - |
1406 | - return tokens, nil |
1407 | -} |
1408 | - |
1409 | func main() { |
1410 | - r := mux.NewRouter().StrictSlash(true) |
1411 | + s := &service{} |
1412 | |
1413 | - var err error |
1414 | - if validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "conf", "default-config")); err != nil { |
1415 | - log.Println("Failed to read default configuration:", err) |
1416 | - } |
1417 | + c := make(chan os.Signal, 1) |
1418 | + signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) |
1419 | + go func(s *service) { |
1420 | + _ = <-c |
1421 | + s.Shutdown() |
1422 | + }(s) |
1423 | |
1424 | - r.HandleFunc(configurationV1Uri, getConfiguration).Methods(http.MethodGet) |
1425 | - r.HandleFunc(configurationV1Uri, changeConfiguration).Methods(http.MethodPost) |
1426 | - |
1427 | - log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(servicePort), r)) |
1428 | -} |
1429 | - |
1430 | -// Convert eg. WIFI_OPERATION_MODE to wifi.operation-mode |
1431 | -func convertKeyToRepresentationFormat(key string) string { |
1432 | - newKey := strings.ToLower(key) |
1433 | - newKey = strings.Replace(newKey, "_", ".", 1) |
1434 | - return strings.Replace(newKey, "_", "-", -1) |
1435 | -} |
1436 | - |
1437 | -func convertKeyToStorageFormat(key string) string { |
1438 | - // Convert eg. wifi.operation-mode to WIFI_OPERATION_MODE |
1439 | - newKey := strings.ToUpper(key) |
1440 | - newKey = strings.Replace(newKey, ".", "_", -1) |
1441 | - return strings.Replace(newKey, "-", "_", -1) |
1442 | -} |
1443 | - |
1444 | -func readConfigurationFile(filePath string, config map[string]string) (err error) { |
1445 | - file, err := os.Open(filePath) |
1446 | - if err != nil { |
1447 | - return nil |
1448 | - } |
1449 | - |
1450 | - defer file.Close() |
1451 | - |
1452 | - for scanner := bufio.NewScanner(file); scanner.Scan(); { |
1453 | - // Ignore all empty or commented lines |
1454 | - if line := scanner.Text(); len(line) != 0 && line[0] != '#' { |
1455 | - // Line must be in the KEY=VALUE format |
1456 | - if parts := strings.Split(line, "="); len(parts) == 2 { |
1457 | - key := convertKeyToRepresentationFormat(parts[0]) |
1458 | - value := unescapeTextByShell(parts[1]) |
1459 | - config[key] = value |
1460 | - } |
1461 | - } |
1462 | - } |
1463 | - |
1464 | - return nil |
1465 | -} |
1466 | - |
1467 | -func readConfiguration(paths []string, config map[string]string) (err error) { |
1468 | - for _, location := range paths { |
1469 | - if readConfigurationFile(location, config) != nil { |
1470 | - return fmt.Errorf(`Failed to read configuration file "%s"`, location) |
1471 | - } |
1472 | + if err := s.Run(); err != nil { |
1473 | + log.Fatalf("Failed to start service: %s", err) |
1474 | } |
1475 | - |
1476 | - return nil |
1477 | -} |
1478 | - |
1479 | -func getConfiguration(writer http.ResponseWriter, request *http.Request) { |
1480 | - config := make(map[string]string) |
1481 | - if err := readConfiguration(configurationPaths, config); err == nil { |
1482 | - sendHTTPResponse(writer, makeResponse(http.StatusOK, config)) |
1483 | - } else { |
1484 | - log.Println("Read configuration failed:", err) |
1485 | - errResponse := makeErrorResponse(http.StatusInternalServerError, "Failed to read configuration data", "internal-error") |
1486 | - sendHTTPResponse(writer, errResponse) |
1487 | - } |
1488 | -} |
1489 | - |
1490 | -// Escape shell special characters, avoid injection |
1491 | -// eg. SSID set to "My AP$(nc -lp 2323 -e /bin/sh)" |
1492 | -// to get a root shell |
1493 | -func escapeTextForShell(input string) string { |
1494 | - if strings.ContainsAny(input, "\\\"'`$\n\t #") { |
1495 | - input = strings.Replace(input, `\`, `\\`, -1) |
1496 | - input = strings.Replace(input, `"`, `\"`, -1) |
1497 | - input = strings.Replace(input, "`", "\\`", -1) |
1498 | - input = strings.Replace(input, `$`, `\$`, -1) |
1499 | - |
1500 | - input = `"` + input + `"` |
1501 | - } |
1502 | - return input |
1503 | -} |
1504 | - |
1505 | -// Do the reverse of escapeTextForShell() here |
1506 | -// strip any \ followed by \$`" |
1507 | -func unescapeTextByShell(input string) string { |
1508 | - input = strings.Trim(input, `"'`) |
1509 | - if strings.ContainsAny(input, "\\") { |
1510 | - re := regexp.MustCompile("\\\\([\\\\$\\`\\\"])") |
1511 | - input = re.ReplaceAllString(input, "$1") |
1512 | - } |
1513 | - return input |
1514 | -} |
1515 | - |
1516 | -func changeConfiguration(writer http.ResponseWriter, request *http.Request) { |
1517 | - path := getConfigOnPath(os.Getenv("SNAP_DATA")) |
1518 | - config := map[string]string{} |
1519 | - if readConfiguration([]string{path}, config) != nil { |
1520 | - errResponse := makeErrorResponse(http.StatusInternalServerError, |
1521 | - "Failed to read existing configuration file", "internal-error") |
1522 | - sendHTTPResponse(writer, errResponse) |
1523 | - return |
1524 | - } |
1525 | - |
1526 | - if validTokens == nil || len(validTokens) == 0 { |
1527 | - errResponse := makeErrorResponse(http.StatusInternalServerError, "No default configuration file available", "internal-error") |
1528 | - sendHTTPResponse(writer, errResponse) |
1529 | - return |
1530 | - } |
1531 | - |
1532 | - file, err := os.Create(path) |
1533 | - if err != nil { |
1534 | - log.Printf("Write to %q failed: %v\n", path, err) |
1535 | - errResponse := makeErrorResponse(http.StatusInternalServerError, "Can't write configuration file", "internal-error") |
1536 | - sendHTTPResponse(writer, errResponse) |
1537 | - return |
1538 | - } |
1539 | - defer file.Close() |
1540 | - |
1541 | - body, err := ioutil.ReadAll(request.Body) |
1542 | - if err != nil { |
1543 | - log.Println("Failed to process incoming configuration change request:", err) |
1544 | - errResponse := makeErrorResponse(http.StatusInternalServerError, "Error reading the request body", "internal-error") |
1545 | - sendHTTPResponse(writer, errResponse) |
1546 | - return |
1547 | - } |
1548 | - |
1549 | - var items map[string]string |
1550 | - if err = json.Unmarshal(body, &items); err != nil { |
1551 | - log.Println("Invalid input data", err) |
1552 | - errResponse := makeErrorResponse(http.StatusInternalServerError, "Malformed request", "internal-error") |
1553 | - sendHTTPResponse(writer, errResponse) |
1554 | - return |
1555 | - } |
1556 | - |
1557 | - // Add the items in the config, but only if all are in the whitelist |
1558 | - for key, value := range items { |
1559 | - if _, present := validTokens[key]; !present { |
1560 | - log.Println(`Invalid key "` + key + `": ignoring request`) |
1561 | - errResponse := makeErrorResponse(http.StatusInternalServerError, `Invalid key "`+key+`"`, "internal-error") |
1562 | - sendHTTPResponse(writer, errResponse) |
1563 | - return |
1564 | - } |
1565 | - config[key] = value |
1566 | - } |
1567 | - |
1568 | - for key, value := range config { |
1569 | - key = convertKeyToStorageFormat(key) |
1570 | - value = escapeTextForShell(value) |
1571 | - file.WriteString(fmt.Sprintf("%s=%s\n", key, value)) |
1572 | - } |
1573 | - |
1574 | - sendHTTPResponse(writer, makeResponse(http.StatusOK, nil)) |
1575 | } |
1576 | diff --git a/cmd/service/main_test.go b/cmd/service/main_test.go |
1577 | deleted file mode 100644 |
1578 | index d0fc6be..0000000 |
1579 | --- a/cmd/service/main_test.go |
1580 | +++ /dev/null |
1581 | @@ -1,298 +0,0 @@ |
1582 | -// |
1583 | -// Copyright (C) 2016 Canonical Ltd |
1584 | -// |
1585 | -// This program is free software: you can redistribute it and/or modify |
1586 | -// it under the terms of the GNU General Public License version 3 as |
1587 | -// published by the Free Software Foundation. |
1588 | -// |
1589 | -// This program is distributed in the hope that it will be useful, |
1590 | -// but WITHOUT ANY WARRANTY; without even the implied warranty of |
1591 | -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1592 | -// GNU General Public License for more details. |
1593 | -// |
1594 | -// You should have received a copy of the GNU General Public License |
1595 | -// along with this program. If not, see <http://www.gnu.org/licenses/>. |
1596 | - |
1597 | -package main |
1598 | - |
1599 | -import ( |
1600 | - "bytes" |
1601 | - "encoding/json" |
1602 | - "io/ioutil" |
1603 | - "net/http" |
1604 | - "net/http/httptest" |
1605 | - "os" |
1606 | - "path/filepath" |
1607 | - "strings" |
1608 | - "testing" |
1609 | - |
1610 | - "gopkg.in/check.v1" |
1611 | -) |
1612 | - |
1613 | -// gopkg.in/check.v1 stuff |
1614 | -func Test(t *testing.T) { check.TestingT(t) } |
1615 | - |
1616 | -type S struct{} |
1617 | - |
1618 | -var _ = check.Suite(&S{}) |
1619 | - |
1620 | -// Test the config file path append routine |
1621 | -func (s *S) TestPath(c *check.C) { |
1622 | - c.Assert(getConfigOnPath("/test"), check.Equals, "/test/config") |
1623 | -} |
1624 | - |
1625 | -// List of tokens to be translated |
1626 | -var cfgKeys = [...][2]string{ |
1627 | - {"DISABLED", "disabled"}, |
1628 | - {"WIFI_SSID", "wifi.ssid"}, |
1629 | - {"WIFI_INTERFACE", "wifi.interface"}, |
1630 | - {"WIFI_INTERFACE_MODE", "wifi.interface-mode"}, |
1631 | - {"DHCP_RANGE_START", "dhcp.range-start"}, |
1632 | - {"MYTOKEN", "mytoken"}, |
1633 | - {"CFG_TOKEN", "cfg.token"}, |
1634 | - {"MY_TOKEN$", "my.token$"}, |
1635 | -} |
1636 | - |
1637 | -// Test token conversion from internal format |
1638 | -func (s *S) TestConvertKeyToRepresentationFormat(c *check.C) { |
1639 | - for _, st := range cfgKeys { |
1640 | - c.Assert(convertKeyToRepresentationFormat(st[0]), check.Equals, st[1]) |
1641 | - } |
1642 | -} |
1643 | - |
1644 | -// Test token conversion to internal format |
1645 | -func (s *S) TestConvertKeyToStorageFormat(c *check.C) { |
1646 | - for _, st := range cfgKeys { |
1647 | - c.Assert(convertKeyToStorageFormat(st[1]), check.Equals, st[0]) |
1648 | - } |
1649 | -} |
1650 | - |
1651 | -// List of malicious tokens which needs to be escaped |
1652 | -func (s *S) TestEscapeShell(c *check.C) { |
1653 | - cmds := [...][2]string{ |
1654 | - {"my_ap", "my_ap"}, |
1655 | - {`my ap`, `"my ap"`}, |
1656 | - {`my "ap"`, `"my \"ap\""`}, |
1657 | - {`$(ps ax)`, `"\$(ps ax)"`}, |
1658 | - {"`ls /`", "\"\\`ls /\\`\""}, |
1659 | - {`c:\dir`, `"c:\\dir"`}, |
1660 | - } |
1661 | - for _, st := range cmds { |
1662 | - c.Assert(escapeTextForShell(st[0]), check.Equals, st[1]) |
1663 | - } |
1664 | -} |
1665 | - |
1666 | -func (s *S) TestGetConfiguration(c *check.C) { |
1667 | - // Check it we get a valid JSON as configuration |
1668 | - req, err := http.NewRequest(http.MethodGet, configurationV1Uri, nil) |
1669 | - c.Assert(err, check.IsNil) |
1670 | - |
1671 | - rec := httptest.NewRecorder() |
1672 | - |
1673 | - getConfiguration(rec, req) |
1674 | - |
1675 | - body, err := ioutil.ReadAll(rec.Body) |
1676 | - c.Assert(err, check.IsNil) |
1677 | - |
1678 | - // Parse the returned JSON |
1679 | - var resp serviceResponse |
1680 | - err = json.Unmarshal(body, &resp) |
1681 | - c.Assert(err, check.IsNil) |
1682 | - |
1683 | - // Check for 200 status code |
1684 | - c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK)) |
1685 | - c.Assert(resp.StatusCode, check.Equals, http.StatusOK) |
1686 | - c.Assert(resp.Type, check.Equals, "sync") |
1687 | -} |
1688 | - |
1689 | -func (s *S) TestNoDefaultConfiguration(c *check.C) { |
1690 | - oldsnap := os.Getenv("SNAP") |
1691 | - os.Setenv("SNAP", "/nodir") |
1692 | - os.Setenv("SNAP_DATA", "/tmp") |
1693 | - |
1694 | - req, err := http.NewRequest(http.MethodPost, configurationV1Uri, nil) |
1695 | - c.Assert(err, check.IsNil) |
1696 | - |
1697 | - rec := httptest.NewRecorder() |
1698 | - |
1699 | - validTokens = nil |
1700 | - |
1701 | - changeConfiguration(rec, req) |
1702 | - |
1703 | - c.Assert(rec.Code, check.Equals, http.StatusInternalServerError) |
1704 | - |
1705 | - body, err := ioutil.ReadAll(rec.Body) |
1706 | - c.Assert(err, check.IsNil) |
1707 | - |
1708 | - // Parse the returned JSON |
1709 | - var resp serviceResponse |
1710 | - err = json.Unmarshal(body, &resp) |
1711 | - c.Assert(err, check.IsNil) |
1712 | - |
1713 | - // Check for 500 status code and other error fields |
1714 | - c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError)) |
1715 | - c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError) |
1716 | - c.Assert(resp.Type, check.Equals, "error") |
1717 | - c.Assert(resp.Result["kind"], check.Equals, "internal-error") |
1718 | - c.Assert(resp.Result["message"], check.Equals, "No default configuration file available") |
1719 | - |
1720 | - os.Setenv("SNAP", oldsnap) |
1721 | -} |
1722 | - |
1723 | -func (s *S) TestWriteError(c *check.C) { |
1724 | - // Test a non writable path: |
1725 | - os.Setenv("SNAP_DATA", "/nodir") |
1726 | - |
1727 | - req, err := http.NewRequest(http.MethodPost, configurationV1Uri, nil) |
1728 | - c.Assert(err, check.IsNil) |
1729 | - |
1730 | - rec := httptest.NewRecorder() |
1731 | - |
1732 | - validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config")) |
1733 | - c.Assert(validTokens, check.NotNil) |
1734 | - c.Assert(err, check.IsNil) |
1735 | - |
1736 | - changeConfiguration(rec, req) |
1737 | - |
1738 | - c.Assert(rec.Code, check.Equals, http.StatusInternalServerError) |
1739 | - |
1740 | - body, err := ioutil.ReadAll(rec.Body) |
1741 | - c.Assert(err, check.IsNil) |
1742 | - |
1743 | - // Parse the returned JSON |
1744 | - var resp serviceResponse |
1745 | - err = json.Unmarshal(body, &resp) |
1746 | - c.Assert(err, check.IsNil) |
1747 | - |
1748 | - // Check for 500 status code and other error fields |
1749 | - c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError)) |
1750 | - c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError) |
1751 | - c.Assert(resp.Type, check.Equals, "error") |
1752 | - c.Assert(resp.Result["kind"], check.Equals, "internal-error") |
1753 | - c.Assert(resp.Result["message"], check.Equals, "Can't write configuration file") |
1754 | -} |
1755 | - |
1756 | -func (s *S) TestInvalidJSON(c *check.C) { |
1757 | - // Test an invalid JSON |
1758 | - os.Setenv("SNAP_DATA", "/tmp") |
1759 | - req, err := http.NewRequest(http.MethodPost, configurationV1Uri, strings.NewReader("not a JSON content")) |
1760 | - c.Assert(err, check.IsNil) |
1761 | - |
1762 | - rec := httptest.NewRecorder() |
1763 | - |
1764 | - changeConfiguration(rec, req) |
1765 | - |
1766 | - c.Assert(rec.Code, check.Equals, http.StatusInternalServerError) |
1767 | - |
1768 | - body, err := ioutil.ReadAll(rec.Body) |
1769 | - c.Assert(err, check.IsNil) |
1770 | - |
1771 | - // Parse the returned JSON |
1772 | - resp := serviceResponse{} |
1773 | - err = json.Unmarshal(body, &resp) |
1774 | - c.Assert(err, check.IsNil) |
1775 | - |
1776 | - // Check for 500 status code and other error fields |
1777 | - c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError)) |
1778 | - c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError) |
1779 | - c.Assert(resp.Type, check.Equals, "error") |
1780 | - c.Assert(resp.Result["kind"], check.Equals, "internal-error") |
1781 | - c.Assert(resp.Result["message"], check.Equals, "Malformed request") |
1782 | -} |
1783 | - |
1784 | -func (s *S) TestInvalidToken(c *check.C) { |
1785 | - // Test a succesful configuration set |
1786 | - // Values to be used in the config |
1787 | - values := map[string]string{ |
1788 | - "wifi.security": "wpa2", |
1789 | - "wifi.ssid": "UbuntuAP", |
1790 | - "wifi.security-passphrase": "12345678", |
1791 | - "bad.token": "xyz", |
1792 | - } |
1793 | - |
1794 | - // Convert the map into JSON |
1795 | - args, err := json.Marshal(values) |
1796 | - c.Assert(err, check.IsNil) |
1797 | - |
1798 | - req, err := http.NewRequest(http.MethodPost, configurationV1Uri, bytes.NewReader(args)) |
1799 | - c.Assert(err, check.IsNil) |
1800 | - |
1801 | - rec := httptest.NewRecorder() |
1802 | - |
1803 | - validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config")) |
1804 | - c.Assert(validTokens, check.NotNil) |
1805 | - c.Assert(err, check.IsNil) |
1806 | - |
1807 | - // Do the request |
1808 | - changeConfiguration(rec, req) |
1809 | - |
1810 | - c.Assert(rec.Code, check.Equals, http.StatusInternalServerError) |
1811 | - |
1812 | - // Read the result JSON |
1813 | - body, err := ioutil.ReadAll(rec.Body) |
1814 | - c.Assert(err, check.IsNil) |
1815 | - |
1816 | - // Parse the returned JSON |
1817 | - resp := serviceResponse{} |
1818 | - err = json.Unmarshal(body, &resp) |
1819 | - c.Assert(err, check.IsNil) |
1820 | - |
1821 | - // Check for 500 status code and other succesful fields |
1822 | - c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError)) |
1823 | - c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError) |
1824 | - c.Assert(resp.Type, check.Equals, "error") |
1825 | -} |
1826 | - |
1827 | -func (s *S) TestChangeConfiguration(c *check.C) { |
1828 | - // Values to be used in the config |
1829 | - values := map[string]string{ |
1830 | - "wifi.security": "wpa2", |
1831 | - "wifi.ssid": "UbuntuAP", |
1832 | - "wifi.security-passphrase": "12345678", |
1833 | - } |
1834 | - |
1835 | - // Convert the map into JSON |
1836 | - args, err := json.Marshal(values) |
1837 | - c.Assert(err, check.IsNil) |
1838 | - |
1839 | - req, err := http.NewRequest(http.MethodPost, configurationV1Uri, bytes.NewReader(args)) |
1840 | - c.Assert(err, check.IsNil) |
1841 | - |
1842 | - rec := httptest.NewRecorder() |
1843 | - |
1844 | - validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config")) |
1845 | - c.Assert(validTokens, check.NotNil) |
1846 | - c.Assert(err, check.IsNil) |
1847 | - |
1848 | - // Do the request |
1849 | - changeConfiguration(rec, req) |
1850 | - |
1851 | - c.Assert(rec.Code, check.Equals, http.StatusOK) |
1852 | - |
1853 | - // Read the result JSON |
1854 | - body, err := ioutil.ReadAll(rec.Body) |
1855 | - c.Assert(err, check.IsNil) |
1856 | - |
1857 | - // Parse the returned JSON |
1858 | - resp := serviceResponse{} |
1859 | - err = json.Unmarshal(body, &resp) |
1860 | - c.Assert(err, check.IsNil) |
1861 | - |
1862 | - // Check for 200 status code and other succesful fields |
1863 | - c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK)) |
1864 | - c.Assert(resp.StatusCode, check.Equals, http.StatusOK) |
1865 | - c.Assert(resp.Type, check.Equals, "sync") |
1866 | - |
1867 | - // Read the generated config and check that values were set |
1868 | - config, err := ioutil.ReadFile(getConfigOnPath(os.Getenv("SNAP_DATA"))) |
1869 | - c.Assert(err, check.IsNil) |
1870 | - |
1871 | - for key, value := range values { |
1872 | - c.Assert(strings.Contains(string(config), |
1873 | - convertKeyToStorageFormat(key)+"="+value+"\n"), |
1874 | - check.Equals, true) |
1875 | - } |
1876 | - |
1877 | - // Don't leave garbage in /tmp |
1878 | - os.Remove(getConfigOnPath(os.Getenv("SNAP_DATA"))) |
1879 | -} |
1880 | diff --git a/cmd/service/response.go b/cmd/service/response.go |
1881 | new file mode 100644 |
1882 | index 0000000..cc8a558 |
1883 | --- /dev/null |
1884 | +++ b/cmd/service/response.go |
1885 | @@ -0,0 +1,73 @@ |
1886 | +// |
1887 | +// Copyright (C) 2016 Canonical Ltd |
1888 | +// |
1889 | +// This program is free software: you can redistribute it and/or modify |
1890 | +// it under the terms of the GNU General Public License version 3 as |
1891 | +// published by the Free Software Foundation. |
1892 | +// |
1893 | +// This program is distributed in the hope that it will be useful, |
1894 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
1895 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1896 | +// GNU General Public License for more details. |
1897 | +// |
1898 | +// You should have received a copy of the GNU General Public License |
1899 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
1900 | + |
1901 | +package main |
1902 | + |
1903 | +import ( |
1904 | + "encoding/json" |
1905 | + "fmt" |
1906 | + "net/http" |
1907 | +) |
1908 | + |
1909 | +/* JSON message format, as described here: |
1910 | +{ |
1911 | + "result": { |
1912 | + "key" : "val" |
1913 | + }, |
1914 | + "status": "OK", |
1915 | + "status-code": 200, |
1916 | + "type": "sync" |
1917 | +} |
1918 | +*/ |
1919 | + |
1920 | +type serviceResponse struct { |
1921 | + Result map[string]string `json:"result"` |
1922 | + Status string `json:"status"` |
1923 | + StatusCode int `json:"status-code"` |
1924 | + Type string `json:"type"` |
1925 | +} |
1926 | + |
1927 | +func makeErrorResponse(code int, message, kind string) *serviceResponse { |
1928 | + return &serviceResponse{ |
1929 | + Type: "error", |
1930 | + Status: http.StatusText(code), |
1931 | + StatusCode: code, |
1932 | + Result: map[string]string{ |
1933 | + "message": message, |
1934 | + "kind": kind, |
1935 | + }, |
1936 | + } |
1937 | +} |
1938 | + |
1939 | +func makeResponse(status int, result map[string]string) *serviceResponse { |
1940 | + resp := &serviceResponse{ |
1941 | + Type: "sync", |
1942 | + Status: http.StatusText(status), |
1943 | + StatusCode: status, |
1944 | + Result: result, |
1945 | + } |
1946 | + |
1947 | + if resp.Result == nil { |
1948 | + resp.Result = make(map[string]string) |
1949 | + } |
1950 | + |
1951 | + return resp |
1952 | +} |
1953 | + |
1954 | +func sendHTTPResponse(writer http.ResponseWriter, response *serviceResponse) { |
1955 | + writer.WriteHeader(response.StatusCode) |
1956 | + data, _ := json.Marshal(response) |
1957 | + fmt.Fprintln(writer, string(data)) |
1958 | +} |
1959 | diff --git a/cmd/service/response_test.go b/cmd/service/response_test.go |
1960 | new file mode 100644 |
1961 | index 0000000..07d78f3 |
1962 | --- /dev/null |
1963 | +++ b/cmd/service/response_test.go |
1964 | @@ -0,0 +1,47 @@ |
1965 | +// |
1966 | +// Copyright (C) 2016 Canonical Ltd |
1967 | +// |
1968 | +// This program is free software: you can redistribute it and/or modify |
1969 | +// it under the terms of the GNU General Public License version 3 as |
1970 | +// published by the Free Software Foundation. |
1971 | +// |
1972 | +// This program is distributed in the hope that it will be useful, |
1973 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
1974 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1975 | +// GNU General Public License for more details. |
1976 | +// |
1977 | +// You should have received a copy of the GNU General Public License |
1978 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
1979 | + |
1980 | +package main |
1981 | + |
1982 | +import ( |
1983 | + "gopkg.in/check.v1" |
1984 | + "net/http" |
1985 | +) |
1986 | + |
1987 | +func (s *S) TestMakeErrorResponse(c *check.C) { |
1988 | + resp := makeErrorResponse(http.StatusInternalServerError, "my error message", "internal-error") |
1989 | + c.Assert(resp.Result, check.DeepEquals, map[string]string{ |
1990 | + "message": "my error message", |
1991 | + "kind": "internal-error", |
1992 | + }) |
1993 | + c.Assert(resp.Status, check.Equals, "Internal Server Error") |
1994 | + c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError) |
1995 | + c.Assert(resp.Type, check.Equals, "error") |
1996 | +} |
1997 | + |
1998 | +func (s *S) TestMakeResponse(c *check.C) { |
1999 | + resp := makeResponse(http.StatusOK, nil) |
2000 | + c.Assert(resp.Result, check.DeepEquals, map[string]string{}) |
2001 | + c.Assert(resp.Status, check.Equals, "OK") |
2002 | + c.Assert(resp.StatusCode, check.Equals, http.StatusOK) |
2003 | + c.Assert(resp.Type, check.Equals, "sync") |
2004 | + |
2005 | + data := map[string]string{"foo": "bar"} |
2006 | + resp = makeResponse(http.StatusOK, data) |
2007 | + c.Assert(resp.Result, check.DeepEquals, data) |
2008 | + c.Assert(resp.Status, check.Equals, "OK") |
2009 | + c.Assert(resp.StatusCode, check.Equals, http.StatusOK) |
2010 | + c.Assert(resp.Type, check.Equals, "sync") |
2011 | +} |
2012 | diff --git a/cmd/service/service.go b/cmd/service/service.go |
2013 | new file mode 100644 |
2014 | index 0000000..0ee04a8 |
2015 | --- /dev/null |
2016 | +++ b/cmd/service/service.go |
2017 | @@ -0,0 +1,159 @@ |
2018 | +// |
2019 | +// Copyright (C) 2016 Canonical Ltd |
2020 | +// |
2021 | +// This program is free software: you can redistribute it and/or modify |
2022 | +// it under the terms of the GNU General Public License version 3 as |
2023 | +// published by the Free Software Foundation. |
2024 | +// |
2025 | +// This program is distributed in the hope that it will be useful, |
2026 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
2027 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2028 | +// GNU General Public License for more details. |
2029 | +// |
2030 | +// You should have received a copy of the GNU General Public License |
2031 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
2032 | + |
2033 | +package main |
2034 | + |
2035 | +import ( |
2036 | + "fmt" |
2037 | + "github.com/gorilla/mux" |
2038 | + "log" |
2039 | + "net" |
2040 | + "net/http" |
2041 | + "os" |
2042 | + "path" |
2043 | + "path/filepath" |
2044 | + "time" |
2045 | + |
2046 | + "gopkg.in/tomb.v2" |
2047 | +) |
2048 | + |
2049 | +const ( |
2050 | + serviceAddress = "127.0.0.1" |
2051 | + servicePort = 5005 |
2052 | +) |
2053 | + |
2054 | +type responceFunc func(*serviceCommand, http.ResponseWriter, *http.Request) |
2055 | + |
2056 | +type serviceCommand struct { |
2057 | + Path string |
2058 | + GET responceFunc |
2059 | + PUT responceFunc |
2060 | + POST responceFunc |
2061 | + DELETE responceFunc |
2062 | + s *service |
2063 | +} |
2064 | + |
2065 | +type service struct { |
2066 | + tomb tomb.Tomb |
2067 | + server *http.Server |
2068 | + listener net.Listener |
2069 | + router *mux.Router |
2070 | + ap BackgroundProcess |
2071 | +} |
2072 | + |
2073 | +func (c *serviceCommand) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
2074 | + var rspf responceFunc |
2075 | + |
2076 | + switch r.Method { |
2077 | + case "GET": |
2078 | + rspf = c.GET |
2079 | + case "PUT": |
2080 | + rspf = c.PUT |
2081 | + case "POST": |
2082 | + rspf = c.POST |
2083 | + case "DELETE": |
2084 | + rspf = c.DELETE |
2085 | + } |
2086 | + |
2087 | + if rspf == nil { |
2088 | + rsp := makeErrorResponse(http.StatusInternalServerError, "Invalid method called", "internal-error") |
2089 | + sendHTTPResponse(w, rsp) |
2090 | + return |
2091 | + } |
2092 | + |
2093 | + rspf(c, w, r) |
2094 | +} |
2095 | + |
2096 | +func (s *service) addRoutes() { |
2097 | + s.router = mux.NewRouter() |
2098 | + |
2099 | + for _, c := range api { |
2100 | + c.s = s |
2101 | + log.Println("Adding route for ", c.Path) |
2102 | + s.router.Handle(c.Path, c).Name(c.Path) |
2103 | + } |
2104 | +} |
2105 | + |
2106 | +func (s *service) setupAccesPoint() error { |
2107 | + path := path.Join(os.Getenv("SNAP"), "bin", "ap.sh") |
2108 | + ap, err := NewBackgroundProcess(path) |
2109 | + if err != nil { |
2110 | + return err |
2111 | + } |
2112 | + |
2113 | + s.ap = ap |
2114 | + err = s.ap.Start() |
2115 | + if err != nil { |
2116 | + return err |
2117 | + } |
2118 | + |
2119 | + return nil |
2120 | +} |
2121 | + |
2122 | +func (s *service) Shutdown() { |
2123 | + log.Println("Shutting down ...") |
2124 | + s.listener.Close() |
2125 | + s.tomb.Kill(nil) |
2126 | + s.tomb.Wait() |
2127 | +} |
2128 | + |
2129 | +func (s *service) Run() error { |
2130 | + s.addRoutes() |
2131 | + if err := s.setupAccesPoint(); err != nil { |
2132 | + return err |
2133 | + } |
2134 | + |
2135 | + var err error |
2136 | + if validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "conf", "default-config")); err != nil { |
2137 | + log.Println("Failed to read default configuration:", err) |
2138 | + } |
2139 | + |
2140 | + addr := fmt.Sprintf("%s:%d", serviceAddress, servicePort) |
2141 | + s.server = &http.Server{Addr: addr, Handler: s.router} |
2142 | + s.listener, err = net.Listen("tcp", addr) |
2143 | + if err != nil { |
2144 | + return err |
2145 | + } |
2146 | + |
2147 | + s.tomb.Go(func() error { |
2148 | + err := s.server.Serve(tcpKeepAliveListener{s.listener.(*net.TCPListener)}) |
2149 | + if err != nil { |
2150 | + return fmt.Errorf("Failed to server HTTP: %s", err) |
2151 | + } |
2152 | + return nil |
2153 | + }) |
2154 | + |
2155 | + s.tomb.Wait() |
2156 | + |
2157 | + if s.ap.Running() { |
2158 | + s.ap.Stop() |
2159 | + } |
2160 | + |
2161 | + return nil |
2162 | +} |
2163 | + |
2164 | +type tcpKeepAliveListener struct { |
2165 | + *net.TCPListener |
2166 | +} |
2167 | + |
2168 | +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { |
2169 | + tc, err := ln.AcceptTCP() |
2170 | + if err != nil { |
2171 | + return |
2172 | + } |
2173 | + tc.SetKeepAlive(true) |
2174 | + tc.SetKeepAlivePeriod(3 * time.Minute) |
2175 | + return tc, nil |
2176 | +} |
2177 | diff --git a/snapcraft.yaml b/snapcraft.yaml |
2178 | index 3a7d218..223498e 100644 |
2179 | --- a/snapcraft.yaml |
2180 | +++ b/snapcraft.yaml |
2181 | @@ -11,18 +11,14 @@ description: | |
2182 | grade: stable |
2183 | |
2184 | apps: |
2185 | - backend: |
2186 | - command: bin/ap.sh |
2187 | - daemon: simple |
2188 | - plugs: |
2189 | - - network-control |
2190 | - - firewall-control |
2191 | - - network-bind |
2192 | - - network-manager |
2193 | config: |
2194 | command: bin/client config |
2195 | plugs: |
2196 | - network |
2197 | + status: |
2198 | + command: bin/client status |
2199 | + plugs: |
2200 | + - network |
2201 | setup-wizard: |
2202 | command: bin/client wizard |
2203 | plugs: |
2204 | @@ -32,6 +28,14 @@ apps: |
2205 | daemon: simple |
2206 | plugs: |
2207 | - network-bind |
2208 | + - network-control |
2209 | + - firewall-control |
2210 | + - network-manager |
2211 | + first-config: |
2212 | + command: bin/first-config.sh |
2213 | + daemon: simple |
2214 | + plugs: |
2215 | + - network-bind |
2216 | |
2217 | parts: |
2218 | common: |
2219 | @@ -42,6 +46,7 @@ parts: |
2220 | - bin/config-internal.sh |
2221 | - bin/ap.sh |
2222 | - bin/helper.sh |
2223 | + - bin/first-config.sh |
2224 | - conf/default-config |
2225 | |
2226 | network-utils: |
2227 | @@ -74,6 +79,7 @@ parts: |
2228 | - bin |
2229 | build-packages: |
2230 | - golang-github-gorilla-mux-dev |
2231 | + - golang-gopkg-tomb.v2-dev |
2232 | |
2233 | dnsmasq: |
2234 | plugin: make |
2235 | diff --git a/tests/lib/prepare-all.sh b/tests/lib/prepare-all.sh |
2236 | index 290b3ed..59e1349 100644 |
2237 | --- a/tests/lib/prepare-all.sh |
2238 | +++ b/tests/lib/prepare-all.sh |
2239 | @@ -6,6 +6,13 @@ if [ -n "$SNAP_CHANNEL" ] ; then |
2240 | exit 0 |
2241 | fi |
2242 | |
2243 | +# If there is a wifi-ap snap prebuilt for us, lets take |
2244 | +# that one to speed things up. |
2245 | +if [ -e /home/wifi-ap/wifi-ap_*_amd64.snap ] ; then |
2246 | + exit 0 |
2247 | +fi |
2248 | + |
2249 | + |
2250 | # Setup classic snap and build the wifi-ap snap in there |
2251 | snap install --devmode --beta classic |
2252 | cat <<-EOF > /home/test/build-snap.sh |
2253 | diff --git a/tests/lib/restore-each.sh b/tests/lib/restore-each.sh |
2254 | index 799c605..1193b58 100644 |
2255 | --- a/tests/lib/restore-each.sh |
2256 | +++ b/tests/lib/restore-each.sh |
2257 | @@ -21,7 +21,6 @@ rm -rf /var/snap/$SNAP_NAME/common/* |
2258 | rm -rf /var/snap/$SNAP_NAME/current/* |
2259 | # Depending on what the test did both services are not meant to be |
2260 | # running here. |
2261 | -systemctl stop snap.wifi-ap.backend || true |
2262 | systemctl stop snap.wifi-ap.management-service || true |
2263 | |
2264 | # Drop any generated or modified netplan configuration files. The original |
2265 | @@ -40,7 +39,5 @@ netplan generate |
2266 | netplan apply |
2267 | |
2268 | # Start services again now that the system is restored |
2269 | -systemctl start snap.wifi-ap.backend |
2270 | systemctl start snap.wifi-ap.management-service |
2271 | -wait_for_systemd_service snap.wifi-ap.backend |
2272 | wait_for_systemd_service snap.wifi-ap.management-service |
2273 | diff --git a/tests/main/background-process-control/task.yaml b/tests/main/background-process-control/task.yaml |
2274 | new file mode 100644 |
2275 | index 0000000..c884b52 |
2276 | --- /dev/null |
2277 | +++ b/tests/main/background-process-control/task.yaml |
2278 | @@ -0,0 +1,46 @@ |
2279 | +summary: Test correct service behavior to ensure the background AP process is running |
2280 | + |
2281 | +prepare: | |
2282 | + # Simulate two WiFi radio network interfaces |
2283 | + modprobe mac80211_hwsim radios=2 |
2284 | + |
2285 | + # We need some tools for scanning etc. |
2286 | + snap install wireless-tools |
2287 | + snap connect wireless-tools:network-control core |
2288 | + |
2289 | +restore: | |
2290 | + rmmod mac80211_hwsim |
2291 | + |
2292 | +execute: | |
2293 | + # Verify first the management service is up and running |
2294 | + /snap/bin/wifi-ap.config get |
2295 | + test "`/snap/bin/wifi-ap.config get disabled`" = "1" |
2296 | + |
2297 | + # AP should be not active at this time as still disabled |
2298 | + /snap/bin/wifi-ap.status | grep "ap.active: 0" |
2299 | + |
2300 | + # Now start the AP and ensure its reported as active |
2301 | + /snap/bin/wifi-ap.config set disabled 0 |
2302 | + /snap/bin/wifi-ap.status | grep "ap.active: 1" |
2303 | + # And if we wait a bit more it should be still active |
2304 | + sleep 5 |
2305 | + /snap/bin/wifi-ap.status | grep "ap.active: 1" |
2306 | + |
2307 | + # Scan for networks on the other side of the WiFi network |
2308 | + # and ensure the network is available. |
2309 | + ifconfig wlan1 up |
2310 | + /snap/bin/wireless-tools.iw dev wlan1 scan | grep 'SSID: Ubuntu' |
2311 | + |
2312 | + # Restart should get us back into the same state we were in before |
2313 | + /snap/bin/wifi-ap.status restart-ap |
2314 | + # Restart needs some time |
2315 | + sleep 5 |
2316 | + /snap/bin/wifi-ap.status | grep "ap.active: 1" |
2317 | + /snap/bin/wireless-tools.iw dev wlan1 scan | grep 'SSID: Ubuntu' |
2318 | + |
2319 | + # If we now stop the management-service the hostapd and dnsmasq |
2320 | + # instances should go away. |
2321 | + systemctl stop snap.wifi-ap.management-service |
2322 | + while /snap/bin/wifi-ap.status | grep "ap.active: 1" ; do |
2323 | + sleep 0.5 |
2324 | + done |
2325 | diff --git a/tests/main/default-conf-brings-up-ap/task.yaml b/tests/main/default-conf-brings-up-ap/task.yaml |
2326 | index c8a09dd..5239813 100644 |
2327 | --- a/tests/main/default-conf-brings-up-ap/task.yaml |
2328 | +++ b/tests/main/default-conf-brings-up-ap/task.yaml |
2329 | @@ -13,9 +13,6 @@ execute: | |
2330 | # Default configuration will use wlan0 which we just created |
2331 | /snap/bin/wifi-ap.config set disabled 0 |
2332 | |
2333 | - systemctl restart snap.wifi-ap.backend |
2334 | - wait_for_systemd_service snap.wifi-ap.backend |
2335 | - |
2336 | snap install wireless-tools |
2337 | snap connect wireless-tools:network-control core |
2338 | |
2339 | diff --git a/tests/main/stress-ap-status-control/task.yaml b/tests/main/stress-ap-status-control/task.yaml |
2340 | new file mode 100644 |
2341 | index 0000000..976c97e |
2342 | --- /dev/null |
2343 | +++ b/tests/main/stress-ap-status-control/task.yaml |
2344 | @@ -0,0 +1,49 @@ |
2345 | +summary: Stress test for the AP status control API |
2346 | + |
2347 | +environment: |
2348 | + RESTART_ITERATIONS: 50 |
2349 | + |
2350 | +prepare: | |
2351 | + # Simulate two WiFi radio network interfaces |
2352 | + modprobe mac80211_hwsim radios=2 |
2353 | + |
2354 | + # We need some tools for scanning etc. |
2355 | + snap install wireless-tools |
2356 | + snap connect wireless-tools:network-control core |
2357 | + |
2358 | +restore: | |
2359 | + rmmod mac80211_hwsim |
2360 | + |
2361 | +execute: | |
2362 | + # Bring up the access point first |
2363 | + /snap/bin/wifi-ap.config set disabled 0 |
2364 | + while ! /snap/bin/wifi-ap.status | grep "ap.active: 1" ; do |
2365 | + sleep 0.5 |
2366 | + done |
2367 | + |
2368 | + # Scan for networks on the other side of the WiFi network |
2369 | + # and ensure the network is available. |
2370 | + sleep 3 |
2371 | + ifconfig wlan1 up |
2372 | + sudo /snap/bin/wireless-tools.iw dev wlan1 scan | grep 'SSID: Ubuntu' |
2373 | + |
2374 | + # We will restart the AP a huge number of times again and again |
2375 | + # and expect that the AP afterwards comes back up normally and |
2376 | + # we can still search and connect to it. |
2377 | + n=0 |
2378 | + while [ $n -lt $RESTART_ITERATIONS ] ; do |
2379 | + /snap/bin/wifi-ap.status restart-ap |
2380 | + let n=n+1 |
2381 | + done |
2382 | + |
2383 | + # The AP should be still available in our scan result |
2384 | + sleep 3 |
2385 | + sudo /snap/bin/wireless-tools.iw dev wlan1 scan | grep 'SSID: Ubuntu' |
2386 | + # Verify we can associate with the AP |
2387 | + sudo /snap/bin/wireless-tools.iw wlan1 connect Ubuntu |
2388 | + sudo /snap/bin/wireless-tools.iw dev wlan1 link | grep 'SSID: Ubuntu' |
2389 | + |
2390 | + # We should only have one hostapd and one dnsmasq process at this time |
2391 | + # (we have to ignore the grep'ing process as otherwise we get a count of 2) |
2392 | + test `ps axu | grep hostapd | grep -v grep | wc -l` -eq 1 |
2393 | + test `ps axu | grep dnsmasq | grep -v grep | wc -l` -eq 1 |
2394 | \ No newline at end of file |
2395 | diff --git a/tests/main/utf8-ssid/task.yaml b/tests/main/utf8-ssid/task.yaml |
2396 | index 903bfdb..67c334f 100644 |
2397 | --- a/tests/main/utf8-ssid/task.yaml |
2398 | +++ b/tests/main/utf8-ssid/task.yaml |
2399 | @@ -13,11 +13,15 @@ execute: | |
2400 | wait_for_systemd_service snap.wifi-ap.management-service |
2401 | |
2402 | # Default configuration will use wlan0 which we just created |
2403 | - /snap/bin/wifi-ap.config set disabled 0 |
2404 | /snap/bin/wifi-ap.config set wifi.ssid 'Ubuntu👍' |
2405 | + /snap/bin/wifi-ap.config set disabled 0 |
2406 | |
2407 | - systemctl restart snap.wifi-ap.backend |
2408 | - wait_for_systemd_service snap.wifi-ap.backend |
2409 | + # Wait for AP to become active |
2410 | + while ! /snap/bin/wifi-ap.status | grep 'ap.active: 1' ; do |
2411 | + sleep 0.5 |
2412 | + done |
2413 | + # Give AP a bit more to settle until it's marked as active |
2414 | + sleep 3 |
2415 | |
2416 | snap install wireless-tools |
2417 | snap connect wireless-tools:network-control core |
FAILED: Continuous integration, rev:93e735e85a5 888659598f8ce31 cc87ff5dc53203 /jenkins. canonical. com/system- enablement/ job/generic- build-snap/ 402/ /jenkins. canonical. com/system- enablement/ job/generic- run-snap- spread- tests/148/ console /jenkins. canonical. com/system- enablement/ job/generic- update- snap-mp/ 310/console
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild: /jenkins. canonical. com/system- enablement/ job/generic- build-snap/ 402/rebuild
https:/