Merge ~teknoraver/snappy-hwe-snaps/+git/wifi-ap:master into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master

Proposed by Matteo Croce
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)
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.

Description of the change

run wizaard of first boot

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

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/MAINTAINERS b/MAINTAINERS
2index 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>
8diff --git a/README.md b/README.md
9index 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
54diff --git a/bin/ap.sh b/bin/ap.sh
55index 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
134diff --git a/bin/first-config.sh b/bin/first-config.sh
135new file mode 100755
136index 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
165diff --git a/bin/helper.sh b/bin/helper.sh
166index 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 }
177diff --git a/cmd/client/client.go b/cmd/client/client.go
178index 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 }
200diff --git a/cmd/client/cmd_config.go b/cmd/client/cmd_config.go
201index 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 }
239diff --git a/cmd/client/cmd_status.go b/cmd/client/cmd_status.go
240new file mode 100644
241index 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+}
303diff --git a/cmd/client/utils.go b/cmd/client/utils.go
304new file mode 100644
305index 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+}
342diff --git a/cmd/service/api.go b/cmd/service/api.go
343new file mode 100644
344index 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+}
524diff --git a/cmd/service/api_test.go b/cmd/service/api_test.go
525new file mode 100644
526index 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+}
874diff --git a/cmd/service/background_process.go b/cmd/service/background_process.go
875new file mode 100644
876index 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+}
1018diff --git a/cmd/service/background_process_test.go b/cmd/service/background_process_test.go
1019new file mode 100644
1020index 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+}
1060diff --git a/cmd/service/config.go b/cmd/service/config.go
1061new file mode 100644
1062index 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+}
1206diff --git a/cmd/service/config_test.go b/cmd/service/config_test.go
1207new file mode 100644
1208index 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+}
1278diff --git a/cmd/service/main.go b/cmd/service/main.go
1279index 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 }
1576diff --git a/cmd/service/main_test.go b/cmd/service/main_test.go
1577deleted file mode 100644
1578index 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-}
1880diff --git a/cmd/service/response.go b/cmd/service/response.go
1881new file mode 100644
1882index 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+}
1959diff --git a/cmd/service/response_test.go b/cmd/service/response_test.go
1960new file mode 100644
1961index 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+}
2012diff --git a/cmd/service/service.go b/cmd/service/service.go
2013new file mode 100644
2014index 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+}
2177diff --git a/snapcraft.yaml b/snapcraft.yaml
2178index 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
2235diff --git a/tests/lib/prepare-all.sh b/tests/lib/prepare-all.sh
2236index 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
2253diff --git a/tests/lib/restore-each.sh b/tests/lib/restore-each.sh
2254index 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
2273diff --git a/tests/main/background-process-control/task.yaml b/tests/main/background-process-control/task.yaml
2274new file mode 100644
2275index 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
2325diff --git a/tests/main/default-conf-brings-up-ap/task.yaml b/tests/main/default-conf-brings-up-ap/task.yaml
2326index 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
2339diff --git a/tests/main/stress-ap-status-control/task.yaml b/tests/main/stress-ap-status-control/task.yaml
2340new file mode 100644
2341index 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
2395diff --git a/tests/main/utf8-ssid/task.yaml b/tests/main/utf8-ssid/task.yaml
2396index 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

Subscribers

People subscribed via source and target branches

to all changes: