Check-in [b9a69f3282]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Finished reworking SUID changes
Timelines: family | ancestors | descendants | both | setuid-fossil
Files: files | file ages | folders
SHA1: b9a69f32828740a53e0dee6526cb79238f2a029c
User & Date: rkeene 2020-08-23 19:26:18.186
Context
2020-08-23
19:31
A bit of cleanup check-in: c0a2a1dc2c user: rkeene tags: setuid-fossil
19:26
Finished reworking SUID changes check-in: b9a69f3282 user: rkeene tags: setuid-fossil
19:14
Customized secure-wrap for Fossil check-in: 78a20d584e user: rkeene tags: setuid-fossil
Changes
Unified Diff Ignore Whitespace Patch
Changes to .fossil-settings/ignore-glob.
1
2

scripts/fossil-as-user/secure-wrap
scripts/fossil-as-user/filter.h



>
1
2
3
scripts/fossil-as-user/secure-wrap
scripts/fossil-as-user/filter.h
repos/*
Changes to nano/fossil.php.
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
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396

397



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430


431

432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449

450
451
452
453
454



455
456
457
458
459
460
461
462
463
464
465
466


467
468
469
470
471
472
473
474
475
476
<?php

class Nano_Fossil
{


    protected absolute_path;
    protected relative_path;
    protected user;
    protected workdir;
    protected use_suid;
    protected fossil_binary;

    public function __construct($user)
    {
        $this->repo_dir = $_SERVER['DOCUMENT_ROOT'] . '/../repos';
        $this->absolute_path = $this->repo_dir . '/' . $user['username'] . '/';

        $this->user = $user;









        $this->use_suid = false;

        if ($this->use_suid) {




            $this->fossil_binary = dirname(__FILE__) . "/../scripts/fossil-as-user/suid-fossil";



            $this->relative_path = '/' . $user['username'] . '/';

        } else {
            $this->fossil_binary = "/usr/local/bin/fossil";
            $this->relative_path = $this->absolute_path;

        }

        if (!$this->use_suid) {
            $this->workdir = "/tmp/workdir-flint-" . bin2hex(openssl_random_pseudo_bytes(20));

            mkdir($this->workdir);
        }
    }

    public function __destruct() {
        if (!$this->use_suid) {
            system("rm -rf '{$this->workdir}'");
        }
    }

    private function fossil_sql_execute($repo, $sql, $bind = array()) {

        Nano_Db::setDb("sqlite:{$this->absolute_path}{$repo}.fossil");
        $result = Nano_Db::execute($sql, $bind);
        Nano_Db::unsetDb();
        return($result);
    }

    private function fossil_sql_query($repo, $sql, $bind = array()) {

        Nano_Db::setDb("sqlite:{$this->absolute_path}{$repo}.fossil");
        $result = Nano_Db::query($sql, $bind);
        Nano_Db::unsetDb();
        return($result);
    }

    private function getFossilCommand($timeout = 0, $cgi = false) {
        $fossil = $this->fossil_binary;

        if ($timeout) {
            $fossil = "timeout {$timeout} {$fossil}";
        }

        $username = escapeshellarg($this->user['username']);

        $cmd = "USER={$username} {$fossil}";

        if (!$this->use_suid) {
            $cmd = "HOME={$this->workdir} {$cmd}";

        } else {
            $userid = escapeshellarg($this->user['id']);
            $cmd = "FLINT_USERID={$userid} FLINT_USERNAME={$username} {$cmd}";
        }

        if ($cgi) {
            $cmd = "GATEWAY_INTERFACE=1 {$cmd}";
        } else {
            $cmd = "unset GATEWAY_INTERFACE; {$cmd}";
        }

        return $cmd;
    }







    public function newRepo($repo, $password = null, $private = 0, $projectCode = null, $sha3 = false)


    {
















        if (!file_exists($this->absolute_path)) {

            mkdir($this->absolute_path);

            $content = "#!/usr/local/bin/fossil\ndirectory: ./\nnotfound: http://{$_SERVER['SERVER_NAME']}/notfound";
            file_put_contents("{$this->absolute_path}repository", $content);
            chmod("{$this->absolute_path}repository", 0555);
        }

        if ($sha3 === true) {
            $shaArg = "";
        } else {
            $shaArg = "--sha1";
        }

        if (!file_exists("{$this->absolute_path}{$repo}.fossil")) {
            exec($this->getFossilCommand() . " new " . $shaArg . " -A " . escapeshellarg($this->user['username']) . " " . escapeshellarg("{$this->relative_path}{$repo}.fossil"), $output, $return);


            if ($return !== 0) {
                if (file_exists("{$this->absolute_path}{$repo}.fossil")) {
                    unlink("{$this->absolute_path}{$repo}.fossil");
                }

                return false;
            }


            /* Install default configuration */
            exec($this->getFossilCommand() . " configuration import -R " . escapeshellarg("{$this->relative_path}{$repo}.fossil") . " " . escapeshellarg($_SERVER['DOCUMENT_ROOT'] . "/../config/fossil-default.cnf"), $output, $return);

            $sql = "INSERT INTO repositories
                           (user_id, name, private, cloned, auto_update)
                    VALUES (:id, :name, :private, 0, 0)";

            $bind = array('id' => $this->user['id'], 'name' => $repo, 'private' => $private);

            if (Nano_Db::execute($sql, $bind)) {
                if ($projectCode) {
                    $sql  = "UPDATE config SET value = :code WHERE name = 'project-code'";
                    $bind = array('code' => $projectCode);
                    $this->fossil_sql_execute($repo, $sql, $bind);
                }

                if ($password) {
                    $sql = "SELECT value FROM config WHERE name = 'project-code'";

                    if ($result = $this->fossil_sql_query($repo, $sql)) {
                        $code     = array_pop($result);
                        $password = sha1("{$code['value']}/{$this->user['username']}/{$password}");

                        $sql  = "UPDATE user SET pw = :password WHERE login = :user";
                        $bind = array('password' => $password, 'user' => $this->user['username']);
                        $this->fossil_sql_execute($repo, $sql, $bind);

                        $return = 'sha1';
                    }
                } else {
                    $sql  = "SELECT pw FROM user WHERE login = :user";
                    $bind = array('user' => $this->user['username']);

                    if ($result = $this->fossil_sql_query($repo, $sql, $bind)) {
                        $password = array_pop($result);
                        $return   = $password['pw'];
                    }
                }

                $sql = "UPDATE config SET value = 1 WHERE name = 'localauth'";
                $this->fossil_sql_execute($repo, $sql);


                return $return;
            }
        }

        return false;
    }





    public function cloneRepo($repo, $password = null, $url, $private = 0, $update = 0)

    {



        if (!file_exists($this->absolute_path)) {


            mkdir($this->absolute_path);







            $content = "#!/usr/local/bin/fossil\ndirectory: ./\nnotfound: http://{$_SERVER['SERVER_NAME']}/notfound";




            file_put_contents("{$this->absolute_path}repository", $content);




            chmod("{$this->absolute_path}repository", 0555);

        }
























        if (!file_exists("{$this->absolute_path}{$repo}.fossil")) {

            exec($this->getFossilCommand(3600) . " clone -A " . escapeshellarg($this->user['username']) . " " . escapeshellarg($url) . " " . escapeshellarg("{$this->relative_path}{$repo}.fossil"), $output,

























                 $return);














            if ($return !== 0) {
                if (file_exists("{$this->absolute_path}{$repo}.fossil")) {
                    unlink("{$this->absolute_path}{$repo}.fossil");
                }



                return false;
            }

            $sql = "INSERT INTO repositories
                           (user_id, name, private, cloned, auto_update)
                    VALUES (:id, :name, :private, 1, :auto)";

            $bind = array('id' => $this->user['id'], 'name' => $repo, 'private' => $private, 'auto' => $update);

            if (Nano_Db::execute($sql, $bind)) {
                if ($password) {
                    $sql = "SELECT value FROM config WHERE name = 'project-code'";

                    if ($result = $this->fossil_sql_query($repo, $sql)) {
                        $code     = array_pop($result);
                        $password = sha1("{$code['value']}/{$this->user['username']}/{$password}");

                        $sql  = "UPDATE user SET pw = :password WHERE login = :user";
                        $bind = array('password' => $password, 'user' => $this->user['username']);
                        $this->fossil_sql_execute($repo, $sql, $bind);


                        $return = 'sha1';
                    }
                } else {
                    $sql  = "SELECT pw FROM user WHERE login = :user";
                    $bind = array('user' => $this->user['username']);

                    if ($result = $this->fossil_sql_query($repo, $sql, $bind)) {
                        $password = array_pop($result);
                        $return   = $password['pw'];
                    }
                }

                $sql = "UPDATE config SET value = 1 WHERE name = 'localauth'";
                $this->fossil_sql_execute($repo, $sql);

                return $return;
            }

        }



        return false;
    }

    public function uploadRepo($repo, $password, $private = 0, array $file)
    {
        if (!file_exists($this->absolute_path)) {
            mkdir($this->absolute_path);

            $content = "#!/usr/local/bin/fossil\ndirectory: ./\nnotfound: http://{$_SERVER['SERVER_NAME']}/notfound";
            file_put_contents("{$this->absolute_path}repository", $content);
            chmod("{$this->absolute_path}repository", 0555);
        }

        if (!file_exists("{$this->absolute_path}{$repo}.fossil")) {
            if (!@move_uploaded_file($file['tmp_name'], "{$this->absolute_path}{$repo}.fossil")) {
                return false;
            }

            exec($this->getFossilCommand() . " config -R " . escapeshellarg("{$this->relative_path}{$repo}.fossil") . " export project /tmp/config",
                 $output, $return);

            if (file_exists('/tmp/config')) {
                unlink('/tmp/config');
            }

            if ($return !== 0) {
                if (file_exists("{$this->absolute_path}{$repo}.fossil")) {
                    unlink("{$this->absolute_path}{$repo}.fossil");
                }



                return false;
            }

            exec($this->getFossilCommand() . " user new " . escapeshellarg($this->user['username']) . " 'Flint User' {$password} -R " . escapeshellarg("{$this->relative_path}{$repo}.fossil"),
                $output, $return);

            if ($return == 0) {
                exec($this->getFossilCommand() . " user capabilities " . escapeshellarg($this->user['username']) . " s -R " . escapeshellarg("{$this->relative_path}{$repo}.fossil"),
                    $output, $return);

                if ($return !== 0) {
                    unlink("{$this->absolute_path}{$repo}.fossil");
                    return false;
                }
            } else if ($return == 1) {
                $sql = "SELECT value FROM config WHERE name = 'project-code'";
                    
                if ($result = $this->fossil_sql_query($repo, $sql)) {
                    $code     = array_pop($result);
                    $password = sha1("{$code['value']}/{$this->user['username']}/{$password}");
                        
                    $sql  = "UPDATE user SET cap = 's', pw = :password WHERE login = :user";
                    $bind = array('password' => $password, 'user' => $this->user['username']);
                    $this->fossil_sql_execute($repo, $sql, $bind);
                }
            } else {
                unlink("{$this->absolute_path}{$repo}.fossil");
                return false;
            }

            $sql = "INSERT INTO repositories
                           (user_id, name, private, cloned, auto_update)
                    VALUES (:id, :name, :private, 0, 0)";

            $bind = array('id' => $this->user['id'], 'name' => $repo, 'private' => $private);

            if (Nano_Db::execute($sql, $bind)) {
                $sql = "UPDATE config SET value = 1 WHERE name = 'localauth'";


                $this->fossil_sql_execute($repo, $sql);


                return 'sha1';
            }
        }


        return false;
    }

    public function pullRepo($repo, $url = '', &$outputstr = null)

    {
        if ($url != '') {
            if (file_exists($url) || preg_match('/:\/\//', $url) == 0) {
                $outputstr = "Invalid URL";
                return false;
            }
        }



        if (file_exists("{$this->absolute_path}{$repo}.fossil")) {



            # Ensure that no non-default SSH command can be used for a pull
            exec($this->getFossilCommand(3600) . " unset ssh-command -R " . escapeshellarg("{$this->relative_path}{$repo}.fossil") . " 2>&1",
              $output, $return);
            if ($return !== 0) {

                return false;
            }

            if ($url == '') {
                exec($this->getFossilCommand(3600) . " pull -R " . escapeshellarg("{$this->relative_path}{$repo}.fossil") . " 2>&1",
                  $output, $return);
            } else {
                exec($this->getFossilCommand(3600) . " pull " . escapeshellarg($url) . " -R " . escapeshellarg("{$this->relative_path}{$repo}.fossil") . " 2>&1",
                  $output, $return);
            }

            $outputstr = join("\n", $output);

            if ($return !== 0) {
                return false;
            }

            return true;
        }

        $outputstr = "Invalid repository";

        return false;
    }

    public function getRepos()
    {
        $sql = "SELECT *
                  FROM repositories
                 WHERE user_id = :id";

        $bind = array('id' => $this->user['id']);

        if ($result = Nano_Db::query($sql, $bind)) {
            return $result;
        }

        return false;
    }

    public function getRepoById($id)
    {
        $sql = "SELECT *
                  FROM repositories
                 WHERE user_id = :user
                   AND id = :id";

        $bind = array('user' => $this->user['id'], 'id' => $id);

        if ($result = Nano_Db::query($sql, $bind)) {
            $return = array_pop($result);
        } else {
            return false;
        }

        $repo = $return['name'];

        $return['repo-file'] = "{$this->absolute_path}{$return['name']}.fossil";

        if ($return['cloned']) {
            $sql  = "SELECT value FROM config WHERE name = 'last-sync-url'";

            if ($result = $this->fossil_sql_query($repo, $sql)) {
                $url                 = array_pop($result);
                $return['clone-url'] = $url['value'];
            }
        }

        $sql  = "SELECT value FROM config WHERE name = 'last-sync-pw'";

        if ($result = $this->fossil_sql_query($repo, $sql)) {
            $password           = array_pop($result);
            $return['clone-pw'] = $password['value'];

            exec($this->getFossilCommand() . " test-obscure " . escapeshellarg($return['clone-pw']), $output, $returnCode);

            if ($returnCode === 0) {
                if (preg_match('/^UNOBSCURE: (.*) -> (.*)$/', $output[1], $matches)) {
                    $return['clone-pw'] = $matches[2];
                }
            }
        }

        if (isset($return)) {
            return $return;
        }

        return false;
    }

    public function updateRepo($repo, $private, $update, $cloned, $password = null)

    {



        if (file_exists("{$this->absolute_path}{$repo}.fossil")) {
            $sql = "UPDATE repositories
                       SET private     = :private,
                           auto_update = :auto,
                           cloned      = :cloned
                     WHERE user_id = :id
                       AND name    = :repo";

            $bind = array(
                'private' => $private,
                'auto'    => $update,
                'cloned'  => $cloned,
                'id'      => $this->user['id'],
                'repo'    => $repo,
            );

            if (Nano_Db::execute($sql, $bind)) {
                if ($password) {
                    $sql = "SELECT value FROM config WHERE name = 'project-code'";

                    if ($result = $this->fossil_sql_query($repo, $sql)) {
                        $code     = array_pop($result);
                        $password = sha1("{$code['value']}/{$this->user['username']}/{$password}");

                        $sql  = "UPDATE user SET pw = :password WHERE login = :user";
                        $bind = array('password' => $password, 'user' => $this->user['username']);
                        $this->fossil_sql_execute($repo, $sql, $bind);
                    }
                }

                return true;
            }
        }




        return false;
    }

    public function remRepo($repo)
    {
        if (file_exists("{$this->absolute_path}{$repo}.fossil")) {
            $sql = "DELETE FROM repositories
                     WHERE user_id = :id
                       AND name    = :repo";

            $bind = array('id' => $this->user['id'], 'repo' => $repo);

            if (Nano_Db::execute($sql, $bind)) {
                unlink("{$this->absolute_path}{$repo}.fossil");

                return true;
            }
        }


        return false;
    }

    public function remAllRepos()



    {
        if (file_exists("{$this->absolute_path}")) {
            $sql = "DELETE FROM repositories
                     WHERE user_id = :id";

            $bind = array('id' => $this->user['id']);

            if (Nano_Db::execute($sql, $bind)) {
                foreach (glob("{$this->absolute_path}*.fossil") as $repo) {
                    unlink($repo);
                }



                unlink("{$this->absolute_path}repository");
                rmdir("{$this->absolute_path}");

                return true;
            }
        }

        return false;
    }
}




>
>
|
|
|
|
|
|

|
<

|
>

>
>
>
>
>
>
>
>
>
|
|
|
>
>
>
>
|
>
>
>
|
>

|

>
















>
|






>
|


















>
|
|
|
|
<









>
>
>
>
>
>
|
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

>


|
|
|

|
<
<
<
<
|
|
<
<
|
>
|
<
<
<
|
<
<
|
>
|
<
|
<
<
<
|
<
|
<
<
<
<
<
|
|
<
<
|
|
<
<

<
<
<
|
<
|
<
<
<
|
<
<
<
|
<
|
<
|

>
|






>
>
>
>
|
>
|
>
>
>
|
>
>
|
>
>
>
>
>
|
>
|
>
>
>
>
|
>
>
>
>
|
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

>
>
>
>
|
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
>
>

|
|

|
|
|

|

|
|
|
|
<
<
<

<
<
<
>

|
|
<
<
<

|
|
|
|
|
|
<
<
|
<
|
>
|
>
>

|
|

<
<
<
<
|
<
<
<
<
|
|
<
|
|

<
<
|
|
|
|

|
|
|
|

>
>
|
|

<
<
|
|
<
<
|
<
|
<
|
<
<
|
<
<
<
|
<
<
<
|
<
<
|
|

|
|
|

|

|
|
>
>
|
>

|
|
|
>

|


|
>
|







>
>
|
>
>
>
|
<
|
|
>
|
|

|
<
|
|
<
|
|

|

|
|
|

|
|

<
<
<
<
<
|
<













|
<














>
|
















|















|
>
|
>
>
>
|
|
|
|
|
|
|

|
|
|
|
|
|
|

|
|
<
|
<
<
<

|
<
|
|
|
<
|
|
|
>
>

>
|
|

<
<
<
|
|
|

|

|
<
<
|
|
|
>

|


|
>
>
>
|
<
|
|

|

|
<
|
|

>
>
|
|

|
<
<
<
<


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
378
379
380
381
382

383
384
385
386
387
388
389

390
391

392
393
394
395
396
397
398
399
400
401
402
403





404

405
406
407
408
409
410
411
412
413
414
415
416
417
418

419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490

491



492
493

494
495
496

497
498
499
500
501
502
503
504
505
506



507
508
509
510
511
512
513


514
515
516
517
518
519
520
521
522
523
524
525
526

527
528
529
530
531
532

533
534
535
536
537
538
539
540
541




542
543
<?php

class Nano_Fossil
{
    protected $absolute_path_root;
    protected $relative_path_root;
    protected $absolute_path;
    protected $relative_path;
    protected $user;
    protected $workdir;
    protected $use_suid;
    protected $fossil_binary;

    public function __construct($user) {

        $this->repo_dir = $_SERVER['DOCUMENT_ROOT'] . '/../repos';
        $this->absolute_path_root = $this->repo_dir . '/' . $user['username'] . '/';
        $this->absolute_path = $this->absolute_path_root . 'data/';
        $this->user = $user;

	$fossil_config_file = dirname(__FILE__) . '/../config/fossil.cnf';
	$fossil_config = array();
	if (file_exists($fossil_config_file)) {
	        $fossil_config = parse_ini_file($fossil_config_file);
	}
        if (isset($fossil_config['use_suid']) && $fossil_config['use_suid'] === '1') {
            $this->use_suid = true;
        } else {
            $this->use_suid = false;
        }

        /*
         * The "suid-fossil" wrapper will either call Fossil directly (if use_suid is false),
         * or invoke a wrapper before calling fossil within a chroot.
         */
        $this->fossil_binary = dirname(__FILE__) . "/../scripts/fossil-as-user/suid-fossil";

        if ($this->use_suid) {
            $this->relative_path_root = '/';
            $this->relative_path = '/data/';
            $this->root_fs = $this->absolute_path_root;
        } else {
            $this->relative_path_root = $this->absolute_path_root;
            $this->relative_path = $this->absolute_path;
            $this->root_fs = '';
        }

        if (!$this->use_suid) {
            $this->workdir = "/tmp/workdir-flint-" . bin2hex(openssl_random_pseudo_bytes(20));

            mkdir($this->workdir);
        }
    }

    public function __destruct() {
        if (!$this->use_suid) {
            system("rm -rf '{$this->workdir}'");
        }
    }

    private function fossil_sql_execute($repo, $sql, $bind = array()) {
        $repo_file = $this->repository_file($repo);
        Nano_Db::setDb("sqlite:{$repo_file}");
        $result = Nano_Db::execute($sql, $bind);
        Nano_Db::unsetDb();
        return($result);
    }

    private function fossil_sql_query($repo, $sql, $bind = array()) {
        $repo_file = $this->repository_file($repo);
        Nano_Db::setDb("sqlite:{$repo_file}");
        $result = Nano_Db::query($sql, $bind);
        Nano_Db::unsetDb();
        return($result);
    }

    private function getFossilCommand($timeout = 0, $cgi = false) {
        $fossil = $this->fossil_binary;

        if ($timeout) {
            $fossil = "timeout {$timeout} {$fossil}";
        }

        $username = escapeshellarg($this->user['username']);

        $cmd = "USER={$username} {$fossil}";

        if (!$this->use_suid) {
            $cmd = "HOME={$this->workdir} {$cmd}";
        }

        $userid = escapeshellarg($this->user['id']);
        $cmd = "FLINT_USERID={$userid} FLINT_USERNAME={$username} {$cmd}";


        if ($cgi) {
            $cmd = "GATEWAY_INTERFACE=1 {$cmd}";
        } else {
            $cmd = "unset GATEWAY_INTERFACE; {$cmd}";
        }

        return $cmd;
    }

    private function repository_file($repo, $absolute = true) {
        if ($absolute) {
            $retval = "{$this->absolute_path}{$repo}.fossil";
        } else {
            $retval = "{$this->relative_path}{$repo}.fossil";
        }

        return $retval;
    }

    private function fossil($repo, $argv, &$output = null, &$return = null, $timeout = 0, $cgi = false) {
        $command = $this->getFossilCommand($timeout, $cgi);

        foreach ($argv as $arg) {
            if ($arg === $this->repository_file($repo)) {
                $arg = $this->repository_file($repo, FALSE);
            }

            $command = $command . " " . escapeshellarg($arg);
        }
        $command = $command . " 2>&1";

        exec($command, $output, $return);
    }

    private function createRepositoryCGI() {
        if (!file_exists($this->absolute_path)) {
            mkdir($this->absolute_path_root);
            mkdir($this->absolute_path);

            $content = "#!{$this->fossil_binary}\ndirectory: ./data/\nnotfound: http://{$_SERVER['SERVER_NAME']}/notfound";
            file_put_contents("{$this->absolute_path_root}repository", $content);
            chmod("{$this->absolute_path_root}repository", 0555);
        }
    }





    private function randomPassword($length = 12) {


        $alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!";
        $alphabet_len = strlen($alphabet);
        $result = "";



        for ($idx = 0; $idx < $length; $idx++) {


            $char_idx = mt_rand(0, $alphabet_len - 1);
            $char = $alphabet[$char_idx];
            $result = $result . $char;

        }



        return $result;

    }






    private function createUser($repo) {


        $username = $this->user['username'];
        $repo_file = $this->repository_file($repo);






        $password = $this->randomPassword(64);





        $this->fossil($repo, array('user', 'new', $username, 'Flint User', $password, '-R', $repo_file), $output, $return);





        if ($return === 0) {

            $this->fossil($repo, array('user', 'capabilities', $username, 's', '-R', $repo_file), $output, $return);

            if ($return === 0) {
                return true;
            }
        }

        return false;
    }

    private function setPassword($repo, $password) {
        if (!$password) {
            $password = $this->randomPassword();
        }

        $this->fossil($repo, array('user', '-R', $this->repository_file($repo), 'password', $this->user['username'], $password), $output, $return);

        if ($return !== 0) {
            return false;
        }

        return $password;
    }

    private function setProjectCode($repo, $projectCode) {
        if ($projectCode) {
            $sql  = "UPDATE config SET value = :code WHERE name = 'project-code'";
            $bind = array('code' => $projectCode);
            $this->fossil_sql_execute($repo, $sql, $bind);
        }
    }

    private function repoConfig($repo) {
        $sql = "UPDATE config SET value = 1 WHERE name = 'localauth'";
        $this->fossil_sql_execute($repo, $sql);
    }

    private function postCreateRepo($repo, $password, $projectCode = null) {
        $this->setProjectCode($repo, $projectCode);
        $this->repoConfig($repo);
        $return = $this->setPassword($repo, $password);

        return $return;
    }

    public function newRepo($repo, $password = null, $private = 0, $projectCode = null, $sha3 = false, &$errorMessage = null) {
        $this->createRepositoryCGI();

        $create_command = array('new');

        if ($sha3 !== true) {
            $create_command[] = '--sha1';
        }

        $repo_file = $this->repository_file($repo);
        if (file_exists($repo_file)) {
            $errorMessage = 'File already exists';

            return false;
        }

        array_push($create_command, '-A', $this->user['username'], $repo_file);
        $this->fossil($repo, $create_command, $output, $return);

        if ($return !== 0) {
            if (file_exists($repo_file)) {
                unlink($repo_file);
            }

            $errorMessage = 'fossil new failed: '. join("\n", $output);

            return false;
        }

        /* Install default configuration */
        /** XXX:TODO: This won't work within the chroot **/
        $this->fossil($repo, array('configuration', 'import', '-R', $repo_file, $_SERVER['DOCUMENT_ROOT'] . "/../config/fossil-default.cnf"));

        $sql = "INSERT INTO repositories
                       (user_id, name, private, cloned, auto_update)
                VALUES (:id, :name, :private, 0, 0)";

        $bind = array('id' => $this->user['id'], 'name' => $repo, 'private' => $private);

        if (!Nano_Db::execute($sql, $bind)) {
            if (file_exists($repo_file)) {
                unlink($repo_file);
            }

            $errorMessage = 'Internal Error: DB Insert failed';

            return false;
        }

        $return = $this->postCreateRepo($repo, $password, $projectCode);

        return $return;
    }

    public function cloneRepo($repo, $password = null, $url, $private = 0, $update = 0, &$errorMessage = null) {
        $this->createRepositoryCGI();

        $repo_file = $this->repository_file($repo);
        if (file_exists($repo_file)) {
            $errorMessage = 'File already exists';

            return false;
        }

        $this->fossil($repo, array('clone', '-A', $this->user['username'], $url, $repo_file), $output, $return, 3600);

        if ($return !== 0) {
            if (file_exists($repo_file)) {
                unlink($repo_file);
            }

            $errorMessage = 'Clone failed: ' . join("\n", $output);

            return false;
        }

        $sql = "INSERT INTO repositories
                       (user_id, name, private, cloned, auto_update)
                VALUES (:id, :name, :private, 1, :auto)";

        $bind = array('id' => $this->user['id'], 'name' => $repo, 'private' => $private, 'auto' => $update);

        if (!Nano_Db::execute($sql, $bind)) {
            if (file_exists($repo_file)) {
                unlink($repo_file);
            }







            $errorMessage = 'Internal Error: DB Insert failed';

            return false;
        }




        $return = $this->postCreateRepo($repo, $password);

        return $return;
    }

    public function uploadRepo($repo, $password, $private = 0, array $file, &$errorMessage = null) {


        $this->createRepositoryCGI();


        $repo_file = $this->repository_file($repo);

        if (file_exists($repo_file)) {
            $errorMessage = 'File already exists';

            return false;
        }





        if (!@move_uploaded_file($file['tmp_name'], $repo_file)) {




            $errorMessage = 'Internal Error: Upload failed';


            return false;
        }



        $this->fossil($repo, array('config', '-R', $repo_file, 'export', 'project', '/tmp/config'), $output, $return);
        if (file_exists($this->root_fs . '/tmp/config')) {
            unlink($this->root_fs . '/tmp/config');
        }

        if ($return !== 0) {
            if (file_exists($repo_file)) {
                unlink($repo_file);
            }

            $errorMessage = 'Invalid repository';

            return false;
        }



        $return = $this->createUser($repo);
        if ($return === false) {


            if (file_exists($repo_file)) {

                unlink($repo_file);

            }






            $errorMessage = 'Failed to create new user';






            return false;
        }

        $sql = "INSERT INTO repositories
                       (user_id, name, private, cloned, auto_update)
                VALUES (:id, :name, :private, 0, 0)";

        $bind = array('id' => $this->user['id'], 'name' => $repo, 'private' => $private);

        if (!Nano_Db::execute($sql, $bind)) {
            if (file_exists($repo_file)) {
                unlink($repo_file);
            }

            $errorMessage = 'Internal Error: DB Insert failed';

            return false;
        }

        $return = $this->postCreateRepo($repo, $password);

        return $return;
    }

    public function pullRepo($repo, $url = '', &$outputstr = null) {
        $repo_file = $this->repository_file($repo);

        if ($url != '') {
            if (file_exists($url) || preg_match('/:\/\//', $url) == 0) {
                $outputstr = "Invalid URL";
                return false;
            }
        }

        if (!file_exists($repo_file)) {
            $outputstr = "Invalid repository";

            return false;
        }

        # Ensure that no non-default SSH command can be used for a pull

        $this->fossil($repo, array('unset', 'ssh-command', '-R', $repo_file), $output, $return);
        if ($return !== 0) {
            $outputstr = "Failed to unset ssh-command: " . join("\n", $output);
            return false;
        }

        if ($url == '') {

            $this->fossil($repo, array('pull', '-R', $repo_file), $output, $return, 3600);
        } else {

            $this->fossil($repo, array('pull', '-R', $repo_file, $url), $output, $return, 3600);
        }

        $outputstr = join("\n", $output);

        if ($return !== 0) {
            return false;
        }

        return true;
    }






    public function getRepos() {

        $sql = "SELECT *
                  FROM repositories
                 WHERE user_id = :id";

        $bind = array('id' => $this->user['id']);

        if ($result = Nano_Db::query($sql, $bind)) {
            return $result;
        }

        return false;
    }

    public function getRepoById($id) {

        $sql = "SELECT *
                  FROM repositories
                 WHERE user_id = :user
                   AND id = :id";

        $bind = array('user' => $this->user['id'], 'id' => $id);

        if ($result = Nano_Db::query($sql, $bind)) {
            $return = array_pop($result);
        } else {
            return false;
        }

        $repo = $return['name'];
        $repo_file = $this->repository_file($repo);
        $return['repo-file'] = $repo_file;

        if ($return['cloned']) {
            $sql  = "SELECT value FROM config WHERE name = 'last-sync-url'";

            if ($result = $this->fossil_sql_query($repo, $sql)) {
                $url                 = array_pop($result);
                $return['clone-url'] = $url['value'];
            }
        }

        $sql  = "SELECT value FROM config WHERE name = 'last-sync-pw'";

        if ($result = $this->fossil_sql_query($repo, $sql)) {
            $password           = array_pop($result);
            $return['clone-pw'] = $password['value'];

            $this->fossil($repo, array('test-obscure', $return['clone-pw']), $output, $returnCode);

            if ($returnCode === 0) {
                if (preg_match('/^UNOBSCURE: (.*) -> (.*)$/', $output[1], $matches)) {
                    $return['clone-pw'] = $matches[2];
                }
            }
        }

        if (isset($return)) {
            return $return;
        }

        return false;
    }

    public function updateRepo($repo, $private, $update, $cloned, $password = null) {
        $repo_file = $this->repository_file($repo);

        if (!file_exists($repo_file)) {
            return false;
        }

        $sql = "UPDATE repositories
                   SET private     = :private,
                       auto_update = :auto,
                       cloned      = :cloned
                 WHERE user_id = :id
                   AND name    = :repo";

        $bind = array(
            'private' => $private,
            'auto'    => $update,
            'cloned'  => $cloned,
            'id'      => $this->user['id'],
            'repo'    => $repo,
        );

        if (!Nano_Db::execute($sql, $bind)) {
            return false;

        }




        if ($password) {

            $this->setPassword($repo, $password);
        }


        return true;
    }

    public function remRepo($repo) {
        $repo_file = $this->repository_file($repo);

        if (!file_exists($repo_file)) {
            return false;
        }




        $sql = "DELETE FROM repositories
                 WHERE user_id = :id
                   AND name    = :repo";

        $bind = array('id' => $this->user['id'], 'repo' => $repo);

        if (!Nano_Db::execute($sql, $bind)) {


            return false;
        }

        unlink($repo_file);

        return true;
    }

    public function remAllRepos() {
        if (!file_exists("{$this->absolute_path}")) {
            return false;
        }


        $sql = "DELETE FROM repositories
                 WHERE user_id = :id";

        $bind = array('id' => $this->user['id']);

        if (!Nano_Db::execute($sql, $bind)) {

            return false;
        }

        system('rm -f ' . escapeshellarg($this->absolute_path) . '/*.fossil');

        unlink("{$this->absolute_path_root}repository");
        rmdir("{$this->absolute_path_root}");

        return true;




    }
}
Changes to public/secure/index.php.
31
32
33
34
35
36
37
38
39
40
41
42

if (isset($_SESSION['pull'])) {
    $view->pull = $_SESSION['pull'];
    unset($_SESSION['pull']);
}

if (isset($_SESSION['update'])) {
    $view->pull = $_SESSION['update'];
    unset($_SESSION['update']);
}

$view->dispatch();







|




31
32
33
34
35
36
37
38
39
40
41
42

if (isset($_SESSION['pull'])) {
    $view->pull = $_SESSION['pull'];
    unset($_SESSION['pull']);
}

if (isset($_SESSION['update'])) {
    $view->pull = array('name' => $_SESSION['update'], 'success' => true);
    unset($_SESSION['update']);
}

$view->dispatch();
Changes to public/secure/presentation/index.tpl.
1
2
3
4
5

6




7
8
9
10
11
12
13
<? if (isset($this->new)): ?>
<p class="success">Your account was created and you are now logged in.</p>
<? endif ?>

<? if (isset($this->pull)): ?>

<p class="success"><?= $this->pull ?> was successfully updated.</p>




<? endif ?>

<h3>Public Repositories</h3>
<? if (count($this->public)): ?>
    <ul>
    <? foreach ($this->public as $repo): ?>
        <li><a href="/user/<?= $this->user['username'] ?>/repository/<?= $repo['name'] ?>"><?= $repo['name'] ?></a> - <? if ($repo['cloned']): ?><a href="/secure/repository/pull/id/<?= $repo['id'] ?>">Pull</a> | <? endif ?><a href="/secure/repository/edit/id/<?= $repo['id'] ?>">Edit</a> | <a href="/secure/repository/remove/id/<?= $repo['id'] ?>" class="remove">Remove</a> | <a href="/secure/repository/help_clone/id/<?= $repo['id'] ?>">How to Clone</a></li>





>
|
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<? if (isset($this->new)): ?>
<p class="success">Your account was created and you are now logged in.</p>
<? endif ?>

<? if (isset($this->pull)): ?>
<? if ($this->pull['success']) { ?>
<p class="success"><?= $this->pull['name'] ?> was successfully updated.</p>
<? } else { ?>
<p class="failure"><?= $this->pull['name'] ?> failed to be updated.</p>
Output: <pre><?= $this->pull['output'] ?></pre>
<? } ?>
<? endif ?>

<h3>Public Repositories</h3>
<? if (count($this->public)): ?>
    <ul>
    <? foreach ($this->public as $repo): ?>
        <li><a href="/user/<?= $this->user['username'] ?>/repository/<?= $repo['name'] ?>"><?= $repo['name'] ?></a> - <? if ($repo['cloned']): ?><a href="/secure/repository/pull/id/<?= $repo['id'] ?>">Pull</a> | <? endif ?><a href="/secure/repository/edit/id/<?= $repo['id'] ?>">Edit</a> | <a href="/secure/repository/remove/id/<?= $repo['id'] ?>" class="remove">Remove</a> | <a href="/secure/repository/help_clone/id/<?= $repo['id'] ?>">How to Clone</a></li>
Changes to public/secure/repository/create.php.
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

    if ($validation->validate($_POST, $rules)) {
        $user   = Nano_Session::user();
        $fossil = new Nano_Fossil($user);

        if (isset($_POST['repository-password']) && !empty($_POST['repository-password'])) {
            $password = $_POST['repository-password'];
        }
        else {
            $password = null;
        }

        $private = isset($_POST['private']) ? '1' : '0';

        if (isset($_POST['project-code']) && !empty($_POST['project-code'])) {
            $projectCode = $_POST['project-code'];
        } else {
            $projectCode = null;
        }

        $sha3 = isset($_POST['sha3']);

        if ($result = $fossil->newRepo($_POST['repository-name'], $password, $private, $projectCode, $sha3)) {
            $view->user     = $user;
            $view->name     = $_POST['repository-name'];
            $view->private  = $private;
            $view->password = $result;
            $view->success  = true;
        }
        else {
            $view->error = true;

        }
    }
    else {
        Nano_Variable::set('validationErrors', $validation->errors());
    }
}

if (isset($_GET['type']) && $_GET['type'] == 'clone' && $_POST) {
    $validation = new Nano_Validation();








<
|













|





<
|

>

<
|







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

    if ($validation->validate($_POST, $rules)) {
        $user   = Nano_Session::user();
        $fossil = new Nano_Fossil($user);

        if (isset($_POST['repository-password']) && !empty($_POST['repository-password'])) {
            $password = $_POST['repository-password'];

        } else {
            $password = null;
        }

        $private = isset($_POST['private']) ? '1' : '0';

        if (isset($_POST['project-code']) && !empty($_POST['project-code'])) {
            $projectCode = $_POST['project-code'];
        } else {
            $projectCode = null;
        }

        $sha3 = isset($_POST['sha3']);

        if ($result = $fossil->newRepo($_POST['repository-name'], $password, $private, $projectCode, $sha3, $errorMessage)) {
            $view->user     = $user;
            $view->name     = $_POST['repository-name'];
            $view->private  = $private;
            $view->password = $result;
            $view->success  = true;

        } else {
            $view->error = true;
            $view->errorMessage = $errorMessage;
        }

    } else {
        Nano_Variable::set('validationErrors', $validation->errors());
    }
}

if (isset($_GET['type']) && $_GET['type'] == 'clone' && $_POST) {
    $validation = new Nano_Validation();

65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

82
83
84
85
86
87
88
            $password = null;
        }

        $private = isset($_POST['private']) ? '1' : '0';
        $update  = isset($_POST['auto-update']) ? '1' : '0';

        if ($result = $fossil->cloneRepo($_POST['repository-name'], $password, $_POST['clone-url'],
                                         $private, $update)) {
            $view->user     = $user;
            $view->name     = $_POST['repository-name'];
            $view->private  = $private;
            $view->update   = $update;
            $view->password = $result;
            $view->success  = true;
        }
        else {
            $view->error = true;

        }
    }
    else {
        Nano_Variable::set('validationErrors', $validation->errors());
    }
}








|









>







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
            $password = null;
        }

        $private = isset($_POST['private']) ? '1' : '0';
        $update  = isset($_POST['auto-update']) ? '1' : '0';

        if ($result = $fossil->cloneRepo($_POST['repository-name'], $password, $_POST['clone-url'],
                                         $private, $update, $errorMessage)) {
            $view->user     = $user;
            $view->name     = $_POST['repository-name'];
            $view->private  = $private;
            $view->update   = $update;
            $view->password = $result;
            $view->success  = true;
        }
        else {
            $view->error = true;
            $view->errorMessage = $errorMessage;
        }
    }
    else {
        Nano_Variable::set('validationErrors', $validation->errors());
    }
}

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
        }
        else {
            $user   = Nano_Session::user();
            $fossil = new Nano_Fossil($user);

            $private = isset($_POST['private']) ? '1' : '0';

            if ($fossil->uploadRepo($_POST['repository-name'], $_POST['repository-password'], $private, $_FILES['upload'])) {
                $view->user     = $user;
                $view->name     = $_POST['repository-name'];
                $view->private  = $private;
                $view->password = 'sha1';
                $view->success  = true;
            }
            else {
                $view->error = true;

            }
        }
    }
    else {
        Nano_Variable::set('validationErrors', $validation->errors());
    }
}

$view->dispatch();







|








>









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
        }
        else {
            $user   = Nano_Session::user();
            $fossil = new Nano_Fossil($user);

            $private = isset($_POST['private']) ? '1' : '0';

            if ($fossil->uploadRepo($_POST['repository-name'], $_POST['repository-password'], $private, $_FILES['upload'], $errorMessage)) {
                $view->user     = $user;
                $view->name     = $_POST['repository-name'];
                $view->private  = $private;
                $view->password = 'sha1';
                $view->success  = true;
            }
            else {
                $view->error = true;
                $view->errorMessage = $errorMessage;
            }
        }
    }
    else {
        Nano_Variable::set('validationErrors', $validation->errors());
    }
}

$view->dispatch();
Changes to public/secure/repository/presentation/create.tpl.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<? if (isset($this->success)): ?>
<p>Your new repository, <a href="/user/<?= $this->user['username'] ?>/repository/<?= $this->name ?>"><?= $this->name ?></a>, was successfully created<? if ($this->update): ?> and set to pull in changes on a periodic basis<? endif ?>!</p>
<p>Remember since fossil is an all in one solution you are required to setup repository specific permissions.<? if ($this->private): ?> While your repository won't show up on your public user page you still need to go in and lock it down yourself.<? endif ?> The default user for your new repository is the same as your flint username, however the password is <? if ($this->password == 'sha1'): ?>user set<? else: ?><?= $this->password ?><? endif ?>, we recommend you log in and change this to something else.</p>
<ul>
    <li>Username: <?= $this->user['username'] ?></li>
    <li>Password: <? if ($this->password == 'sha1'): ?>User set<? else: ?><?= $this->password ?><? endif ?></li>
    <li>URL: http(s)://<?= $_SERVER['SERVER_NAME'] ?>/user/<?= $this->user['username'] ?>/repository/<?= $this->name ?></li>
</ul>
<p><a href="/secure/">Return to dashboard</a></p>
<? else: ?>
<? if ((isset($_GET['type']) && $_GET['type'] == 'new') || !isset($_GET['type'])): ?>
<p>Please fill out the form below to create a new repository. If a password is not set a random one will be created for you.</p>

<? if (isset($this->error)): ?>
<p class="error">Something failed during the creation process please try again.</p>
<? endif ?>

<? if (isset($this->max)): ?>
<p class="error">You are currently limited to 10 repositories at a time.</p>
<? endif ?>

<form action="/secure/repository/create/type/new/" method="post">


|


|








|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<? if (isset($this->success)): ?>
<p>Your new repository, <a href="/user/<?= $this->user['username'] ?>/repository/<?= $this->name ?>"><?= $this->name ?></a>, was successfully created<? if ($this->update): ?> and set to pull in changes on a periodic basis<? endif ?>!</p>
<p>Remember since fossil is an all in one solution you are required to setup repository specific permissions.<? if ($this->private): ?> While your repository won't show up on your public user page you still need to go in and lock it down yourself.<? endif ?> The default user for your new repository is the same as your flint username, however the password is <? if ($this->password == '@@user_set@@'): ?>user set<? else: ?><?= $this->password ?><? endif ?>, we recommend you log in and change this to something else.</p>
<ul>
    <li>Username: <?= $this->user['username'] ?></li>
    <li>Password: <? if ($this->password == '@@user_set@@'): ?>User set<? else: ?><?= $this->password ?><? endif ?></li>
    <li>URL: http(s)://<?= $_SERVER['SERVER_NAME'] ?>/user/<?= $this->user['username'] ?>/repository/<?= $this->name ?></li>
</ul>
<p><a href="/secure/">Return to dashboard</a></p>
<? else: ?>
<? if ((isset($_GET['type']) && $_GET['type'] == 'new') || !isset($_GET['type'])): ?>
<p>Please fill out the form below to create a new repository. If a password is not set a random one will be created for you.</p>

<? if (isset($this->error)): ?>
<p class="error">Something failed during the creation process please try again.  Error: <pre><?= $this->errorMessage ?></pre></p>
<? endif ?>

<? if (isset($this->max)): ?>
<p class="error">You are currently limited to 10 repositories at a time.</p>
<? endif ?>

<form action="/secure/repository/create/type/new/" method="post">
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    </p>
</form>
<? endif ?>
<? if (isset($_GET['type']) && $_GET['type'] == 'clone'): ?>
<p>Please fill out the form below to clone an existing repository. If a password is not set a random one will be created for you.</p>

<? if (isset($this->error)): ?>
<p class="error">Something failed during the creation process please try again.</p>
<? endif ?>

<? if (isset($this->max)): ?>
<p class="error">You are currently limited to 10 repositories at a time.</p>
<? endif ?>

<form action="/secure/repository/create/type/clone/" method="post">







|







35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    </p>
</form>
<? endif ?>
<? if (isset($_GET['type']) && $_GET['type'] == 'clone'): ?>
<p>Please fill out the form below to clone an existing repository. If a password is not set a random one will be created for you.</p>

<? if (isset($this->error)): ?>
<p class="error">Something failed during the creation process please try again:  Error: <pre><?= $this->errorMessage ?></pre></p>
<? endif ?>

<? if (isset($this->max)): ?>
<p class="error">You are currently limited to 10 repositories at a time.</p>
<? endif ?>

<form action="/secure/repository/create/type/clone/" method="post">
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
    </p>
</form>
<? endif ?>
<? if (isset($_GET['type']) && $_GET['type'] == 'upload'): ?>
<p>Please fill out the form below to upload an existing repository. A new super-user will be created that matches your flint username and the password you provide below. Limit 8M in size. If your repository is larger than this, create a new empty project and push to it instead.</p>

<? if (isset($this->error)): ?>
<p class="error">Something failed during the creation process please try again.</p>
<? endif ?>

<? if (isset($this->max)): ?>
<p class="error">You are currently limited to 10 repositories at a time.</p>
<? endif ?>

<form action="/secure/repository/create/type/upload/" method="post" enctype="multipart/form-data">







|







64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
    </p>
</form>
<? endif ?>
<? if (isset($_GET['type']) && $_GET['type'] == 'upload'): ?>
<p>Please fill out the form below to upload an existing repository. A new super-user will be created that matches your flint username and the password you provide below. Limit 8M in size. If your repository is larger than this, create a new empty project and push to it instead.</p>

<? if (isset($this->error)): ?>
<p class="error">Something failed during the creation process please try again.  Error: <pre><?= $this->errorMessage ?></pre></p>
<? endif ?>

<? if (isset($this->max)): ?>
<p class="error">You are currently limited to 10 repositories at a time.</p>
<? endif ?>

<form action="/secure/repository/create/type/upload/" method="post" enctype="multipart/form-data">
Changes to public/secure/repository/pull.php.
1
2
3
4
5
6
7
8




9
10
11
12
<?php

$user   = Nano_Session::user();
$fossil = new Nano_Fossil($user);

if ($repo = $fossil->getRepoById($_GET['id'])) {
    $fossil->pullRepo($repo['name']);
    $_SESSION['pull'] = $repo['name'];




}

header('Location: /secure/');
die();






|
|
>
>
>
>




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

$user   = Nano_Session::user();
$fossil = new Nano_Fossil($user);

if ($repo = $fossil->getRepoById($_GET['id'])) {
    $success = $fossil->pullRepo($repo['name'], '', $output);
    $_SESSION['pull'] = array(
        'name' => $repo['name'],
        'success' => $success,
        'output' => $output
    );
}

header('Location: /secure/');
die();
Changes to scripts/fossil-as-user/suid-fossil.
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
#! /usr/bin/env php
<?php

include(dirname(__FILE__) . '/../include.php');

function get_user_id_from_username($username) {
	$sql = "SELECT * FROM users WHERE username = :username";
	$bind = array(
		'username' => $username
	);

	if ($result = Nano_Db::query($sql, $bind)) {
		foreach ($result as $user) {
			return($user['id']);
		}
	}

	return(null);
}





















function get_user_id($path) {
	/*
	 * Pick out the username from something that has been
	 * explicitly set
	 */
	$userid = getenv('FLINT_USERID');
	if (isset($userid) && $userid) {
		return($userid);
	}

	$username = getenv('FLINT_USERNAME');
	if (isset($username) && $username) {
		$userid = get_user_id_from_username($username);
		if (isset($userid)) {
			return($userid);
		}
	}

	/*
	 * Pick out the username from the path we are running
	 */


	/* XXX:TODO */






	return(null);
}



























































































































































































































$userid = get_user_id('');


if (!isset($userid)) {


	exit(1);
}
echo "{$x}\n";
?>
|



















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|




















>
>
|
>
>
>
|
>
>



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|
>
>
|
>
>
|
|
<

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
#! /usr/bin/php
<?php

include(dirname(__FILE__) . '/../include.php');

function get_user_id_from_username($username) {
	$sql = "SELECT * FROM users WHERE username = :username";
	$bind = array(
		'username' => $username
	);

	if ($result = Nano_Db::query($sql, $bind)) {
		foreach ($result as $user) {
			return($user['id']);
		}
	}

	return(null);
}

function get_user_name($userid) {
	$username = getenv('FLINT_USERNAME');
	if (isset($username) && $username) {
		return $username;
	}

	$sql = "SELECT * FROM users WHERE id = :id";
	$bind = array(
		'id' => $userid
	);

	if ($result = Nano_Db::query($sql, $bind)) {
		foreach ($result as $user) {
			return($user['username']);
		}
	}

	return(null);
}

function get_user_id($argv) {
	/*
	 * Pick out the username from something that has been
	 * explicitly set
	 */
	$userid = getenv('FLINT_USERID');
	if (isset($userid) && $userid) {
		return($userid);
	}

	$username = getenv('FLINT_USERNAME');
	if (isset($username) && $username) {
		$userid = get_user_id_from_username($username);
		if (isset($userid)) {
			return($userid);
		}
	}

	/*
	 * Pick out the username from the path we are running
	 */
	if ($argv[1] === 'repository') {
		$username = end(preg_split("/\//", getcwd()));

		$userid = get_user_id_from_username($username);
		if (isset($userid)) {
			return($userid);
		}
	}

	return(null);
}

function mkdir_p($dir) {
	if (file_exists($dir)) {
		return;
	}

	mkdir($dir, 0755, TRUE);

	return;
}

function unlink_f($file) {
	if (!file_exists($file)) {
		return;
	}

	unlink($file);
}

function log_message($message) {
error_log($message);
	$fd = fopen("/var/tmp/flint-suid-fossil.log", "a+");
	fwrite($fd, $message . "\n");
	fclose($fd);
}

function exec_log($command) {
	log_message("Running: {$command}");
	return(exec($command));
}

$fossil_config = array();
$fossil_config_file = dirname(__FILE__) . '/../../config/fossil.cnf';
if (file_exists($fossil_config_file)) {
	$fossil_config = parse_ini_file($fossil_config_file);
}

if (isset($fossil_config['use_suid']) && $fossil_config['use_suid'] === '1') {
	$use_suid = true;
} else {
	$use_suid = false;
}

if (isset($fossil_config['binary'])) {
	$fossil_binary_real = $fossil_config['binary'];
} else {
	$fossil_binary_real = '/usr/local/bin/fossil';
}

if (isset($fossil_config['root_link_prefix'])) {
	$root_link_prefix = $fossil_config['root_link_prefix'];
} else {
	$root_link_prefix = '';
}

$userid = get_user_id($argv);

if (isset($userid)) {
	$username = get_user_name($userid);

	$user_directory = dirname(__FILE__) . "/../../repos/{$username}";
	$repo_directory = $user_directory . "/data";

	$work_dir = '/root';
	$home_dir = $work_dir . '/home';

	$work_dir_outside = $user_directory . $work_dir;
	$home_dir_outside = $user_directory . $home_dir;

	$fossil_binary = $work_dir . '/bin/' . basename($fossil_binary_real);
	$fossil_binary_outside = $user_directory . $fossil_binary;
	$fossil_binary_symlink = dirname($fossil_binary_outside) . "/fossil";

	$real_user_id = (1024 * 1024) + $userid;
	$current_user_id = posix_getuid();
}

if ($use_suid) {
	if (!isset($userid)) {
		exit(1);
	}

	/*
	 * If the binary has not been setup, take this as a cue that the project
	 * directory needs to be setup.
	 */
	$upgrade_required = false;
	if (!file_exists($fossil_binary_outside)) {
		$upgrade_required = true;
	}

	if (!file_exists($fossil_binary_symlink)) {
		$upgrade_required = true;
	}
		
	if ($upgrade_required) {
		log_message("Upgrading {$user_directory} to {$fossil_binary_real}");

		mkdir_p(dirname($fossil_binary_outside));
		mkdir_p($home_dir_outside);

		$fossil_binary_resolved = realpath($fossil_binary_real);
		if (!$fossil_binary_resolved) {
			exit(1);
		}

		/*
		 * Create the Fossil named either after the tail of its
		 * external name or the version number if the name
		 * is just "fossil" (since we will symlink it into place)
		 */
		if (basename($fossil_binary_outside) === 'fossil') {
			$fossil_version = `$fossil_binary_resolved version | sed 's@^.* version @@;s@ .*$@@'`;
			$fossil_binary_outside = $fossil_binary_outside . '-' . $fossil_version;
		}

		unlink_f($fossil_binary_outside);
		link($fossil_binary_resolved, $fossil_binary_outside);

		unlink_f($fossil_binary_symlink);
		symlink(basename($fossil_binary_outside), $fossil_binary_symlink);

		/*
		 * Setup a usable environment
		 */
		mkdir_p($user_directory);
		mkdir_p($user_directory . '/etc');

		/**
		 ** User/Group Lookup
		 **/
		$fd = fopen($user_directory . '/etc/passwd', 'w');
		fwrite($fd, "{$username}:x:{$real_user_id}:{$real_user_id}::{$home_dir}:{$work_dir}/bin/sh\n");
		fclose($fd);

		$fd = fopen($user_directory . '/etc/group', 'w');
		fwrite($fd, "{$username}:x:{$real_user_id}:\n");
		fclose($fd);

		/**
		 ** DNS
		 **/
		unlink_f($user_directory . '/etc/resolv.conf');
		link('/etc/resolv.conf', $user_directory . '/etc/resolv.conf');

		/**
		 ** SSL
		 **/
		mkdir_p($user_directory . '/etc/ssl/certs');
		system('rm -f ' . escapeshellarg($user_directory . '/etc/ssl/certs/') . '*');
		system('cp -L /etc/ssl/certs/????????.? ' . escapeshellarg($user_directory . '/etc/ssl/certs/'));

		/**
		 ** Temporary storage
		 **/
		mkdir_p($user_directory . '/tmp');
		chmod($user_directory . '/tmp', 01777);

		/**
		 ** Device nodes
		 **/
		mkdir_p($user_directory . '/dev');
		system("cp -l {$root_link_prefix}/dev/null {$root_link_prefix}/dev/urandom {$root_link_prefix}/dev/random {$root_link_prefix}/dev/zero " . escapeshellarg($root_link_prefix . '/' . $user_directory . '/dev'));

		/*
		 * Ensure that the directory has the appropriate permissions
		 */
		mkdir_p($repo_directory);

		putenv("PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin");

		exec_log('setfacl -RP -m u:' . $real_user_id . ':rwx ' . escapeshellarg($repo_directory));
		exec_log('setfacl   -m d:u:' . $real_user_id . ':rwx ' . escapeshellarg($repo_directory));
		exec_log('setfacl -RP -m m::rwx  -m u:' . $current_user_id . ':rwx ' . escapeshellarg($repo_directory));
		exec_log('setfacl   -m d:m::rwx -m d:u:' . $current_user_id . ':rwx ' . escapeshellarg($repo_directory));
		exec_log('setfacl -m   u:' . $real_user_id . ':rwx ' . escapeshellarg($home_dir_outside));
		exec_log('setfacl -m d:u:' . $real_user_id . ':rwx ' . escapeshellarg($home_dir_outside));
		exec_log('setfacl -m   u:' . $current_user_id . ':rwx ' . escapeshellarg($home_dir_outside));
		exec_log('setfacl -m d:u:' . $current_user_id . ':rwx ' . escapeshellarg($home_dir_outside));
	}

	$command = escapeshellarg(dirname(__FILE__) . "/secure-wrap") . " " . escapeshellarg($userid) . " " . escapeshellarg($user_directory) . " " . escapeshellarg($fossil_binary);

	putenv("USER={$username}");
	putenv("HOME={$home_dir}");
} else {
	$downgrade_required = false;
	if (isset($fossil_binary_outside) && file_exists($fossil_binary_outside)) {
		$downgrade_required = true;
	}
	if (isset($fossil_binary_symlink) && file_exists($fossil_binary_symlink)) {
		$downgrade_required = true;
	}
	if ($downgrade_required) {
		log_message("Downgrading {$user_directory} from {$fossil_binary_real}");

		unlink_f($fossil_binary_outside);
		unlink_f($fossil_binary_symlink);

		putenv("PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin");

		exec_log('setfacl -RP -x   u:' . $real_user_id . ' ' . escapeshellarg($repo_directory));
		exec_log('setfacl -RP -x d:u:' . $real_user_id . ' ' . escapeshellarg($repo_directory));
		exec_log('setfacl -RP -m   m::rwx   -m u:' . $current_user_id . ':rwx ' . escapeshellarg($repo_directory));
		exec_log('setfacl -RP -m d:m::rwx -m d:u:' . $current_user_id . ':rwx ' . escapeshellarg($repo_directory));
		exec_log('setfacl -RP -x   u:' . $real_user_id . ' ' . escapeshellarg($home_dir_outside));
		exec_log('setfacl -RP -x d:u:' . $real_user_id . ' ' . escapeshellarg($home_dir_outside));
		exec_log('setfacl -RP -m   u:' . $current_user_id . ':rwx ' . escapeshellarg($home_dir_outside));
		exec_log('setfacl -RP -m d:u:' . $current_user_id . ':rwx ' . escapeshellarg($home_dir_outside));
	}

	$command = escapeshellarg($fossil_binary_real);
}

foreach (array_slice($argv, 1) as $arg) {
	$command = $command . " " . escapeshellarg($arg);
}
$command = trim($command);

if (getenv("SERVER_PORT") === '443') {
	putenv("HTTPS=on");
}

log_message("Running: >> {$command}");
passthru($command, $rc);
exit($rc);


?>