Merge ~teknoraver/snappy-hwe-snaps/+git/wifi-ap:master into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master
- Git
- lp:~teknoraver/snappy-hwe-snaps/+git/wifi-ap
- master
- Merge into master
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) |
Related bugs: |
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 |
Commit message
Description of the change
Konrad Zapałowicz (kzapalowicz) wrote : | # |
Simon Fels (morphis) wrote : | # |
See comments in line.
Matteo Croce (teknoraver) : | # |
Matteo Croce (teknoraver) : | # |
Matteo Croce (teknoraver) : | # |
Simon Fels (morphis) wrote : | # |
Also missing changes to snapcraft.yaml to build the service as part of the snap.
Simon Fels (morphis) wrote : | # |
See comments in line
Tony Espy (awe) wrote : | # |
Based on our earlier discussion, +1 from me.
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 unescapeTextByS
input = strings.Trim(input, `"'`)
if strings.
re := regexp.
input = re.ReplaceAllSt
}
return input
}
I'm thinking if we should rather strip the characters $"\` instead of messing with such regular expressions.
Simon Fels (morphis) wrote : | # |
@matteo: if we test this there should not be any issue with using such a regular expression.
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
Simon Fels (morphis) wrote : | # |
Did you already added the tests you described in your last comment? Otherwise I would like to merge this now.
Matteo Croce (teknoraver) wrote : | # |
Not yet, sorry
Simon Fels (morphis) wrote : | # |
@Matteo: Then lets do this as follow up MP. I will merge this one now.
Preview Diff
1 | diff --git a/service/main.go b/service/main.go |
2 | new file mode 100644 |
3 | index 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 | +} |
224 | diff --git a/service/main_test.go b/service/main_test.go |
225 | new file mode 100644 |
226 | index 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 | +} |
439 | diff --git a/snapcraft.yaml b/snapcraft.yaml |
440 | index 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 |
Code looks ok, but IMHO needs more documentation