Merge ~morphis/snappy-hwe-snaps/+git/wifi-ap:merge-mgmt-and-ap into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master

Proposed by Simon Fels
Status: Merged
Approved by: Matteo Croce
Approved revision: b5e4aca99518cb7f691ed0d021e8c423eb561b2e
Merged at revision: 4318e8678f66ecc1c602b3c8735aac1a0f809a70
Proposed branch: ~morphis/snappy-hwe-snaps/+git/wifi-ap:merge-mgmt-and-ap
Merge into: ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master
Prerequisite: ~morphis/snappy-hwe-snaps/+git/wifi-ap:rework-test-infrastructure
Diff against target: 2407 lines (+1510/-604)
26 files modified
MAINTAINERS (+1/-0)
README.md (+35/-0)
bin/ap.sh (+31/-9)
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 (+147/-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 (+8/-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 (+75/-0)
tests/main/utf8-ssid/task.yaml (+7/-3)
Reviewer Review Type Date Requested Status
System Enablement Bot continuous-integration Approve
Matteo Croce (community) Approve
Konrad Zapałowicz (community) code Approve
Review via email: mp+311704@code.launchpad.net

This proposal supersedes a proposal from 2016-11-24.

Description of the change

Combine the management service with the AP one

This gives us full control over the AP, when it is started, when it needs to be restarted etc. For that this MP also reworks the whole service infrastructure to be better maintainable and flexible for future additions.

FIXME: spread tests are missing here, as they are not yet fully working. Please don't top-approve until I've added them and they are working fine.

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: Needs Fixing (continuous-integration)
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: Needs Fixing (continuous-integration)
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: Needs Fixing (continuous-integration)
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)
Revision history for this message
Matteo Croce (teknoraver) :
Revision history for this message
Konrad Zapałowicz (kzapalowicz) wrote :

lgtm with minor change needed to for loop

review: Needs Fixing (code)
Revision history for this message
Konrad Zapałowicz (kzapalowicz) wrote :

Ack

review: Approve (code)
Revision history for this message
Matteo Croce (teknoraver) :
review: Approve
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Simon Fels (morphis) wrote :

Rebased on master to get merge conflicts solved.

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: Needs Fixing (continuous-integration)
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: Needs Fixing (continuous-integration)
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: Needs Fixing (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Matteo Croce (teknoraver) :
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: Needs Fixing (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)

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

Subscribers

People subscribed via source and target branches

to all changes: