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