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