Merge ~morphis/snappy-hwe-snaps/+git/wifi-ap:fixes-and-client-ctl into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master

Proposed by Simon Fels
Status: Merged
Approved by: Simon Fels
Approved revision: 020ff32dd281b86853dbececbae3c9cb9510ec4f
Merged at revision: b257c1126c87a716df4e3d66a3304b144b64a970
Proposed branch: ~morphis/snappy-hwe-snaps/+git/wifi-ap:fixes-and-client-ctl
Merge into: ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master
Diff against target: 1514 lines (+766/-212)
8 files modified
cmd/client/client.go (+78/-0)
cmd/client/client_test.go (+120/-0)
cmd/client/cmd_config.go (+77/-0)
cmd/client/main.go (+42/-0)
cmd/service/main.go (+226/-0)
cmd/service/main_test.go (+210/-0)
dev/null (+0/-209)
snapcraft.yaml (+13/-3)
Reviewer Review Type Date Requested Status
Matteo Croce (community) Approve
Review via email: mp+306189@code.launchpad.net

Description of the change

Various changes and fixes

* Add missing grade property (set to stable)
* Add missing network-bind plug for the management service
* Parse the configuration from the default one if no user modifications are available
* Add client implementation for the REST service which is now used instead of the shell script one
* Move service/client into a cmd subdirectory

To post a comment you must log in.
Revision history for this message
Matteo Croce (teknoraver) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/bin/config.sh b/bin/config.sh
2deleted file mode 100755
3index 0bf7839..0000000
4--- a/bin/config.sh
5+++ /dev/null
6@@ -1,230 +0,0 @@
7-#!/bin/bash
8-#
9-# Copyright (C) 2015, 2016 Canonical Ltd
10-#
11-# This program is free software: you can redistribute it and/or modify
12-# it under the terms of the GNU General Public License version 3 as
13-# published by the Free Software Foundation.
14-#
15-# This program is distributed in the hope that it will be useful,
16-# but WITHOUT ANY WARRANTY; without even the implied warranty of
17-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18-# GNU General Public License for more details.
19-#
20-# You should have received a copy of the GNU General Public License
21-# along with this program. If not, see <http://www.gnu.org/licenses/>.
22-
23-. $SNAP/bin/config-internal.sh
24-
25-set_item() {
26- if [ -z "$1" ] || [ -z "$2" ] ; then
27- echo "ERROR: You need to provide a key and a value to set"
28- echo
29- echo "You can call '$0 get' to list of all possible keys"
30- exit 1
31- fi
32- case $1 in
33- disabled)
34- DISABLED=$2
35- if [ "$DISABLED" == "0" ] ; then
36- echo "You have successfully enabled the access point but you still"
37- echo "need to either reboot the device or restart the systemd"
38- echo "service to make the service reloading its configuration."
39- echo "You can just run the following command (as root) if you"
40- echo "do not want to reboot your device:"
41- echo
42- echo " $ systemctl restart snap.wifi-ap.backend"
43- fi
44- ;;
45- debug)
46- DEBUG=$2
47- ;;
48- wifi.interface)
49- WIFI_INTERFACE=$2
50- ;;
51- wifi.address)
52- WIFI_ADDRESS=$2
53- ;;
54- wifi.netmask)
55- WIFI_NETMASK=$2
56- ;;
57- wifi.interface-mode)
58- WIFI_INTERFACE_MODE=$2
59- ;;
60- wifi.hostapd-driver)
61- WIFI_HOSTAPD_DRIVER=$2
62- if [ "$WIFI_HOSTAPD_DRIVER" == "rtl8188" ] ; then
63- # Select correct mode for the rtl8188 driver
64- WIFI_INTERFACE_MODE=direct
65- fi
66- ;;
67- wifi.ssid)
68- WIFI_SSID=$2
69- ;;
70- wifi.security)
71- WIFI_SECURITY=$2
72- ;;
73- wifi.security-passphrase)
74- WIFI_SECURITY_PASSPHRASE=$2
75- ;;
76- wifi.channel)
77- WIFI_CHANNEL=$2
78- ;;
79- wifi.operation-mode)
80- WIFI_OPERATION_MODE=$2
81- ;;
82- share.network-interface)
83- SHARE_NETWORK_INTERFACE=$2
84- ;;
85- dhcp.range-start)
86- DHCP_RANGE_START=$2
87- ;;
88- dhcp.range-stop)
89- DHCP_RANGE_STOP=$2
90- ;;
91- dhcp.lease-time)
92- DHCP_LEASE_TIME=$2
93- ;;
94- *)
95- echo "ERROR: Unknown config item '$1'"
96- exit 1
97- esac
98-}
99-
100-get_item() {
101- case $1 in
102- disabled)
103- echo $DISABLED
104- ;;
105- debug)
106- echo $DEBUG
107- ;;
108- wifi.interface)
109- echo $WIFI_INTERFACE
110- ;;
111- wifi.address)
112- echo $WIFI_ADDRESS
113- ;;
114- wifi.netmask)
115- echo $WIFI_NETMASK
116- ;;
117- wifi.interface-mode)
118- echo $WIFI_INTERFACE_MODE
119- ;;
120- wifi.hostapd-driver)
121- echo $WIFI_HOSTAPD_DRIVER
122- ;;
123- wifi.ssid)
124- echo $WIFI_SSID
125- ;;
126- wifi.security)
127- echo $WIFI_SECURITY
128- ;;
129- wifi.security-passphrase)
130- echo $WIFI_SECURITY_PASSPHRASE
131- ;;
132- wifi.channel)
133- echo $WIFI_CHANNEL
134- ;;
135- wifi.operation-mode)
136- echo $WIFI_OPERATION_MODE
137- ;;
138- share.network-interface)
139- echo $SHARE_NETWORK_INTERFACE
140- ;;
141- dhcp.range-start)
142- echo $DHCP_RANGE_START
143- ;;
144- dhcp.range-stop)
145- echo $DHCP_RANGE_STOP
146- ;;
147- dhcp.lease-time)
148- echo $DHCP_LEASE_TIME
149- ;;
150- *)
151- echo "Unknown config item '$1'"
152- exit 1
153- esac
154-}
155-
156-dump_config() {
157- echo "disabled: $DISABLED"
158- echo "debug: $DEBUG"
159- echo "wifi.interface: $WIFI_INTERFACE"
160- echo "wifi.address: $WIFI_ADDRESS"
161- echo "wifi.netmask: $WIFI_NETMASK"
162- echo "wifi.interface-mode: $WIFI_INTERFACE_MODE"
163- echo "wifi.hostapd-driver: $WIFI_HOSTAPD_DRIVER"
164- echo "wifi.ssid: $WIFI_SSID"
165- echo "wifi.security: $WIFI_SECURITY"
166- echo "wifi.security-passphrase: $WIFI_SECURITY_PASSPHRASE"
167- echo "wifi.channel: $WIFI_CHANNEL"
168- echo "wifi.operation-mode: $WIFI_OPERATION_MODE"
169- echo "share.network-interface: $SHARE_NETWORK_INTERFACE"
170- echo "dhcp.range-start: $DHCP_RANGE_START"
171- echo "dhcp.range-stop: $DHCP_RANGE_STOP"
172- echo "dhcp.lease-time: $DHCP_LEASE_TIME"
173-}
174-
175-write_configuration() {
176- cat <<-EOF > $SNAP_DATA/config
177- #!/bin/bash
178- # THIS FILE IS AUTOGENERATED. Please use the the wifi-ap.config
179- # command to change the configuration or create a overlay
180- # configuration file at $SNAP_USER_DATA/config
181- DISABLED=$DISABLED
182- DEBUG=$DEBUG
183- WIFI_INTERFACE=$WIFI_INTERFACE
184- WIFI_ADDRESS=$WIFI_ADDRESS
185- WIFI_NETMASK=$WIFI_NETMASK
186- WIFI_INTERFACE_MODE=$WIFI_INTERFACE_MODE
187- WIFI_HOSTAPD_DRIVER=$WIFI_HOSTAPD_DRIVER
188- WIFI_SSID=$WIFI_SSID
189- WIFI_SECURITY=$WIFI_SECURITY
190- WIFI_SECURITY_PASSPHRASE=$WIFI_SECURITY_PASSPHRASE
191- WIFI_CHANNEL=$WIFI_CHANNEL
192- WIFI_OPERATION_MODE=$WIFI_OPERATION_MODE
193- SHARE_NETWORK_INTERFACE=$SHARE_NETWORK_INTERFACE
194- DHCP_RANGE_START=$DHCP_RANGE_START
195- DHCP_RANGE_STOP=$DHCP_RANGE_STOP
196- DHCP_LEASE_TIME=$DHCP_LEASE_TIME
197- EOF
198-}
199-
200-if [ -z "$1" ] ; then
201- echo "Usage: $0 get|set <key> [<value>]"
202- echo
203- echo "You can call '$0 get' to list of all possible keys"
204- exit
205-fi
206-
207-
208-
209-case "$1" in
210- set)
211- if [ $(id -u) -ne 0 ] ; then
212- echo "ERROR: '$@' needs to be executed as root!"
213- exit 1
214- fi
215- shift
216- key=$1
217- shift
218- value=$1
219- shift
220- set_item $key $value
221- write_configuration
222- ;;
223- get)
224- shift
225- if [ "$1" = "" ] ; then
226- dump_config
227- else
228- echo "$1: $(get_item $1)"
229- fi
230- shift
231- ;;
232- *)
233- echo "Unknown command '$1'."
234- exit 1
235- ;;
236-esac
237diff --git a/cmd/client/client.go b/cmd/client/client.go
238new file mode 100644
239index 0000000..0a770d7
240--- /dev/null
241+++ b/cmd/client/client.go
242@@ -0,0 +1,78 @@
243+//
244+// Copyright (C) 2016 Canonical Ltd
245+//
246+// This program is free software: you can redistribute it and/or modify
247+// it under the terms of the GNU General Public License version 3 as
248+// published by the Free Software Foundation.
249+//
250+// This program is distributed in the hope that it will be useful,
251+// but WITHOUT ANY WARRANTY; without even the implied warranty of
252+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
253+// GNU General Public License for more details.
254+//
255+// You should have received a copy of the GNU General Public License
256+// along with this program. If not, see <http://www.gnu.org/licenses/>.
257+
258+package main
259+
260+import (
261+ "encoding/json"
262+ "fmt"
263+ "io"
264+ "net/http"
265+)
266+
267+const (
268+ servicePort = 5005
269+ configurationV1Uri = "/v1/configuration"
270+)
271+
272+type serviceResponse struct {
273+ Result map[string]string `json:"result"`
274+ Status string `json:"status"`
275+ StatusCode int `json:"status-code"`
276+ Type string `json:"type"`
277+}
278+
279+func getServiceConfigurationURI() string {
280+ return fmt.Sprintf("http://localhost:%d%s", servicePort, configurationV1Uri)
281+}
282+
283+type doer interface {
284+ Do(*http.Request) (*http.Response, error)
285+}
286+
287+var customDoer doer
288+
289+func sendHTTPRequest(uri string, method string, body io.Reader) (*serviceResponse, error) {
290+ req, err := http.NewRequest(method, uri, body)
291+ if err != nil {
292+ return nil, err
293+ }
294+
295+ var resp *http.Response
296+
297+ if customDoer == nil {
298+ client := &http.Client{}
299+ resp, err = client.Do(req)
300+ } else {
301+ resp, err = customDoer.Do(req)
302+ }
303+
304+ if err != nil {
305+ return nil, err
306+ }
307+
308+ defer resp.Body.Close()
309+
310+ realResponse := &serviceResponse{}
311+ if err := json.NewDecoder(resp.Body).Decode(&realResponse); err != nil {
312+ return nil, err
313+ }
314+
315+ if realResponse.StatusCode != http.StatusOK {
316+ return nil, fmt.Errorf("Failed: %s", realResponse.Result["message"])
317+ }
318+
319+ return realResponse, nil
320+}
321diff --git a/cmd/client/client_test.go b/cmd/client/client_test.go
322new file mode 100644
323index 0000000..5cf5196
324--- /dev/null
325+++ b/cmd/client/client_test.go
326@@ -0,0 +1,120 @@
327+//
328+// Copyright (C) 2016 Canonical Ltd
329+//
330+// This program is free software: you can redistribute it and/or modify
331+// it under the terms of the GNU General Public License version 3 as
332+// published by the Free Software Foundation.
333+//
334+// This program is distributed in the hope that it will be useful,
335+// but WITHOUT ANY WARRANTY; without even the implied warranty of
336+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
337+// GNU General Public License for more details.
338+//
339+// You should have received a copy of the GNU General Public License
340+// along with this program. If not, see <http://www.gnu.org/licenses/>.
341+
342+package main
343+
344+import (
345+ "bytes"
346+ "encoding/json"
347+ "fmt"
348+ "io/ioutil"
349+ "net/http"
350+ "strings"
351+ "testing"
352+
353+ "gopkg.in/check.v1"
354+)
355+
356+// gopkg.in/check.v1 stuff
357+func Test(t *testing.T) { check.TestingT(t) }
358+
359+type ClientSuite struct {
360+ req *http.Request
361+ rsp string
362+ err error
363+ doCalls int
364+ header http.Header
365+ status int
366+}
367+
368+var _ = check.Suite(&ClientSuite{})
369+
370+func (s *ClientSuite) SetUpTest(c *check.C) {
371+ s.req = nil
372+ s.rsp = ""
373+ s.err = nil
374+ s.doCalls = 0
375+ s.header = nil
376+ s.status = http.StatusOK
377+ // Inject ourself as doer into the client so we get called
378+ // for the actual http requests and they are not send out
379+ // over the network to a not existing service.
380+ customDoer = s
381+}
382+
383+func (s *ClientSuite) Do(req *http.Request) (*http.Response, error) {
384+ s.req = req
385+ rsp := &http.Response{
386+ Body: ioutil.NopCloser(strings.NewReader(s.rsp)),
387+ Header: s.header,
388+ StatusCode: s.status,
389+ }
390+ s.doCalls++
391+ return rsp, s.err
392+}
393+
394+func (s *ClientSuite) TestServiceConfigurationUriIsCorrect(c *check.C) {
395+ c.Assert(getServiceConfigurationURI(), check.Equals, "http://localhost:5005/v1/configuration")
396+}
397+
398+func (s *ClientSuite) TestSendHTTPRequestWithSuccessfullResponse(c *check.C) {
399+ s.rsp = `{"result":{"test1":"abc"},"status":"OK","status-code":200,"type":"sync"}`
400+ rsp, err := sendHTTPRequest(getServiceConfigurationURI(), "GET", nil)
401+ c.Assert(s.doCalls, check.Equals, 1)
402+ c.Assert(rsp, check.NotNil)
403+ c.Assert(err, check.IsNil)
404+ c.Assert(s.req.Method, check.Equals, "GET")
405+ c.Assert(rsp.Status, check.Equals, "OK")
406+ c.Assert(rsp.StatusCode, check.Equals, 200)
407+ c.Assert(rsp.Type, check.Equals, "sync")
408+ c.Assert(rsp.Result["test1"], check.Equals, "abc")
409+}
410+
411+func (s *ClientSuite) TestSendHTTPRequestFails(c *check.C) {
412+ s.err = fmt.Errorf("Failed")
413+ rsp, err := sendHTTPRequest(getServiceConfigurationURI(), "GET", nil)
414+ c.Assert(rsp, check.IsNil)
415+ c.Assert(err, check.Equals, s.err)
416+ c.Assert(s.req.Method, check.Equals, "GET")
417+}
418+
419+func (s *ClientSuite) TestSendHTTPRequestInvalidResponseJson(c *check.C) {
420+ s.rsp = `{invalid}`
421+ rsp, err := sendHTTPRequest(getServiceConfigurationURI(), "GET", nil)
422+ c.Assert(rsp, check.IsNil)
423+ c.Assert(err, check.NotNil)
424+ c.Assert(s.req.Method, check.Equals, "GET")
425+}
426+
427+func (s *ClientSuite) TestSendHTTPRequestErrorFromService(c *check.C) {
428+ s.rsp = `{"result":{},"status":"Failed","status-code":500,"type":"sync"}`
429+ rsp, err := sendHTTPRequest(getServiceConfigurationURI(), "GET", nil)
430+ c.Assert(rsp, check.IsNil)
431+ c.Assert(err, check.NotNil)
432+ c.Assert(s.req.Method, check.Equals, "GET")
433+}
434+
435+func (s *ClientSuite) TestSendHTTPRequestSendsCorrectContent(c *check.C) {
436+ s.rsp = `{"result":{},"status":"OK","status-code":200,"type":"sync"}`
437+ request := make(map[string]string)
438+ request["test1"] = "abc"
439+ b, err := json.Marshal(request)
440+ c.Assert(err, check.IsNil)
441+ c.Assert(b, check.NotNil)
442+ rsp, err := sendHTTPRequest(getServiceConfigurationURI(), "POST", bytes.NewReader(b))
443+ c.Assert(rsp, check.NotNil)
444+ c.Assert(err, check.IsNil)
445+ c.Assert(s.req.Body, check.NotNil)
446+}
447diff --git a/cmd/client/cmd_config.go b/cmd/client/cmd_config.go
448new file mode 100644
449index 0000000..c801b47
450--- /dev/null
451+++ b/cmd/client/cmd_config.go
452@@ -0,0 +1,77 @@
453+//
454+// Copyright (C) 2016 Canonical Ltd
455+//
456+// This program is free software: you can redistribute it and/or modify
457+// it under the terms of the GNU General Public License version 3 as
458+// published by the Free Software Foundation.
459+//
460+// This program is distributed in the hope that it will be useful,
461+// but WITHOUT ANY WARRANTY; without even the implied warranty of
462+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
463+// GNU General Public License for more details.
464+//
465+// You should have received a copy of the GNU General Public License
466+// along with this program. If not, see <http://www.gnu.org/licenses/>.
467+
468+package main
469+
470+import (
471+ "bytes"
472+ "encoding/json"
473+ "fmt"
474+ "os"
475+)
476+
477+type setCommand struct{}
478+
479+func (cmd *setCommand) Execute(args []string) error {
480+ if len(args) != 2 {
481+ return fmt.Errorf("usage: %s set <key> <value>\n", os.Args[0])
482+ }
483+
484+ request := make(map[string]string)
485+ request[args[0]] = args[1]
486+ b, err := json.Marshal(request)
487+
488+ _, err = sendHTTPRequest(getServiceConfigurationURI(), "POST", bytes.NewReader(b))
489+ if err != nil {
490+ return err
491+ }
492+
493+ return nil
494+}
495+
496+type getCommand struct{}
497+
498+func (cmd *getCommand) Execute(args []string) error {
499+ if len(args) != 1 {
500+ return fmt.Errorf("usage: %s get <key>\n", os.Args[0])
501+ }
502+
503+ response, err := sendHTTPRequest(getServiceConfigurationURI(), "GET", nil)
504+ if err != nil {
505+ return err
506+ }
507+
508+ wantedKey := args[0]
509+
510+ if val, ok := response.Result[wantedKey]; ok {
511+ fmt.Fprintf(os.Stdout, "%s\n", val)
512+ } else {
513+ return fmt.Errorf("Config item '%s' does not exist", wantedKey)
514+ }
515+
516+ return nil
517+}
518+
519+type configCommand struct{}
520+
521+func (cmd *configCommand) Execute(args []string) error {
522+ return nil
523+}
524+
525+func init() {
526+ cmdConfig, _ := addCommand("config", "Adjust the service configuration", "", &configCommand{})
527+ cmdConfig.AddCommand("set", "", "", &setCommand{})
528+ cmdConfig.AddCommand("get", "", "", &getCommand{})
529+}
530diff --git a/cmd/client/main.go b/cmd/client/main.go
531new file mode 100644
532index 0000000..581b347
533--- /dev/null
534+++ b/cmd/client/main.go
535@@ -0,0 +1,42 @@
536+//
537+// Copyright (C) 2016 Canonical Ltd
538+//
539+// This program is free software: you can redistribute it and/or modify
540+// it under the terms of the GNU General Public License version 3 as
541+// published by the Free Software Foundation.
542+//
543+// This program is distributed in the hope that it will be useful,
544+// but WITHOUT ANY WARRANTY; without even the implied warranty of
545+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
546+// GNU General Public License for more details.
547+//
548+// You should have received a copy of the GNU General Public License
549+// along with this program. If not, see <http://www.gnu.org/licenses/>.
550+
551+package main
552+
553+import (
554+ "os"
555+
556+ "github.com/jessevdk/go-flags"
557+)
558+
559+type commonOptions struct {
560+ Verbose []bool `short:"v" long:"verbose" description:"Verbose output"`
561+}
562+
563+var parser = flags.NewParser(&commonOptions{}, flags.Default)
564+
565+func addCommand(name string, shortHelp string, longHelp string, data interface{}) (*flags.Command, error) {
566+ cmd, err := parser.AddCommand(name, shortHelp, longHelp, data)
567+ if err != nil {
568+ return nil, err
569+ }
570+ return cmd, nil
571+}
572+
573+func main() {
574+ if _, err := parser.Parse(); err != nil {
575+ os.Exit(1)
576+ }
577+}
578diff --git a/cmd/service/main.go b/cmd/service/main.go
579new file mode 100644
580index 0000000..0f86fbe
581--- /dev/null
582+++ b/cmd/service/main.go
583@@ -0,0 +1,226 @@
584+//
585+// Copyright (C) 2016 Canonical Ltd
586+//
587+// This program is free software: you can redistribute it and/or modify
588+// it under the terms of the GNU General Public License version 3 as
589+// published by the Free Software Foundation.
590+//
591+// This program is distributed in the hope that it will be useful,
592+// but WITHOUT ANY WARRANTY; without even the implied warranty of
593+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
594+// GNU General Public License for more details.
595+//
596+// You should have received a copy of the GNU General Public License
597+// along with this program. If not, see <http://www.gnu.org/licenses/>.
598+
599+package main
600+
601+import (
602+ "bufio"
603+ "encoding/json"
604+ "fmt"
605+ "io/ioutil"
606+ "log"
607+ "net/http"
608+ "os"
609+ "path/filepath"
610+ "regexp"
611+ "strconv"
612+ "strings"
613+
614+ "github.com/gorilla/mux"
615+)
616+
617+/* JSON message format, as described here:
618+{
619+ "result": {
620+ "key" : "val"
621+ },
622+ "status": "OK",
623+ "status-code": 200,
624+ "type": "sync"
625+}
626+*/
627+
628+type serviceResponse struct {
629+ Result map[string]string `json:"result"`
630+ Status string `json:"status"`
631+ StatusCode int `json:"status-code"`
632+ Type string `json:"type"`
633+}
634+
635+func makeErrorResponse(code int, message, kind string) *serviceResponse {
636+ return &serviceResponse{
637+ Type: "error",
638+ Status: http.StatusText(code),
639+ StatusCode: code,
640+ Result: map[string]string{
641+ "message": message,
642+ "kind": kind,
643+ },
644+ }
645+}
646+
647+func makeResponse(status int, result map[string]string) *serviceResponse {
648+ resp := &serviceResponse{
649+ Type: "sync",
650+ Status: http.StatusText(status),
651+ StatusCode: status,
652+ Result: result,
653+ }
654+
655+ if resp.Result == nil {
656+ resp.Result = make(map[string]string)
657+ }
658+
659+ return resp
660+}
661+
662+func sendHTTPResponse(writer http.ResponseWriter, response *serviceResponse) {
663+ writer.WriteHeader(response.StatusCode)
664+ data, _ := json.Marshal(response)
665+ fmt.Fprintln(writer, string(data))
666+}
667+
668+func getConfigOnPath(path string) string {
669+ return filepath.Join(path, "config")
670+}
671+
672+// Array of paths where the config file can be found.
673+// The first one is readonly, the others are writable
674+// they are readed in order and the configuration is merged
675+var configurationPaths = []string{
676+ filepath.Join(os.Getenv("SNAP"), "conf", "default-config"),
677+ getConfigOnPath(os.Getenv("SNAP_DATA")),
678+ getConfigOnPath(os.Getenv("SNAP_USER_DATA"))}
679+
680+const (
681+ servicePort = 5005
682+ configurationV1Uri = "/v1/configuration"
683+)
684+
685+func main() {
686+ r := mux.NewRouter().StrictSlash(true)
687+
688+ r.HandleFunc(configurationV1Uri, getConfiguration).Methods(http.MethodGet)
689+ r.HandleFunc(configurationV1Uri, changeConfiguration).Methods(http.MethodPost)
690+
691+ log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(servicePort), r))
692+}
693+
694+// Convert eg. WIFI_OPERATION_MODE to wifi.operation-mode
695+func convertKeyToRepresentationFormat(key string) string {
696+ newKey := strings.ToLower(key)
697+ newKey = strings.Replace(newKey, "_", ".", 1)
698+ return strings.Replace(newKey, "_", "-", -1)
699+}
700+
701+func convertKeyToStorageFormat(key string) string {
702+ // Convert eg. wifi.operation-mode to WIFI_OPERATION_MODE
703+ newKey := strings.ToUpper(key)
704+ newKey = strings.Replace(newKey, ".", "_", -1)
705+ return strings.Replace(newKey, "-", "_", -1)
706+}
707+
708+func readConfigurationFile(filePath string, config map[string]string) (err error) {
709+ file, err := os.Open(filePath)
710+ if err != nil {
711+ return nil
712+ }
713+
714+ defer file.Close()
715+
716+ for scanner := bufio.NewScanner(file); scanner.Scan(); {
717+ // Ignore all empty or commented lines
718+ if line := scanner.Text(); len(line) != 0 && line[0] != '#' {
719+ // Line must be in the KEY=VALUE format
720+ if parts := strings.Split(line, "="); len(parts) == 2 {
721+ value := unescapeTextByShell(parts[1])
722+ config[convertKeyToRepresentationFormat(parts[0])] = value
723+ }
724+ }
725+ }
726+
727+ return nil
728+}
729+
730+func readConfiguration(paths []string, config map[string]string) (err error) {
731+ for _, location := range paths {
732+ if readConfigurationFile(location, config) != nil {
733+ return fmt.Errorf("Failed to read configuration file '%s'", location)
734+ }
735+ }
736+
737+ return nil
738+}
739+
740+func getConfiguration(writer http.ResponseWriter, request *http.Request) {
741+ config := make(map[string]string)
742+ if readConfiguration(configurationPaths, config) == nil {
743+ sendHTTPResponse(writer, makeResponse(http.StatusOK, config))
744+ } else {
745+ errResponse := makeErrorResponse(http.StatusInternalServerError, "Failed to read configuration data", "internal-error")
746+ sendHTTPResponse(writer, errResponse)
747+ }
748+}
749+
750+// Escape shell special characters, avoid injection
751+// eg. SSID set to "My AP$(nc -lp 2323 -e /bin/sh)"
752+// to get a root shell
753+func escapeTextForShell(input string) string {
754+ if strings.ContainsAny(input, "\\\"'`$\n\t #") {
755+ input = strings.Replace(input, `\`, `\\`, -1)
756+ input = strings.Replace(input, `"`, `\"`, -1)
757+ input = strings.Replace(input, "`", "\\`", -1)
758+ input = strings.Replace(input, `$`, `\$`, -1)
759+
760+ input = `"` + input + `"`
761+ }
762+ return input
763+}
764+
765+// Do the reverse of escapeTextForShell() here
766+// strip any \ followed by \$`"
767+func unescapeTextByShell(input string) string {
768+ input = strings.Trim(input, `"'`)
769+ if strings.ContainsAny(input, "\\") {
770+ re := regexp.MustCompile("\\\\([\\\\$\\`\\\"])")
771+ input = re.ReplaceAllString(input, "$1")
772+ }
773+ return input
774+}
775+
776+func changeConfiguration(writer http.ResponseWriter, request *http.Request) {
777+ // Write in SNAP_DATA
778+ confWrite := getConfigOnPath(os.Getenv("SNAP_DATA"))
779+
780+ file, err := os.Create(confWrite)
781+ if err != nil {
782+ errResponse := makeErrorResponse(http.StatusInternalServerError, "Can't write configuration file", "internal-error")
783+ sendHTTPResponse(writer, errResponse)
784+ return
785+ }
786+ defer file.Close()
787+
788+ body, err := ioutil.ReadAll(request.Body)
789+ if err != nil {
790+ errResponse := makeErrorResponse(http.StatusInternalServerError, "Error reading the request body", "internal-error")
791+ sendHTTPResponse(writer, errResponse)
792+ return
793+ }
794+
795+ var items map[string]string
796+ if json.Unmarshal(body, &items) != nil {
797+ errResponse := makeErrorResponse(http.StatusInternalServerError, "Malformed request", "internal-error")
798+ sendHTTPResponse(writer, errResponse)
799+ return
800+ }
801+
802+ for key, value := range items {
803+ key = convertKeyToStorageFormat(key)
804+ value = escapeTextForShell(value)
805+ file.WriteString(fmt.Sprintf("%s=%s\n", key, value))
806+ }
807+
808+ sendHTTPResponse(writer, makeResponse(http.StatusOK, nil))
809+}
810diff --git a/cmd/service/main_test.go b/cmd/service/main_test.go
811new file mode 100644
812index 0000000..b0db78d
813--- /dev/null
814+++ b/cmd/service/main_test.go
815@@ -0,0 +1,210 @@
816+//
817+// Copyright (C) 2016 Canonical Ltd
818+//
819+// This program is free software: you can redistribute it and/or modify
820+// it under the terms of the GNU General Public License version 3 as
821+// published by the Free Software Foundation.
822+//
823+// This program is distributed in the hope that it will be useful,
824+// but WITHOUT ANY WARRANTY; without even the implied warranty of
825+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
826+// GNU General Public License for more details.
827+//
828+// You should have received a copy of the GNU General Public License
829+// along with this program. If not, see <http://www.gnu.org/licenses/>.
830+
831+package main
832+
833+import (
834+ "bytes"
835+ "encoding/json"
836+ "io/ioutil"
837+ "net/http"
838+ "net/http/httptest"
839+ "os"
840+ "strings"
841+ "testing"
842+
843+ "gopkg.in/check.v1"
844+)
845+
846+// gopkg.in/check.v1 stuff
847+func Test(t *testing.T) { check.TestingT(t) }
848+
849+type S struct{}
850+
851+var _ = check.Suite(&S{})
852+
853+// Test the config file path append routine
854+func (s *S) TestPath(c *check.C) {
855+ c.Assert(getConfigOnPath("/test"), check.Equals, "/test/config")
856+}
857+
858+// List of tokens to be translated
859+var cfgKeys = [...][2]string{
860+ {"DISABLED", "disabled"},
861+ {"WIFI_SSID", "wifi.ssid"},
862+ {"WIFI_INTERFACE", "wifi.interface"},
863+ {"WIFI_INTERFACE_MODE", "wifi.interface-mode"},
864+ {"DHCP_RANGE_START", "dhcp.range-start"},
865+ {"MYTOKEN", "mytoken"},
866+ {"CFG_TOKEN", "cfg.token"},
867+ {"MY_TOKEN$", "my.token$"},
868+}
869+
870+// Test token conversion from internal format
871+func (s *S) TestConvertKeyToRepresentationFormat(c *check.C) {
872+ for _, st := range cfgKeys {
873+ c.Assert(convertKeyToRepresentationFormat(st[0]), check.Equals, st[1])
874+ }
875+}
876+
877+// Test token conversion to internal format
878+func (s *S) TestConvertKeyToStorageFormat(c *check.C) {
879+ for _, st := range cfgKeys {
880+ c.Assert(convertKeyToStorageFormat(st[1]), check.Equals, st[0])
881+ }
882+}
883+
884+// List of malicious tokens which needs to be escaped
885+func (s *S) TestEscapeShell(c *check.C) {
886+ cmds := [...][2]string{
887+ {"my_ap", "my_ap"},
888+ {`my ap`, `"my ap"`},
889+ {`my "ap"`, `"my \"ap\""`},
890+ {`$(ps ax)`, `"\$(ps ax)"`},
891+ {"`ls /`", "\"\\`ls /\\`\""},
892+ {`c:\dir`, `"c:\\dir"`},
893+ }
894+ for _, st := range cmds {
895+ c.Assert(escapeTextForShell(st[0]), check.Equals, st[1])
896+ }
897+}
898+
899+func (s *S) TestGetConfiguration(c *check.C) {
900+ // Check it we get a valid JSON as configuration
901+ req, err := http.NewRequest(http.MethodGet, configurationV1Uri, nil)
902+ c.Assert(err, check.IsNil)
903+
904+ rec := httptest.NewRecorder()
905+
906+ getConfiguration(rec, req)
907+
908+ body, err := ioutil.ReadAll(rec.Body)
909+ c.Assert(err, check.IsNil)
910+
911+ // Parse the returned JSON
912+ var resp serviceResponse
913+ err = json.Unmarshal(body, &resp)
914+ c.Assert(err, check.IsNil)
915+
916+ // Check for 200 status code
917+ c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
918+ c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
919+ c.Assert(resp.Type, check.Equals, "sync")
920+}
921+
922+func (s *S) TestChangeConfiguration(c *check.C) {
923+ // Test a non writable path:
924+ os.Setenv("SNAP_DATA", "/nodir")
925+
926+ req, err := http.NewRequest(http.MethodPost, configurationV1Uri, nil)
927+ c.Assert(err, check.IsNil)
928+
929+ rec := httptest.NewRecorder()
930+
931+ changeConfiguration(rec, req)
932+
933+ c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
934+
935+ body, err := ioutil.ReadAll(rec.Body)
936+ c.Assert(err, check.IsNil)
937+
938+ // Parse the returned JSON
939+ var resp serviceResponse
940+ err = json.Unmarshal(body, &resp)
941+ c.Assert(err, check.IsNil)
942+
943+ // Check for 500 status code and other error fields
944+ c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
945+ c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
946+ c.Assert(resp.Type, check.Equals, "error")
947+ c.Assert(resp.Result["kind"], check.Equals, "internal-error")
948+ c.Assert(resp.Result["message"], check.Equals, "Can't write configuration file")
949+
950+ // Test an invalid JSON
951+ os.Setenv("SNAP_DATA", "/tmp")
952+ req, err = http.NewRequest(http.MethodPost, configurationV1Uri, strings.NewReader("not a JSON content"))
953+ c.Assert(err, check.IsNil)
954+
955+ rec = httptest.NewRecorder()
956+
957+ changeConfiguration(rec, req)
958+
959+ c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
960+
961+ body, err = ioutil.ReadAll(rec.Body)
962+ c.Assert(err, check.IsNil)
963+
964+ // Parse the returned JSON
965+ resp = serviceResponse{}
966+ err = json.Unmarshal(body, &resp)
967+ c.Assert(err, check.IsNil)
968+
969+ // Check for 500 status code and other error fields
970+ c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
971+ c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
972+ c.Assert(resp.Type, check.Equals, "error")
973+ c.Assert(resp.Result["kind"], check.Equals, "internal-error")
974+ c.Assert(resp.Result["message"], check.Equals, "Malformed request")
975+
976+ // Test a succesful configuration set
977+
978+ // Values to be used in the config
979+ values := map[string]string{
980+ "wifi.security": "wpa2",
981+ "wifi.ssid": "UbuntuAP",
982+ "wifi.security-passphrase": "12345678",
983+ }
984+
985+ // Convert the map into JSON
986+ args, err := json.Marshal(values)
987+ c.Assert(err, check.IsNil)
988+
989+ req, err = http.NewRequest(http.MethodPost, configurationV1Uri, bytes.NewReader(args))
990+ c.Assert(err, check.IsNil)
991+
992+ rec = httptest.NewRecorder()
993+
994+ // Do the request
995+ changeConfiguration(rec, req)
996+
997+ c.Assert(rec.Code, check.Equals, http.StatusOK)
998+
999+ // Read the result JSON
1000+ body, err = ioutil.ReadAll(rec.Body)
1001+ c.Assert(err, check.IsNil)
1002+
1003+ // Parse the returned JSON
1004+ resp = serviceResponse{}
1005+ err = json.Unmarshal(body, &resp)
1006+ c.Assert(err, check.IsNil)
1007+
1008+ // Check for 200 status code and other succesful fields
1009+ c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
1010+ c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
1011+ c.Assert(resp.Type, check.Equals, "sync")
1012+
1013+ // Read the generated config and check that values were set
1014+ config, err := ioutil.ReadFile(getConfigOnPath(os.Getenv("SNAP_DATA")))
1015+ c.Assert(err, check.IsNil)
1016+
1017+ for key, value := range values {
1018+ c.Assert(strings.Contains(string(config),
1019+ convertKeyToStorageFormat(key)+"="+value+"\n"),
1020+ check.Equals, true)
1021+ }
1022+
1023+ // don't leave garbage in /tmp
1024+ os.Remove(getConfigOnPath(os.Getenv("SNAP_DATA")))
1025+}
1026diff --git a/service/main.go b/service/main.go
1027deleted file mode 100644
1028index 17134ed..0000000
1029--- a/service/main.go
1030+++ /dev/null
1031@@ -1,217 +0,0 @@
1032-//
1033-// Copyright (C) 2016 Canonical Ltd
1034-//
1035-// This program is free software: you can redistribute it and/or modify
1036-// it under the terms of the GNU General Public License version 3 as
1037-// published by the Free Software Foundation.
1038-//
1039-// This program is distributed in the hope that it will be useful,
1040-// but WITHOUT ANY WARRANTY; without even the implied warranty of
1041-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1042-// GNU General Public License for more details.
1043-//
1044-// You should have received a copy of the GNU General Public License
1045-// along with this program. If not, see <http://www.gnu.org/licenses/>.
1046-
1047-package main
1048-
1049-import (
1050- "bufio"
1051- "encoding/json"
1052- "fmt"
1053- "io/ioutil"
1054- "log"
1055- "net/http"
1056- "os"
1057- "regexp"
1058- "strconv"
1059- "strings"
1060-
1061- "github.com/gorilla/mux"
1062-)
1063-
1064-/* JSON message format, as described here:
1065-{
1066- "result": {
1067- "key" : "val"
1068- },
1069- "status": "OK",
1070- "status-code": 200,
1071- "type": "sync"
1072-}
1073-*/
1074-
1075-type Response struct {
1076- Result map[string]string `json:"result"`
1077- Status string `json:"status"`
1078- StatusCode int `json:"status-code"`
1079- Type string `json:"type"`
1080-}
1081-
1082-func makeErrorResponse(code int, message, kind string) Response {
1083- return Response{
1084- Type: "error",
1085- Status: http.StatusText(code),
1086- StatusCode: code,
1087- Result: map[string]string{
1088- "message": message,
1089- "kind": kind,
1090- },
1091- }
1092-}
1093-
1094-func makeResponse(status int, result map[string]string) Response {
1095- return Response{
1096- Type: "sync",
1097- Status: http.StatusText(status),
1098- StatusCode: status,
1099- Result: result,
1100- }
1101-}
1102-
1103-func sendHttpResponse(writer http.ResponseWriter, response Response) {
1104- writer.WriteHeader(response.StatusCode)
1105- data, _ := json.Marshal(response)
1106- fmt.Fprintln(writer, string(data))
1107-}
1108-
1109-func getConfigOnPath(path string) string {
1110- return path + "/config"
1111-}
1112-
1113-// Array of paths where the config file can be found.
1114-// The first one is readonly, the others are writable
1115-// they are readed in order and the configuration is merged
1116-var cfgpaths []string = []string{getConfigOnPath(os.Getenv("SNAP")),
1117- getConfigOnPath(os.Getenv("SNAP_DATA")), getConfigOnPath(os.Getenv("SNAP_USER_DATA"))}
1118-
1119-const (
1120- PORT = 5005
1121- CONFIGURATION_API_URI = "/v1/configuration"
1122-)
1123-
1124-func main() {
1125- r := mux.NewRouter().StrictSlash(true)
1126-
1127- r.HandleFunc(CONFIGURATION_API_URI, getConfiguration).Methods(http.MethodGet)
1128- r.HandleFunc(CONFIGURATION_API_URI, changeConfiguration).Methods(http.MethodPost)
1129-
1130- log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(PORT), r))
1131-}
1132-
1133-// Convert eg. WIFI_OPERATION_MODE to wifi.operation-mode
1134-func convertKeyToRepresentationFormat(key string) string {
1135- new_key := strings.ToLower(key)
1136- new_key = strings.Replace(new_key, "_", ".", 1)
1137- return strings.Replace(new_key, "_", "-", -1)
1138-}
1139-
1140-func convertKeyToStorageFormat(key string) string {
1141- // Convert eg. wifi.operation-mode to WIFI_OPERATION_MODE
1142- key = strings.ToUpper(key)
1143- key = strings.Replace(key, ".", "_", -1)
1144- return strings.Replace(key, "-", "_", -1)
1145-}
1146-
1147-func readConfigurationFile(filePath string, config map[string]string) (err error) {
1148- file, err := os.Open(filePath)
1149- if err != nil {
1150- return nil
1151- }
1152-
1153- defer file.Close()
1154-
1155- for scanner := bufio.NewScanner(file); scanner.Scan(); {
1156- // Ignore all empty or commented lines
1157- if line := scanner.Text(); len(line) != 0 && line[0] != '#' {
1158- // Line must be in the KEY=VALUE format
1159- if parts := strings.Split(line, "="); len(parts) == 2 {
1160- value := unescapeTextByShell(parts[1])
1161- config[convertKeyToRepresentationFormat(parts[0])] = value
1162- }
1163- }
1164- }
1165-
1166- return nil
1167-}
1168-
1169-func readConfiguration(paths []string, config map[string]string) (err error) {
1170- for _, location := range paths {
1171- if readConfigurationFile(location, config) != nil {
1172- return fmt.Errorf("Failed to read configuration file '%s'", location)
1173- }
1174- }
1175-
1176- return nil
1177-}
1178-
1179-func getConfiguration(writer http.ResponseWriter, request *http.Request) {
1180- config := make(map[string]string)
1181- if readConfiguration(cfgpaths, config) == nil {
1182- sendHttpResponse(writer, makeResponse(http.StatusOK, config))
1183- } else {
1184- errResponse := makeErrorResponse(http.StatusInternalServerError, "Failed to read configuration data", "internal-error")
1185- sendHttpResponse(writer, errResponse)
1186- }
1187-}
1188-
1189-// Escape shell special characters, avoid injection
1190-// eg. SSID set to "My AP$(nc -lp 2323 -e /bin/sh)"
1191-// to get a root shell
1192-func escapeTextForShell(input string) string {
1193- if strings.ContainsAny(input, "\\\"'`$\n\t #") {
1194- input = strings.Replace(input, `\`, `\\`, -1)
1195- input = strings.Replace(input, `"`, `\"`, -1)
1196- input = strings.Replace(input, "`", "\\`", -1)
1197- input = strings.Replace(input, `$`, `\$`, -1)
1198-
1199- input = `"` + input + `"`
1200- }
1201- return input
1202-}
1203-
1204-// Do the reverse of escapeTextForShell() here
1205-// strip any \ followed by \$`"
1206-func unescapeTextByShell(input string) string {
1207- input = strings.Trim(input, `"'`)
1208- if strings.ContainsAny(input, "\\") {
1209- re := regexp.MustCompile("\\\\([\\\\$\\`\\\"])")
1210- input = re.ReplaceAllString(input, "$1")
1211- }
1212- return input
1213-}
1214-
1215-func changeConfiguration(writer http.ResponseWriter, request *http.Request) {
1216- // Write in SNAP_DATA
1217- confWrite := getConfigOnPath(os.Getenv("SNAP_DATA"))
1218-
1219- file, err := os.Create(confWrite)
1220- if err != nil {
1221- errResponse := makeErrorResponse(http.StatusInternalServerError, "Can't write configuration file", "internal-error")
1222- sendHttpResponse(writer, errResponse)
1223- return
1224- }
1225- defer file.Close()
1226-
1227- body, err := ioutil.ReadAll(request.Body)
1228- if err != nil {
1229- errResponse := makeErrorResponse(http.StatusInternalServerError, "Error reading the request body", "internal-error")
1230- sendHttpResponse(writer, errResponse)
1231- return
1232- }
1233-
1234- var items map[string]string
1235- if json.Unmarshal(body, &items) != nil {
1236- errResponse := makeErrorResponse(http.StatusInternalServerError, "Malformed request", "internal-error")
1237- sendHttpResponse(writer, errResponse)
1238- return
1239- }
1240-
1241- for key, value := range items {
1242- key = convertKeyToStorageFormat(key)
1243- value = escapeTextForShell(value)
1244- file.WriteString(fmt.Sprintf("%s=%s\n", key, value))
1245- }
1246-
1247- sendHttpResponse(writer, makeResponse(http.StatusOK, nil))
1248-}
1249diff --git a/service/main_test.go b/service/main_test.go
1250deleted file mode 100644
1251index d15430b..0000000
1252--- a/service/main_test.go
1253+++ /dev/null
1254@@ -1,209 +0,0 @@
1255-//
1256-// Copyright (C) 2016 Canonical Ltd
1257-//
1258-// This program is free software: you can redistribute it and/or modify
1259-// it under the terms of the GNU General Public License version 3 as
1260-// published by the Free Software Foundation.
1261-//
1262-// This program is distributed in the hope that it will be useful,
1263-// but WITHOUT ANY WARRANTY; without even the implied warranty of
1264-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1265-// GNU General Public License for more details.
1266-//
1267-// You should have received a copy of the GNU General Public License
1268-// along with this program. If not, see <http://www.gnu.org/licenses/>.
1269-
1270-package main
1271-
1272-import (
1273- "bytes"
1274- "encoding/json"
1275- "gopkg.in/check.v1"
1276- "io/ioutil"
1277- "net/http"
1278- "net/http/httptest"
1279- "os"
1280- "strings"
1281- "testing"
1282-)
1283-
1284-// gopkg.in/check.v1 stuff
1285-func Test(t *testing.T) { check.TestingT(t) }
1286-
1287-type S struct{}
1288-
1289-var _ = check.Suite(&S{})
1290-
1291-// Test the config file path append routine
1292-func (s *S) TestPath(c *check.C) {
1293- c.Assert(getConfigOnPath("/test"), check.Equals, "/test/config")
1294-}
1295-
1296-// List of tokens to be translated
1297-var cfgKeys = [...][2]string{
1298- {"DISABLED", "disabled"},
1299- {"WIFI_SSID", "wifi.ssid"},
1300- {"WIFI_INTERFACE", "wifi.interface"},
1301- {"WIFI_INTERFACE_MODE", "wifi.interface-mode"},
1302- {"DHCP_RANGE_START", "dhcp.range-start"},
1303- {"MYTOKEN", "mytoken"},
1304- {"CFG_TOKEN", "cfg.token"},
1305- {"MY_TOKEN$", "my.token$"},
1306-}
1307-
1308-// Test token conversion from internal format
1309-func (s *S) TestConvertKeyToRepresentationFormat(c *check.C) {
1310- for _, st := range cfgKeys {
1311- c.Assert(convertKeyToRepresentationFormat(st[0]), check.Equals, st[1])
1312- }
1313-}
1314-
1315-// Test token conversion to internal format
1316-func (s *S) TestConvertKeyToStorageFormat(c *check.C) {
1317- for _, st := range cfgKeys {
1318- c.Assert(convertKeyToStorageFormat(st[1]), check.Equals, st[0])
1319- }
1320-}
1321-
1322-// List of malicious tokens which needs to be escaped
1323-func (s *S) TestEscapeShell(c *check.C) {
1324- cmds := [...][2]string{
1325- {"my_ap", "my_ap"},
1326- {`my ap`, `"my ap"`},
1327- {`my "ap"`, `"my \"ap\""`},
1328- {`$(ps ax)`, `"\$(ps ax)"`},
1329- {"`ls /`", "\"\\`ls /\\`\""},
1330- {`c:\dir`, `"c:\\dir"`},
1331- }
1332- for _, st := range cmds {
1333- c.Assert(escapeTextForShell(st[0]), check.Equals, st[1])
1334- }
1335-}
1336-
1337-func (s *S) TestGetConfiguration(c *check.C) {
1338- // Check it we get a valid JSON as configuration
1339- req, err := http.NewRequest(http.MethodGet, CONFIGURATION_API_URI, nil)
1340- c.Assert(err, check.IsNil)
1341-
1342- rec := httptest.NewRecorder()
1343-
1344- getConfiguration(rec, req)
1345-
1346- body, err := ioutil.ReadAll(rec.Result().Body)
1347- c.Assert(err, check.IsNil)
1348-
1349- // Parse the returned JSON
1350- var resp Response
1351- err = json.Unmarshal(body, &resp)
1352- c.Assert(err, check.IsNil)
1353-
1354- // Check for 200 status code
1355- c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
1356- c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
1357- c.Assert(resp.Type, check.Equals, "sync")
1358-}
1359-
1360-func (s *S) TestChangeConfiguration(c *check.C) {
1361- // Test a non writable path:
1362- os.Setenv("SNAP_DATA", "/nodir")
1363-
1364- req, err := http.NewRequest(http.MethodPost, CONFIGURATION_API_URI, nil)
1365- c.Assert(err, check.IsNil)
1366-
1367- rec := httptest.NewRecorder()
1368-
1369- changeConfiguration(rec, req)
1370-
1371- c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
1372-
1373- body, err := ioutil.ReadAll(rec.Result().Body)
1374- c.Assert(err, check.IsNil)
1375-
1376- // Parse the returned JSON
1377- var resp Response
1378- err = json.Unmarshal(body, &resp)
1379- c.Assert(err, check.IsNil)
1380-
1381- // Check for 500 status code and other error fields
1382- c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
1383- c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
1384- c.Assert(resp.Type, check.Equals, "error")
1385- c.Assert(resp.Result["kind"], check.Equals, "internal-error")
1386- c.Assert(resp.Result["message"], check.Equals, "Can't write configuration file")
1387-
1388- // Test an invalid JSON
1389- os.Setenv("SNAP_DATA", "/tmp")
1390- req, err = http.NewRequest(http.MethodPost, CONFIGURATION_API_URI, strings.NewReader("not a JSON content"))
1391- c.Assert(err, check.IsNil)
1392-
1393- rec = httptest.NewRecorder()
1394-
1395- changeConfiguration(rec, req)
1396-
1397- c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
1398-
1399- body, err = ioutil.ReadAll(rec.Result().Body)
1400- c.Assert(err, check.IsNil)
1401-
1402- // Parse the returned JSON
1403- resp = Response{}
1404- err = json.Unmarshal(body, &resp)
1405- c.Assert(err, check.IsNil)
1406-
1407- // Check for 500 status code and other error fields
1408- c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
1409- c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
1410- c.Assert(resp.Type, check.Equals, "error")
1411- c.Assert(resp.Result["kind"], check.Equals, "internal-error")
1412- c.Assert(resp.Result["message"], check.Equals, "Malformed request")
1413-
1414- // Test a succesful configuration set
1415-
1416- // Values to be used in the config
1417- values := map[string]string{
1418- "wifi.security": "wpa2",
1419- "wifi.ssid": "UbuntuAP",
1420- "wifi.security-passphrase": "12345678",
1421- }
1422-
1423- // Convert the map into JSON
1424- args, err := json.Marshal(values)
1425- c.Assert(err, check.IsNil)
1426-
1427- req, err = http.NewRequest(http.MethodPost, CONFIGURATION_API_URI, bytes.NewReader(args))
1428- c.Assert(err, check.IsNil)
1429-
1430- rec = httptest.NewRecorder()
1431-
1432- // Do the request
1433- changeConfiguration(rec, req)
1434-
1435- c.Assert(rec.Code, check.Equals, http.StatusOK)
1436-
1437- // Read the result JSON
1438- body, err = ioutil.ReadAll(rec.Result().Body)
1439- c.Assert(err, check.IsNil)
1440-
1441- // Parse the returned JSON
1442- resp = Response{}
1443- err = json.Unmarshal(body, &resp)
1444- c.Assert(err, check.IsNil)
1445-
1446- // Check for 200 status code and other succesful fields
1447- c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
1448- c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
1449- c.Assert(resp.Type, check.Equals, "sync")
1450-
1451- // Read the generated config and check that values were set
1452- config, err := ioutil.ReadFile(getConfigOnPath(os.Getenv("SNAP_DATA")))
1453- c.Assert(err, check.IsNil)
1454-
1455- for key, value := range values {
1456- c.Assert(strings.Contains(string(config),
1457- convertKeyToStorageFormat(key)+"="+value+"\n"),
1458- check.Equals, true)
1459- }
1460-
1461- // don't leave garbage in /tmp
1462- os.Remove(getConfigOnPath(os.Getenv("SNAP_DATA")))
1463-}
1464diff --git a/snapcraft.yaml b/snapcraft.yaml
1465index d2ded89..479b348 100644
1466--- a/snapcraft.yaml
1467+++ b/snapcraft.yaml
1468@@ -8,6 +8,7 @@ description: |
1469 easily connect to.
1470 Please find the source of this snap at:
1471 https://code.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap
1472+grade: stable
1473
1474 apps:
1475 backend:
1476@@ -19,17 +20,20 @@ apps:
1477 - network-bind
1478 - network-manager
1479 config:
1480- command: bin/config.sh
1481+ command: bin/client config
1482+ plugs:
1483+ - network
1484 management-service:
1485 command: bin/service
1486 daemon: simple
1487+ plugs:
1488+ - network-bind
1489
1490 parts:
1491 scripts:
1492 plugin: dump
1493 source: .
1494 snap:
1495- - bin/config.sh
1496 - bin/config-internal.sh
1497 - bin/ap.sh
1498 - bin/helper.sh
1499@@ -53,9 +57,15 @@ parts:
1500 snap:
1501 - $binaries
1502
1503+ config:
1504+ plugin: go
1505+ source: cmd/client
1506+ snap:
1507+ - bin
1508+
1509 management-service:
1510 plugin: go
1511- source: ./service
1512+ source: cmd/service
1513 snap:
1514 - bin
1515

Subscribers

People subscribed via source and target branches

to all changes: