~daisy-pluckers/whoopsie/trunk

« back to all changes in this revision

Viewing changes to src/whoopsie.c

  • Committer: Brian Murray
  • Date: 2022-11-14 19:11:19 UTC
  • Revision ID: brian@canonical.com-20221114191119-omsz8aicjaeqxsza
Move to https://code.launchpad.net/~ubuntu-core-dev/whoopsie/+git/main

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
/* whoopsie
2
 
 * 
3
 
 * Copyright © 2011-2013 Canonical Ltd.
4
 
 * Author: Evan Dandrea <evan.dandrea@canonical.com>
5
 
 * 
6
 
 * This program is free software: you can redistribute it and/or modify
7
 
 * it under the terms of the GNU General Public License as published by
8
 
 * the Free Software Foundation; version 3 of the License.
9
 
 *
10
 
 * This program is distributed in the hope that it will be useful,
11
 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 
 * GNU General Public License for more details.
14
 
 *
15
 
 * You should have received a copy of the GNU General Public License
16
 
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 
 */
18
 
 
19
 
#define _XOPEN_SOURCE
20
 
#define _GNU_SOURCE
21
 
 
22
 
#include <limits.h>
23
 
#include <stdlib.h>
24
 
#include <stdio.h>
25
 
#include <string.h>
26
 
#include <glib.h>
27
 
#include <glib/gstdio.h>
28
 
#include <gio/gio.h>
29
 
#include <assert.h>
30
 
#include <curl/curl.h>
31
 
#include <sys/stat.h>
32
 
#include <sys/types.h>
33
 
#include <sys/file.h>
34
 
#include <errno.h>
35
 
#include <signal.h>
36
 
#include <pwd.h>
37
 
#include <grp.h>
38
 
#include <sys/capability.h>
39
 
#include <sys/prctl.h>
40
 
#include <sys/mount.h>
41
 
#include <sys/time.h>
42
 
#include <sys/resource.h>
43
 
 
44
 
#include "bson/bson.h"
45
 
#include "whoopsie.h"
46
 
#include "utils.h"
47
 
#include "connectivity.h"
48
 
#include "monitor.h"
49
 
#include "identifier.h"
50
 
#include "logging.h"
51
 
#include "globals.h"
52
 
 
53
 
/* The length of time to wait before processing outstanding crashes, in seconds
54
 
 */
55
 
#define PROCESS_OUTSTANDING_TIMEOUT 7200
56
 
 
57
 
/* The maximum allowed size of a report file, if not overridden
58
 
 */
59
 
#define MAX_REPORT_FILESIZE 1000000000
60
 
 
61
 
/* If true, we have an active Internet connection. True by default in case we
62
 
 * can't bring up GNetworkMonitor */
63
 
static gboolean online_state = TRUE;
64
 
 
65
 
/* The URL of the crash database. */
66
 
static char* crash_db_url = NULL;
67
 
 
68
 
/* Username we will run under */
69
 
static const char* username = "whoopsie";
70
 
 
71
 
/*  The database identifier. Either:
72
 
 *  - The android serial number, taken from sys, and SHA-512 hashed
73
 
 *  - The system UUID, taken from the DMI tables and SHA-512 hashed
74
 
 *  - The MAC address of the first non-loopback device, SHA-512 hashed */
75
 
static char* whoopsie_identifier = NULL;
76
 
 
77
 
/*  Whether or not to verify the ssl peer when submitting crashes. Either:
78
 
 *  - 0 verification will NOT occur
79
 
 *  - 1 verification will occur */
80
 
static long verifypeer = 1;
81
 
 
82
 
/* The URL for sending the initial crash report */
83
 
static char* crash_db_submit_url = NULL;
84
 
 
85
 
/* OOPS ID of the last successfully-uploaded crash file */
86
 
char* last_uploaded_oopsid = NULL;
87
 
 
88
 
/* The file path and descriptor for our instance lock */
89
 
static const char* lock_path = "/var/lock/whoopsie/lock";
90
 
static int lock_fd = 0;
91
 
 
92
 
/* The report directory */
93
 
static const char* report_dir = "/var/crash";
94
 
 
95
 
/* Options */
96
 
int foreground = 0;
97
 
 
98
 
#ifndef TEST
99
 
static int assume_online = 0;
100
 
 
101
 
/*
102
 
 * Tells whether whoopsie will exit right after processing existing files.
103
 
 */
104
 
static int use_polling = 1;
105
 
 
106
 
static GOptionEntry option_entries[] = {
107
 
    { "foreground", 'f', 0, G_OPTION_ARG_NONE, &foreground, "Run in the foreground", NULL },
108
 
    { "assume-online", 'a', 0, G_OPTION_ARG_NONE, &assume_online, "Always assume there is a route to $CRASH_DB_URL.", NULL },
109
 
    { "no-polling", 0, G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &use_polling, "Process existing files and exit. Implies --assume-online.", NULL },
110
 
    { NULL }
111
 
};
112
 
#endif
113
 
 
114
 
/* Fields that can be larger than 1KB if present, always send them */
115
 
static const char* acceptable_fields[] = {
116
 
    "ProblemType",
117
 
    "Date",
118
 
    "Traceback",
119
 
    "Signal",
120
 
    "PythonArgs",
121
 
    "Package",
122
 
    "SourcePackage",
123
 
    "PackageArchitecture",
124
 
    "Dependencies",
125
 
    "MachineType",
126
 
    "StacktraceAddressSignature",
127
 
    "ApportVersion",
128
 
    "DuplicateSignature",
129
 
    /* add_os_info */
130
 
    "DistroRelease",
131
 
    "Uname",
132
 
    "Architecture",
133
 
    "NonfreeKernelModules",
134
 
    "LiveMediaBuild",
135
 
    /* add_user_info */
136
 
    "UserGroups",
137
 
    /* add_proc_info */
138
 
    "ExecutablePath",
139
 
    "InterpreterPath",
140
 
    "ExecutableTimestamp",
141
 
    "ProcCwd",
142
 
    "ProcEnviron",
143
 
    "ProcCmdline",
144
 
    "ProcStatus",
145
 
    "ProcMaps",
146
 
    "ProcAttrCurrent",
147
 
    "ProcCpuinfoMinimal",
148
 
    /* add_gdb_info */
149
 
    "Registers",
150
 
    "Disassembly",
151
 
    /* used to repair the StacktraceAddressSignature if it is corrupt */
152
 
    "StacktraceTop",
153
 
    "AssertionMessage",
154
 
    "ProcAttrCurrent",
155
 
    "CoreDump",
156
 
    /* add_kernel_crash_info */
157
 
    "VmCore",
158
 
    /* We use package-from-proposed tag to determine if a problem is occuring
159
 
     * in the release-proposed pocket. */
160
 
    "Tags",
161
 
    /* We need the OopsText field to be able to generate a crash signature from
162
 
     * KernelOops problems */
163
 
    "OopsText",
164
 
    /* We use the UpgradeStatus field to determine the time between upgrading
165
 
     * and the initial error report */
166
 
    "UpgradeStatus",
167
 
    /* We use the InstallationDate and InstallationMedia fields to determine
168
 
     * the time between installing and the initial error report */
169
 
    "InstallationDate",
170
 
    "InstallationMedia",
171
 
    /* Dumps for debugging iwlwifi firmware crashes */
172
 
    "IwlFwDump",
173
 
    /* System Image information from imaged systems */
174
 
    "SystemImageInfo",
175
 
    /* We always want to know why apport thought it shouldn't be reported. */
176
 
    "UnreportableReason",
177
 
    /* Package Install Failure Information */
178
 
    /* update-manager */
179
 
    "DpkgHistoryLog.txt",
180
 
    "DpkgTerminalLog.txt",
181
 
    "AptDaemon",
182
 
    "HWEunsupported",
183
 
    /* aptdaemon */
184
 
    "AptConfig",
185
 
    "SourcesList",
186
 
    "AptHistoryLog",
187
 
    "AptTermLog",
188
 
    "DpkgLog",
189
 
    "SysLog",
190
 
    /* openjdk-8 */
191
 
    "HotspotError",
192
 
    /* desktop app debugging info */
193
 
    "JournalErrors",
194
 
 
195
 
    NULL,
196
 
};
197
 
 
198
 
/* Fields that we don't ever need, always */
199
 
static const char* unacceptable_fields[] = {
200
 
    /* We do not need these since we retrace with ddebs on errors */
201
 
    "Stacktrace",
202
 
    "ThreadStacktrace",
203
 
 
204
 
    /* We'll have our own count in the database. */
205
 
    "CrashCounter",
206
 
 
207
 
    /* MarkForUpload is redundant since the crash was uploaded. */
208
 
    "_MarkForUpload",
209
 
 
210
 
    "Title",
211
 
 
212
 
    /* Redundant with the addition of pa-info LP: #1893899 */
213
 
    "AlsaInfo",
214
 
 
215
 
    NULL
216
 
};
217
 
 
218
 
static gboolean
219
 
is_in_field_list (const char* field, const char * field_list[])
220
 
{
221
 
    const char** p;
222
 
 
223
 
    g_return_val_if_fail (field, FALSE);
224
 
 
225
 
    p = field_list;
226
 
    while (*p) {
227
 
        if (strcmp (*p, field) == 0)
228
 
            return TRUE;
229
 
        p++;
230
 
    }
231
 
    return FALSE;
232
 
}
233
 
 
234
 
gboolean
235
 
append_key_value (gpointer key, gpointer value, gpointer bson_string)
236
 
{
237
 
    /* Takes a key and its value from a #GHashTable and adds it to a BSON string
238
 
     * as key and its string value. Return %FALSE on error. */
239
 
 
240
 
    bson* str = (bson*) bson_string;
241
 
    char* k = (char*) key;
242
 
    char* v = (char*) value;
243
 
 
244
 
    /* We don't send the core dump in the first upload, as the server might not
245
 
     * need it */
246
 
    if (!strcmp ("CoreDump", k))
247
 
        return TRUE;
248
 
    if (!strcmp ("VmCore", k))
249
 
        return TRUE;
250
 
 
251
 
    return bson_append_string (str, k, v) != BSON_ERROR;
252
 
}
253
 
 
254
 
size_t
255
 
server_response (char* ptr, size_t size, size_t nmemb, void* s)
256
 
{
257
 
    GString *resp = (GString *) s;
258
 
    if (size != 1) {
259
 
        g_warning("CURLOPT_WRITEFUNCTION callback received size !=1, against spec!\n");
260
 
        return 0;
261
 
    }
262
 
    g_string_append_len (resp, ptr, nmemb);
263
 
    return nmemb;
264
 
}
265
 
 
266
 
void
267
 
split_string (char* head, char** tail)
268
 
{
269
 
    g_return_if_fail (head);
270
 
    g_return_if_fail (tail);
271
 
 
272
 
    *tail = strchr (head, ' ');
273
 
    if (*tail) {
274
 
        **tail = '\0';
275
 
        (*tail)++;
276
 
    }
277
 
}
278
 
 
279
 
 
280
 
gboolean
281
 
bsonify (GHashTable* report, bson* b, const char** bson_message,
282
 
         uint32_t* bson_message_len)
283
 
{
284
 
    /* Attempt to convert a #GHashTable of the report into a BSON string.
285
 
     * On error return %FALSE. */
286
 
 
287
 
    GHashTableIter iter;
288
 
    gpointer key, value;
289
 
 
290
 
    *bson_message = NULL;
291
 
    *bson_message_len = 0;
292
 
 
293
 
    g_return_val_if_fail (report, FALSE);
294
 
 
295
 
    bson_init (b);
296
 
    if (!bson_data (b))
297
 
        return FALSE;
298
 
 
299
 
    g_hash_table_iter_init (&iter, report);
300
 
    while (g_hash_table_iter_next (&iter, &key, &value)) {
301
 
        if (!append_key_value (key, value, b))
302
 
            return FALSE;
303
 
    }
304
 
    if (bson_finish (b) == BSON_ERROR)
305
 
        return FALSE;
306
 
 
307
 
    *bson_message = bson_data (b);
308
 
    *bson_message_len = bson_size (b);
309
 
    if (*bson_message_len > 0 && *bson_message)
310
 
        return TRUE;
311
 
    else
312
 
        return FALSE;
313
 
}
314
 
 
315
 
int
316
 
upload_report (const char* message_data, uint32_t message_len, GString *s)
317
 
{
318
 
    CURL* curl = NULL;
319
 
    CURLcode result_code = 0;
320
 
    long response_code = 0;
321
 
    struct curl_slist* list = NULL;
322
 
 
323
 
    g_return_val_if_fail (message_data, -1);
324
 
 
325
 
    /* TODO use curl_share for DNS caching. */
326
 
    /* Repeated calls to curl_global_init will have no effect. */
327
 
    if (curl_global_init (CURL_GLOBAL_SSL)) {
328
 
        log_msg ("Unable to initialize curl.\n\n");
329
 
        exit (EXIT_FAILURE);
330
 
    }
331
 
 
332
 
    if ((curl = curl_easy_init ()) == NULL) {
333
 
        log_msg ("Couldn't init curl.\n");
334
 
        return FALSE;
335
 
    }
336
 
    curl_easy_setopt (curl, CURLOPT_POST, 1);
337
 
    curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 1);
338
 
    list = curl_slist_append (list, "Content-Type: application/octet-stream");
339
 
    list = curl_slist_append (list, "X-Whoopsie-Version: " VERSION);
340
 
    curl_easy_setopt (curl, CURLOPT_URL, crash_db_submit_url);
341
 
    curl_easy_setopt (curl, CURLOPT_HTTPHEADER, list);
342
 
    curl_easy_setopt (curl, CURLOPT_POSTFIELDSIZE, message_len);
343
 
    curl_easy_setopt (curl, CURLOPT_POSTFIELDS, (void*)message_data);
344
 
    curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, server_response);
345
 
    curl_easy_setopt (curl, CURLOPT_WRITEDATA, s);
346
 
    curl_easy_setopt (curl, CURLOPT_VERBOSE, 0L);
347
 
    curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER, verifypeer);
348
 
 
349
 
    result_code = curl_easy_perform (curl);
350
 
    curl_slist_free_all(list);
351
 
    curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &response_code);
352
 
 
353
 
    log_msg ("Sent; server replied with: %s\n",
354
 
        curl_easy_strerror (result_code));
355
 
    log_msg ("Response code: %ld\n", response_code);
356
 
    curl_easy_cleanup (curl);
357
 
 
358
 
    if (result_code != CURLE_OK)
359
 
        return result_code;
360
 
    else
361
 
        return response_code;
362
 
}
363
 
 
364
 
gsize
365
 
get_report_max_size (void)
366
 
{
367
 
    const char* value = NULL;
368
 
    gsize max_size;
369
 
 
370
 
    value = g_getenv ("REPORT_MAX_SIZE");
371
 
    if (value == NULL)
372
 
        return MAX_REPORT_FILESIZE;
373
 
 
374
 
    max_size = atol(value);
375
 
    if (max_size == 0)
376
 
        return MAX_REPORT_FILESIZE;
377
 
 
378
 
    return max_size;
379
 
}
380
 
 
381
 
GHashTable*
382
 
parse_report (const char* report_path, gboolean full_report, GError** error)
383
 
{
384
 
    /* We'll eventually modify the contents of the report, rather than sending
385
 
     * it as-is, to make it more amenable to what the server has to stick in
386
 
     * the database, and thus creating less work server-side.
387
 
     */
388
 
 
389
 
    GMappedFile* fp = NULL;
390
 
    GHashTable* hash_table = NULL;
391
 
    gchar* contents = NULL;
392
 
    gsize file_len = 0;
393
 
    /* Our position in the file. */
394
 
    gchar* p = NULL;
395
 
    /* The end or length of the token. */
396
 
    gchar* token_p = NULL;
397
 
    char* key = NULL;
398
 
    char* value = NULL;
399
 
    char* old_value = NULL;
400
 
    gchar* value_p = NULL;
401
 
    GError* err = NULL;
402
 
    gchar* end = NULL;
403
 
    size_t value_length;
404
 
    size_t value_pos;
405
 
    struct stat st;
406
 
    int fd;
407
 
    int res;
408
 
    gsize max_size;
409
 
 
410
 
    g_return_val_if_fail (report_path, NULL);
411
 
 
412
 
    max_size = get_report_max_size();
413
 
 
414
 
    fd = open(report_path, O_RDONLY | O_NOFOLLOW);
415
 
    res = fstat(fd, &st);
416
 
    if (res == -1) {
417
 
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
418
 
                     "%s could not be opened.", report_path);
419
 
        if (fd >= 0)
420
 
            close(fd);
421
 
        return NULL;
422
 
    }
423
 
 
424
 
    if(!S_ISREG(st.st_mode)) {
425
 
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
426
 
                     "%s is not a regular file.", report_path);
427
 
        close(fd);
428
 
        return NULL;
429
 
    }
430
 
 
431
 
    /* Limit the size of a report */
432
 
    if(st.st_size > max_size) {
433
 
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
434
 
                     "%s is too big to be processed.", report_path);
435
 
        close(fd);
436
 
        return NULL;
437
 
    }
438
 
 
439
 
    /* TODO handle the file being modified underneath us. */
440
 
    fp = g_mapped_file_new_from_fd (fd, FALSE, &err);
441
 
    if (err) {
442
 
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
443
 
                     "Unable to map report: %s", err->message);
444
 
        g_error_free (err);
445
 
        goto error;
446
 
    }
447
 
 
448
 
    contents = g_mapped_file_get_contents (fp);
449
 
    file_len = g_mapped_file_get_length (fp);
450
 
 
451
 
    /* Check report size again to make sure */
452
 
    if(file_len > max_size) {
453
 
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
454
 
                     "%s is too big to be processed.", report_path);
455
 
        goto error;
456
 
    }
457
 
 
458
 
    end = contents + file_len;
459
 
    hash_table = g_hash_table_new_full (g_str_hash, g_str_equal,
460
 
                                        g_free, g_free);
461
 
    p = contents;
462
 
 
463
 
    while (p < end) {
464
 
        /* We're either at the beginning of the file or the start of a line,
465
 
         * otherwise this report is corrupted. */
466
 
        if (!(p == contents || *(p-1) == '\n')) {
467
 
            g_set_error (error, g_quark_from_static_string ("whoopsie-quark"),
468
 
                         0, "Malformed report.");
469
 
            goto error;
470
 
        }
471
 
        if (*p == ' ') {
472
 
            if (!key) {
473
 
                g_set_error (error,
474
 
                             g_quark_from_static_string ("whoopsie-quark"), 0,
475
 
                             "Report may not start with a value.");
476
 
                goto error;
477
 
            }
478
 
            /* Skip the space. */
479
 
            p++;
480
 
            token_p = p;
481
 
            while (token_p < end && *token_p != '\n')
482
 
                token_p++;
483
 
 
484
 
            /* The length of this value string */
485
 
            value_length = token_p - p;
486
 
            if (value) {
487
 
                /* Space for the leading newline too. */
488
 
                value_pos = value_p - value;
489
 
                if (INT_MAX - (1 + value_length + 1) < value_pos) {
490
 
                    g_set_error (error,
491
 
                                 g_quark_from_static_string ("whoopsie-quark"),
492
 
                                 0, "Report value too long.");
493
 
                    goto error;
494
 
                }
495
 
                g_hash_table_steal (hash_table, key);
496
 
                value = g_realloc (value, value_pos + 1 + value_length + 1);
497
 
                value_p = value + value_pos;
498
 
                *value_p = '\n';
499
 
                value_p++;
500
 
            } else {
501
 
                /* Make sure we properly free the old empty string value */
502
 
                old_value = g_hash_table_lookup (hash_table, key);
503
 
                if (old_value)
504
 
                    g_free (old_value);
505
 
                g_hash_table_steal (hash_table, key);
506
 
                value = g_realloc (value, value_length + 1);
507
 
                value_p = value;
508
 
            }
509
 
            memcpy (value_p, p, value_length);
510
 
            value_p[value_length] = '\0';
511
 
            for (char *c = value_p; c < value_p + value_length; c++)
512
 
                /* If c is a control character but not TAB. */
513
 
                if (*c != '\t' && *c >= '\0' && *c < ' ')
514
 
                    *c = '?';
515
 
            value_p += value_length;
516
 
            if (g_hash_table_contains (hash_table, key) == TRUE) {
517
 
                g_set_error (error,
518
 
                    g_quark_from_static_string ("whoopsie-quark"),
519
 
                    0, "Report key must not be a duplicate.");
520
 
                goto error;
521
 
            }
522
 
            g_hash_table_insert (hash_table, key, value ? value : g_strdup(""));
523
 
            p = token_p + 1;
524
 
        } else {
525
 
            /* Reset the value pointer. */
526
 
            value = NULL;
527
 
            /* Key. */
528
 
            token_p = p;
529
 
            while (token_p < end) {
530
 
                if (*token_p != ':') {
531
 
                    if (*token_p == '\n') {
532
 
                        /* No colon character found on this line */
533
 
                        g_set_error (error,
534
 
                                g_quark_from_static_string ("whoopsie-quark"),
535
 
                                0, "Report key must have a value.");
536
 
                        goto error;
537
 
                    }
538
 
                    token_p++;
539
 
                } else if ((*(token_p + 1) == '\n' &&
540
 
                            *(token_p + 2) != ' ')) {
541
 
                        /* The next line doesn't start with a value */
542
 
                        g_set_error (error,
543
 
                                g_quark_from_static_string ("whoopsie-quark"),
544
 
                                0, "Report key must have a value.");
545
 
                        goto error;
546
 
                } else {
547
 
                    break;
548
 
                }
549
 
            }
550
 
            key = g_malloc ((token_p - p) + 1);
551
 
            memcpy (key, p, (token_p - p));
552
 
            key[(token_p - p)] = '\0';
553
 
 
554
 
            /* Replace any embedded NUL bytes. */
555
 
            for (char *c = key; c < key + (token_p - p); c++)
556
 
                if (*c >= '\0' && *c < ' ')
557
 
                    *c = '?';
558
 
 
559
 
            /* Eat the semicolon. */
560
 
            token_p++;
561
 
 
562
 
            /* Skip any leading spaces. */
563
 
            while (token_p < end && *token_p == ' ')
564
 
                token_p++;
565
 
 
566
 
            /* Start of the value. */
567
 
            p = token_p;
568
 
 
569
 
            while (token_p < end && *token_p != '\n')
570
 
                token_p++;
571
 
            if ((token_p - p) == 0) {
572
 
                /* Empty value. The key likely has a child. */
573
 
                value = NULL;
574
 
            } else {
575
 
                if (!strncmp ("base64", p, 6)) {
576
 
                    /* Just a marker that the following lines are base64
577
 
                     * encoded. Don't include it in the value. */
578
 
                    value = NULL;
579
 
                } else {
580
 
                    /* Value. */
581
 
                    value = g_malloc ((token_p - p) + 1);
582
 
                    memcpy (value, p, (token_p - p));
583
 
                    value[(token_p - p)] = '\0';
584
 
                    for (char *c = value; c < value + (token_p - p); c++)
585
 
                        if (*c >= '\0' && *c < ' ')
586
 
                            *c = '?';
587
 
                    value_p = value + (token_p - p);
588
 
                }
589
 
            }
590
 
            p = token_p + 1;
591
 
 
592
 
            if (g_hash_table_contains (hash_table, key) == TRUE) {
593
 
                g_set_error (error,
594
 
                    g_quark_from_static_string ("whoopsie-quark"),
595
 
                    0, "Report key must not be a duplicate.");
596
 
                goto error;
597
 
            }
598
 
            g_hash_table_insert (hash_table, key, value ? value : g_strdup(""));
599
 
        }
600
 
    }
601
 
    g_mapped_file_unref (fp);
602
 
    close(fd);
603
 
 
604
 
    /* Remove entries that we don't want to send */
605
 
    if (!full_report) {
606
 
        GHashTableIter iter;
607
 
        gpointer key, value;
608
 
        g_hash_table_iter_init(&iter, hash_table);
609
 
 
610
 
        /* We want everything that is in our white list or less than
611
 
           1 KB so that we don't end up DoSing our database. */
612
 
        while (g_hash_table_iter_next(&iter, &key, &value)) {
613
 
            if (!is_in_field_list((const char *)key, unacceptable_fields)) {
614
 
                if (is_in_field_list((const char *)key, acceptable_fields))
615
 
                    continue;
616
 
                if (strlen((const char *)value) < 1024)
617
 
                    continue;
618
 
            }
619
 
 
620
 
            g_hash_table_iter_remove(&iter);
621
 
        }
622
 
    }
623
 
 
624
 
    return hash_table;
625
 
 
626
 
error:
627
 
    if (hash_table) {
628
 
        g_hash_table_destroy (hash_table);
629
 
    }
630
 
    g_mapped_file_unref (fp);
631
 
    close(fd);
632
 
    return NULL;
633
 
}
634
 
 
635
 
gboolean
636
 
upload_core (const char* uuid, const char* arch, const char* core_data) {
637
 
 
638
 
    CURL* curl = NULL;
639
 
    CURLcode result_code = 0;
640
 
    long response_code = 0;
641
 
    struct curl_slist* list = NULL;
642
 
    char* crash_db_core_url = NULL;
643
 
    GString *s = NULL;
644
 
 
645
 
 
646
 
    g_return_val_if_fail (uuid, FALSE);
647
 
    g_return_val_if_fail (arch, FALSE);
648
 
    g_return_val_if_fail (core_data, FALSE);
649
 
 
650
 
    crash_db_core_url = g_strdup_printf ("%s/%s/submit-core/%s/%s",
651
 
                                         crash_db_url, uuid, arch,
652
 
                                         whoopsie_identifier);
653
 
 
654
 
    /* TODO use CURLOPT_READFUNCTION to transparently compress data with
655
 
     * Snappy. */
656
 
    if ((curl = curl_easy_init ()) == NULL) {
657
 
        log_msg ("Couldn't init curl.\n");
658
 
        g_free (crash_db_core_url);
659
 
        return FALSE;
660
 
    }
661
 
    s = g_string_new (NULL);
662
 
    curl_easy_setopt (curl, CURLOPT_POST, 1);
663
 
    curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 1);
664
 
    list = curl_slist_append (list, "Content-Type: application/octet-stream");
665
 
    list = curl_slist_append (list, "X-Whoopsie-Version: " VERSION);
666
 
    curl_easy_setopt (curl, CURLOPT_URL, crash_db_core_url);
667
 
    curl_easy_setopt (curl, CURLOPT_HTTPHEADER, list);
668
 
    curl_easy_setopt (curl, CURLOPT_POSTFIELDS, (void*)core_data);
669
 
    curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, server_response);
670
 
    curl_easy_setopt (curl, CURLOPT_WRITEDATA, s);
671
 
    curl_easy_setopt (curl, CURLOPT_VERBOSE, 0L);
672
 
    curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER, verifypeer);
673
 
 
674
 
    result_code = curl_easy_perform (curl);
675
 
    curl_slist_free_all(list);
676
 
 
677
 
    curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &response_code);
678
 
    /* this actually what curl replied with */
679
 
    log_msg ("Sent; server replied with: %s\n",
680
 
        curl_easy_strerror (result_code));
681
 
    log_msg ("Response code: %ld\n", response_code);
682
 
    curl_easy_cleanup (curl);
683
 
    g_free (crash_db_core_url);
684
 
    g_string_free (s, TRUE);
685
 
 
686
 
    return result_code == CURLE_OK && response_code == 200;
687
 
}
688
 
 
689
 
void unset_last_uploaded_oopsid ()
690
 
{
691
 
    if (last_uploaded_oopsid) {
692
 
        g_free (last_uploaded_oopsid);
693
 
        last_uploaded_oopsid = NULL;
694
 
    }
695
 
}
696
 
 
697
 
void set_last_uploaded_oopsid (char* response)
698
 
{
699
 
    unset_last_uploaded_oopsid();
700
 
    last_uploaded_oopsid = g_strdup_printf ("%.36s", response);
701
 
}
702
 
 
703
 
void
704
 
handle_response (GHashTable* report, char* response_data)
705
 
{
706
 
    char* command = NULL;
707
 
    char* core = NULL;
708
 
    char* arch = NULL;
709
 
 
710
 
    g_return_if_fail (report);
711
 
 
712
 
    /* Command could be CORE, which requests the core dump, BUG ######, if in a
713
 
     * development release, which points to the bug report, or UPDATE, if this
714
 
     * is fixed in an update. */
715
 
    split_string (response_data, &command);
716
 
    if (command) {
717
 
        if (strcmp (command, "CORE") == 0) {
718
 
            log_msg ("Reported OOPS ID %.36s\n", response_data);
719
 
            set_last_uploaded_oopsid(response_data);
720
 
            core = g_hash_table_lookup (report, "CoreDump");
721
 
            arch = g_hash_table_lookup (report, "Architecture");
722
 
            if (core && arch) {
723
 
                if (!upload_core (response_data, arch, core))
724
 
                    /* We do not retry the upload. Once is a big enough hit to
725
 
                     * their Internet connection, and we can always count on
726
 
                     * the next person in line to send it. */
727
 
                    log_msg ("Upload of the core dump failed.\n");
728
 
            } else if (strcmp (command, "OOPSID") == 0) {
729
 
                log_msg ("Reported OOPS ID %.36s\n", response_data);
730
 
                set_last_uploaded_oopsid(response_data);
731
 
            } else
732
 
                log_msg ("Asked for a core dump that we don't have.\n");
733
 
        } else if (strcmp (command, "OOPSID") == 0) {
734
 
            log_msg ("Reported OOPS ID %.36s\n", response_data);
735
 
            set_last_uploaded_oopsid(response_data);
736
 
        } else
737
 
            log_msg ("Got command: %s\n", command);
738
 
    }
739
 
}
740
 
 
741
 
gboolean
742
 
parse_and_upload_report (const char* crash_file)
743
 
{
744
 
    GHashTable* report = NULL;
745
 
    gboolean success = FALSE;
746
 
    uint32_t message_len = 0;
747
 
    const char* message_data = NULL;
748
 
    GError* error = NULL;
749
 
    bson b[1];
750
 
    int response = 0;
751
 
 
752
 
    log_msg ("Parsing %s.\n", crash_file);
753
 
    report = parse_report (crash_file, FALSE, &error);
754
 
    if (!report) {
755
 
        if (error) {
756
 
            log_msg ("Unable to parse report (%s): %s\n", crash_file,
757
 
                       error->message);
758
 
            g_error_free (error);
759
 
        } else {
760
 
            log_msg ("Unable to parse report (%s)\n", crash_file);
761
 
        }
762
 
        /* Do not keep trying to parse and upload this */
763
 
        return TRUE;
764
 
    }
765
 
 
766
 
    if (!bsonify (report, b, &message_data, &message_len)) {
767
 
        log_msg ("Unable to bsonify report (%s)\n", crash_file);
768
 
        if (bson_data (b))
769
 
            bson_destroy (b);
770
 
        /* Do not keep trying to parse and upload this */
771
 
        success = TRUE;
772
 
    } else {
773
 
        log_msg ("Uploading %s.\n", crash_file);
774
 
        GString* s = g_string_new (NULL);
775
 
        response = upload_report (message_data, message_len, s);
776
 
        if (bson_data (b))
777
 
            bson_destroy (b);
778
 
 
779
 
        /* If the response code is 400, the server did not like what we sent it.
780
 
         * Sending the same thing again is not likely to change that */
781
 
        /* TODO check that there aren't 400 responses that we care about seeing
782
 
         * again, such as a transient database failure. */
783
 
        if (response == 200 || response == 400)
784
 
            success = TRUE;
785
 
        else
786
 
            success = FALSE;
787
 
 
788
 
        if (response > 200) {
789
 
            log_msg ("Server replied with:\n");
790
 
            log_msg ("%s\n", s->str);
791
 
        }
792
 
 
793
 
        if (response == 200 && s->len > 0)
794
 
            handle_response (report, s->str);
795
 
 
796
 
        g_string_free (s, TRUE);
797
 
    }
798
 
 
799
 
    g_hash_table_destroy (report);
800
 
 
801
 
    return success;
802
 
}
803
 
 
804
 
static gboolean
805
 
process_existing_files_if_online (const char* report_dir)
806
 
{
807
 
    if (online_state) {
808
 
        process_existing_files (report_dir);
809
 
    }
810
 
    return G_SOURCE_CONTINUE;
811
 
}
812
 
 
813
 
void
814
 
process_existing_files (const char* report_dir)
815
 
{
816
 
    GDir* dir = NULL;
817
 
    const gchar* file = NULL;
818
 
    const gchar* ext = NULL;
819
 
    char* upload_file = NULL;
820
 
    char* crash_file = NULL;
821
 
 
822
 
    dir = g_dir_open (report_dir, 0, NULL);
823
 
    while ((file = g_dir_read_name (dir)) != NULL) {
824
 
 
825
 
        upload_file = g_build_filename (report_dir, file, NULL);
826
 
        if (!upload_file)
827
 
            continue;
828
 
 
829
 
        ext = strrchr (upload_file, '.');
830
 
        if (ext && strcmp(++ext, "upload") != 0) {
831
 
            g_free (upload_file);
832
 
            continue;
833
 
        }
834
 
 
835
 
        crash_file = change_file_extension (upload_file, ".crash");
836
 
        if (!crash_file) {
837
 
            g_free (upload_file);
838
 
            continue;
839
 
        }
840
 
 
841
 
        if (already_handled_report (crash_file)) {
842
 
            g_free (upload_file);
843
 
            g_free (crash_file);
844
 
            continue;
845
 
        } else if (parse_and_upload_report (crash_file)) {
846
 
            if (!mark_handled (crash_file, last_uploaded_oopsid)) {
847
 
                log_msg ("Unable to mark report as seen (%s) removing it.\n", crash_file);
848
 
                g_unlink (crash_file);
849
 
            }
850
 
        }
851
 
 
852
 
        g_free (upload_file);
853
 
        g_free (crash_file);
854
 
    }
855
 
    g_dir_close (dir);
856
 
}
857
 
 
858
 
void daemonize (void)
859
 
{
860
 
    pid_t pid, sid;
861
 
    int i;
862
 
    struct rlimit rl = {0};
863
 
 
864
 
    if (getrlimit (RLIMIT_NOFILE, &rl) < 0) {
865
 
        log_msg ("Could not get resource limits.\n");
866
 
        exit (EXIT_FAILURE);
867
 
    }
868
 
 
869
 
    umask (0);
870
 
    pid = fork();
871
 
    if (pid < 0)
872
 
        exit (EXIT_FAILURE);
873
 
    if (pid > 0)
874
 
        exit (EXIT_SUCCESS);
875
 
    sid = setsid ();
876
 
    if (sid < 0)
877
 
        exit (EXIT_FAILURE);
878
 
 
879
 
    if ((chdir ("/")) < 0)
880
 
        exit (EXIT_FAILURE);
881
 
 
882
 
    for (i = 0; i < rl.rlim_max && i < 1024; i++) {
883
 
        if (i != lock_fd)
884
 
            close (i);
885
 
    }
886
 
    if ((open ("/dev/null", O_RDWR) != 0) ||
887
 
        (dup (0) != 1) ||
888
 
        (dup (0) != 2)) {
889
 
        log_msg ("Could not redirect file descriptors to /dev/null.\n");
890
 
        exit (EXIT_FAILURE);
891
 
    }
892
 
}
893
 
 
894
 
void
895
 
exit_if_already_running (void)
896
 
{
897
 
    int rc = 0;
898
 
 
899
 
    if (g_getenv ("APPORT_REPORT_DIR")) {
900
 
        /* keep lock file in custom report directory */
901
 
        lock_path = g_build_filename (report_dir, "whoopsie_lock", NULL);
902
 
    } else {
903
 
        /* use system directory */
904
 
        if (mkdir ("/var/lock/whoopsie", 0755) < 0) {
905
 
            if (errno != EEXIST) {
906
 
                log_msg ("Could not create lock directory.\n");
907
 
            }
908
 
        }
909
 
    }
910
 
 
911
 
    log_msg ("Using lock path: %s\n", lock_path);
912
 
 
913
 
    lock_fd = open (lock_path, O_CREAT | O_RDWR, 0600);
914
 
    rc = flock (lock_fd, LOCK_EX | LOCK_NB);
915
 
    if (rc) {
916
 
        if (EWOULDBLOCK == errno) {
917
 
            log_msg ("Another instance is already running.\n");
918
 
            exit (1);
919
 
        } else {
920
 
            log_msg ("Could not create lock file: %s\n", strerror (errno));
921
 
        }
922
 
    }
923
 
}
924
 
 
925
 
char*
926
 
get_crash_db_url (void)
927
 
{
928
 
    const char* url = NULL;
929
 
 
930
 
    url = g_getenv ("CRASH_DB_URL");
931
 
    if (url == NULL)
932
 
        return NULL;
933
 
 
934
 
    if ((strncasecmp ("http://", url, 7) || url[7] == '\0') &&
935
 
        (strncasecmp ("https://", url, 8) || url[8] == '\0'))
936
 
        return NULL;
937
 
    return g_strdup (url);
938
 
}
939
 
 
940
 
void
941
 
drop_privileges (GError** error)
942
 
{
943
 
    struct passwd *pw = NULL;
944
 
 
945
 
    if (getuid () != 0) {
946
 
        if (g_getenv ("CRASH_DB_IDENTIFIER") == NULL) {
947
 
            g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
948
 
                         "You must be root to run this program, or set $CRASH_DB_IDENTIFIER.");
949
 
        }
950
 
        return;
951
 
    }
952
 
    if (!(pw = getpwnam (username))) {
953
 
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
954
 
                     "Failed to find user: %s", username);
955
 
        return;
956
 
    }
957
 
 
958
 
    /* Drop privileges */
959
 
    if (setgroups (1, &pw->pw_gid) < 0 ||
960
 
        setresgid (pw->pw_gid, pw->pw_gid, pw->pw_gid) < 0 ||
961
 
        setresuid (pw->pw_uid, pw->pw_uid, pw->pw_uid) < 0) {
962
 
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
963
 
                     "Failed to become user: %s", username);
964
 
        return;
965
 
    }
966
 
 
967
 
    if (prctl (PR_SET_DUMPABLE, 1))
968
 
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
969
 
                     "Failed to ensure core dump production.");
970
 
 
971
 
    if ((setenv ("USER", username, 1) < 0) ||
972
 
        (setenv ("USERNAME", username, 1) < 0)) {
973
 
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
974
 
                     "Failed to set user environment variables.");
975
 
        return;
976
 
    }
977
 
}
978
 
 
979
 
void
980
 
network_changed (gboolean available)
981
 
{
982
 
    if (online_state != available)
983
 
        log_msg (available ? "online\n" : "offline\n");
984
 
 
985
 
    if (!available) {
986
 
        online_state = FALSE;
987
 
        return;
988
 
    }
989
 
 
990
 
    if (online_state && available)
991
 
        return;
992
 
 
993
 
    online_state = available;
994
 
 
995
 
    if (online_state)
996
 
        process_existing_files (report_dir);
997
 
}
998
 
 
999
 
gboolean
1000
 
check_online_then_upload (const char* crash_file) {
1001
 
 
1002
 
    if (!online_state) {
1003
 
        log_msg ("Not online; processing later (%s).\n", crash_file);
1004
 
        return FALSE;
1005
 
    }
1006
 
 
1007
 
    if (!parse_and_upload_report (crash_file)) {
1008
 
        log_msg ("Could not upload; processing later (%s).\n", crash_file);
1009
 
        return FALSE;
1010
 
    }
1011
 
 
1012
 
    return TRUE;
1013
 
}
1014
 
 
1015
 
void
1016
 
create_crash_directory (void)
1017
 
{
1018
 
    struct passwd *pw = NULL;
1019
 
 
1020
 
    if (mkdir (report_dir, 0755) < 0) {
1021
 
        if (errno != EEXIST) {
1022
 
            log_msg ("Could not create non-existent report_directory to monitor (%d): %s.\n", errno, report_dir);
1023
 
            exit (EXIT_FAILURE);
1024
 
        }
1025
 
    } else {
1026
 
        /* Only change the permissions if we've just created it */
1027
 
        if (!(pw = getpwnam (username))) {
1028
 
            log_msg ("Could not find user, %s.\n", username);
1029
 
            exit (EXIT_FAILURE);
1030
 
        }
1031
 
        if (chown (report_dir, -1, pw->pw_gid) < 0) {
1032
 
            log_msg ("Could not change ownership of %s.\n", report_dir);
1033
 
            exit (EXIT_FAILURE);
1034
 
        }
1035
 
        if (chmod (report_dir, 03777) < 0) {
1036
 
            log_msg ("Could not change permissions on %s.\n", report_dir);
1037
 
            exit (EXIT_FAILURE);
1038
 
        }
1039
 
    }
1040
 
}
1041
 
 
1042
 
#ifndef TEST
1043
 
static GMainLoop* loop = NULL;
1044
 
 
1045
 
static void
1046
 
parse_arguments (int* argc, char** argv[])
1047
 
{
1048
 
    GError* err = NULL;
1049
 
    GOptionContext* context;
1050
 
 
1051
 
    context = g_option_context_new (NULL);
1052
 
    g_option_context_add_main_entries (context, option_entries, NULL);
1053
 
    if (!g_option_context_parse (context, argc, argv, &err)) {
1054
 
        log_msg ("whoopsie: %s\n", err->message);
1055
 
        g_error_free (err);
1056
 
        exit (EXIT_FAILURE);
1057
 
    }
1058
 
    g_option_context_free (context);
1059
 
}
1060
 
 
1061
 
static void
1062
 
handle_signals (int signo)
1063
 
{
1064
 
    if (loop)
1065
 
        g_main_loop_quit (loop);
1066
 
    else
1067
 
        exit (0);
1068
 
}
1069
 
 
1070
 
static void
1071
 
setup_signals (void)
1072
 
{
1073
 
    struct sigaction action;
1074
 
    sigset_t mask;
1075
 
 
1076
 
    sigemptyset (&mask);
1077
 
    action.sa_handler = handle_signals;
1078
 
    action.sa_mask = mask;
1079
 
    action.sa_flags = 0;
1080
 
    sigaction (SIGTERM, &action, NULL);
1081
 
    sigaction (SIGINT, &action, NULL);
1082
 
}
1083
 
 
1084
 
static void
1085
 
start_polling (void)
1086
 
{
1087
 
    g_timeout_add_seconds (PROCESS_OUTSTANDING_TIMEOUT,
1088
 
                           (GSourceFunc) process_existing_files_if_online, (gpointer) report_dir);
1089
 
 
1090
 
    loop = g_main_loop_new (NULL, FALSE);
1091
 
    g_main_loop_run (loop);
1092
 
}
1093
 
 
1094
 
int
1095
 
main (int argc, char** argv)
1096
 
{
1097
 
    GError* err = NULL;
1098
 
    const gchar* env;
1099
 
    GFileMonitor* monitor;
1100
 
 
1101
 
    setup_signals ();
1102
 
    parse_arguments (&argc, &argv);
1103
 
 
1104
 
    if (!use_polling) {
1105
 
        /*
1106
 
         * In non polling mode, we exit after the first invocation of
1107
 
         * process_existing_files.
1108
 
         * Since we start the process with online_status already set to TRUE,
1109
 
         * the first invocation of process_existing_files will always ignore
1110
 
         * the assume_online variable. Therefore, assume_online is irrelevant
1111
 
         * in non polling mode.
1112
 
         */
1113
 
        assume_online = TRUE;
1114
 
    }
1115
 
 
1116
 
    if (!foreground) {
1117
 
        open_log ();
1118
 
        log_msg ("whoopsie " VERSION " starting up.\n");
1119
 
    }
1120
 
 
1121
 
    if ((crash_db_url = get_crash_db_url ()) == NULL) {
1122
 
        log_msg ("Could not get crash database location.\n");
1123
 
        exit (EXIT_FAILURE);
1124
 
    }
1125
 
 
1126
 
    /* environment might change report directory and identifier */
1127
 
    env = g_getenv ("APPORT_REPORT_DIR");
1128
 
    if (env != NULL && *env != '\0')
1129
 
        report_dir = g_strdup (env);
1130
 
 
1131
 
    env = g_getenv ("CRASH_DB_IDENTIFIER");
1132
 
    if (env != NULL)
1133
 
        whoopsie_identifier = g_strdup (env);
1134
 
    else
1135
 
        whoopsie_identifier_generate (&whoopsie_identifier, &err);
1136
 
 
1137
 
    env = g_getenv ("CRASH_DB_NOVERIFYPEER");
1138
 
    if (env != NULL) {
1139
 
        log_msg ("CRASH_DB_NOVERIFYPEER is set, not verifying ssl cert.\n");
1140
 
        verifypeer = 0;
1141
 
    }
1142
 
 
1143
 
    if (err) {
1144
 
        log_msg ("%s\n", err->message);
1145
 
        g_error_free (err);
1146
 
        err = NULL;
1147
 
        crash_db_submit_url = strdup (crash_db_url);
1148
 
    } else {
1149
 
        crash_db_submit_url = g_strdup_printf ("%s/%s", crash_db_url,
1150
 
                                               whoopsie_identifier);
1151
 
    }
1152
 
 
1153
 
    /* Only publish whoopsie-id if it was generated. LP: #1389357 */
1154
 
    if (env == NULL) {
1155
 
        g_file_set_contents (WHOOPSIE_ID_PATH, whoopsie_identifier, -1, NULL);
1156
 
        chmod (WHOOPSIE_ID_PATH, 00600);
1157
 
    }
1158
 
 
1159
 
    create_crash_directory ();
1160
 
 
1161
 
    drop_privileges (&err);
1162
 
    if (err) {
1163
 
        log_msg ("%s\n", err->message);
1164
 
        g_error_free (err);
1165
 
        exit (EXIT_FAILURE);
1166
 
    }
1167
 
    exit_if_already_running ();
1168
 
 
1169
 
    if (!foreground) {
1170
 
        close_log ();
1171
 
        daemonize ();
1172
 
        open_log();
1173
 
    }
1174
 
 
1175
 
#if GLIB_MAJOR_VERSION <= 2 && GLIB_MINOR_VERSION < 35
1176
 
    /* Deprecated in glib 2.35/2.36. */
1177
 
    g_type_init ();
1178
 
#endif
1179
 
 
1180
 
    monitor = monitor_directory (report_dir, check_online_then_upload);
1181
 
    if (!monitor)
1182
 
        exit (EXIT_FAILURE);
1183
 
 
1184
 
    if (!assume_online)
1185
 
        monitor_connectivity (crash_db_url, network_changed);
1186
 
 
1187
 
    /* As long as we keep online_state to TRUE by default, this initial call
1188
 
     * happens unconditionally. */
1189
 
    process_existing_files_if_online (report_dir);
1190
 
 
1191
 
    if (use_polling)
1192
 
        start_polling ();
1193
 
 
1194
 
    unmonitor_directory (monitor, check_online_then_upload);
1195
 
    if (!assume_online)
1196
 
        unmonitor_connectivity ();
1197
 
 
1198
 
    close_log ();
1199
 
 
1200
 
    g_unlink (lock_path);
1201
 
    close (lock_fd);
1202
 
    curl_global_cleanup ();
1203
 
 
1204
 
    g_free (crash_db_url);
1205
 
    g_free (crash_db_submit_url);
1206
 
    return 0;
1207
 
}
1208
 
#endif