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
diff --git a/MAINTAINERS b/MAINTAINERS
index 420159f..1fed46c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1 +1,2 @@
1Simon Fels <simon.fels@canonical.com>1Simon Fels <simon.fels@canonical.com>
2Matteo Croce <matteo.croce@canonical.com>
diff --git a/README.md b/README.md
index 3c89bcd..1a49a98 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,41 @@ This snap provided WiFi AP functionality out of the box.
5Documentation is currently available at5Documentation is currently available at
6https://docs.google.com/document/d/1vNu3fBqpOkBkjv_Vs9NZyTv50vOEfugrQqgxD0_f0rE/edit#6https://docs.google.com/document/d/1vNu3fBqpOkBkjv_Vs9NZyTv50vOEfugrQqgxD0_f0rE/edit#
77
8## Development
9
10To modify any of the included services written in Go you need setup
11your build environment first.
12
13```
14 $ snapcraft clean
15 $ snapcraft
16```
17
18Now we need to export the GOPATH and point it to the directory
19snapcraft already created for us.
20
21```
22 $ export GOPATH=$PWD/parts/management-service/go
23```
24
25Now you can build the management-service by running
26
27```
28 $ cd cmd/service
29 $ go build -o management-service *.go
30```
31
32If you want to start it afterwards outside of a snap environment you
33need to setup the right environment variables.
34
35```
36 # Needs to be the top source dir which contains the snapcraft.yaml
37 $ export SNAP=`pwd`
38 $ mkdir tmp-data
39 $ export SNAP_DATA=$PWD/tmp-data
40 $ cmd/service/management-service
41```
42
8## Running tests43## Running tests
944
10We have a set of spread (https://github.com/snapcore/spread) tests which45We have a set of spread (https://github.com/snapcore/spread) tests which
diff --git a/bin/ap.sh b/bin/ap.sh
index 54c12ac..be8dac4 100755
--- a/bin/ap.sh
+++ b/bin/ap.sh
@@ -39,10 +39,18 @@ if ! ifconfig $WIFI_INTERFACE ; then
39fi39fi
4040
41cleanup_on_exit() {41cleanup_on_exit() {
42 read HOSTAPD_PID <$SNAP_DATA/hostapd.pid
43 if [ -n "$HOSTAPD_PID" ] ; then
44 kill -TERM $HOSTAPD_PID || true
45 wait $HOSTAPD_PID
46 fi
47
42 read DNSMASQ_PID <$SNAP_DATA/dnsmasq.pid48 read DNSMASQ_PID <$SNAP_DATA/dnsmasq.pid
43 # If dnsmasq is already gone don't error out here49 if [ -n "$DNSMASQ_PID" ] ; then
44 kill -TERM $DNSMASQ_PID || true50 # If dnsmasq is already gone don't error out here
45 wait $DNSMASQ_PID51 kill -TERM $DNSMASQ_PID || true
52 wait $DNSMASQ_PID
53 fi
4654
47 iface=$WIFI_INTERFACE55 iface=$WIFI_INTERFACE
48 if [ "$WIFI_INTERFACE_MODE" = "virtual" ] ; then56 if [ "$WIFI_INTERFACE_MODE" = "virtual" ] ; then
@@ -56,18 +64,22 @@ cleanup_on_exit() {
56 sysctl -w net.ipv4.ip_forward=064 sysctl -w net.ipv4.ip_forward=0
57 fi65 fi
5866
59 if [ "$WIFI_INTERFACE_MODE" = "virtual" ] && does_interface_exist $iface ; then
60 $SNAP/bin/iw dev $iface del
61 fi
62
63 if is_nm_running ; then67 if is_nm_running ; then
64 # Hand interface back to network-manager. This will also trigger the68 # Hand interface back to network-manager. This will also trigger the
65 # auto connection process inside network-manager to get connected69 # auto connection process inside network-manager to get connected
66 # with the previous network.70 # with the previous network.
67 $SNAP/bin/nmcli d set $iface managed yes71 $SNAP/bin/nmcli d set $iface managed yes
68 fi72 fi
73
74 if [ "$WIFI_INTERFACE_MODE" = "virtual" ] ; then
75 $SNAP/bin/iw dev $iface del
76 fi
69}77}
7078
79# We need to install this right before we do anything to
80# ensure that we cleanup everything again when we termiante.
81trap cleanup_on_exit TERM
82
71iface=$WIFI_INTERFACE83iface=$WIFI_INTERFACE
72if [ "$WIFI_INTERFACE_MODE" = "virtual" ] ; then84if [ "$WIFI_INTERFACE_MODE" = "virtual" ] ; then
73 iface=$DEFAULT_ACCESS_POINT_INTERFACE85 iface=$DEFAULT_ACCESS_POINT_INTERFACE
@@ -139,7 +151,13 @@ if [ $SHARE_DISABLED -eq 0 ] ; then
139fi151fi
140152
141generate_dnsmasq_config $SNAP_DATA/dnsmasq.conf153generate_dnsmasq_config $SNAP_DATA/dnsmasq.conf
142$SNAP/bin/dnsmasq -k -C $SNAP_DATA/dnsmasq.conf -l $SNAP_DATA/dnsmasq.leases -x $SNAP_DATA/dnsmasq.pid &154$SNAP/bin/dnsmasq \
155 -k \
156 -C $SNAP_DATA/dnsmasq.conf \
157 -l $SNAP_DATA/dnsmasq.leases \
158 -x $SNAP_DATA/dnsmasq.pid \
159 -u root -g root \
160 &
143161
144driver=$WIFI_HOSTAPD_DRIVER162driver=$WIFI_HOSTAPD_DRIVER
145if [ "$driver" = "rtl8188" ] ; then163if [ "$driver" = "rtl8188" ] ; then
@@ -219,6 +237,10 @@ case "$WIFI_HOSTAPD_DRIVER" in
219esac237esac
220238
221# Startup hostapd with the configuration we've put in place239# Startup hostapd with the configuration we've put in place
222$hostapd $EXTRA_ARGS $SNAP_DATA/hostapd.conf240$hostapd $EXTRA_ARGS $SNAP_DATA/hostapd.conf &
241hostapd_pid=$!
242echo $hostapd_pid > $SNAP_DATA/hostapd.pid
243wait $hostapd_pid
244
223cleanup_on_exit245cleanup_on_exit
224exit 0246exit 0
diff --git a/bin/helper.sh b/bin/helper.sh
index 718a2d6..4a557d6 100644
--- a/bin/helper.sh
+++ b/bin/helper.sh
@@ -56,6 +56,5 @@ generate_dnsmasq_config() {
5656
57is_nm_running() {57is_nm_running() {
58 nm_status=`$SNAP/bin/nmcli -t -f RUNNING general`58 nm_status=`$SNAP/bin/nmcli -t -f RUNNING general`
59 [ "$nm_status" = "running" ] && return 159 [ "$nm_status" = "running" ]
60 return 0
61}60}
diff --git a/cmd/client/client.go b/cmd/client/client.go
index 0a770d7..ec8bb76 100644
--- a/cmd/client/client.go
+++ b/cmd/client/client.go
@@ -25,6 +25,7 @@ import (
25const (25const (
26 servicePort = 500526 servicePort = 5005
27 configurationV1Uri = "/v1/configuration"27 configurationV1Uri = "/v1/configuration"
28 statusV1Uri = "/v1/status"
28)29)
2930
30type serviceResponse struct {31type serviceResponse struct {
@@ -38,6 +39,10 @@ func getServiceConfigurationURI() string {
38 return fmt.Sprintf("http://localhost:%d%s", servicePort, configurationV1Uri)39 return fmt.Sprintf("http://localhost:%d%s", servicePort, configurationV1Uri)
39}40}
4041
42func getServiceStatusURI() string {
43 return fmt.Sprintf("http://localhost:%d%s", servicePort, statusV1Uri)
44}
45
41type doer interface {46type doer interface {
42 Do(*http.Request) (*http.Response, error)47 Do(*http.Request) (*http.Response, error)
43}48}
diff --git a/cmd/client/cmd_config.go b/cmd/client/cmd_config.go
index 8a82f4a..f0d6b4c 100644
--- a/cmd/client/cmd_config.go
+++ b/cmd/client/cmd_config.go
@@ -20,7 +20,6 @@ import (
20 "encoding/json"20 "encoding/json"
21 "fmt"21 "fmt"
22 "os"22 "os"
23 "sort"
24)23)
2524
26type setCommand struct{}25type setCommand struct{}
@@ -58,14 +57,7 @@ func (cmd *getCommand) Execute(args []string) error {
58 return fmt.Errorf("Config item '%s' does not exist", wantedKey)57 return fmt.Errorf("Config item '%s' does not exist", wantedKey)
59 }58 }
60 } else {59 } else {
61 sortedKeys := make([]string, 0, len(response.Result))60 printMapSorted(response.Result)
62 for key, _ := range response.Result {
63 sortedKeys = append(sortedKeys, key)
64 }
65 sort.Strings(sortedKeys)
66 for n := range sortedKeys {
67 fmt.Fprintf(os.Stdout, "%s: %s\n", sortedKeys[n], response.Result[sortedKeys[n]])
68 }
69 }61 }
7062
71 return nil63 return nil
@@ -78,7 +70,7 @@ func (cmd *configCommand) Execute(args []string) error {
78}70}
7971
80func init() {72func init() {
81 cmdConfig, _ := addCommand("config", "Adjust the service configuration", "", &configCommand{})73 cmd, _ := addCommand("config", "Adjust the service configuration", "", &configCommand{})
82 cmdConfig.AddCommand("set", "", "", &setCommand{})74 cmd.AddCommand("set", "", "", &setCommand{})
83 cmdConfig.AddCommand("get", "", "", &getCommand{})75 cmd.AddCommand("get", "", "", &getCommand{})
84}76}
diff --git a/cmd/client/cmd_status.go b/cmd/client/cmd_status.go
85new file mode 10064477new file mode 100644
index 0000000..e120e47
--- /dev/null
+++ b/cmd/client/cmd_status.go
@@ -0,0 +1,58 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "bytes"
20 "encoding/json"
21)
22
23type restartCommand struct{}
24
25func (cmd *restartCommand) Execute(args []string) error {
26 req := make(map[string]string)
27 req["action"] = "restart-ap"
28
29 b, err := json.Marshal(req)
30 if err != nil {
31 return err
32 }
33
34 _, err = sendHTTPRequest(getServiceStatusURI(), "POST", bytes.NewReader(b))
35 if err != nil {
36 return err
37 }
38
39 return nil
40}
41
42type statusCommand struct{}
43
44func (cmd *statusCommand) Execute(args []string) error {
45 response, err := sendHTTPRequest(getServiceStatusURI(), "GET", nil)
46 if err != nil {
47 return err
48 }
49 printMapSorted(response.Result)
50 return nil
51}
52
53func init() {
54 cmd, _ := addCommand("status", "Show various status information about the access point", "", &statusCommand{})
55 cmd.SubcommandsOptional = true
56
57 cmd.AddCommand("restart-ap", "Restart access point", "", &restartCommand{})
58}
diff --git a/cmd/client/utils.go b/cmd/client/utils.go
0new file mode 10064459new file mode 100644
index 0000000..f5c55a3
--- /dev/null
+++ b/cmd/client/utils.go
@@ -0,0 +1,33 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "fmt"
20 "os"
21 "sort"
22)
23
24func printMapSorted(m map[string]string) {
25 sortedKeys := make([]string, 0, len(m))
26 for key, _ := range m {
27 sortedKeys = append(sortedKeys, key)
28 }
29 sort.Strings(sortedKeys)
30 for _, k := range sortedKeys {
31 fmt.Fprintf(os.Stdout, "%s: %s\n", k, m[k])
32 }
33}
diff --git a/cmd/service/api.go b/cmd/service/api.go
0new file mode 10064434new file mode 100644
index 0000000..5c4c6fd
--- /dev/null
+++ b/cmd/service/api.go
@@ -0,0 +1,176 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "encoding/json"
20 "fmt"
21 "io/ioutil"
22 "net/http"
23 "os"
24)
25
26var api = []*serviceCommand{
27 configurationCmd,
28 statusCmd,
29}
30
31var (
32 configurationCmd = &serviceCommand{
33 Path: "/v1/configuration",
34 GET: getConfiguration,
35 POST: postConfiguration,
36 }
37 statusCmd = &serviceCommand{
38 Path: "/v1/status",
39 GET: getStatus,
40 POST: postStatus,
41 }
42 validTokens map[string]bool
43)
44
45func getConfiguration(c *serviceCommand, writer http.ResponseWriter, request *http.Request) {
46 config := make(map[string]string)
47 if err := readConfiguration(configurationPaths, config); err == nil {
48 sendHTTPResponse(writer, makeResponse(http.StatusOK, config))
49 } else {
50 resp := makeErrorResponse(http.StatusInternalServerError, "Failed to read configuration data", "internal-error")
51 sendHTTPResponse(writer, resp)
52 }
53}
54
55func postConfiguration(c *serviceCommand, writer http.ResponseWriter, request *http.Request) {
56 path := getConfigOnPath(os.Getenv("SNAP_DATA"))
57 config := map[string]string{}
58 if readConfiguration([]string{path}, config) != nil {
59 resp := makeErrorResponse(http.StatusInternalServerError,
60 "Failed to read existing configuration file", "internal-error")
61 sendHTTPResponse(writer, resp)
62 return
63 }
64
65 if validTokens == nil || len(validTokens) == 0 {
66 errResponse := makeErrorResponse(http.StatusInternalServerError, "No default configuration file available", "internal-error")
67 sendHTTPResponse(writer, errResponse)
68 return
69 }
70
71 file, err := os.Create(path)
72 if err != nil {
73 resp := makeErrorResponse(http.StatusInternalServerError, "Can't write configuration file", "internal-error")
74 sendHTTPResponse(writer, resp)
75 return
76 }
77 defer file.Close()
78
79 body, err := ioutil.ReadAll(request.Body)
80 if err != nil {
81 resp := makeErrorResponse(http.StatusInternalServerError, "Error reading the request body", "internal-error")
82 sendHTTPResponse(writer, resp)
83 return
84 }
85
86 var items map[string]string
87 if err = json.Unmarshal(body, &items); err != nil {
88 resp := makeErrorResponse(http.StatusInternalServerError, "Malformed request", "internal-error")
89 sendHTTPResponse(writer, resp)
90 return
91 }
92
93 // Add the items in the config, but only if all are in the whitelist
94 for key, value := range items {
95 if _, present := validTokens[key]; !present {
96 errResponse := makeErrorResponse(http.StatusInternalServerError, `Invalid key "`+key+`"`, "internal-error")
97 sendHTTPResponse(writer, errResponse)
98 return
99 }
100 config[key] = value
101 }
102
103 for key, value := range config {
104 key = convertKeyToStorageFormat(key)
105 value = escapeTextForShell(value)
106 file.WriteString(fmt.Sprintf("%s=%s\n", key, value))
107 }
108
109 if err := restartAccessPoint(c); err != nil {
110 resp := makeErrorResponse(http.StatusInternalServerError, "Failed to restart AP process", "internal-error")
111 sendHTTPResponse(writer, resp)
112 return
113 }
114
115 sendHTTPResponse(writer, makeResponse(http.StatusOK, nil))
116}
117
118func restartAccessPoint(c *serviceCommand) error {
119 if c.s.ap != nil {
120 // Now that we have all configuration changes successfully applied
121 // we can safely restart the service.
122 if err := c.s.ap.Restart(); err != nil {
123 return err
124 }
125 }
126 return nil
127}
128
129func getStatus(c *serviceCommand, writer http.ResponseWriter, request *http.Request) {
130 status := make(map[string]string)
131
132 status["ap.active"] = "0"
133 if c.s.ap != nil && c.s.ap.Running() {
134 status["ap.active"] = "1"
135 }
136
137 sendHTTPResponse(writer, makeResponse(http.StatusOK, status))
138}
139
140func postStatus(c *serviceCommand, writer http.ResponseWriter, request *http.Request) {
141 body, err := ioutil.ReadAll(request.Body)
142 if err != nil {
143 resp := makeErrorResponse(http.StatusInternalServerError, "Error reading the request body", "internal-error")
144 sendHTTPResponse(writer, resp)
145 return
146 }
147
148 var items map[string]string
149 if json.Unmarshal(body, &items) != nil {
150 resp := makeErrorResponse(http.StatusInternalServerError, "Malformed request", "internal-error")
151 sendHTTPResponse(writer, resp)
152 return
153 }
154
155 action, ok := items["action"]
156 if !ok {
157 resp := makeErrorResponse(http.StatusInternalServerError, "Mailformed request", "internal-error")
158 sendHTTPResponse(writer, resp)
159 return
160 }
161
162 switch action {
163 case "restart-ap":
164 if err = restartAccessPoint(c); err != nil {
165 resp := makeErrorResponse(http.StatusInternalServerError, "Failed to restart AP process", "internal-error")
166 sendHTTPResponse(writer, resp)
167 return
168 }
169
170 resp := makeResponse(http.StatusOK, nil)
171 sendHTTPResponse(writer, resp)
172 }
173
174 resp := makeErrorResponse(http.StatusInternalServerError, "Invalid request", "internal-error")
175 sendHTTPResponse(writer, resp)
176}
diff --git a/cmd/service/api_test.go b/cmd/service/api_test.go
0new file mode 100644177new file mode 100644
index 0000000..433843b
--- /dev/null
+++ b/cmd/service/api_test.go
@@ -0,0 +1,344 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "bytes"
20 "encoding/json"
21 "io/ioutil"
22 "net/http"
23 "net/http/httptest"
24 "os"
25 "path/filepath"
26 "strings"
27 "testing"
28
29 "gopkg.in/check.v1"
30)
31
32// gopkg.in/check.v1 stuff
33func Test(t *testing.T) { check.TestingT(t) }
34
35type S struct{}
36
37var _ = check.Suite(&S{})
38
39type mockBackgroundProcess struct {
40 running bool
41}
42
43func (p *mockBackgroundProcess) Start() error {
44 p.running = true
45 return nil
46}
47
48func (p *mockBackgroundProcess) Stop() error {
49 p.running = false
50 return nil
51}
52
53func (p *mockBackgroundProcess) Restart() error {
54 p.running = true
55 return nil
56}
57
58func (p *mockBackgroundProcess) Running() bool {
59 return p.running
60}
61
62func newMockServiceCommand() *serviceCommand {
63 return &serviceCommand{
64 s: &service{
65 ap: &mockBackgroundProcess{},
66 },
67 }
68}
69
70func (s *S) TestGetConfiguration(c *check.C) {
71 // Check it we get a valid JSON as configuration
72 req, err := http.NewRequest(http.MethodGet, "/v1/configuration", nil)
73 c.Assert(err, check.IsNil)
74
75 rec := httptest.NewRecorder()
76
77 cmd := newMockServiceCommand()
78 getConfiguration(cmd, rec, req)
79
80 body, err := ioutil.ReadAll(rec.Body)
81 c.Assert(err, check.IsNil)
82
83 // Parse the returned JSON
84 var resp serviceResponse
85 err = json.Unmarshal(body, &resp)
86 c.Assert(err, check.IsNil)
87
88 // Check for 200 status code
89 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
90 c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
91 c.Assert(resp.Type, check.Equals, "sync")
92}
93
94func (s *S) TestNoDefaultConfiguration(c *check.C) {
95 oldsnap := os.Getenv("SNAP")
96 os.Setenv("SNAP", "/nodir")
97 os.Setenv("SNAP_DATA", "/tmp")
98
99 req, err := http.NewRequest(http.MethodPost, "/v1/configuration", nil)
100 c.Assert(err, check.IsNil)
101
102 rec := httptest.NewRecorder()
103 cmd := newMockServiceCommand()
104
105 validTokens = nil
106
107 postConfiguration(cmd, rec, req)
108
109 c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
110
111 body, err := ioutil.ReadAll(rec.Body)
112 c.Assert(err, check.IsNil)
113
114 // Parse the returned JSON
115 var resp serviceResponse
116 err = json.Unmarshal(body, &resp)
117 c.Assert(err, check.IsNil)
118
119 // Check for 500 status code and other error fields
120 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
121 c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
122 c.Assert(resp.Type, check.Equals, "error")
123 c.Assert(resp.Result["kind"], check.Equals, "internal-error")
124 c.Assert(resp.Result["message"], check.Equals, "No default configuration file available")
125
126 os.Setenv("SNAP", oldsnap)
127}
128
129func (s *S) TestWriteError(c *check.C) {
130 // Test a non writable path:
131 os.Setenv("SNAP_DATA", "/nodir")
132
133 req, err := http.NewRequest(http.MethodPost, "/v1/configuration", nil)
134 c.Assert(err, check.IsNil)
135
136 rec := httptest.NewRecorder()
137 cmd := newMockServiceCommand()
138
139 validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config"))
140 c.Assert(validTokens, check.NotNil)
141 c.Assert(err, check.IsNil)
142
143 postConfiguration(cmd, rec, req)
144
145 c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
146
147 body, err := ioutil.ReadAll(rec.Body)
148 c.Assert(err, check.IsNil)
149
150 // Parse the returned JSON
151 var resp serviceResponse
152 err = json.Unmarshal(body, &resp)
153 c.Assert(err, check.IsNil)
154
155 // Check for 500 status code and other error fields
156 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
157 c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
158 c.Assert(resp.Type, check.Equals, "error")
159 c.Assert(resp.Result["kind"], check.Equals, "internal-error")
160 c.Assert(resp.Result["message"], check.Equals, "Can't write configuration file")
161}
162
163func (s *S) TestInvalidJSON(c *check.C) {
164 // Test an invalid JSON
165 os.Setenv("SNAP_DATA", "/tmp")
166 req, err := http.NewRequest(http.MethodPost, "/v1/configuration", strings.NewReader("not a JSON content"))
167 c.Assert(err, check.IsNil)
168
169 rec := httptest.NewRecorder()
170 cmd := newMockServiceCommand()
171
172 postConfiguration(cmd, rec, req)
173
174 c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
175
176 body, err := ioutil.ReadAll(rec.Body)
177 c.Assert(err, check.IsNil)
178
179 // Parse the returned JSON
180 resp := serviceResponse{}
181 err = json.Unmarshal(body, &resp)
182 c.Assert(err, check.IsNil)
183
184 // Check for 500 status code and other error fields
185 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
186 c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
187 c.Assert(resp.Type, check.Equals, "error")
188 c.Assert(resp.Result["kind"], check.Equals, "internal-error")
189 c.Assert(resp.Result["message"], check.Equals, "Malformed request")
190}
191
192func (s *S) TestInvalidToken(c *check.C) {
193 // Test a succesful configuration set
194 // Values to be used in the config
195 values := map[string]string{
196 "wifi.security": "wpa2",
197 "wifi.ssid": "UbuntuAP",
198 "wifi.security-passphrase": "12345678",
199 "bad.token": "xyz",
200 }
201
202 // Convert the map into JSON
203 args, err := json.Marshal(values)
204 c.Assert(err, check.IsNil)
205
206 req, err := http.NewRequest(http.MethodPost, "/v1/configuration", bytes.NewReader(args))
207 c.Assert(err, check.IsNil)
208
209 rec := httptest.NewRecorder()
210 cmd := newMockServiceCommand()
211
212 validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config"))
213 c.Assert(validTokens, check.NotNil)
214 c.Assert(err, check.IsNil)
215
216 // Do the request
217 postConfiguration(cmd, rec, req)
218
219 c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
220
221 // Read the result JSON
222 body, err := ioutil.ReadAll(rec.Body)
223 c.Assert(err, check.IsNil)
224
225 // Parse the returned JSON
226 resp := serviceResponse{}
227 err = json.Unmarshal(body, &resp)
228 c.Assert(err, check.IsNil)
229
230 // Check for 500 status code and other succesful fields
231 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
232 c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
233 c.Assert(resp.Type, check.Equals, "error")
234}
235
236func (s *S) TestChangeConfiguration(c *check.C) {
237 // Values to be used in the config
238 values := map[string]string{
239 "disabled": "0",
240 "wifi.security": "wpa2",
241 "wifi.ssid": "UbuntuAP",
242 "wifi.security-passphrase": "12345678",
243 }
244
245 // Convert the map into JSON
246 args, err := json.Marshal(values)
247 c.Assert(err, check.IsNil)
248
249 req, err := http.NewRequest(http.MethodPost, "/v1/configuration", bytes.NewReader(args))
250 c.Assert(err, check.IsNil)
251
252 rec := httptest.NewRecorder()
253 cmd := newMockServiceCommand()
254
255 validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config"))
256 c.Assert(validTokens, check.NotNil)
257 c.Assert(err, check.IsNil)
258
259 // Do the request
260 postConfiguration(cmd, rec, req)
261
262 c.Assert(rec.Code, check.Equals, http.StatusOK)
263
264 // Read the result JSON
265 body, err := ioutil.ReadAll(rec.Body)
266 c.Assert(err, check.IsNil)
267
268 // Parse the returned JSON
269 resp := serviceResponse{}
270 err = json.Unmarshal(body, &resp)
271 c.Assert(err, check.IsNil)
272
273 // Check for 200 status code and other succesful fields
274 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
275 c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
276 c.Assert(resp.Type, check.Equals, "sync")
277
278 // Read the generated config and check that values were set
279 config, err := ioutil.ReadFile(getConfigOnPath(os.Getenv("SNAP_DATA")))
280 c.Assert(err, check.IsNil)
281
282 for key, value := range values {
283 c.Assert(strings.Contains(string(config),
284 convertKeyToStorageFormat(key)+"="+value+"\n"),
285 check.Equals, true)
286 }
287
288 // As we've set 'disabled' to '0' above the AP should be active
289 // now as the configuration post request will trigger an automatic
290 // restart of the relevant background processes.
291 c.Assert(cmd.s.ap.Running(), check.Equals, true)
292
293 // Don't leave garbage in /tmp
294 os.Remove(getConfigOnPath(os.Getenv("SNAP_DATA")))
295}
296
297func (s *S) TestGetStatusDefaultOk(c *check.C) {
298 req, err := http.NewRequest(http.MethodGet, "/v1/status", nil)
299 c.Assert(err, check.IsNil)
300
301 rec := httptest.NewRecorder()
302
303 cmd := newMockServiceCommand()
304
305 getStatus(cmd, rec, req)
306
307 body, err := ioutil.ReadAll(rec.Body)
308 c.Assert(err, check.IsNil)
309
310 var resp serviceResponse
311 err = json.Unmarshal(body, &resp)
312 c.Assert(err, check.IsNil)
313
314 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
315 c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
316 c.Assert(resp.Type, check.Equals, "sync")
317
318 c.Assert(resp.Result["ap.active"], check.Equals, "0")
319}
320
321func (s *S) TestGetStatusReturnsCorrectApStatus(c *check.C) {
322 req, err := http.NewRequest(http.MethodGet, "/v1/status", nil)
323 c.Assert(err, check.IsNil)
324
325 rec := httptest.NewRecorder()
326
327 cmd := newMockServiceCommand()
328 cmd.s.ap.Start()
329
330 getStatus(cmd, rec, req)
331
332 body, err := ioutil.ReadAll(rec.Body)
333 c.Assert(err, check.IsNil)
334
335 var resp serviceResponse
336 err = json.Unmarshal(body, &resp)
337 c.Assert(err, check.IsNil)
338
339 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
340 c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
341 c.Assert(resp.Type, check.Equals, "sync")
342
343 c.Assert(resp.Result["ap.active"], check.Equals, "1")
344}
diff --git a/cmd/service/background_process.go b/cmd/service/background_process.go
0new file mode 100644345new file mode 100644
index 0000000..6a1ea7e
--- /dev/null
+++ b/cmd/service/background_process.go
@@ -0,0 +1,147 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "fmt"
20 "os"
21 "os/exec"
22 "syscall"
23 "sync"
24 "time"
25
26 "gopkg.in/tomb.v2"
27)
28
29type backgroundProcessImpl struct {
30 path string
31 args []string
32 command *exec.Cmd
33 tomb *tomb.Tomb
34 mutex sync.Mutex
35}
36
37// BackgroundProcess provides control over a process running in the
38// background.
39type BackgroundProcess interface {
40 Start() error
41 Stop() error
42 Restart() error
43 Running() bool
44}
45
46func NewBackgroundProcess(path string, args ...string) (BackgroundProcess, error) {
47 p := &backgroundProcessImpl{
48 path: path,
49 args: args,
50 command: nil,
51 }
52 if p == nil {
53 return nil, fmt.Errorf("Failed to create background process")
54 }
55
56 return p, nil
57}
58
59func (p *backgroundProcessImpl) Start() error {
60 if p.Running() {
61 return fmt.Errorf("Background process is already running")
62 }
63
64 p.mutex.Lock()
65
66 p.command = exec.Command(p.path, p.args...)
67 if p.command == nil {
68 return fmt.Errorf("Failed to create background process")
69 }
70
71 // Forward output to regular stdout/stderr
72 p.command.Stdout = os.Stdout
73 p.command.Stderr = os.Stderr
74
75 // Create a new process group
76 p.command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
77
78 // We need to recreate the tomb here everytime as otherwise
79 // it will not cleanup its state from the last time.
80 p.tomb = &tomb.Tomb{}
81
82 c := make(chan int)
83 p.tomb.Go(func() error {
84 err := p.command.Start()
85 if err != nil {
86 fmt.Printf("Failed to execute process for binary '%s'", p.path)
87 return err
88 }
89 c <- 1
90 p.command.Wait()
91 p.command = nil
92 return nil
93 })
94
95 // Wait until the process is really started
96 _ = <-c
97
98 p.mutex.Unlock()
99
100 return nil
101}
102
103func (p *backgroundProcessImpl) Restart() error {
104 if err := p.Stop(); err != nil {
105 return err
106 }
107 if err := p.Start(); err != nil {
108 return err
109 }
110 return nil
111}
112
113func (p *backgroundProcessImpl) killProcess(signal syscall.Signal) error {
114 if p == nil || p.command == nil {
115 return fmt.Errorf("Process is not running")
116 }
117 // We need to kill the whole process group as otherwise some
118 // child processes are still around
119 pgid, err := syscall.Getpgid(p.command.Process.Pid)
120 if err == nil {
121 syscall.Kill(-pgid, signal)
122 } else {
123 syscall.Kill(p.command.Process.Pid, signal)
124 }
125 return nil
126}
127
128func (p *backgroundProcessImpl) Stop() error {
129 if !p.Running() {
130 return nil
131 }
132 p.mutex.Lock()
133 timer := time.AfterFunc(10*time.Second, func() {
134 p.killProcess(syscall.SIGKILL)
135 })
136 p.killProcess(syscall.SIGTERM)
137 p.tomb.Kill(nil)
138 p.tomb.Wait()
139 timer.Stop()
140 p.mutex.Unlock()
141 p.command = nil
142 return nil
143}
144
145func (p *backgroundProcessImpl) Running() bool {
146 return p.command != nil
147}
diff --git a/cmd/service/background_process_test.go b/cmd/service/background_process_test.go
0new file mode 100644148new file mode 100644
index 0000000..5ae9cd7
--- /dev/null
+++ b/cmd/service/background_process_test.go
@@ -0,0 +1,36 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "fmt"
20
21 "gopkg.in/check.v1"
22)
23
24func (s *S) TestBackgroundProcessStartStop(c *check.C) {
25 p, err := NewBackgroundProcess("/bin/sleep", "1000")
26 c.Assert(err, check.IsNil)
27 c.Assert(p.Running(), check.Equals, false)
28 c.Assert(p.Start(), check.IsNil)
29 c.Assert(p.Running(), check.Equals, true)
30 c.Assert(p.Stop(), check.IsNil)
31 c.Assert(p.Running(), check.Equals, false)
32 c.Assert(p.Restart(), check.IsNil)
33 c.Assert(p.Running(), check.Equals, true)
34 c.Assert(p.Start(), check.DeepEquals, fmt.Errorf("Background process is already running"))
35 c.Assert(p.Running(), check.Equals, true)
36}
diff --git a/cmd/service/config.go b/cmd/service/config.go
0new file mode 10064437new file mode 100644
index 0000000..bbc047e
--- /dev/null
+++ b/cmd/service/config.go
@@ -0,0 +1,140 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "bufio"
20 "fmt"
21 "os"
22 "path/filepath"
23 "regexp"
24 "strings"
25)
26
27func getConfigOnPath(path string) string {
28 return filepath.Join(path, "config")
29}
30
31// Array of paths where the config file can be found.
32// The first one is readonly, the others are writable
33// they are readed in order and the configuration is merged
34var configurationPaths = []string{
35 filepath.Join(os.Getenv("SNAP"), "conf", "default-config"),
36 getConfigOnPath(os.Getenv("SNAP_DATA")),
37 getConfigOnPath(os.Getenv("SNAP_USER_DATA"))}
38
39// Convert eg. WIFI_OPERATION_MODE to wifi.operation-mode
40func convertKeyToRepresentationFormat(key string) string {
41 newKey := strings.ToLower(key)
42 newKey = strings.Replace(newKey, "_", ".", 1)
43 return strings.Replace(newKey, "_", "-", -1)
44}
45
46func convertKeyToStorageFormat(key string) string {
47 // Convert eg. wifi.operation-mode to WIFI_OPERATION_MODE
48 newKey := strings.ToUpper(key)
49 newKey = strings.Replace(newKey, ".", "_", -1)
50 return strings.Replace(newKey, "-", "_", -1)
51}
52
53func readConfigurationFile(filePath string, config map[string]string) (err error) {
54 file, err := os.Open(filePath)
55 if err != nil {
56 return nil
57 }
58
59 defer file.Close()
60
61 for scanner := bufio.NewScanner(file); scanner.Scan(); {
62 // Ignore all empty or commented lines
63 if line := scanner.Text(); len(line) != 0 && line[0] != '#' {
64 // Line must be in the KEY=VALUE format
65 if parts := strings.Split(line, "="); len(parts) == 2 {
66 value := unescapeTextByShell(parts[1])
67 config[convertKeyToRepresentationFormat(parts[0])] = value
68 }
69 }
70 }
71
72 return nil
73}
74
75func readConfiguration(paths []string, config map[string]string) (err error) {
76 for _, location := range paths {
77 if readConfigurationFile(location, config) != nil {
78 return fmt.Errorf("Failed to read configuration file '%s'", location)
79 }
80 }
81
82 return nil
83}
84
85// Escape shell special characters, avoid injection
86// eg. SSID set to "My AP$(nc -lp 2323 -e /bin/sh)"
87// to get a root shell
88func escapeTextForShell(input string) string {
89 if strings.ContainsAny(input, "\\\"'`$\n\t #") {
90 input = strings.Replace(input, `\`, `\\`, -1)
91 input = strings.Replace(input, `"`, `\"`, -1)
92 input = strings.Replace(input, "`", "\\`", -1)
93 input = strings.Replace(input, `$`, `\$`, -1)
94
95 input = `"` + input + `"`
96 }
97 return input
98}
99
100// Do the reverse of escapeTextForShell() here
101// strip any \ followed by \$`"
102func unescapeTextByShell(input string) string {
103 input = strings.Trim(input, `"'`)
104 if strings.ContainsAny(input, "\\") {
105 re := regexp.MustCompile("\\\\([\\\\$\\`\\\"])")
106 input = re.ReplaceAllString(input, "$1")
107 }
108 return input
109}
110
111func loadValidTokens(path string) (map[string]bool, error) {
112 def, err := os.Open(path)
113 if err != nil {
114 return nil, err
115 }
116 defer def.Close()
117
118 tokens := map[string]bool{}
119
120 scanner := bufio.NewScanner(def)
121 for scanner.Scan() {
122 line := scanner.Text()
123
124 // Skip empty lines and comments
125 if len(line) == 0 || line[0] == '#' {
126 continue
127 }
128
129 // Get the substring before the '='
130 if eq := strings.IndexRune(line, '='); eq > 0 {
131 // Add the token to the whitelist, converted in our format
132 tokens[convertKeyToRepresentationFormat(line[:eq])] = true
133 }
134 }
135 if err := scanner.Err(); err != nil {
136 return nil, err
137 }
138
139 return tokens, nil
140}
diff --git a/cmd/service/config_test.go b/cmd/service/config_test.go
0new file mode 100644141new file mode 100644
index 0000000..9f59ea9
--- /dev/null
+++ b/cmd/service/config_test.go
@@ -0,0 +1,66 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "gopkg.in/check.v1"
20)
21
22// Test the config file path append routine
23func (s *S) TestPath(c *check.C) {
24 c.Assert(getConfigOnPath("/test"), check.Equals, "/test/config")
25}
26
27// List of tokens to be translated
28var cfgKeys = [...][2]string{
29 {"DISABLED", "disabled"},
30 {"WIFI_SSID", "wifi.ssid"},
31 {"WIFI_INTERFACE", "wifi.interface"},
32 {"WIFI_INTERFACE_MODE", "wifi.interface-mode"},
33 {"DHCP_RANGE_START", "dhcp.range-start"},
34 {"MYTOKEN", "mytoken"},
35 {"CFG_TOKEN", "cfg.token"},
36 {"MY_TOKEN$", "my.token$"},
37}
38
39// Test token conversion from internal format
40func (s *S) TestConvertKeyToRepresentationFormat(c *check.C) {
41 for _, st := range cfgKeys {
42 c.Assert(convertKeyToRepresentationFormat(st[0]), check.Equals, st[1])
43 }
44}
45
46// Test token conversion to internal format
47func (s *S) TestConvertKeyToStorageFormat(c *check.C) {
48 for _, st := range cfgKeys {
49 c.Assert(convertKeyToStorageFormat(st[1]), check.Equals, st[0])
50 }
51}
52
53// List of malicious tokens which needs to be escaped
54func (s *S) TestEscapeShell(c *check.C) {
55 cmds := [...][2]string{
56 {"my_ap", "my_ap"},
57 {`my ap`, `"my ap"`},
58 {`my "ap"`, `"my \"ap\""`},
59 {`$(ps ax)`, `"\$(ps ax)"`},
60 {"`ls /`", "\"\\`ls /\\`\""},
61 {`c:\dir`, `"c:\\dir"`},
62 }
63 for _, st := range cmds {
64 c.Assert(escapeTextForShell(st[0]), check.Equals, st[1])
65 }
66}
diff --git a/cmd/service/main.go b/cmd/service/main.go
index 0fd82e7..ae5e679 100644
--- a/cmd/service/main.go
+++ b/cmd/service/main.go
@@ -1,4 +1,3 @@
1//
2// Copyright (C) 2016 Canonical Ltd1// Copyright (C) 2016 Canonical Ltd
3//2//
4// This program is free software: you can redistribute it and/or modify3// This program is free software: you can redistribute it and/or modify
@@ -16,277 +15,23 @@
16package main15package main
1716
18import (17import (
19 "bufio"
20 "encoding/json"
21 "fmt"
22 "io/ioutil"
23 "log"18 "log"
24 "net/http"
25 "os"19 "os"
26 "path/filepath"20 "os/signal"
27 "regexp"21 "syscall"
28 "strconv"
29 "strings"
30
31 "github.com/gorilla/mux"
32)
33
34/* JSON message format, as described here:
35{
36 "result": {
37 "key" : "val"
38 },
39 "status": "OK",
40 "status-code": 200,
41 "type": "sync"
42}
43*/
44
45type serviceResponse struct {
46 Result map[string]string `json:"result"`
47 Status string `json:"status"`
48 StatusCode int `json:"status-code"`
49 Type string `json:"type"`
50}
51
52func makeErrorResponse(code int, message, kind string) *serviceResponse {
53 return &serviceResponse{
54 Type: "error",
55 Status: http.StatusText(code),
56 StatusCode: code,
57 Result: map[string]string{
58 "message": message,
59 "kind": kind,
60 },
61 }
62}
63
64func makeResponse(status int, result map[string]string) *serviceResponse {
65 resp := &serviceResponse{
66 Type: "sync",
67 Status: http.StatusText(status),
68 StatusCode: status,
69 Result: result,
70 }
71
72 if resp.Result == nil {
73 resp.Result = make(map[string]string)
74 }
75
76 return resp
77}
78
79func sendHTTPResponse(writer http.ResponseWriter, response *serviceResponse) {
80 writer.WriteHeader(response.StatusCode)
81 data, _ := json.Marshal(response)
82 fmt.Fprintln(writer, string(data))
83}
84
85func getConfigOnPath(confPath string) string {
86 return filepath.Join(confPath, "config")
87}
88
89// Array of paths where the config file can be found.
90// The first one is readonly, the others are writable
91// they are readed in order and the configuration is merged
92var configurationPaths = []string{
93 filepath.Join(os.Getenv("SNAP"), "conf", "default-config"),
94 getConfigOnPath(os.Getenv("SNAP_DATA")),
95 getConfigOnPath(os.Getenv("SNAP_USER_DATA"))}
96
97const (
98 servicePort = 5005
99 configurationV1Uri = "/v1/configuration"
100)22)
10123
102var validTokens map[string]bool
103
104func loadValidTokens(path string) (map[string]bool, error) {
105 def, err := os.Open(path)
106 if err != nil {
107 return nil, err
108 }
109 defer def.Close()
110
111 tokens := map[string]bool{}
112
113 scanner := bufio.NewScanner(def)
114 for scanner.Scan() {
115 line := scanner.Text()
116
117 // Skip empty lines and comments
118 if len(line) == 0 || line[0] == '#' {
119 continue
120 }
121
122 // Get the substring before the '='
123 if eq := strings.IndexRune(line, '='); eq > 0 {
124 // Add the token to the whitelist, converted in our format
125 tokens[convertKeyToRepresentationFormat(line[:eq])] = true
126 }
127 }
128 if err := scanner.Err(); err != nil {
129 return nil, err
130 }
131
132 return tokens, nil
133}
134
135func main() {24func main() {
136 r := mux.NewRouter().StrictSlash(true)25 s := &service{}
13726
138 var err error27 c := make(chan os.Signal, 1)
139 if validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "conf", "default-config")); err != nil {28 signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
140 log.Println("Failed to read default configuration:", err)29 go func(s *service) {
141 }30 _ = <-c
31 s.Shutdown()
32 }(s)
14233
143 r.HandleFunc(configurationV1Uri, getConfiguration).Methods(http.MethodGet)34 if err := s.Run(); err != nil {
144 r.HandleFunc(configurationV1Uri, changeConfiguration).Methods(http.MethodPost)35 log.Fatalf("Failed to start service: %s", err)
145
146 log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(servicePort), r))
147}
148
149// Convert eg. WIFI_OPERATION_MODE to wifi.operation-mode
150func convertKeyToRepresentationFormat(key string) string {
151 newKey := strings.ToLower(key)
152 newKey = strings.Replace(newKey, "_", ".", 1)
153 return strings.Replace(newKey, "_", "-", -1)
154}
155
156func convertKeyToStorageFormat(key string) string {
157 // Convert eg. wifi.operation-mode to WIFI_OPERATION_MODE
158 newKey := strings.ToUpper(key)
159 newKey = strings.Replace(newKey, ".", "_", -1)
160 return strings.Replace(newKey, "-", "_", -1)
161}
162
163func readConfigurationFile(filePath string, config map[string]string) (err error) {
164 file, err := os.Open(filePath)
165 if err != nil {
166 return nil
167 }
168
169 defer file.Close()
170
171 for scanner := bufio.NewScanner(file); scanner.Scan(); {
172 // Ignore all empty or commented lines
173 if line := scanner.Text(); len(line) != 0 && line[0] != '#' {
174 // Line must be in the KEY=VALUE format
175 if parts := strings.Split(line, "="); len(parts) == 2 {
176 key := convertKeyToRepresentationFormat(parts[0])
177 value := unescapeTextByShell(parts[1])
178 config[key] = value
179 }
180 }
181 }
182
183 return nil
184}
185
186func readConfiguration(paths []string, config map[string]string) (err error) {
187 for _, location := range paths {
188 if readConfigurationFile(location, config) != nil {
189 return fmt.Errorf(`Failed to read configuration file "%s"`, location)
190 }
191 }36 }
192
193 return nil
194}
195
196func getConfiguration(writer http.ResponseWriter, request *http.Request) {
197 config := make(map[string]string)
198 if err := readConfiguration(configurationPaths, config); err == nil {
199 sendHTTPResponse(writer, makeResponse(http.StatusOK, config))
200 } else {
201 log.Println("Read configuration failed:", err)
202 errResponse := makeErrorResponse(http.StatusInternalServerError, "Failed to read configuration data", "internal-error")
203 sendHTTPResponse(writer, errResponse)
204 }
205}
206
207// Escape shell special characters, avoid injection
208// eg. SSID set to "My AP$(nc -lp 2323 -e /bin/sh)"
209// to get a root shell
210func escapeTextForShell(input string) string {
211 if strings.ContainsAny(input, "\\\"'`$\n\t #") {
212 input = strings.Replace(input, `\`, `\\`, -1)
213 input = strings.Replace(input, `"`, `\"`, -1)
214 input = strings.Replace(input, "`", "\\`", -1)
215 input = strings.Replace(input, `$`, `\$`, -1)
216
217 input = `"` + input + `"`
218 }
219 return input
220}
221
222// Do the reverse of escapeTextForShell() here
223// strip any \ followed by \$`"
224func unescapeTextByShell(input string) string {
225 input = strings.Trim(input, `"'`)
226 if strings.ContainsAny(input, "\\") {
227 re := regexp.MustCompile("\\\\([\\\\$\\`\\\"])")
228 input = re.ReplaceAllString(input, "$1")
229 }
230 return input
231}
232
233func changeConfiguration(writer http.ResponseWriter, request *http.Request) {
234 path := getConfigOnPath(os.Getenv("SNAP_DATA"))
235 config := map[string]string{}
236 if readConfiguration([]string{path}, config) != nil {
237 errResponse := makeErrorResponse(http.StatusInternalServerError,
238 "Failed to read existing configuration file", "internal-error")
239 sendHTTPResponse(writer, errResponse)
240 return
241 }
242
243 if validTokens == nil || len(validTokens) == 0 {
244 errResponse := makeErrorResponse(http.StatusInternalServerError, "No default configuration file available", "internal-error")
245 sendHTTPResponse(writer, errResponse)
246 return
247 }
248
249 file, err := os.Create(path)
250 if err != nil {
251 log.Printf("Write to %q failed: %v\n", path, err)
252 errResponse := makeErrorResponse(http.StatusInternalServerError, "Can't write configuration file", "internal-error")
253 sendHTTPResponse(writer, errResponse)
254 return
255 }
256 defer file.Close()
257
258 body, err := ioutil.ReadAll(request.Body)
259 if err != nil {
260 log.Println("Failed to process incoming configuration change request:", err)
261 errResponse := makeErrorResponse(http.StatusInternalServerError, "Error reading the request body", "internal-error")
262 sendHTTPResponse(writer, errResponse)
263 return
264 }
265
266 var items map[string]string
267 if err = json.Unmarshal(body, &items); err != nil {
268 log.Println("Invalid input data", err)
269 errResponse := makeErrorResponse(http.StatusInternalServerError, "Malformed request", "internal-error")
270 sendHTTPResponse(writer, errResponse)
271 return
272 }
273
274 // Add the items in the config, but only if all are in the whitelist
275 for key, value := range items {
276 if _, present := validTokens[key]; !present {
277 log.Println(`Invalid key "` + key + `": ignoring request`)
278 errResponse := makeErrorResponse(http.StatusInternalServerError, `Invalid key "`+key+`"`, "internal-error")
279 sendHTTPResponse(writer, errResponse)
280 return
281 }
282 config[key] = value
283 }
284
285 for key, value := range config {
286 key = convertKeyToStorageFormat(key)
287 value = escapeTextForShell(value)
288 file.WriteString(fmt.Sprintf("%s=%s\n", key, value))
289 }
290
291 sendHTTPResponse(writer, makeResponse(http.StatusOK, nil))
292}37}
diff --git a/cmd/service/main_test.go b/cmd/service/main_test.go
293deleted file mode 10064438deleted file mode 100644
index d0fc6be..0000000
--- a/cmd/service/main_test.go
+++ /dev/null
@@ -1,298 +0,0 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "bytes"
20 "encoding/json"
21 "io/ioutil"
22 "net/http"
23 "net/http/httptest"
24 "os"
25 "path/filepath"
26 "strings"
27 "testing"
28
29 "gopkg.in/check.v1"
30)
31
32// gopkg.in/check.v1 stuff
33func Test(t *testing.T) { check.TestingT(t) }
34
35type S struct{}
36
37var _ = check.Suite(&S{})
38
39// Test the config file path append routine
40func (s *S) TestPath(c *check.C) {
41 c.Assert(getConfigOnPath("/test"), check.Equals, "/test/config")
42}
43
44// List of tokens to be translated
45var cfgKeys = [...][2]string{
46 {"DISABLED", "disabled"},
47 {"WIFI_SSID", "wifi.ssid"},
48 {"WIFI_INTERFACE", "wifi.interface"},
49 {"WIFI_INTERFACE_MODE", "wifi.interface-mode"},
50 {"DHCP_RANGE_START", "dhcp.range-start"},
51 {"MYTOKEN", "mytoken"},
52 {"CFG_TOKEN", "cfg.token"},
53 {"MY_TOKEN$", "my.token$"},
54}
55
56// Test token conversion from internal format
57func (s *S) TestConvertKeyToRepresentationFormat(c *check.C) {
58 for _, st := range cfgKeys {
59 c.Assert(convertKeyToRepresentationFormat(st[0]), check.Equals, st[1])
60 }
61}
62
63// Test token conversion to internal format
64func (s *S) TestConvertKeyToStorageFormat(c *check.C) {
65 for _, st := range cfgKeys {
66 c.Assert(convertKeyToStorageFormat(st[1]), check.Equals, st[0])
67 }
68}
69
70// List of malicious tokens which needs to be escaped
71func (s *S) TestEscapeShell(c *check.C) {
72 cmds := [...][2]string{
73 {"my_ap", "my_ap"},
74 {`my ap`, `"my ap"`},
75 {`my "ap"`, `"my \"ap\""`},
76 {`$(ps ax)`, `"\$(ps ax)"`},
77 {"`ls /`", "\"\\`ls /\\`\""},
78 {`c:\dir`, `"c:\\dir"`},
79 }
80 for _, st := range cmds {
81 c.Assert(escapeTextForShell(st[0]), check.Equals, st[1])
82 }
83}
84
85func (s *S) TestGetConfiguration(c *check.C) {
86 // Check it we get a valid JSON as configuration
87 req, err := http.NewRequest(http.MethodGet, configurationV1Uri, nil)
88 c.Assert(err, check.IsNil)
89
90 rec := httptest.NewRecorder()
91
92 getConfiguration(rec, req)
93
94 body, err := ioutil.ReadAll(rec.Body)
95 c.Assert(err, check.IsNil)
96
97 // Parse the returned JSON
98 var resp serviceResponse
99 err = json.Unmarshal(body, &resp)
100 c.Assert(err, check.IsNil)
101
102 // Check for 200 status code
103 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
104 c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
105 c.Assert(resp.Type, check.Equals, "sync")
106}
107
108func (s *S) TestNoDefaultConfiguration(c *check.C) {
109 oldsnap := os.Getenv("SNAP")
110 os.Setenv("SNAP", "/nodir")
111 os.Setenv("SNAP_DATA", "/tmp")
112
113 req, err := http.NewRequest(http.MethodPost, configurationV1Uri, nil)
114 c.Assert(err, check.IsNil)
115
116 rec := httptest.NewRecorder()
117
118 validTokens = nil
119
120 changeConfiguration(rec, req)
121
122 c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
123
124 body, err := ioutil.ReadAll(rec.Body)
125 c.Assert(err, check.IsNil)
126
127 // Parse the returned JSON
128 var resp serviceResponse
129 err = json.Unmarshal(body, &resp)
130 c.Assert(err, check.IsNil)
131
132 // Check for 500 status code and other error fields
133 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
134 c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
135 c.Assert(resp.Type, check.Equals, "error")
136 c.Assert(resp.Result["kind"], check.Equals, "internal-error")
137 c.Assert(resp.Result["message"], check.Equals, "No default configuration file available")
138
139 os.Setenv("SNAP", oldsnap)
140}
141
142func (s *S) TestWriteError(c *check.C) {
143 // Test a non writable path:
144 os.Setenv("SNAP_DATA", "/nodir")
145
146 req, err := http.NewRequest(http.MethodPost, configurationV1Uri, nil)
147 c.Assert(err, check.IsNil)
148
149 rec := httptest.NewRecorder()
150
151 validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config"))
152 c.Assert(validTokens, check.NotNil)
153 c.Assert(err, check.IsNil)
154
155 changeConfiguration(rec, req)
156
157 c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
158
159 body, err := ioutil.ReadAll(rec.Body)
160 c.Assert(err, check.IsNil)
161
162 // Parse the returned JSON
163 var resp serviceResponse
164 err = json.Unmarshal(body, &resp)
165 c.Assert(err, check.IsNil)
166
167 // Check for 500 status code and other error fields
168 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
169 c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
170 c.Assert(resp.Type, check.Equals, "error")
171 c.Assert(resp.Result["kind"], check.Equals, "internal-error")
172 c.Assert(resp.Result["message"], check.Equals, "Can't write configuration file")
173}
174
175func (s *S) TestInvalidJSON(c *check.C) {
176 // Test an invalid JSON
177 os.Setenv("SNAP_DATA", "/tmp")
178 req, err := http.NewRequest(http.MethodPost, configurationV1Uri, strings.NewReader("not a JSON content"))
179 c.Assert(err, check.IsNil)
180
181 rec := httptest.NewRecorder()
182
183 changeConfiguration(rec, req)
184
185 c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
186
187 body, err := ioutil.ReadAll(rec.Body)
188 c.Assert(err, check.IsNil)
189
190 // Parse the returned JSON
191 resp := serviceResponse{}
192 err = json.Unmarshal(body, &resp)
193 c.Assert(err, check.IsNil)
194
195 // Check for 500 status code and other error fields
196 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
197 c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
198 c.Assert(resp.Type, check.Equals, "error")
199 c.Assert(resp.Result["kind"], check.Equals, "internal-error")
200 c.Assert(resp.Result["message"], check.Equals, "Malformed request")
201}
202
203func (s *S) TestInvalidToken(c *check.C) {
204 // Test a succesful configuration set
205 // Values to be used in the config
206 values := map[string]string{
207 "wifi.security": "wpa2",
208 "wifi.ssid": "UbuntuAP",
209 "wifi.security-passphrase": "12345678",
210 "bad.token": "xyz",
211 }
212
213 // Convert the map into JSON
214 args, err := json.Marshal(values)
215 c.Assert(err, check.IsNil)
216
217 req, err := http.NewRequest(http.MethodPost, configurationV1Uri, bytes.NewReader(args))
218 c.Assert(err, check.IsNil)
219
220 rec := httptest.NewRecorder()
221
222 validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config"))
223 c.Assert(validTokens, check.NotNil)
224 c.Assert(err, check.IsNil)
225
226 // Do the request
227 changeConfiguration(rec, req)
228
229 c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
230
231 // Read the result JSON
232 body, err := ioutil.ReadAll(rec.Body)
233 c.Assert(err, check.IsNil)
234
235 // Parse the returned JSON
236 resp := serviceResponse{}
237 err = json.Unmarshal(body, &resp)
238 c.Assert(err, check.IsNil)
239
240 // Check for 500 status code and other succesful fields
241 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
242 c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
243 c.Assert(resp.Type, check.Equals, "error")
244}
245
246func (s *S) TestChangeConfiguration(c *check.C) {
247 // Values to be used in the config
248 values := map[string]string{
249 "wifi.security": "wpa2",
250 "wifi.ssid": "UbuntuAP",
251 "wifi.security-passphrase": "12345678",
252 }
253
254 // Convert the map into JSON
255 args, err := json.Marshal(values)
256 c.Assert(err, check.IsNil)
257
258 req, err := http.NewRequest(http.MethodPost, configurationV1Uri, bytes.NewReader(args))
259 c.Assert(err, check.IsNil)
260
261 rec := httptest.NewRecorder()
262
263 validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "/conf/default-config"))
264 c.Assert(validTokens, check.NotNil)
265 c.Assert(err, check.IsNil)
266
267 // Do the request
268 changeConfiguration(rec, req)
269
270 c.Assert(rec.Code, check.Equals, http.StatusOK)
271
272 // Read the result JSON
273 body, err := ioutil.ReadAll(rec.Body)
274 c.Assert(err, check.IsNil)
275
276 // Parse the returned JSON
277 resp := serviceResponse{}
278 err = json.Unmarshal(body, &resp)
279 c.Assert(err, check.IsNil)
280
281 // Check for 200 status code and other succesful fields
282 c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
283 c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
284 c.Assert(resp.Type, check.Equals, "sync")
285
286 // Read the generated config and check that values were set
287 config, err := ioutil.ReadFile(getConfigOnPath(os.Getenv("SNAP_DATA")))
288 c.Assert(err, check.IsNil)
289
290 for key, value := range values {
291 c.Assert(strings.Contains(string(config),
292 convertKeyToStorageFormat(key)+"="+value+"\n"),
293 check.Equals, true)
294 }
295
296 // Don't leave garbage in /tmp
297 os.Remove(getConfigOnPath(os.Getenv("SNAP_DATA")))
298}
diff --git a/cmd/service/response.go b/cmd/service/response.go
299new file mode 1006440new file mode 100644
index 0000000..cc8a558
--- /dev/null
+++ b/cmd/service/response.go
@@ -0,0 +1,73 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "encoding/json"
20 "fmt"
21 "net/http"
22)
23
24/* JSON message format, as described here:
25{
26 "result": {
27 "key" : "val"
28 },
29 "status": "OK",
30 "status-code": 200,
31 "type": "sync"
32}
33*/
34
35type serviceResponse struct {
36 Result map[string]string `json:"result"`
37 Status string `json:"status"`
38 StatusCode int `json:"status-code"`
39 Type string `json:"type"`
40}
41
42func makeErrorResponse(code int, message, kind string) *serviceResponse {
43 return &serviceResponse{
44 Type: "error",
45 Status: http.StatusText(code),
46 StatusCode: code,
47 Result: map[string]string{
48 "message": message,
49 "kind": kind,
50 },
51 }
52}
53
54func makeResponse(status int, result map[string]string) *serviceResponse {
55 resp := &serviceResponse{
56 Type: "sync",
57 Status: http.StatusText(status),
58 StatusCode: status,
59 Result: result,
60 }
61
62 if resp.Result == nil {
63 resp.Result = make(map[string]string)
64 }
65
66 return resp
67}
68
69func sendHTTPResponse(writer http.ResponseWriter, response *serviceResponse) {
70 writer.WriteHeader(response.StatusCode)
71 data, _ := json.Marshal(response)
72 fmt.Fprintln(writer, string(data))
73}
diff --git a/cmd/service/response_test.go b/cmd/service/response_test.go
0new file mode 10064474new file mode 100644
index 0000000..07d78f3
--- /dev/null
+++ b/cmd/service/response_test.go
@@ -0,0 +1,47 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "gopkg.in/check.v1"
20 "net/http"
21)
22
23func (s *S) TestMakeErrorResponse(c *check.C) {
24 resp := makeErrorResponse(http.StatusInternalServerError, "my error message", "internal-error")
25 c.Assert(resp.Result, check.DeepEquals, map[string]string{
26 "message": "my error message",
27 "kind": "internal-error",
28 })
29 c.Assert(resp.Status, check.Equals, "Internal Server Error")
30 c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
31 c.Assert(resp.Type, check.Equals, "error")
32}
33
34func (s *S) TestMakeResponse(c *check.C) {
35 resp := makeResponse(http.StatusOK, nil)
36 c.Assert(resp.Result, check.DeepEquals, map[string]string{})
37 c.Assert(resp.Status, check.Equals, "OK")
38 c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
39 c.Assert(resp.Type, check.Equals, "sync")
40
41 data := map[string]string{"foo": "bar"}
42 resp = makeResponse(http.StatusOK, data)
43 c.Assert(resp.Result, check.DeepEquals, data)
44 c.Assert(resp.Status, check.Equals, "OK")
45 c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
46 c.Assert(resp.Type, check.Equals, "sync")
47}
diff --git a/cmd/service/service.go b/cmd/service/service.go
0new file mode 10064448new file mode 100644
index 0000000..0ee04a8
--- /dev/null
+++ b/cmd/service/service.go
@@ -0,0 +1,159 @@
1//
2// Copyright (C) 2016 Canonical Ltd
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License version 3 as
6// published by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16package main
17
18import (
19 "fmt"
20 "github.com/gorilla/mux"
21 "log"
22 "net"
23 "net/http"
24 "os"
25 "path"
26 "path/filepath"
27 "time"
28
29 "gopkg.in/tomb.v2"
30)
31
32const (
33 serviceAddress = "127.0.0.1"
34 servicePort = 5005
35)
36
37type responceFunc func(*serviceCommand, http.ResponseWriter, *http.Request)
38
39type serviceCommand struct {
40 Path string
41 GET responceFunc
42 PUT responceFunc
43 POST responceFunc
44 DELETE responceFunc
45 s *service
46}
47
48type service struct {
49 tomb tomb.Tomb
50 server *http.Server
51 listener net.Listener
52 router *mux.Router
53 ap BackgroundProcess
54}
55
56func (c *serviceCommand) ServeHTTP(w http.ResponseWriter, r *http.Request) {
57 var rspf responceFunc
58
59 switch r.Method {
60 case "GET":
61 rspf = c.GET
62 case "PUT":
63 rspf = c.PUT
64 case "POST":
65 rspf = c.POST
66 case "DELETE":
67 rspf = c.DELETE
68 }
69
70 if rspf == nil {
71 rsp := makeErrorResponse(http.StatusInternalServerError, "Invalid method called", "internal-error")
72 sendHTTPResponse(w, rsp)
73 return
74 }
75
76 rspf(c, w, r)
77}
78
79func (s *service) addRoutes() {
80 s.router = mux.NewRouter()
81
82 for _, c := range api {
83 c.s = s
84 log.Println("Adding route for ", c.Path)
85 s.router.Handle(c.Path, c).Name(c.Path)
86 }
87}
88
89func (s *service) setupAccesPoint() error {
90 path := path.Join(os.Getenv("SNAP"), "bin", "ap.sh")
91 ap, err := NewBackgroundProcess(path)
92 if err != nil {
93 return err
94 }
95
96 s.ap = ap
97 err = s.ap.Start()
98 if err != nil {
99 return err
100 }
101
102 return nil
103}
104
105func (s *service) Shutdown() {
106 log.Println("Shutting down ...")
107 s.listener.Close()
108 s.tomb.Kill(nil)
109 s.tomb.Wait()
110}
111
112func (s *service) Run() error {
113 s.addRoutes()
114 if err := s.setupAccesPoint(); err != nil {
115 return err
116 }
117
118 var err error
119 if validTokens, err = loadValidTokens(filepath.Join(os.Getenv("SNAP"), "conf", "default-config")); err != nil {
120 log.Println("Failed to read default configuration:", err)
121 }
122
123 addr := fmt.Sprintf("%s:%d", serviceAddress, servicePort)
124 s.server = &http.Server{Addr: addr, Handler: s.router}
125 s.listener, err = net.Listen("tcp", addr)
126 if err != nil {
127 return err
128 }
129
130 s.tomb.Go(func() error {
131 err := s.server.Serve(tcpKeepAliveListener{s.listener.(*net.TCPListener)})
132 if err != nil {
133 return fmt.Errorf("Failed to server HTTP: %s", err)
134 }
135 return nil
136 })
137
138 s.tomb.Wait()
139
140 if s.ap.Running() {
141 s.ap.Stop()
142 }
143
144 return nil
145}
146
147type tcpKeepAliveListener struct {
148 *net.TCPListener
149}
150
151func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
152 tc, err := ln.AcceptTCP()
153 if err != nil {
154 return
155 }
156 tc.SetKeepAlive(true)
157 tc.SetKeepAlivePeriod(3 * time.Minute)
158 return tc, nil
159}
diff --git a/snapcraft.yaml b/snapcraft.yaml
index 3a7d218..072189b 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -11,18 +11,14 @@ description: |
11grade: stable11grade: stable
1212
13apps:13apps:
14 backend:
15 command: bin/ap.sh
16 daemon: simple
17 plugs:
18 - network-control
19 - firewall-control
20 - network-bind
21 - network-manager
22 config:14 config:
23 command: bin/client config15 command: bin/client config
24 plugs:16 plugs:
25 - network17 - network
18 status:
19 command: bin/client status
20 plugs:
21 - network
26 setup-wizard:22 setup-wizard:
27 command: bin/client wizard23 command: bin/client wizard
28 plugs:24 plugs:
@@ -32,6 +28,9 @@ apps:
32 daemon: simple28 daemon: simple
33 plugs:29 plugs:
34 - network-bind30 - network-bind
31 - network-control
32 - firewall-control
33 - network-manager
3534
36parts:35parts:
37 common:36 common:
@@ -74,6 +73,7 @@ parts:
74 - bin73 - bin
75 build-packages:74 build-packages:
76 - golang-github-gorilla-mux-dev75 - golang-github-gorilla-mux-dev
76 - golang-gopkg-tomb.v2-dev
7777
78 dnsmasq:78 dnsmasq:
79 plugin: make79 plugin: make
diff --git a/tests/lib/prepare-all.sh b/tests/lib/prepare-all.sh
index 290b3ed..59e1349 100644
--- a/tests/lib/prepare-all.sh
+++ b/tests/lib/prepare-all.sh
@@ -6,6 +6,13 @@ if [ -n "$SNAP_CHANNEL" ] ; then
6 exit 06 exit 0
7fi7fi
88
9# If there is a wifi-ap snap prebuilt for us, lets take
10# that one to speed things up.
11if [ -e /home/wifi-ap/wifi-ap_*_amd64.snap ] ; then
12 exit 0
13fi
14
15
9# Setup classic snap and build the wifi-ap snap in there16# Setup classic snap and build the wifi-ap snap in there
10snap install --devmode --beta classic17snap install --devmode --beta classic
11cat <<-EOF > /home/test/build-snap.sh18cat <<-EOF > /home/test/build-snap.sh
diff --git a/tests/lib/restore-each.sh b/tests/lib/restore-each.sh
index 799c605..1193b58 100644
--- a/tests/lib/restore-each.sh
+++ b/tests/lib/restore-each.sh
@@ -21,7 +21,6 @@ rm -rf /var/snap/$SNAP_NAME/common/*
21rm -rf /var/snap/$SNAP_NAME/current/*21rm -rf /var/snap/$SNAP_NAME/current/*
22# Depending on what the test did both services are not meant to be22# Depending on what the test did both services are not meant to be
23# running here.23# running here.
24systemctl stop snap.wifi-ap.backend || true
25systemctl stop snap.wifi-ap.management-service || true24systemctl stop snap.wifi-ap.management-service || true
2625
27# Drop any generated or modified netplan configuration files. The original26# Drop any generated or modified netplan configuration files. The original
@@ -40,7 +39,5 @@ netplan generate
40netplan apply39netplan apply
4140
42# Start services again now that the system is restored41# Start services again now that the system is restored
43systemctl start snap.wifi-ap.backend
44systemctl start snap.wifi-ap.management-service42systemctl start snap.wifi-ap.management-service
45wait_for_systemd_service snap.wifi-ap.backend
46wait_for_systemd_service snap.wifi-ap.management-service43wait_for_systemd_service snap.wifi-ap.management-service
diff --git a/tests/main/background-process-control/task.yaml b/tests/main/background-process-control/task.yaml
47new file mode 10064444new file mode 100644
index 0000000..c884b52
--- /dev/null
+++ b/tests/main/background-process-control/task.yaml
@@ -0,0 +1,46 @@
1summary: Test correct service behavior to ensure the background AP process is running
2
3prepare: |
4 # Simulate two WiFi radio network interfaces
5 modprobe mac80211_hwsim radios=2
6
7 # We need some tools for scanning etc.
8 snap install wireless-tools
9 snap connect wireless-tools:network-control core
10
11restore: |
12 rmmod mac80211_hwsim
13
14execute: |
15 # Verify first the management service is up and running
16 /snap/bin/wifi-ap.config get
17 test "`/snap/bin/wifi-ap.config get disabled`" = "1"
18
19 # AP should be not active at this time as still disabled
20 /snap/bin/wifi-ap.status | grep "ap.active: 0"
21
22 # Now start the AP and ensure its reported as active
23 /snap/bin/wifi-ap.config set disabled 0
24 /snap/bin/wifi-ap.status | grep "ap.active: 1"
25 # And if we wait a bit more it should be still active
26 sleep 5
27 /snap/bin/wifi-ap.status | grep "ap.active: 1"
28
29 # Scan for networks on the other side of the WiFi network
30 # and ensure the network is available.
31 ifconfig wlan1 up
32 /snap/bin/wireless-tools.iw dev wlan1 scan | grep 'SSID: Ubuntu'
33
34 # Restart should get us back into the same state we were in before
35 /snap/bin/wifi-ap.status restart-ap
36 # Restart needs some time
37 sleep 5
38 /snap/bin/wifi-ap.status | grep "ap.active: 1"
39 /snap/bin/wireless-tools.iw dev wlan1 scan | grep 'SSID: Ubuntu'
40
41 # If we now stop the management-service the hostapd and dnsmasq
42 # instances should go away.
43 systemctl stop snap.wifi-ap.management-service
44 while /snap/bin/wifi-ap.status | grep "ap.active: 1" ; do
45 sleep 0.5
46 done
diff --git a/tests/main/default-conf-brings-up-ap/task.yaml b/tests/main/default-conf-brings-up-ap/task.yaml
index c8a09dd..5239813 100644
--- a/tests/main/default-conf-brings-up-ap/task.yaml
+++ b/tests/main/default-conf-brings-up-ap/task.yaml
@@ -13,9 +13,6 @@ execute: |
13 # Default configuration will use wlan0 which we just created13 # Default configuration will use wlan0 which we just created
14 /snap/bin/wifi-ap.config set disabled 014 /snap/bin/wifi-ap.config set disabled 0
1515
16 systemctl restart snap.wifi-ap.backend
17 wait_for_systemd_service snap.wifi-ap.backend
18
19 snap install wireless-tools16 snap install wireless-tools
20 snap connect wireless-tools:network-control core17 snap connect wireless-tools:network-control core
2118
diff --git a/tests/main/stress-ap-status-control/task.yaml b/tests/main/stress-ap-status-control/task.yaml
22new file mode 10064419new file mode 100644
index 0000000..62f24d7
--- /dev/null
+++ b/tests/main/stress-ap-status-control/task.yaml
@@ -0,0 +1,75 @@
1summary: Stress test for the AP status control API
2
3environment:
4 RESTART_ITERATIONS: 15
5 SCAN_ITERATIONS: 10
6
7prepare: |
8 # Simulate two WiFi radio network interfaces
9 modprobe mac80211_hwsim radios=2
10
11 # We need some tools for scanning etc.
12 snap install wireless-tools
13 snap connect wireless-tools:network-control core
14
15restore: |
16 rmmod mac80211_hwsim
17
18execute: |
19 # Bring up the access point first
20 /snap/bin/wifi-ap.config set disabled 0
21 while ! /snap/bin/wifi-ap.status | grep "ap.active: 1" ; do
22 sleep 0.5
23 done
24
25 # Scan for networks on the other side of the WiFi network
26 # and ensure the network is available.
27 ifconfig wlan1 up
28 n=0
29 found_ap=0
30 while [ $n -lt $SCAN_ITERATIONS ] ; do
31 if /snap/bin/wireless-tools.iw dev wlan1 scan | grep 'SSID: Ubuntu'; then
32 found_ap=1
33 break
34 fi
35 sleep 0.5
36 let n=n+1
37 done
38 test $found_ap -eq 1
39
40 # We will restart the AP a huge number of times again and again
41 # and expect that the AP afterwards comes back up normally and
42 # we can still search and connect to it.
43 n=0
44 while [ $n -lt $RESTART_ITERATIONS ] ; do
45 /snap/bin/wifi-ap.status restart-ap
46 sleep 0.5
47 let n=n+1
48 done
49
50 # Wait for AP to be marked as active again
51 while ! /snap/bin/wifi-ap.status | grep "ap.active: 1" ; do
52 sleep 0.5
53 done
54
55 # The AP should be still available in our scan result
56 n=0
57 found_ap=0
58 while [ $n -lt $SCAN_ITERATIONS ] ; do
59 if /snap/bin/wireless-tools.iw dev wlan1 scan | grep 'SSID: Ubuntu'; then
60 found_ap=1
61 break
62 fi
63 sleep 0.5
64 let n=n+1
65 done
66 test $found_ap -eq 1
67
68 # Verify we can associate with the AP
69 sudo /snap/bin/wireless-tools.iw wlan1 connect Ubuntu
70 sudo /snap/bin/wireless-tools.iw dev wlan1 link | grep 'SSID: Ubuntu'
71
72 # We should only have one hostapd and one dnsmasq process at this time
73 # (we have to ignore the grep'ing process as otherwise we get a count of 2)
74 test `ps axu | grep hostapd | grep -v grep | wc -l` -eq 1
75 test `ps axu | grep dnsmasq | grep -v grep | wc -l` -eq 1
diff --git a/tests/main/utf8-ssid/task.yaml b/tests/main/utf8-ssid/task.yaml
index 903bfdb..67c334f 100644
--- a/tests/main/utf8-ssid/task.yaml
+++ b/tests/main/utf8-ssid/task.yaml
@@ -13,11 +13,15 @@ execute: |
13 wait_for_systemd_service snap.wifi-ap.management-service13 wait_for_systemd_service snap.wifi-ap.management-service
1414
15 # Default configuration will use wlan0 which we just created15 # Default configuration will use wlan0 which we just created
16 /snap/bin/wifi-ap.config set disabled 0
17 /snap/bin/wifi-ap.config set wifi.ssid 'Ubuntu👍'16 /snap/bin/wifi-ap.config set wifi.ssid 'Ubuntu👍'
17 /snap/bin/wifi-ap.config set disabled 0
1818
19 systemctl restart snap.wifi-ap.backend19 # Wait for AP to become active
20 wait_for_systemd_service snap.wifi-ap.backend20 while ! /snap/bin/wifi-ap.status | grep 'ap.active: 1' ; do
21 sleep 0.5
22 done
23 # Give AP a bit more to settle until it's marked as active
24 sleep 3
2125
22 snap install wireless-tools26 snap install wireless-tools
23 snap connect wireless-tools:network-control core27 snap connect wireless-tools:network-control core

Subscribers

People subscribed via source and target branches

to all changes: