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