~alecu/unity-lens-music/fix-1168674

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
/*
 * Copyright (C) 2012 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authored by Alejandro J. Cura <alecu@canonical.com>
 *
 */

using Soup;

[DBus (name = "com.ubuntuone.CredentialsManagement")]
interface CredentialsManagement : GLib.Object {
    public signal void credentials_found (HashTable <string, string> info);
    public signal void credentials_not_found ();
    public signal void credentials_error (HashTable <string, string> error_dict);

    [DBus (name = "find_credentials")]
    public abstract void find_credentials () throws IOError;
}

const string WEBAPI_SERVER = "https://one.ubuntu.com/";
const string ACCOUNT_PATH = "api/account";
const string PAYMENT_METHOD_PATH = "music-store-up/api/1/user/retrieve-payment-method?purchase_sku=%s";
const string PURCHASE_WITH_DEFAULT_PAYMENT_PATH = "music-store-up/api/1/user/purchase-with-default-payment?purchase_sku=%s&authentication=%s";
const string AUTHENTICATION_SERVER = "https://login.ubuntu.com/";
const string AUTHENTICATION_PATH = "api/1.1/authentications";
const string AUTHENTICATE_PARAMS = "ws.op=authenticate&token_name=Purchase_Token";


namespace Ubuntuone.Webservice
{
    public errordomain PurchaseError
    {
        MISSING_CREDENTIALS_ERROR,
        PURCHASE_ERROR,
        WRONG_PASSWORD_ERROR,
        UNSPECIFIED_ERROR
    }

    public class PurchaseService : GLib.Object
    {
        internal Soup.SessionAsync http_session;
        Soup.SessionAsync http_session_sso;
        CredentialsManagement credentials_management;
        public string nickname { get; private set; default = null; }
        public string email { get; private set; default = null; }
        public string selected_payment_method { get; internal set; default = null; }
        public string consumer_key { get; private set; default = null; }
        public string token { get; private set; default = null; }
        public string open_url { get; private set; default = null; }
        internal HashTable <string, string> _ubuntuone_credentials = null;

        construct {
            http_session = build_http_session ();
            http_session_sso = build_http_session ();

            credentials_management = build_credentials_management ();
        }

        internal Soup.SessionAsync build_http_session ()
        {
            var session = new Soup.SessionAsync ();
            session.user_agent = "%s/%s (libsoup)".printf("UbuntuOneMusicstoreLens", "1.0");
            return session;
        }

        string webapi_server ()
        {
            string staging_webapi = Environment.get_variable ("U1_STAGING_WEBAPI");
            return staging_webapi != null ? staging_webapi : WEBAPI_SERVER;
        }

        string account_uri ()
        {
            return webapi_server() + ACCOUNT_PATH;
        }

        string payment_method_uri ()
        {
            return webapi_server() + PAYMENT_METHOD_PATH;
        }

        string purchase_with_default_payment_uri ()
        {
            return webapi_server() + PURCHASE_WITH_DEFAULT_PAYMENT_PATH;
        }

        string authentication_server ()
        {
            string staging_authentication = Environment.get_variable ("U1_STAGING_AUTHENTICATION");
            return staging_authentication != null ? staging_authentication : AUTHENTICATION_SERVER;
        }

        string authentication_uri ()
        {
            return authentication_server() + AUTHENTICATION_PATH;
        }

        public bool got_credentials () {
            return _ubuntuone_credentials != null;
        }

        internal virtual CredentialsManagement build_credentials_management ()
        {
            try {
                return Bus.get_proxy_sync (BusType.SESSION, "com.ubuntuone.Credentials",
                                           "/credentials", DBusProxyFlags.DO_NOT_AUTO_START);
            } catch (IOError e) {
                error ("Can't connect to DBus: %s", e.message);
            }
        }

        public bool ready_to_purchase {
            get { return selected_payment_method != null; }
        }

        internal Json.Object parse_json (string json_string) throws GLib.Error
        {
            var parser = new Json.Parser();
            parser.load_from_data(json_string, -1);
            return parser.get_root().get_object();
        }

        internal void parse_account_json (string json_string) throws GLib.Error
        {
            var root_object = parse_json (json_string);
            nickname = root_object.get_string_member("nickname");
            email = root_object.get_string_member("email");
        }

        internal void parse_payment_method_json (string json_string) throws GLib.Error, PurchaseError
        {
            var root_object = parse_json (json_string);
            if (root_object.has_member ("selected_payment_method")) {
                selected_payment_method = root_object.get_string_member("selected_payment_method");
            } else {
                open_url = root_object.get_string_member ("open_url");
                var error_message = root_object.get_string_member ("error_message");
                throw new PurchaseError.PURCHASE_ERROR (error_message);
            }
        }

        internal void parse_authentication_json (string json_string) throws GLib.Error
        {
            var root_object = parse_json (json_string);
            consumer_key = root_object.get_string_member ("consumer_key");
            token = root_object.get_string_member ("token");
        }

        internal string parse_purchase_json (string json_string) throws GLib.Error
        {
            var root_object = parse_json (json_string);
            if (root_object.has_member ("open_url")) {
                return root_object.get_string_member("open_url");
            } else {
                return "";
            }
        }

        public virtual async void fetch_credentials () throws PurchaseError
        {
            PurchaseError error = null;

            ulong found_handler = credentials_management.credentials_found.connect ((credentials) => {
                _ubuntuone_credentials = credentials;
                debug ("got credentials");
                fetch_credentials.callback ();
            });
            ulong not_found_handler = credentials_management.credentials_not_found.connect (() => {
				error = new PurchaseError.MISSING_CREDENTIALS_ERROR ("No Ubuntu One tokens.");
				fetch_credentials.callback ();
			});
            ulong error_handler = credentials_management.credentials_error.connect ((error_dict) => {
                error = new PurchaseError.MISSING_CREDENTIALS_ERROR ("Can't get Ubuntu One tokens.");
                fetch_credentials.callback ();
            });

            try {
                credentials_management.find_credentials ();
                yield;
            } catch (IOError e) {
                error = new PurchaseError.MISSING_CREDENTIALS_ERROR ("Can't get Ubuntu One tokens: %s", e.message);
            }

            credentials_management.disconnect (found_handler);
            credentials_management.disconnect (not_found_handler);
            credentials_management.disconnect (error_handler);

            if (error != null) {
                debug ("Can't get Ubuntu One tokens: %s", error.message);
                throw error;
            }
        }

        string oauth_sign (string uri)
        {
            return OAuth.sign_url2(uri, null,
                                   OAuth.Method.PLAINTEXT, "GET",
                                   _ubuntuone_credentials["consumer_key"],
                                   _ubuntuone_credentials["consumer_secret"],
                                   _ubuntuone_credentials["token"],
                                   _ubuntuone_credentials["token_secret"]);
        }

        internal virtual async PurchaseError call_api (string method, string uri, out string response)
        {
            PurchaseError error = null;
            var signed_uri = oauth_sign (uri);
            var message = new Soup.Message (method, signed_uri);
            http_session.queue_message (message, (session, message) => {
                if (message.status_code != Soup.KnownStatusCode.OK) {
                    debug ("Web request failed: HTTP %u %s - %s",
                           message.status_code, message.reason_phrase, uri);
                    error = new PurchaseError.PURCHASE_ERROR (message.reason_phrase);
                }
                call_api.callback ();
            });
            yield;
            message.response_body.flatten ();
            response = (string) message.response_body.data;
            return error;
        }

        internal virtual async void fetch_account () throws PurchaseError
        {
            string response;
            PurchaseError error = yield call_api ("GET", account_uri(), out response);

            if (error != null) {
                debug ("Error while fetching U1 account: %s.", error.message);
                throw error;
            }

            try {
                parse_account_json (response);
                debug ("got account");
            } catch (GLib.Error e) {
                debug ("Error while parsing U1 account: %s.", e.message);
                throw new PurchaseError.PURCHASE_ERROR (e.message);
            }
        }

        internal virtual void fetch_payment_method (string purchase_sku) throws PurchaseError
        {
            var uri = payment_method_uri().printf (purchase_sku);

            var message = send_signed_webservice_call ("GET", uri);
            if (message.status_code != Soup.KnownStatusCode.OK) {
                debug ("Purchase request failed: HTTP %u", message.status_code);
                debug ("Reason: %s", message.reason_phrase);
                try {
                    message.response_body.flatten ();
                    debug ("body: ------\n%s\n------\n", (string) message.response_body.data);
                } catch (Error e) {
                }
                throw new PurchaseError.PURCHASE_ERROR ("Retrieve payment method failed: %s".printf (message.reason_phrase));
            }
            try {
                message.response_body.flatten ();
                var result = (string) message.response_body.data;
                parse_payment_method_json (result);
            } catch (GLib.Error e) {
                throw new PurchaseError.PURCHASE_ERROR (e.message);
            }
        }

        public virtual async void fetch_account_info () throws PurchaseError
        {
            yield fetch_credentials ();
            yield fetch_account ();
        }

        public virtual void fetch_payment_info (string purchase_sku) throws PurchaseError
        {
            fetch_payment_method (purchase_sku);
        }

        internal virtual void _do_sso_webcall (Soup.Message message, string password)
        {
            var handler = http_session_sso.authenticate.connect ((session, message, auth, retrying) => {
                if (!retrying) {
                    auth.authenticate (email, password);
                }
            });
            http_session_sso.send_message (message);
            http_session_sso.disconnect (handler);
        }

        internal virtual string authenticated_sso_webcall (string method, string uri, string operation, string password)
            throws PurchaseError
        {
            var message = new Soup.Message (method, uri);
            message.set_request ("application/x-www-form-urlencoded", Soup.MemoryUse.COPY, operation.data);
            _do_sso_webcall (message, password);
            if (message.status_code != Soup.KnownStatusCode.OK) {
                debug ("Authentication request failed: HTTP %u", message.status_code);
                debug ("Reason: %s", message.reason_phrase);
                if (message.status_code == Soup.KnownStatusCode.UNAUTHORIZED) {
                    throw new PurchaseError.WRONG_PASSWORD_ERROR ("Wrong password");
                }
                try {
                    message.response_body.flatten ();
                    debug ("body: ------\n%s\n------\n", (string) message.response_body.data);
                } catch (Error e) {
                }
                throw new PurchaseError.PURCHASE_ERROR (message.reason_phrase);
            }
            message.response_body.flatten ();
            return (string) message.response_body.data;
        }

        internal virtual string get_purchase_token (string password) throws PurchaseError
        {
            var result = authenticated_sso_webcall ("POST", authentication_uri(), AUTHENTICATE_PARAMS, password);
            try {
                parse_authentication_json (result);
            } catch (GLib.Error e) {
                throw new PurchaseError.PURCHASE_ERROR (e.message);
            }
            return "%s:%s".printf (consumer_key, token);
        }

        internal virtual Soup.Message send_signed_webservice_call (string method, string uri)
        {
            var signed_uri = oauth_sign (uri);
            var message = new Soup.Message (method, signed_uri);
            http_session.send_message (message);
            return message;
        }

        internal virtual void purchase_with_default_payment (string album_id, string purchase_token) throws PurchaseError
        {
            var uri = purchase_with_default_payment_uri().printf (album_id, purchase_token);
            var message = send_signed_webservice_call ("GET", uri);

            if (message.status_code != Soup.KnownStatusCode.OK) {
                debug ("Purchase request failed: HTTP %u", message.status_code);
                debug ("Reason: %s", message.reason_phrase);
                try {
                    message.response_body.flatten ();
                    debug ("body: ------\n%s\n------\n", (string) message.response_body.data);
                } catch (Error e) {
                }
                throw new PurchaseError.PURCHASE_ERROR ("Purchase failed: %s".printf (message.reason_phrase));
            }
            try {
                message.response_body.flatten ();
                var result = (string) message.response_body.data;
                var open_url = parse_purchase_json (result);
                if (open_url != "") {
                    throw new PurchaseError.PURCHASE_ERROR (open_url);
                }
            } catch (GLib.Error e) {
                throw new PurchaseError.PURCHASE_ERROR (e.message);
            }
        }

        public void purchase (string album_id, string password) throws PurchaseError
        {
            var purchase_token = get_purchase_token (password);
            debug ("purchasing...");
            purchase_with_default_payment (album_id, purchase_token);
            debug ("purchase completed.");
        }
    }
}