Merge lp:~chipaca/ubuntu-push/shuffle-signing-bits into lp:ubuntu-push
- shuffle-signing-bits
- Merge into trunk
Proposed by
John Lenton
Status: | Superseded |
---|---|
Proposed branch: | lp:~chipaca/ubuntu-push/shuffle-signing-bits |
Merge into: | lp:ubuntu-push |
Diff against target: |
959 lines (+525/-68) 18 files modified
client/client.go (+21/-1) client/client_test.go (+36/-1) client/session/session.go (+5/-17) client/session/session_test.go (+6/-21) debian/changelog (+14/-0) debian/rules (+0/-1) scripts/register (+43/-0) scripts/unicast (+2/-0) server/acceptance/acceptanceclient.go (+1/-1) server/acceptance/cmd/acceptanceclient.go (+3/-1) server/acceptance/suites/suite.go (+2/-2) server/acceptance/suites/unicast.go (+11/-3) server/api/handlers.go (+104/-8) server/api/handlers_test.go (+193/-2) server/store/inmemory.go (+26/-0) server/store/inmemory_test.go (+44/-0) server/store/store.go (+8/-0) signing-helper/signing-helper.cpp (+6/-10) |
To merge this branch: | bzr merge lp:~chipaca/ubuntu-push/shuffle-signing-bits |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu Push Hackers | Pending | ||
Review via email: mp+222782@code.launchpad.net |
This proposal has been superseded by a proposal from 2014-06-11.
Commit message
Description of the change
Move the signing bits about a bit (up from session to client, for reuse from service).
To post a comment you must log in.
- 181. By John Lenton
-
merged trunk
Unmerged revisions
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'client/client.go' |
2 | --- client/client.go 2014-05-21 10:03:18 +0000 |
3 | +++ client/client.go 2014-06-11 11:43:11 +0000 |
4 | @@ -28,6 +28,7 @@ |
5 | "fmt" |
6 | "io/ioutil" |
7 | "os" |
8 | + "os/exec" |
9 | "strings" |
10 | |
11 | "launchpad.net/go-dbus/v1" |
12 | @@ -160,7 +161,26 @@ |
13 | ExpectAllRepairedTime: client.config.ExpectAllRepairedTime.TimeDuration(), |
14 | PEM: client.pem, |
15 | Info: info, |
16 | - AuthHelper: client.config.AuthHelper, |
17 | + AuthGetter: client.getAuthorization, |
18 | + } |
19 | +} |
20 | + |
21 | +// getAuthorization gets the authorization blob to send to the server |
22 | +func (client *PushClient) getAuthorization() string { |
23 | + client.log.Debugf("getting authorization") |
24 | + // using a helper, for now at least |
25 | + if len(client.config.AuthHelper) == 0 { |
26 | + // do nothing if helper is unset or empty |
27 | + return "" |
28 | + } |
29 | + |
30 | + auth, err := exec.Command(client.config.AuthHelper[0], client.config.AuthHelper[1:]...).Output() |
31 | + if err != nil { |
32 | + // For now we just log the error, as we don't want to block unauthorized users |
33 | + client.log.Errorf("unable to get the authorization token from the account: %v", err) |
34 | + return "" |
35 | + } else { |
36 | + return strings.TrimSpace(string(auth)) |
37 | } |
38 | } |
39 | |
40 | |
41 | === modified file 'client/client_test.go' |
42 | --- client/client_test.go 2014-05-20 13:56:37 +0000 |
43 | +++ client/client_test.go 2014-06-11 11:43:11 +0000 |
44 | @@ -272,7 +272,7 @@ |
45 | ExpectAllRepairedTime: 30 * time.Minute, |
46 | PEM: cli.pem, |
47 | Info: info, |
48 | - AuthHelper: []string{"auth", "helper"}, |
49 | + AuthGetter: func() string { return "" }, |
50 | } |
51 | // sanity check that we are looking at all fields |
52 | vExpected := reflect.ValueOf(expected) |
53 | @@ -284,6 +284,11 @@ |
54 | } |
55 | // finally compare |
56 | conf := cli.deriveSessionConfig(info) |
57 | + // compare authGetter by string |
58 | + c.Check(fmt.Sprintf("%v", conf.AuthGetter), Equals, fmt.Sprintf("%v", cli.getAuthorization)) |
59 | + // and set it to nil |
60 | + conf.AuthGetter = nil |
61 | + expected.AuthGetter = nil |
62 | c.Check(conf, DeepEquals, expected) |
63 | } |
64 | |
65 | @@ -951,3 +956,33 @@ |
66 | c.Assert(err, NotNil) |
67 | c.Check(cs.log.Captured(), Matches, "(?msi).*showing notification: no way$") |
68 | } |
69 | + |
70 | +/***************************************************************** |
71 | + getAuthorization() tests |
72 | +******************************************************************/ |
73 | + |
74 | +func (cs *clientSuite) TestGetAuthorizationIgnoresErrors(c *C) { |
75 | + cli := NewPushClient(cs.configPath, cs.leveldbPath) |
76 | + cli.configure() |
77 | + cli.config.AuthHelper = []string{"sh", "-c", "echo hello; false"} |
78 | + |
79 | + c.Check(cli.getAuthorization(), Equals, "") |
80 | +} |
81 | + |
82 | +func (cs *clientSuite) TestGetAuthorizationGetsIt(c *C) { |
83 | + cli := NewPushClient(cs.configPath, cs.leveldbPath) |
84 | + cli.configure() |
85 | + cli.config.AuthHelper = []string{"echo", "hello"} |
86 | + |
87 | + c.Check(cli.getAuthorization(), Equals, "hello") |
88 | +} |
89 | + |
90 | +func (cs *clientSuite) TestGetAuthorizationWorksIfUnsetOrNil(c *C) { |
91 | + cli := NewPushClient(cs.configPath, cs.leveldbPath) |
92 | + cli.log = cs.log |
93 | + |
94 | + c.Assert(cli.config, NotNil) |
95 | + c.Check(cli.getAuthorization(), Equals, "") |
96 | + cli.configure() |
97 | + c.Check(cli.getAuthorization(), Equals, "") |
98 | +} |
99 | |
100 | === modified file 'client/session/session.go' |
101 | --- client/session/session.go 2014-05-23 05:59:38 +0000 |
102 | +++ client/session/session.go 2014-06-11 11:43:11 +0000 |
103 | @@ -26,7 +26,6 @@ |
104 | "fmt" |
105 | "math/rand" |
106 | "net" |
107 | - "os/exec" |
108 | "strings" |
109 | "sync" |
110 | "sync/atomic" |
111 | @@ -87,7 +86,7 @@ |
112 | ExpectAllRepairedTime time.Duration |
113 | PEM []byte |
114 | Info map[string]interface{} |
115 | - AuthHelper []string |
116 | + AuthGetter func() string |
117 | } |
118 | |
119 | // ClientSession holds a client<->server session and its configuration. |
120 | @@ -244,21 +243,10 @@ |
121 | // addAuthorization gets the authorization blob to send to the server |
122 | // and adds it to the session. |
123 | func (sess *ClientSession) addAuthorization() error { |
124 | - sess.Log.Debugf("adding authorization") |
125 | - // using a helper, for now at least |
126 | - if len(sess.AuthHelper) == 0 { |
127 | - // do nothing if helper is unset or empty |
128 | - return nil |
129 | - } |
130 | - |
131 | - auth, err := exec.Command(sess.AuthHelper[0], sess.AuthHelper[1:]...).Output() |
132 | - if err != nil { |
133 | - // For now we just log the error, as we don't want to block unauthorized users |
134 | - sess.Log.Errorf("unable to get the authorization token from the account: %v", err) |
135 | - } else { |
136 | - sess.auth = strings.TrimSpace(string(auth)) |
137 | - } |
138 | - |
139 | + if sess.AuthGetter != nil { |
140 | + sess.Log.Debugf("adding authorization") |
141 | + sess.auth = sess.AuthGetter() |
142 | + } |
143 | return nil |
144 | } |
145 | |
146 | |
147 | === modified file 'client/session/session_test.go' |
148 | --- client/session/session_test.go 2014-05-23 05:59:38 +0000 |
149 | +++ client/session/session_test.go 2014-06-11 11:43:11 +0000 |
150 | @@ -354,33 +354,18 @@ |
151 | |
152 | func (cs *clientSessionSuite) TestAddAuthorizationAddsAuthorization(c *C) { |
153 | sess := &ClientSession{Log: cs.log} |
154 | - sess.AuthHelper = []string{"echo", "some auth"} |
155 | + sess.AuthGetter = func() string { return "some auth" } |
156 | c.Assert(sess.auth, Equals, "") |
157 | err := sess.addAuthorization() |
158 | c.Assert(err, IsNil) |
159 | c.Check(sess.auth, Equals, "some auth") |
160 | } |
161 | |
162 | -func (cs *clientSessionSuite) TestAddAuthorizationIgnoresErrors(c *C) { |
163 | - sess := &ClientSession{Log: cs.log} |
164 | - sess.AuthHelper = []string{"sh", "-c", "echo hello; false"} |
165 | - |
166 | - c.Assert(sess.auth, Equals, "") |
167 | - err := sess.addAuthorization() |
168 | - c.Assert(err, IsNil) |
169 | - c.Check(sess.auth, Equals, "") |
170 | -} |
171 | - |
172 | -func (cs *clientSessionSuite) TestAddAuthorizationSkipsIfUnsetOrNil(c *C) { |
173 | - sess := &ClientSession{Log: cs.log} |
174 | - sess.AuthHelper = nil |
175 | - c.Assert(sess.auth, Equals, "") |
176 | - err := sess.addAuthorization() |
177 | - c.Assert(err, IsNil) |
178 | - c.Check(sess.auth, Equals, "") |
179 | - |
180 | - sess.AuthHelper = []string{} |
181 | - err = sess.addAuthorization() |
182 | +func (cs *clientSessionSuite) TestAddAuthorizationSkipsIfUnset(c *C) { |
183 | + sess := &ClientSession{Log: cs.log} |
184 | + sess.AuthGetter = nil |
185 | + c.Assert(sess.auth, Equals, "") |
186 | + err := sess.addAuthorization() |
187 | c.Assert(err, IsNil) |
188 | c.Check(sess.auth, Equals, "") |
189 | } |
190 | |
191 | === modified file 'debian/changelog' |
192 | --- debian/changelog 2014-06-05 09:42:22 +0000 |
193 | +++ debian/changelog 2014-06-11 11:43:11 +0000 |
194 | @@ -1,3 +1,17 @@ |
195 | +ubuntu-push (0.31ubuntu1) UNRELEASED; urgency=medium |
196 | + |
197 | + [ Samuele Pedroni ] |
198 | + * Support registering tokens and sending notifications with a token |
199 | + * Register script and scripts unicast support |
200 | + |
201 | + [ Roberto Alsina ] |
202 | + * Make signing-helper generate a HTTP header instead of a querystring. |
203 | + |
204 | + [ John R. Lenton ] |
205 | + * Move signing bits up from session to client, for reuse by service. |
206 | + |
207 | + -- John R. Lenton <john.lenton@canonical.com> Fri, 06 Jun 2014 12:02:56 +0100 |
208 | + |
209 | ubuntu-push (0.3+14.10.20140605-0ubuntu1) utopic; urgency=medium |
210 | |
211 | [ John Lenton ] |
212 | |
213 | === modified file 'debian/rules' |
214 | --- debian/rules 2014-05-02 12:42:27 +0000 |
215 | +++ debian/rules 2014-06-11 11:43:11 +0000 |
216 | @@ -5,7 +5,6 @@ |
217 | export UBUNTU_PUSH_TEST_RESOURCES_ROOT := $(CURDIR) |
218 | |
219 | override_dh_auto_build: |
220 | - cd $$( find ./ -type d -regex '\./[^/]*/src/launchpad.net' -printf "%h\n" | head -n1) |
221 | dh_auto_build --buildsystem=golang |
222 | (cd signing-helper && cmake . && make) |
223 | |
224 | |
225 | === added file 'scripts/register' |
226 | --- scripts/register 1970-01-01 00:00:00 +0000 |
227 | +++ scripts/register 2014-06-11 11:43:11 +0000 |
228 | @@ -0,0 +1,43 @@ |
229 | +#!/usr/bin/python3 |
230 | +""" |
231 | +request a unicast registration |
232 | +""" |
233 | +import argparse |
234 | +import json |
235 | +import requests |
236 | +import subprocess |
237 | +import datetime |
238 | +import sys |
239 | + |
240 | + |
241 | +def main(): |
242 | + parser = argparse.ArgumentParser(description=__doc__) |
243 | + parser.add_argument('deviceid', nargs=1) |
244 | + parser.add_argument('appid', nargs=1) |
245 | + parser.add_argument('-H', '--host', |
246 | + help="host:port (default: %(default)s)", |
247 | + default="localhost:8080") |
248 | + parser.add_argument('--no-https', action='store_true', default=False) |
249 | + parser.add_argument('--insecure', action='store_true', default=False, |
250 | + help="don't check host/certs with https") |
251 | + parser.add_argument('--auth_helper', default="") |
252 | + args = parser.parse_args() |
253 | + scheme = 'https' |
254 | + if args.no_https: |
255 | + scheme = 'http' |
256 | + url = "%s://%s/register" % (scheme, args.host) |
257 | + body = { |
258 | + 'deviceid': args.deviceid[0], |
259 | + 'appid': args.appid[0], |
260 | + } |
261 | + headers = {'Content-Type': 'application/json'} |
262 | + if args.auth_helper: |
263 | + auth = subprocess.check_output([args.auth_helper, url]).strip() |
264 | + headers['Authorization'] = auth |
265 | + r = requests.post(url, data=json.dumps(body), headers=headers, |
266 | + verify=not args.insecure) |
267 | + print(r.status_code) |
268 | + print(r.text) |
269 | + |
270 | +if __name__ == '__main__': |
271 | + main() |
272 | |
273 | === modified file 'scripts/unicast' |
274 | --- scripts/unicast 2014-05-29 14:55:19 +0000 |
275 | +++ scripts/unicast 2014-06-11 11:43:11 +0000 |
276 | @@ -52,6 +52,8 @@ |
277 | userid, devid = reg.split(':', 1) |
278 | body['userid'] = userid |
279 | body['deviceid'] = devid |
280 | + else: |
281 | + body['token'] = reg |
282 | xauth = {} |
283 | if args.user and args.password: |
284 | xauth = {'auth': requests.auth.HTTPBasicAuth(args.user, args.password)} |
285 | |
286 | === modified file 'server/acceptance/acceptanceclient.go' |
287 | --- server/acceptance/acceptanceclient.go 2014-05-23 12:30:32 +0000 |
288 | +++ server/acceptance/acceptanceclient.go 2014-06-11 11:43:11 +0000 |
289 | @@ -155,7 +155,7 @@ |
290 | return err |
291 | } |
292 | events <- fmt.Sprintf("%sbroadcast chan:%v app:%v topLevel:%d payloads:%s", sess.Prefix, recv.ChanId, recv.AppId, recv.TopLevel, pack) |
293 | - case "connwarn": |
294 | + case "warn", "connwarn": |
295 | events <- fmt.Sprintf("%sconnwarn %s", sess.Prefix, recv.Reason) |
296 | } |
297 | } |
298 | |
299 | === modified file 'server/acceptance/cmd/acceptanceclient.go' |
300 | --- server/acceptance/cmd/acceptanceclient.go 2014-05-21 07:58:26 +0000 |
301 | +++ server/acceptance/cmd/acceptanceclient.go 2014-06-11 11:43:11 +0000 |
302 | @@ -90,7 +90,9 @@ |
303 | } |
304 | } |
305 | if len(cfg.AuthHelper) != 0 { |
306 | - auth, err := exec.Command(cfg.AuthHelper[0], cfg.AuthHelper[1:]...).Output() |
307 | + helperArgs := cfg.AuthHelper[1:] |
308 | + helperArgs = append(helperArgs, "https://push.ubuntu.com/") |
309 | + auth, err := exec.Command(cfg.AuthHelper[0], helperArgs...).Output() |
310 | if err != nil { |
311 | log.Fatalf("auth helper: %v", err) |
312 | } |
313 | |
314 | === modified file 'server/acceptance/suites/suite.go' |
315 | --- server/acceptance/suites/suite.go 2014-05-02 09:56:49 +0000 |
316 | +++ server/acceptance/suites/suite.go 2014-06-11 11:43:11 +0000 |
317 | @@ -89,7 +89,7 @@ |
318 | // to kill the server process |
319 | KillGroup map[string]func(os.Signal) |
320 | // hook to adjust requests |
321 | - MassageRequest func(req *http.Request) *http.Request |
322 | + MassageRequest func(req *http.Request, message interface{}) *http.Request |
323 | // other state |
324 | httpClient *http.Client |
325 | } |
326 | @@ -124,7 +124,7 @@ |
327 | request.Header.Set("Content-Type", "application/json") |
328 | |
329 | if s.MassageRequest != nil { |
330 | - request = s.MassageRequest(request) |
331 | + request = s.MassageRequest(request, message) |
332 | } |
333 | |
334 | resp, err := s.httpClient.Do(request) |
335 | |
336 | === modified file 'server/acceptance/suites/unicast.go' |
337 | --- server/acceptance/suites/unicast.go 2014-05-15 16:41:54 +0000 |
338 | +++ server/acceptance/suites/unicast.go 2014-06-11 11:43:11 +0000 |
339 | @@ -40,11 +40,19 @@ |
340 | } |
341 | |
342 | func (s *UnicastAcceptanceSuite) TestUnicastToConnected(c *C) { |
343 | - userId, auth := s.associatedAuth("DEV1") |
344 | + _, auth := s.associatedAuth("DEV1") |
345 | + res, err := s.PostRequest("/register", &api.Registration{ |
346 | + DeviceId: "DEV1", |
347 | + AppId: "app1", |
348 | + }) |
349 | + c.Assert(err, IsNil) |
350 | + c.Assert(res, Matches, ".*ok.*") |
351 | + var reg map[string]interface{} |
352 | + err = json.Unmarshal([]byte(res), ®) |
353 | + c.Assert(err, IsNil) |
354 | events, errCh, stop := s.StartClientAuth(c, "DEV1", nil, auth) |
355 | got, err := s.PostRequest("/notify", &api.Unicast{ |
356 | - UserId: userId, |
357 | - DeviceId: "DEV1", |
358 | + Token: reg["token"].(string), |
359 | AppId: "app1", |
360 | ExpireOn: future, |
361 | Data: json.RawMessage(`{"a": 42}`), |
362 | |
363 | === modified file 'server/api/handlers.go' |
364 | --- server/api/handlers.go 2014-05-29 16:22:00 +0000 |
365 | +++ server/api/handlers.go 2014-06-11 11:43:11 +0000 |
366 | @@ -51,6 +51,8 @@ |
367 | ioError = "io-error" |
368 | invalidRequest = "invalid-request" |
369 | unknownChannel = "unknown-channel" |
370 | + unknownToken = "unknown-token" |
371 | + unauthorized = "unauthorized" |
372 | unavailable = "unavailable" |
373 | internalError = "internal" |
374 | ) |
375 | @@ -121,6 +123,11 @@ |
376 | unknownChannel, |
377 | "Unknown channel", |
378 | } |
379 | + ErrUnknownToken = &APIError{ |
380 | + http.StatusBadRequest, |
381 | + unknownToken, |
382 | + "Unknown token", |
383 | + } |
384 | ErrUnknown = &APIError{ |
385 | http.StatusInternalServerError, |
386 | internalError, |
387 | @@ -136,16 +143,33 @@ |
388 | unavailable, |
389 | "Could not store notification", |
390 | } |
391 | + ErrCouldNotMakeToken = &APIError{ |
392 | + http.StatusServiceUnavailable, |
393 | + unavailable, |
394 | + "Could not make token", |
395 | + } |
396 | + ErrCouldNotResolveToken = &APIError{ |
397 | + http.StatusServiceUnavailable, |
398 | + unavailable, |
399 | + "Could not resolve token", |
400 | + } |
401 | + ErrUnauthorized = &APIError{ |
402 | + http.StatusUnauthorized, |
403 | + unauthorized, |
404 | + "Unauthorized", |
405 | + } |
406 | ) |
407 | |
408 | -type castCommon struct { |
409 | +type Registration struct { |
410 | + DeviceId string `json:"deviceid"` |
411 | + AppId string `json:"appid"` |
412 | } |
413 | |
414 | type Unicast struct { |
415 | + Token string `json:"token"` |
416 | UserId string `json:"userid"` |
417 | DeviceId string `json:"deviceid"` |
418 | AppId string `json:"appid"` |
419 | - //Registration string `json:"registration"` |
420 | //CoalesceTag string `json:"coalesce_tag"` |
421 | ExpireOn string `json:"expire_on"` |
422 | Data json.RawMessage `json:"data"` |
423 | @@ -183,15 +207,15 @@ |
424 | } |
425 | |
426 | func checkRequestAsPost(request *http.Request, maxBodySize int64) *APIError { |
427 | + if request.Method != "POST" { |
428 | + return ErrWrongRequestMethod |
429 | + } |
430 | if err := checkContentLength(request, maxBodySize); err != nil { |
431 | return err |
432 | } |
433 | if request.Header.Get("Content-Type") != JSONMediaType { |
434 | return ErrWrongContentType |
435 | } |
436 | - if request.Method != "POST" { |
437 | - return ErrWrongRequestMethod |
438 | - } |
439 | return nil |
440 | } |
441 | |
442 | @@ -325,7 +349,10 @@ |
443 | } |
444 | |
445 | func checkUnicast(ucast *Unicast) (time.Time, *APIError) { |
446 | - if ucast.UserId == "" || ucast.DeviceId == "" || ucast.AppId == "" { |
447 | + if ucast.AppId == "" { |
448 | + return zeroTime, ErrMissingIdField |
449 | + } |
450 | + if ucast.Token == "" && (ucast.UserId == "" || ucast.DeviceId == "") { |
451 | return zeroTime, ErrMissingIdField |
452 | } |
453 | return checkCastCommon(ucast.Data, ucast.ExpireOn) |
454 | @@ -341,9 +368,21 @@ |
455 | if apiErr != nil { |
456 | return apiErr |
457 | } |
458 | - chanId := store.UnicastInternalChannelId(ucast.UserId, ucast.DeviceId) |
459 | + chanId, err := sto.GetInternalChannelIdFromToken(ucast.Token, ucast.AppId, ucast.UserId, ucast.DeviceId) |
460 | + if err != nil { |
461 | + switch err { |
462 | + case store.ErrUnknownToken: |
463 | + return ErrUnknownToken |
464 | + case store.ErrUnauthorized: |
465 | + return ErrUnauthorized |
466 | + default: |
467 | + h.logger.Errorf("could not resolve token: %v", err) |
468 | + return ErrCouldNotResolveToken |
469 | + } |
470 | + } |
471 | + |
472 | msgId := generateMsgId() |
473 | - err := sto.AppendToUnicastChannel(chanId, ucast.AppId, ucast.Data, msgId, expire) |
474 | + err = sto.AppendToUnicastChannel(chanId, ucast.AppId, ucast.Data, msgId, expire) |
475 | if err != nil { |
476 | h.logger.Errorf("could not store notification: %v", err) |
477 | return ErrCouldNotStoreNotification |
478 | @@ -378,6 +417,62 @@ |
479 | fmt.Fprintf(writer, `{"ok":true}`) |
480 | } |
481 | |
482 | +type RegisterHandler struct { |
483 | + *context |
484 | +} |
485 | + |
486 | +func checkRegister(reg *Registration) *APIError { |
487 | + if reg.DeviceId == "" || reg.AppId == "" { |
488 | + return ErrMissingIdField |
489 | + } |
490 | + return nil |
491 | +} |
492 | + |
493 | +func (h *RegisterHandler) doRegister(sto store.PendingStore, reg *Registration) (string, *APIError) { |
494 | + apiErr := checkRegister(reg) |
495 | + if apiErr != nil { |
496 | + return "", apiErr |
497 | + } |
498 | + token, err := sto.Register(reg.DeviceId, reg.AppId) |
499 | + if err != nil { |
500 | + h.logger.Errorf("could not make a token: %v", err) |
501 | + return "", ErrCouldNotMakeToken |
502 | + } |
503 | + return token, nil |
504 | +} |
505 | + |
506 | +func (h *RegisterHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { |
507 | + var apiErr *APIError |
508 | + defer func() { |
509 | + if apiErr != nil { |
510 | + RespondError(writer, apiErr) |
511 | + } |
512 | + }() |
513 | + |
514 | + reg := &Registration{} |
515 | + |
516 | + sto, apiErr := h.prepare(writer, request, reg) |
517 | + if apiErr != nil { |
518 | + return |
519 | + } |
520 | + defer sto.Close() |
521 | + |
522 | + token, apiErr := h.doRegister(sto, reg) |
523 | + if apiErr != nil { |
524 | + return |
525 | + } |
526 | + |
527 | + writer.Header().Set("Content-Type", "application/json") |
528 | + res, err := json.Marshal(map[string]interface{}{ |
529 | + "ok": true, |
530 | + "token": token, |
531 | + }) |
532 | + if err != nil { |
533 | + panic(fmt.Errorf("couldn't marshal our own response: %v", err)) |
534 | + } |
535 | + writer.Write(res) |
536 | +} |
537 | + |
538 | // MakeHandlersMux makes a handler that dispatches for the various API endpoints. |
539 | func MakeHandlersMux(storeForRequest StoreForRequest, broker broker.BrokerSending, logger logger.Logger) *http.ServeMux { |
540 | ctx := &context{ |
541 | @@ -388,5 +483,6 @@ |
542 | mux := http.NewServeMux() |
543 | mux.Handle("/broadcast", &BroadcastHandler{context: ctx}) |
544 | mux.Handle("/notify", &UnicastHandler{context: ctx}) |
545 | + mux.Handle("/register", &RegisterHandler{context: ctx}) |
546 | return mux |
547 | } |
548 | |
549 | === modified file 'server/api/handlers_test.go' |
550 | --- server/api/handlers_test.go 2014-05-01 18:58:16 +0000 |
551 | +++ server/api/handlers_test.go 2014-06-11 11:43:11 +0000 |
552 | @@ -192,6 +192,16 @@ |
553 | intercept func(meth string, err error) error |
554 | } |
555 | |
556 | +func (isto *interceptInMemoryPendingStore) Register(appId, deviceId string) (string, error) { |
557 | + token, err := isto.InMemoryPendingStore.Register(appId, deviceId) |
558 | + return token, isto.intercept("Register", err) |
559 | +} |
560 | + |
561 | +func (isto *interceptInMemoryPendingStore) GetInternalChannelIdFromToken(token, appId, userId, deviceId string) (store.InternalChannelId, error) { |
562 | + chanId, err := isto.InMemoryPendingStore.GetInternalChannelIdFromToken(token, appId, userId, deviceId) |
563 | + return chanId, isto.intercept("GetInternalChannelIdFromToken", err) |
564 | +} |
565 | + |
566 | func (isto *interceptInMemoryPendingStore) GetInternalChannelId(channel string) (store.InternalChannelId, error) { |
567 | chanId, err := isto.InMemoryPendingStore.GetInternalChannelId(channel) |
568 | return chanId, isto.intercept("GetInternalChannelId", err) |
569 | @@ -262,6 +272,14 @@ |
570 | |
571 | u = unicast() |
572 | u.UserId = "" |
573 | + u.DeviceId = "" |
574 | + u.Token = "TOKEN" |
575 | + expire, apiErr = checkUnicast(u) |
576 | + c.Assert(apiErr, IsNil) |
577 | + c.Check(expire.Format(time.RFC3339), Equals, future) |
578 | + |
579 | + u = unicast() |
580 | + u.UserId = "" |
581 | expire, apiErr = checkUnicast(u) |
582 | c.Check(apiErr, Equals, ErrMissingIdField) |
583 | |
584 | @@ -353,6 +371,40 @@ |
585 | c.Check(s.testlog.Captured(), Equals, "ERROR could not store notification: fail\n") |
586 | } |
587 | |
588 | +func (s *handlersSuite) TestDoUnicastFromTokenFailures(c *C) { |
589 | + fail := errors.New("fail") |
590 | + sto := &interceptInMemoryPendingStore{ |
591 | + store.NewInMemoryPendingStore(), |
592 | + func(meth string, err error) error { |
593 | + if meth == "GetInternalChannelIdFromToken" { |
594 | + return fail |
595 | + } |
596 | + return err |
597 | + }, |
598 | + } |
599 | + ctx := &context{logger: s.testlog} |
600 | + bh := &UnicastHandler{ctx} |
601 | + u := &Unicast{ |
602 | + Token: "tok", |
603 | + AppId: "app1", |
604 | + ExpireOn: future, |
605 | + Data: json.RawMessage(`{"a": 1}`), |
606 | + } |
607 | + apiErr := bh.doUnicast(sto, u) |
608 | + c.Check(apiErr, Equals, ErrCouldNotResolveToken) |
609 | + c.Check(s.testlog.Captured(), Equals, "ERROR could not resolve token: fail\n") |
610 | + s.testlog.ResetCapture() |
611 | + |
612 | + fail = store.ErrUnknownToken |
613 | + apiErr = bh.doUnicast(sto, u) |
614 | + c.Check(apiErr, Equals, ErrUnknownToken) |
615 | + c.Check(s.testlog.Captured(), Equals, "") |
616 | + fail = store.ErrUnauthorized |
617 | + apiErr = bh.doUnicast(sto, u) |
618 | + c.Check(apiErr, Equals, ErrUnauthorized) |
619 | + c.Check(s.testlog.Captured(), Equals, "") |
620 | +} |
621 | + |
622 | func newPostRequest(path string, message interface{}, server *httptest.Server) *http.Request { |
623 | packedMessage, err := json.Marshal(message) |
624 | if err != nil { |
625 | @@ -427,7 +479,7 @@ |
626 | dest := make(map[string]bool) |
627 | err = json.Unmarshal(body, &dest) |
628 | c.Assert(err, IsNil) |
629 | - c.Check(dest, DeepEquals, map[string]bool{"ok": true}) |
630 | + c.Assert(dest, DeepEquals, map[string]bool{"ok": true}) |
631 | |
632 | top, _, err := sto.GetChannelSnapshot(store.SystemInternalChannelId) |
633 | c.Assert(err, IsNil) |
634 | @@ -639,7 +691,7 @@ |
635 | c.Check(response.Header.Get("Content-Type"), Equals, "application/json") |
636 | body, err := getResponseBody(response) |
637 | c.Assert(err, IsNil) |
638 | - c.Check(string(body), Matches, ".*ok.*") |
639 | + c.Assert(string(body), Matches, ".*ok.*") |
640 | |
641 | chanId := store.UnicastInternalChannelId("user2", "dev3") |
642 | c.Check(<-bsend.chanId, Equals, chanId) |
643 | @@ -682,3 +734,142 @@ |
644 | c.Assert(err, IsNil) |
645 | checkError(c, response, ErrMissingIdField) |
646 | } |
647 | + |
648 | +func (s *handlersSuite) TestCheckRegister(c *C) { |
649 | + registration := func() *Registration { |
650 | + return &Registration{ |
651 | + DeviceId: "DEV1", |
652 | + AppId: "app1", |
653 | + } |
654 | + } |
655 | + reg := registration() |
656 | + apiErr := checkRegister(reg) |
657 | + c.Assert(apiErr, IsNil) |
658 | + |
659 | + reg = registration() |
660 | + reg.AppId = "" |
661 | + apiErr = checkRegister(reg) |
662 | + c.Check(apiErr, Equals, ErrMissingIdField) |
663 | + |
664 | + reg = registration() |
665 | + reg.DeviceId = "" |
666 | + apiErr = checkRegister(reg) |
667 | + c.Check(apiErr, Equals, ErrMissingIdField) |
668 | +} |
669 | + |
670 | +func (s *handlersSuite) TestDoRegisterMissingIdField(c *C) { |
671 | + sto := store.NewInMemoryPendingStore() |
672 | + rh := &RegisterHandler{} |
673 | + token, apiErr := rh.doRegister(sto, &Registration{}) |
674 | + c.Check(apiErr, Equals, ErrMissingIdField) |
675 | + c.Check(token, Equals, "") |
676 | +} |
677 | + |
678 | +func (s *handlersSuite) TestDoRegisterCouldNotMakeToken(c *C) { |
679 | + sto := &interceptInMemoryPendingStore{ |
680 | + store.NewInMemoryPendingStore(), |
681 | + func(meth string, err error) error { |
682 | + if meth == "Register" { |
683 | + return errors.New("fail") |
684 | + } |
685 | + return err |
686 | + }, |
687 | + } |
688 | + ctx := &context{logger: s.testlog} |
689 | + rh := &RegisterHandler{ctx} |
690 | + _, apiErr := rh.doRegister(sto, &Registration{ |
691 | + DeviceId: "DEV1", |
692 | + AppId: "app1", |
693 | + }) |
694 | + c.Check(apiErr, Equals, ErrCouldNotMakeToken) |
695 | + c.Check(s.testlog.Captured(), Equals, "ERROR could not make a token: fail\n") |
696 | +} |
697 | + |
698 | +func (s *handlersSuite) TestRespondsToRegisterAndUnicast(c *C) { |
699 | + sto := store.NewInMemoryPendingStore() |
700 | + stoForReq := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) { |
701 | + return sto, nil |
702 | + } |
703 | + bsend := testBrokerSending{make(chan store.InternalChannelId, 1)} |
704 | + testServer := httptest.NewServer(MakeHandlersMux(stoForReq, bsend, nil)) |
705 | + defer testServer.Close() |
706 | + |
707 | + request := newPostRequest("/register", &Registration{ |
708 | + DeviceId: "dev3", |
709 | + AppId: "app2", |
710 | + }, testServer) |
711 | + |
712 | + response, err := s.client.Do(request) |
713 | + c.Assert(err, IsNil) |
714 | + |
715 | + c.Check(response.StatusCode, Equals, http.StatusOK) |
716 | + c.Check(response.Header.Get("Content-Type"), Equals, "application/json") |
717 | + body, err := getResponseBody(response) |
718 | + c.Assert(err, IsNil) |
719 | + c.Assert(string(body), Matches, ".*ok.*") |
720 | + var reg map[string]interface{} |
721 | + err = json.Unmarshal(body, ®) |
722 | + c.Assert(err, IsNil) |
723 | + |
724 | + token, ok := reg["token"].(string) |
725 | + c.Assert(ok, Equals, true) |
726 | + c.Check(token, Not(Equals), nil) |
727 | + |
728 | + payload := json.RawMessage(`{"foo":"bar"}`) |
729 | + |
730 | + request = newPostRequest("/notify", &Unicast{ |
731 | + Token: token, |
732 | + AppId: "app2", |
733 | + ExpireOn: future, |
734 | + Data: payload, |
735 | + }, testServer) |
736 | + |
737 | + response, err = s.client.Do(request) |
738 | + c.Assert(err, IsNil) |
739 | + |
740 | + c.Check(response.StatusCode, Equals, http.StatusOK) |
741 | + c.Check(response.Header.Get("Content-Type"), Equals, "application/json") |
742 | + body, err = getResponseBody(response) |
743 | + c.Assert(err, IsNil) |
744 | + c.Assert(string(body), Matches, ".*ok.*") |
745 | + |
746 | + chanId := store.UnicastInternalChannelId("dev3", "dev3") |
747 | + c.Check(<-bsend.chanId, Equals, chanId) |
748 | + top, notifications, err := sto.GetChannelSnapshot(chanId) |
749 | + c.Assert(err, IsNil) |
750 | + c.Check(top, Equals, int64(0)) |
751 | + c.Check(notifications, HasLen, 1) |
752 | +} |
753 | + |
754 | +func (s *handlersSuite) TestCannotRegisterWithMissingFields(c *C) { |
755 | + stoForReq := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) { |
756 | + return store.NewInMemoryPendingStore(), nil |
757 | + } |
758 | + ctx := &context{stoForReq, nil, nil} |
759 | + testServer := httptest.NewServer(&RegisterHandler{ctx}) |
760 | + defer testServer.Close() |
761 | + |
762 | + request := newPostRequest("/", &Registration{ |
763 | + DeviceId: "DEV1", |
764 | + }, testServer) |
765 | + |
766 | + response, err := s.client.Do(request) |
767 | + c.Assert(err, IsNil) |
768 | + checkError(c, response, ErrMissingIdField) |
769 | +} |
770 | + |
771 | +func (s *handlersSuite) TestCannotRegisterWithNonPOST(c *C) { |
772 | + stoForReq := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) { |
773 | + return store.NewInMemoryPendingStore(), nil |
774 | + } |
775 | + ctx := &context{stoForReq, nil, nil} |
776 | + testServer := httptest.NewServer(&RegisterHandler{ctx}) |
777 | + defer testServer.Close() |
778 | + |
779 | + request, err := http.NewRequest("GET", testServer.URL, nil) |
780 | + c.Assert(err, IsNil) |
781 | + |
782 | + response, err := s.client.Do(request) |
783 | + c.Assert(err, IsNil) |
784 | + checkError(c, response, ErrWrongRequestMethod) |
785 | +} |
786 | |
787 | === modified file 'server/store/inmemory.go' |
788 | --- server/store/inmemory.go 2014-05-02 15:10:18 +0000 |
789 | +++ server/store/inmemory.go 2014-06-11 11:43:11 +0000 |
790 | @@ -17,7 +17,10 @@ |
791 | package store |
792 | |
793 | import ( |
794 | + "encoding/base64" |
795 | "encoding/json" |
796 | + "fmt" |
797 | + "strings" |
798 | "sync" |
799 | "time" |
800 | |
801 | @@ -44,6 +47,29 @@ |
802 | } |
803 | } |
804 | |
805 | +func (sto *InMemoryPendingStore) Register(deviceId, appId string) (string, error) { |
806 | + return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s::%s", appId, deviceId))), nil |
807 | +} |
808 | + |
809 | +func (sto *InMemoryPendingStore) GetInternalChannelIdFromToken(token, appId, userId, deviceId string) (InternalChannelId, error) { |
810 | + if token != "" && appId != "" { |
811 | + decoded, err := base64.StdEncoding.DecodeString(token) |
812 | + if err != nil { |
813 | + return "", ErrUnknownToken |
814 | + } |
815 | + token = string(decoded) |
816 | + if !strings.HasPrefix(token, appId+"::") { |
817 | + return "", ErrUnauthorized |
818 | + } |
819 | + deviceId := token[len(appId)+2:] |
820 | + return UnicastInternalChannelId(deviceId, deviceId), nil |
821 | + } |
822 | + if userId != "" && deviceId != "" { |
823 | + return UnicastInternalChannelId(userId, deviceId), nil |
824 | + } |
825 | + return "", ErrUnknownToken |
826 | +} |
827 | + |
828 | func (sto *InMemoryPendingStore) GetInternalChannelId(name string) (InternalChannelId, error) { |
829 | if name == "system" { |
830 | return SystemInternalChannelId, nil |
831 | |
832 | === modified file 'server/store/inmemory_test.go' |
833 | --- server/store/inmemory_test.go 2014-04-30 17:44:56 +0000 |
834 | +++ server/store/inmemory_test.go 2014-06-11 11:43:11 +0000 |
835 | @@ -30,6 +30,50 @@ |
836 | |
837 | var _ = Suite(&inMemorySuite{}) |
838 | |
839 | +func (s *inMemorySuite) TestRegister(c *C) { |
840 | + sto := NewInMemoryPendingStore() |
841 | + |
842 | + tok1, err := sto.Register("DEV1", "app1") |
843 | + c.Assert(err, IsNil) |
844 | + tok2, err := sto.Register("DEV1", "app1") |
845 | + c.Assert(err, IsNil) |
846 | + c.Check(len(tok1), Not(Equals), 0) |
847 | + c.Check(tok1, Equals, tok2) |
848 | +} |
849 | + |
850 | +func (s *inMemorySuite) TestGetInternalChannelIdFromToken(c *C) { |
851 | + sto := NewInMemoryPendingStore() |
852 | + |
853 | + tok1, err := sto.Register("DEV1", "app1") |
854 | + c.Assert(err, IsNil) |
855 | + chanId, err := sto.GetInternalChannelIdFromToken(tok1, "app1", "", "") |
856 | + c.Assert(err, IsNil) |
857 | + c.Check(chanId, Equals, UnicastInternalChannelId("DEV1", "DEV1")) |
858 | +} |
859 | + |
860 | +func (s *inMemorySuite) TestGetInternalChannelIdFromTokenFallback(c *C) { |
861 | + sto := NewInMemoryPendingStore() |
862 | + |
863 | + chanId, err := sto.GetInternalChannelIdFromToken("", "app1", "u1", "d1") |
864 | + c.Assert(err, IsNil) |
865 | + c.Check(chanId, Equals, UnicastInternalChannelId("u1", "d1")) |
866 | +} |
867 | + |
868 | +func (s *inMemorySuite) TestGetInternalChannelIdFromTokenErrors(c *C) { |
869 | + sto := NewInMemoryPendingStore() |
870 | + tok1, err := sto.Register("DEV1", "app1") |
871 | + c.Assert(err, IsNil) |
872 | + |
873 | + _, err = sto.GetInternalChannelIdFromToken(tok1, "app2", "", "") |
874 | + c.Assert(err, Equals, ErrUnauthorized) |
875 | + |
876 | + _, err = sto.GetInternalChannelIdFromToken("", "app2", "", "") |
877 | + c.Assert(err, Equals, ErrUnknownToken) |
878 | + |
879 | + _, err = sto.GetInternalChannelIdFromToken("****", "app2", "", "") |
880 | + c.Assert(err, Equals, ErrUnknownToken) |
881 | +} |
882 | + |
883 | func (s *inMemorySuite) TestGetInternalChannelId(c *C) { |
884 | sto := NewInMemoryPendingStore() |
885 | |
886 | |
887 | === modified file 'server/store/store.go' |
888 | --- server/store/store.go 2014-05-02 15:10:18 +0000 |
889 | +++ server/store/store.go 2014-06-11 11:43:11 +0000 |
890 | @@ -52,6 +52,8 @@ |
891 | } |
892 | |
893 | var ErrUnknownChannel = errors.New("unknown channel name") |
894 | +var ErrUnknownToken = errors.New("unknown token") |
895 | +var ErrUnauthorized = errors.New("unauthorized") |
896 | var ErrFull = errors.New("channel is full") |
897 | var ErrExpected128BitsHexRepr = errors.New("expected 128 bits hex repr") |
898 | |
899 | @@ -98,11 +100,17 @@ |
900 | |
901 | // PendingStore let store notifications into channels. |
902 | type PendingStore interface { |
903 | + // Register returns a token for a device id, application id pair. |
904 | + Register(deviceId, appId string) (token string, err error) |
905 | // GetInternalChannelId returns the internal store id for a channel |
906 | // given the name. |
907 | GetInternalChannelId(name string) (InternalChannelId, error) |
908 | // AppendToChannel appends a notification to the channel. |
909 | AppendToChannel(chanId InternalChannelId, notification json.RawMessage, expiration time.Time) error |
910 | + // GetInternalChannelIdFromToken returns the matching internal store |
911 | + // id for a channel given a registered token and application id or |
912 | + // directly a device id, user id pair. |
913 | + GetInternalChannelIdFromToken(token, appId, userId, deviceId string) (InternalChannelId, error) |
914 | // AppendToUnicastChannel appends a notification to the unicast channel. |
915 | // GetChannelSnapshot gets all the current notifications and |
916 | AppendToUnicastChannel(chanId InternalChannelId, appId string, notification json.RawMessage, msgId string, expiration time.Time) error |
917 | |
918 | === modified file 'signing-helper/signing-helper.cpp' |
919 | --- signing-helper/signing-helper.cpp 2014-05-01 10:24:23 +0000 |
920 | +++ signing-helper/signing-helper.cpp 2014-06-11 11:43:11 +0000 |
921 | @@ -33,6 +33,7 @@ |
922 | #include <QObject> |
923 | #include <QString> |
924 | #include <QTimer> |
925 | +#include <QUrlQuery> |
926 | |
927 | #include "ssoservice.h" |
928 | #include "token.h" |
929 | @@ -63,12 +64,8 @@ |
930 | void SigningExample::handleCredentialsFound(Token token) |
931 | { |
932 | qDebug() << "Credentials found, signing url."; |
933 | - |
934 | - QString authHeader = token.signUrl(this->url, QStringLiteral("GET"), true); |
935 | - |
936 | - std::cout << authHeader.toStdString() << "\n"; |
937 | + std::cout << token.signUrl(this->url, QStringLiteral("POST")).toStdString(); |
938 | QCoreApplication::instance()->exit(0); |
939 | - |
940 | } |
941 | |
942 | void SigningExample::handleCredentialsNotFound() |
943 | @@ -84,13 +81,12 @@ |
944 | int main(int argc, char *argv[]) |
945 | { |
946 | QCoreApplication a(argc, argv); |
947 | - |
948 | - UbuntuOne::SigningExample *example = new UbuntuOne::SigningExample(&a); |
949 | - |
950 | + if (argc<2) { |
951 | + return 2; |
952 | + } |
953 | + UbuntuOne::SigningExample *example = new UbuntuOne::SigningExample(&a, argv[1]); |
954 | QObject::connect(example, SIGNAL(finished()), &a, SLOT(quit())); |
955 | - |
956 | QTimer::singleShot(0, example, SLOT(doExample())); |
957 | - |
958 | return a.exec(); |
959 | } |
960 |