Merge ~teknoraver/snappy-hwe-snaps/+git/wifi-ap:master into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master

Proposed by Matteo Croce
Status: Merged
Approved by: Simon Fels
Approved revision: 24193779420b08050c35cbd5daa919a5eda74447
Merged at revision: e256b54b6cc438b9bfd3bc063c89ac6765190bee
Proposed branch: ~teknoraver/snappy-hwe-snaps/+git/wifi-ap:master
Merge into: ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master
Diff against target: 475 lines (+437/-1)
3 files modified
service/main.go (+217/-0)
service/main_test.go (+209/-0)
snapcraft.yaml (+11/-1)
Reviewer Review Type Date Requested Status
Tony Espy Approve
Simon Fels Approve
Konrad Zapałowicz (community) code Needs Fixing
Matteo Croce Pending
Review via email: mp+304793@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Konrad Zapałowicz (kzapalowicz) wrote :

Code looks ok, but IMHO needs more documentation

review: Needs Fixing (code)
Revision history for this message
Simon Fels (morphis) wrote :

See comments in line.

review: Needs Fixing
Revision history for this message
Matteo Croce (teknoraver) :
Revision history for this message
Matteo Croce (teknoraver) :
Revision history for this message
Matteo Croce (teknoraver) :
Revision history for this message
Simon Fels (morphis) wrote :

Also missing changes to snapcraft.yaml to build the service as part of the snap.

review: Needs Fixing
Revision history for this message
Simon Fels (morphis) wrote :

See comments in line

review: Needs Fixing
Revision history for this message
Simon Fels (morphis) wrote :

LGTM

review: Approve
Revision history for this message
Tony Espy (awe) wrote :

Based on our earlier discussion, +1 from me.

review: Approve
Revision history for this message
Matteo Croce (teknoraver) wrote :

I'm thinking that the opposite of the shell escape should be done when reading the configuration file with something like:

func unescapeTextByShell(input string) string {
 input = strings.Trim(input, `"'`)
 if strings.ContainsAny(input, "\\") {
  re := regexp.MustCompile("\\\\([\\\\$\\`\\\"])")
  input = re.ReplaceAllString(input, "$1")
 }
 return input
}

I'm thinking if we should rather strip the characters $"\` instead of messing with such regular expressions.

Revision history for this message
Simon Fels (morphis) wrote :

@matteo: if we test this there should not be any issue with using such a regular expression.

Revision history for this message
Matteo Croce (teknoraver) wrote :

> @matteo: if we test this there should not be any issue with using such a
> regular expression.

The test actually are configuration get, and JSON -> config file.
I'll add a test which does JSON -> config -> JSON to test escape and unescape

Revision history for this message
Simon Fels (morphis) wrote :

Did you already added the tests you described in your last comment? Otherwise I would like to merge this now.

Revision history for this message
Matteo Croce (teknoraver) wrote :

Not yet, sorry

Revision history for this message
Simon Fels (morphis) wrote :

@Matteo: Then lets do this as follow up MP. I will merge this one now.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/service/main.go b/service/main.go
2new file mode 100644
3index 0000000..17134ed
4--- /dev/null
5+++ b/service/main.go
6@@ -0,0 +1,217 @@
7+//
8+// Copyright (C) 2016 Canonical Ltd
9+//
10+// This program is free software: you can redistribute it and/or modify
11+// it under the terms of the GNU General Public License version 3 as
12+// published by the Free Software Foundation.
13+//
14+// This program is distributed in the hope that it will be useful,
15+// but WITHOUT ANY WARRANTY; without even the implied warranty of
16+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+// GNU General Public License for more details.
18+//
19+// You should have received a copy of the GNU General Public License
20+// along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
22+package main
23+
24+import (
25+ "bufio"
26+ "encoding/json"
27+ "fmt"
28+ "io/ioutil"
29+ "log"
30+ "net/http"
31+ "os"
32+ "regexp"
33+ "strconv"
34+ "strings"
35+
36+ "github.com/gorilla/mux"
37+)
38+
39+/* JSON message format, as described here:
40+{
41+ "result": {
42+ "key" : "val"
43+ },
44+ "status": "OK",
45+ "status-code": 200,
46+ "type": "sync"
47+}
48+*/
49+
50+type Response struct {
51+ Result map[string]string `json:"result"`
52+ Status string `json:"status"`
53+ StatusCode int `json:"status-code"`
54+ Type string `json:"type"`
55+}
56+
57+func makeErrorResponse(code int, message, kind string) Response {
58+ return Response{
59+ Type: "error",
60+ Status: http.StatusText(code),
61+ StatusCode: code,
62+ Result: map[string]string{
63+ "message": message,
64+ "kind": kind,
65+ },
66+ }
67+}
68+
69+func makeResponse(status int, result map[string]string) Response {
70+ return Response{
71+ Type: "sync",
72+ Status: http.StatusText(status),
73+ StatusCode: status,
74+ Result: result,
75+ }
76+}
77+
78+func sendHttpResponse(writer http.ResponseWriter, response Response) {
79+ writer.WriteHeader(response.StatusCode)
80+ data, _ := json.Marshal(response)
81+ fmt.Fprintln(writer, string(data))
82+}
83+
84+func getConfigOnPath(path string) string {
85+ return path + "/config"
86+}
87+
88+// Array of paths where the config file can be found.
89+// The first one is readonly, the others are writable
90+// they are readed in order and the configuration is merged
91+var cfgpaths []string = []string{getConfigOnPath(os.Getenv("SNAP")),
92+ getConfigOnPath(os.Getenv("SNAP_DATA")), getConfigOnPath(os.Getenv("SNAP_USER_DATA"))}
93+
94+const (
95+ PORT = 5005
96+ CONFIGURATION_API_URI = "/v1/configuration"
97+)
98+
99+func main() {
100+ r := mux.NewRouter().StrictSlash(true)
101+
102+ r.HandleFunc(CONFIGURATION_API_URI, getConfiguration).Methods(http.MethodGet)
103+ r.HandleFunc(CONFIGURATION_API_URI, changeConfiguration).Methods(http.MethodPost)
104+
105+ log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(PORT), r))
106+}
107+
108+// Convert eg. WIFI_OPERATION_MODE to wifi.operation-mode
109+func convertKeyToRepresentationFormat(key string) string {
110+ new_key := strings.ToLower(key)
111+ new_key = strings.Replace(new_key, "_", ".", 1)
112+ return strings.Replace(new_key, "_", "-", -1)
113+}
114+
115+func convertKeyToStorageFormat(key string) string {
116+ // Convert eg. wifi.operation-mode to WIFI_OPERATION_MODE
117+ key = strings.ToUpper(key)
118+ key = strings.Replace(key, ".", "_", -1)
119+ return strings.Replace(key, "-", "_", -1)
120+}
121+
122+func readConfigurationFile(filePath string, config map[string]string) (err error) {
123+ file, err := os.Open(filePath)
124+ if err != nil {
125+ return nil
126+ }
127+
128+ defer file.Close()
129+
130+ for scanner := bufio.NewScanner(file); scanner.Scan(); {
131+ // Ignore all empty or commented lines
132+ if line := scanner.Text(); len(line) != 0 && line[0] != '#' {
133+ // Line must be in the KEY=VALUE format
134+ if parts := strings.Split(line, "="); len(parts) == 2 {
135+ value := unescapeTextByShell(parts[1])
136+ config[convertKeyToRepresentationFormat(parts[0])] = value
137+ }
138+ }
139+ }
140+
141+ return nil
142+}
143+
144+func readConfiguration(paths []string, config map[string]string) (err error) {
145+ for _, location := range paths {
146+ if readConfigurationFile(location, config) != nil {
147+ return fmt.Errorf("Failed to read configuration file '%s'", location)
148+ }
149+ }
150+
151+ return nil
152+}
153+
154+func getConfiguration(writer http.ResponseWriter, request *http.Request) {
155+ config := make(map[string]string)
156+ if readConfiguration(cfgpaths, config) == nil {
157+ sendHttpResponse(writer, makeResponse(http.StatusOK, config))
158+ } else {
159+ errResponse := makeErrorResponse(http.StatusInternalServerError, "Failed to read configuration data", "internal-error")
160+ sendHttpResponse(writer, errResponse)
161+ }
162+}
163+
164+// Escape shell special characters, avoid injection
165+// eg. SSID set to "My AP$(nc -lp 2323 -e /bin/sh)"
166+// to get a root shell
167+func escapeTextForShell(input string) string {
168+ if strings.ContainsAny(input, "\\\"'`$\n\t #") {
169+ input = strings.Replace(input, `\`, `\\`, -1)
170+ input = strings.Replace(input, `"`, `\"`, -1)
171+ input = strings.Replace(input, "`", "\\`", -1)
172+ input = strings.Replace(input, `$`, `\$`, -1)
173+
174+ input = `"` + input + `"`
175+ }
176+ return input
177+}
178+
179+// Do the reverse of escapeTextForShell() here
180+// strip any \ followed by \$`"
181+func unescapeTextByShell(input string) string {
182+ input = strings.Trim(input, `"'`)
183+ if strings.ContainsAny(input, "\\") {
184+ re := regexp.MustCompile("\\\\([\\\\$\\`\\\"])")
185+ input = re.ReplaceAllString(input, "$1")
186+ }
187+ return input
188+}
189+
190+func changeConfiguration(writer http.ResponseWriter, request *http.Request) {
191+ // Write in SNAP_DATA
192+ confWrite := getConfigOnPath(os.Getenv("SNAP_DATA"))
193+
194+ file, err := os.Create(confWrite)
195+ if err != nil {
196+ errResponse := makeErrorResponse(http.StatusInternalServerError, "Can't write configuration file", "internal-error")
197+ sendHttpResponse(writer, errResponse)
198+ return
199+ }
200+ defer file.Close()
201+
202+ body, err := ioutil.ReadAll(request.Body)
203+ if err != nil {
204+ errResponse := makeErrorResponse(http.StatusInternalServerError, "Error reading the request body", "internal-error")
205+ sendHttpResponse(writer, errResponse)
206+ return
207+ }
208+
209+ var items map[string]string
210+ if json.Unmarshal(body, &items) != nil {
211+ errResponse := makeErrorResponse(http.StatusInternalServerError, "Malformed request", "internal-error")
212+ sendHttpResponse(writer, errResponse)
213+ return
214+ }
215+
216+ for key, value := range items {
217+ key = convertKeyToStorageFormat(key)
218+ value = escapeTextForShell(value)
219+ file.WriteString(fmt.Sprintf("%s=%s\n", key, value))
220+ }
221+
222+ sendHttpResponse(writer, makeResponse(http.StatusOK, nil))
223+}
224diff --git a/service/main_test.go b/service/main_test.go
225new file mode 100644
226index 0000000..d15430b
227--- /dev/null
228+++ b/service/main_test.go
229@@ -0,0 +1,209 @@
230+//
231+// Copyright (C) 2016 Canonical Ltd
232+//
233+// This program is free software: you can redistribute it and/or modify
234+// it under the terms of the GNU General Public License version 3 as
235+// published by the Free Software Foundation.
236+//
237+// This program is distributed in the hope that it will be useful,
238+// but WITHOUT ANY WARRANTY; without even the implied warranty of
239+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
240+// GNU General Public License for more details.
241+//
242+// You should have received a copy of the GNU General Public License
243+// along with this program. If not, see <http://www.gnu.org/licenses/>.
244+
245+package main
246+
247+import (
248+ "bytes"
249+ "encoding/json"
250+ "gopkg.in/check.v1"
251+ "io/ioutil"
252+ "net/http"
253+ "net/http/httptest"
254+ "os"
255+ "strings"
256+ "testing"
257+)
258+
259+// gopkg.in/check.v1 stuff
260+func Test(t *testing.T) { check.TestingT(t) }
261+
262+type S struct{}
263+
264+var _ = check.Suite(&S{})
265+
266+// Test the config file path append routine
267+func (s *S) TestPath(c *check.C) {
268+ c.Assert(getConfigOnPath("/test"), check.Equals, "/test/config")
269+}
270+
271+// List of tokens to be translated
272+var cfgKeys = [...][2]string{
273+ {"DISABLED", "disabled"},
274+ {"WIFI_SSID", "wifi.ssid"},
275+ {"WIFI_INTERFACE", "wifi.interface"},
276+ {"WIFI_INTERFACE_MODE", "wifi.interface-mode"},
277+ {"DHCP_RANGE_START", "dhcp.range-start"},
278+ {"MYTOKEN", "mytoken"},
279+ {"CFG_TOKEN", "cfg.token"},
280+ {"MY_TOKEN$", "my.token$"},
281+}
282+
283+// Test token conversion from internal format
284+func (s *S) TestConvertKeyToRepresentationFormat(c *check.C) {
285+ for _, st := range cfgKeys {
286+ c.Assert(convertKeyToRepresentationFormat(st[0]), check.Equals, st[1])
287+ }
288+}
289+
290+// Test token conversion to internal format
291+func (s *S) TestConvertKeyToStorageFormat(c *check.C) {
292+ for _, st := range cfgKeys {
293+ c.Assert(convertKeyToStorageFormat(st[1]), check.Equals, st[0])
294+ }
295+}
296+
297+// List of malicious tokens which needs to be escaped
298+func (s *S) TestEscapeShell(c *check.C) {
299+ cmds := [...][2]string{
300+ {"my_ap", "my_ap"},
301+ {`my ap`, `"my ap"`},
302+ {`my "ap"`, `"my \"ap\""`},
303+ {`$(ps ax)`, `"\$(ps ax)"`},
304+ {"`ls /`", "\"\\`ls /\\`\""},
305+ {`c:\dir`, `"c:\\dir"`},
306+ }
307+ for _, st := range cmds {
308+ c.Assert(escapeTextForShell(st[0]), check.Equals, st[1])
309+ }
310+}
311+
312+func (s *S) TestGetConfiguration(c *check.C) {
313+ // Check it we get a valid JSON as configuration
314+ req, err := http.NewRequest(http.MethodGet, CONFIGURATION_API_URI, nil)
315+ c.Assert(err, check.IsNil)
316+
317+ rec := httptest.NewRecorder()
318+
319+ getConfiguration(rec, req)
320+
321+ body, err := ioutil.ReadAll(rec.Result().Body)
322+ c.Assert(err, check.IsNil)
323+
324+ // Parse the returned JSON
325+ var resp Response
326+ err = json.Unmarshal(body, &resp)
327+ c.Assert(err, check.IsNil)
328+
329+ // Check for 200 status code
330+ c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
331+ c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
332+ c.Assert(resp.Type, check.Equals, "sync")
333+}
334+
335+func (s *S) TestChangeConfiguration(c *check.C) {
336+ // Test a non writable path:
337+ os.Setenv("SNAP_DATA", "/nodir")
338+
339+ req, err := http.NewRequest(http.MethodPost, CONFIGURATION_API_URI, nil)
340+ c.Assert(err, check.IsNil)
341+
342+ rec := httptest.NewRecorder()
343+
344+ changeConfiguration(rec, req)
345+
346+ c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
347+
348+ body, err := ioutil.ReadAll(rec.Result().Body)
349+ c.Assert(err, check.IsNil)
350+
351+ // Parse the returned JSON
352+ var resp Response
353+ err = json.Unmarshal(body, &resp)
354+ c.Assert(err, check.IsNil)
355+
356+ // Check for 500 status code and other error fields
357+ c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
358+ c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
359+ c.Assert(resp.Type, check.Equals, "error")
360+ c.Assert(resp.Result["kind"], check.Equals, "internal-error")
361+ c.Assert(resp.Result["message"], check.Equals, "Can't write configuration file")
362+
363+ // Test an invalid JSON
364+ os.Setenv("SNAP_DATA", "/tmp")
365+ req, err = http.NewRequest(http.MethodPost, CONFIGURATION_API_URI, strings.NewReader("not a JSON content"))
366+ c.Assert(err, check.IsNil)
367+
368+ rec = httptest.NewRecorder()
369+
370+ changeConfiguration(rec, req)
371+
372+ c.Assert(rec.Code, check.Equals, http.StatusInternalServerError)
373+
374+ body, err = ioutil.ReadAll(rec.Result().Body)
375+ c.Assert(err, check.IsNil)
376+
377+ // Parse the returned JSON
378+ resp = Response{}
379+ err = json.Unmarshal(body, &resp)
380+ c.Assert(err, check.IsNil)
381+
382+ // Check for 500 status code and other error fields
383+ c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusInternalServerError))
384+ c.Assert(resp.StatusCode, check.Equals, http.StatusInternalServerError)
385+ c.Assert(resp.Type, check.Equals, "error")
386+ c.Assert(resp.Result["kind"], check.Equals, "internal-error")
387+ c.Assert(resp.Result["message"], check.Equals, "Malformed request")
388+
389+ // Test a succesful configuration set
390+
391+ // Values to be used in the config
392+ values := map[string]string{
393+ "wifi.security": "wpa2",
394+ "wifi.ssid": "UbuntuAP",
395+ "wifi.security-passphrase": "12345678",
396+ }
397+
398+ // Convert the map into JSON
399+ args, err := json.Marshal(values)
400+ c.Assert(err, check.IsNil)
401+
402+ req, err = http.NewRequest(http.MethodPost, CONFIGURATION_API_URI, bytes.NewReader(args))
403+ c.Assert(err, check.IsNil)
404+
405+ rec = httptest.NewRecorder()
406+
407+ // Do the request
408+ changeConfiguration(rec, req)
409+
410+ c.Assert(rec.Code, check.Equals, http.StatusOK)
411+
412+ // Read the result JSON
413+ body, err = ioutil.ReadAll(rec.Result().Body)
414+ c.Assert(err, check.IsNil)
415+
416+ // Parse the returned JSON
417+ resp = Response{}
418+ err = json.Unmarshal(body, &resp)
419+ c.Assert(err, check.IsNil)
420+
421+ // Check for 200 status code and other succesful fields
422+ c.Assert(resp.Status, check.Equals, http.StatusText(http.StatusOK))
423+ c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
424+ c.Assert(resp.Type, check.Equals, "sync")
425+
426+ // Read the generated config and check that values were set
427+ config, err := ioutil.ReadFile(getConfigOnPath(os.Getenv("SNAP_DATA")))
428+ c.Assert(err, check.IsNil)
429+
430+ for key, value := range values {
431+ c.Assert(strings.Contains(string(config),
432+ convertKeyToStorageFormat(key)+"="+value+"\n"),
433+ check.Equals, true)
434+ }
435+
436+ // don't leave garbage in /tmp
437+ os.Remove(getConfigOnPath(os.Getenv("SNAP_DATA")))
438+}
439diff --git a/snapcraft.yaml b/snapcraft.yaml
440index c4f1c0d..bba52b0 100644
441--- a/snapcraft.yaml
442+++ b/snapcraft.yaml
443@@ -20,6 +20,9 @@ apps:
444 - network-manager
445 config:
446 command: bin/config.sh
447+ management-service:
448+ command: bin/service
449+ daemon: simple
450
451 parts:
452 scripts:
453@@ -44,6 +47,13 @@ parts:
454 - bin/iwconfig
455 snap:
456 - $binaries
457+
458+ management-service:
459+ plugin: go
460+ source: ./service
461+ snap:
462+ - bin
463+
464 dnsmasq:
465 plugin: make
466 source: https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap
467@@ -99,7 +109,7 @@ parts:
468 - rtl8188/hostapd
469 snap:
470 - $binaries
471- nmlci:
472+ nmcli:
473 plugin: nil
474 stage-packages:
475 - network-manager

Subscribers

People subscribed via source and target branches

to all changes: