|
1
|
/* |
|
2
|
** Copyright (c) 2007 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
** |
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This file contains code to implement the file transfer protocol. |
|
19
|
*/ |
|
20
|
#include "config.h" |
|
21
|
#include "xfer.h" |
|
22
|
|
|
23
|
#include <time.h> |
|
24
|
|
|
25
|
/* |
|
26
|
** Maximum number of HTTP redirects that any http_exchange() call will |
|
27
|
** follow before throwing a fatal error. Most browsers use a limit of 20. |
|
28
|
*/ |
|
29
|
#define MAX_REDIRECTS 20 |
|
30
|
|
|
31
|
/* |
|
32
|
** This structure holds information about the current state of either |
|
33
|
** a client or a server that is participating in xfer. |
|
34
|
*/ |
|
35
|
typedef struct Xfer Xfer; |
|
36
|
struct Xfer { |
|
37
|
Blob *pIn; /* Input text from the other side */ |
|
38
|
Blob *pOut; /* Compose our reply here */ |
|
39
|
Blob line; /* The current line of input */ |
|
40
|
Blob aToken[6]; /* Tokenized version of line */ |
|
41
|
Blob err; /* Error message text */ |
|
42
|
int nToken; /* Number of tokens in line */ |
|
43
|
int nIGotSent; /* Number of "igot" cards sent */ |
|
44
|
int nPrivIGot; /* Number of private "igot" cards */ |
|
45
|
int nGimmeSent; /* Number of gimme cards sent */ |
|
46
|
int nFileSent; /* Number of files sent */ |
|
47
|
int nDeltaSent; /* Number of deltas sent */ |
|
48
|
int nFileRcvd; /* Number of files received */ |
|
49
|
int nDeltaRcvd; /* Number of deltas received */ |
|
50
|
int nDanglingFile; /* Number of dangling deltas received */ |
|
51
|
int mxSend; /* Stop sending "file" when pOut reaches this size */ |
|
52
|
int resync; /* Send igot cards for all holdings */ |
|
53
|
u8 syncPrivate; /* True to enable syncing private content */ |
|
54
|
u8 nextIsPrivate; /* If true, next "file" received is a private */ |
|
55
|
u32 remoteVersion; /* Version of fossil running on the other side */ |
|
56
|
u32 remoteDate; /* Date for specific client software edition */ |
|
57
|
u32 remoteTime; /* Time of date corresponding on remoteDate */ |
|
58
|
time_t maxTime; /* Time when this transfer should be finished */ |
|
59
|
}; |
|
60
|
|
|
61
|
|
|
62
|
/* |
|
63
|
** The input blob contains an artifact. Convert it into a record ID. |
|
64
|
** Create a phantom record if no prior record exists and |
|
65
|
** phantomize is true. |
|
66
|
** |
|
67
|
** Compare to uuid_to_rid(). This routine takes a blob argument |
|
68
|
** and does less error checking. |
|
69
|
*/ |
|
70
|
static int rid_from_uuid(Blob *pUuid, int phantomize, int isPrivate){ |
|
71
|
static Stmt q; |
|
72
|
int rid; |
|
73
|
|
|
74
|
db_static_prepare(&q, "SELECT rid FROM blob WHERE uuid=:uuid"); |
|
75
|
db_bind_str(&q, ":uuid", pUuid); |
|
76
|
if( db_step(&q)==SQLITE_ROW ){ |
|
77
|
rid = db_column_int(&q, 0); |
|
78
|
}else{ |
|
79
|
rid = 0; |
|
80
|
} |
|
81
|
db_reset(&q); |
|
82
|
if( rid==0 && phantomize ){ |
|
83
|
rid = content_new(blob_str(pUuid), isPrivate); |
|
84
|
} |
|
85
|
return rid; |
|
86
|
} |
|
87
|
|
|
88
|
/* |
|
89
|
** Remember that the other side of the connection already has a copy |
|
90
|
** of the file rid. |
|
91
|
*/ |
|
92
|
static void remote_has(int rid){ |
|
93
|
if( rid ){ |
|
94
|
static Stmt q; |
|
95
|
db_static_prepare(&q, "INSERT OR IGNORE INTO onremote VALUES(:r)"); |
|
96
|
db_bind_int(&q, ":r", rid); |
|
97
|
db_step(&q); |
|
98
|
db_reset(&q); |
|
99
|
} |
|
100
|
} |
|
101
|
|
|
102
|
/* |
|
103
|
** Remember that the other side of the connection lacks a copy of |
|
104
|
** the artifact with the given hash. |
|
105
|
*/ |
|
106
|
static void remote_unk(Blob *pHash){ |
|
107
|
static Stmt q; |
|
108
|
db_static_prepare(&q, "INSERT OR IGNORE INTO unk VALUES(:h)"); |
|
109
|
db_bind_text(&q, ":h", blob_str(pHash)); |
|
110
|
db_step(&q); |
|
111
|
db_reset(&q); |
|
112
|
} |
|
113
|
|
|
114
|
/* |
|
115
|
** The aToken[0..nToken-1] blob array is a parse of a "file" line |
|
116
|
** message. This routine finishes parsing that message and does |
|
117
|
** a record insert of the file. |
|
118
|
** |
|
119
|
** The file line is in one of the following two forms: |
|
120
|
** |
|
121
|
** file HASH SIZE \n CONTENT |
|
122
|
** file HASH DELTASRC SIZE \n CONTENT |
|
123
|
** |
|
124
|
** The content is SIZE bytes immediately following the newline. |
|
125
|
** If DELTASRC exists, then the CONTENT is a delta against the |
|
126
|
** content of DELTASRC. |
|
127
|
** |
|
128
|
** If any error occurs, write a message into pErr which has already |
|
129
|
** be initialized to an empty string. |
|
130
|
** |
|
131
|
** Any artifact successfully received by this routine is considered to |
|
132
|
** be public and is therefore removed from the "private" table. |
|
133
|
*/ |
|
134
|
static void xfer_accept_file( |
|
135
|
Xfer *pXfer, |
|
136
|
int cloneFlag, |
|
137
|
char **pzUuidList, |
|
138
|
int *pnUuidList |
|
139
|
){ |
|
140
|
int n; |
|
141
|
int rid; |
|
142
|
int srcid = 0; |
|
143
|
Blob content; |
|
144
|
int isPriv; |
|
145
|
Blob *pUuid; |
|
146
|
|
|
147
|
isPriv = pXfer->nextIsPrivate; |
|
148
|
pXfer->nextIsPrivate = 0; |
|
149
|
if( pXfer->nToken<3 |
|
150
|
|| pXfer->nToken>4 |
|
151
|
|| !blob_is_hname(&pXfer->aToken[1]) |
|
152
|
|| !blob_is_int(&pXfer->aToken[pXfer->nToken-1], &n) |
|
153
|
|| n<0 |
|
154
|
|| (pXfer->nToken==4 && !blob_is_hname(&pXfer->aToken[2])) |
|
155
|
){ |
|
156
|
blob_appendf(&pXfer->err, "malformed file line"); |
|
157
|
return; |
|
158
|
} |
|
159
|
blob_zero(&content); |
|
160
|
blob_extract(pXfer->pIn, n, &content); |
|
161
|
pUuid = &pXfer->aToken[1]; |
|
162
|
if( !cloneFlag && uuid_is_shunned(blob_str(pUuid)) ){ |
|
163
|
/* Ignore files that have been shunned */ |
|
164
|
blob_reset(&content); |
|
165
|
return; |
|
166
|
} |
|
167
|
if( isPriv && !g.perm.Private ){ |
|
168
|
/* Do not accept private files if not authorized */ |
|
169
|
blob_reset(&content); |
|
170
|
return; |
|
171
|
} |
|
172
|
if( cloneFlag ){ |
|
173
|
if( pXfer->nToken==4 ){ |
|
174
|
srcid = rid_from_uuid(&pXfer->aToken[2], 1, isPriv); |
|
175
|
pXfer->nDeltaRcvd++; |
|
176
|
}else{ |
|
177
|
srcid = 0; |
|
178
|
pXfer->nFileRcvd++; |
|
179
|
} |
|
180
|
rid = content_put_ex(&content, blob_str(pUuid), srcid, |
|
181
|
0, isPriv); |
|
182
|
Th_AppendToList(pzUuidList, pnUuidList, blob_str(pUuid), |
|
183
|
blob_size(pUuid)); |
|
184
|
remote_has(rid); |
|
185
|
blob_reset(&content); |
|
186
|
return; |
|
187
|
} |
|
188
|
if( pXfer->nToken==4 ){ |
|
189
|
Blob src, next; |
|
190
|
srcid = rid_from_uuid(&pXfer->aToken[2], 1, isPriv); |
|
191
|
if( content_get(srcid, &src)==0 ){ |
|
192
|
rid = content_put_ex(&content, blob_str(pUuid), srcid, |
|
193
|
0, isPriv); |
|
194
|
Th_AppendToList(pzUuidList, pnUuidList, blob_str(pUuid), |
|
195
|
blob_size(pUuid)); |
|
196
|
pXfer->nDanglingFile++; |
|
197
|
db_multi_exec("DELETE FROM phantom WHERE rid=%d", rid); |
|
198
|
if( !isPriv ) content_make_public(rid); |
|
199
|
blob_reset(&src); |
|
200
|
blob_reset(&content); |
|
201
|
return; |
|
202
|
} |
|
203
|
pXfer->nDeltaRcvd++; |
|
204
|
blob_delta_apply(&src, &content, &next); |
|
205
|
blob_reset(&src); |
|
206
|
blob_reset(&content); |
|
207
|
content = next; |
|
208
|
}else{ |
|
209
|
pXfer->nFileRcvd++; |
|
210
|
} |
|
211
|
if( hname_verify_hash(&content, blob_buffer(pUuid), blob_size(pUuid))==0 ){ |
|
212
|
blob_appendf(&pXfer->err, "wrong hash on received artifact: %b", pUuid); |
|
213
|
} |
|
214
|
rid = content_put_ex(&content, blob_str(pUuid), 0, 0, isPriv); |
|
215
|
Th_AppendToList(pzUuidList, pnUuidList, blob_str(pUuid), blob_size(pUuid)); |
|
216
|
if( rid==0 ){ |
|
217
|
blob_appendf(&pXfer->err, "%s", g.zErrMsg); |
|
218
|
blob_reset(&content); |
|
219
|
}else{ |
|
220
|
if( !isPriv ) content_make_public(rid); |
|
221
|
manifest_crosslink(rid, &content, MC_NO_ERRORS); |
|
222
|
} |
|
223
|
assert( blob_is_reset(&content) ); |
|
224
|
remote_has(rid); |
|
225
|
} |
|
226
|
|
|
227
|
/* |
|
228
|
** The aToken[0..nToken-1] blob array is a parse of a "cfile" line |
|
229
|
** message. This routine finishes parsing that message and does |
|
230
|
** a record insert of the file. The difference between "file" and |
|
231
|
** "cfile" is that with "cfile" the content is already compressed. |
|
232
|
** |
|
233
|
** The file line is in one of the following two forms: |
|
234
|
** |
|
235
|
** cfile HASH USIZE CSIZE \n CONTENT |
|
236
|
** cfile HASH DELTASRC USIZE CSIZE \n CONTENT |
|
237
|
** |
|
238
|
** The content is CSIZE bytes immediately following the newline. |
|
239
|
** If DELTASRC exists, then the CONTENT is a delta against the |
|
240
|
** content of DELTASRC. |
|
241
|
** |
|
242
|
** The original size of the HASH artifact is USIZE. |
|
243
|
** |
|
244
|
** If any error occurs, write a message into pErr which has already |
|
245
|
** be initialized to an empty string. |
|
246
|
** |
|
247
|
** Any artifact successfully received by this routine is considered to |
|
248
|
** be public and is therefore removed from the "private" table. |
|
249
|
*/ |
|
250
|
static void xfer_accept_compressed_file( |
|
251
|
Xfer *pXfer, |
|
252
|
char **pzUuidList, |
|
253
|
int *pnUuidList |
|
254
|
){ |
|
255
|
int szC; /* CSIZE */ |
|
256
|
int szU; /* USIZE */ |
|
257
|
int rid; |
|
258
|
int srcid = 0; |
|
259
|
Blob content; |
|
260
|
int isPriv; |
|
261
|
|
|
262
|
isPriv = pXfer->nextIsPrivate; |
|
263
|
pXfer->nextIsPrivate = 0; |
|
264
|
if( pXfer->nToken<4 |
|
265
|
|| pXfer->nToken>5 |
|
266
|
|| !blob_is_hname(&pXfer->aToken[1]) |
|
267
|
|| !blob_is_int(&pXfer->aToken[pXfer->nToken-2], &szU) |
|
268
|
|| !blob_is_int(&pXfer->aToken[pXfer->nToken-1], &szC) |
|
269
|
|| szC<0 || szU<0 |
|
270
|
|| (pXfer->nToken==5 && !blob_is_hname(&pXfer->aToken[2])) |
|
271
|
){ |
|
272
|
blob_appendf(&pXfer->err, "malformed cfile line"); |
|
273
|
return; |
|
274
|
} |
|
275
|
if( isPriv && !g.perm.Private ){ |
|
276
|
/* Do not accept private files if not authorized */ |
|
277
|
return; |
|
278
|
} |
|
279
|
blob_zero(&content); |
|
280
|
blob_extract(pXfer->pIn, szC, &content); |
|
281
|
if( uuid_is_shunned(blob_str(&pXfer->aToken[1])) ){ |
|
282
|
/* Ignore files that have been shunned */ |
|
283
|
blob_reset(&content); |
|
284
|
return; |
|
285
|
} |
|
286
|
if( pXfer->nToken==5 ){ |
|
287
|
srcid = rid_from_uuid(&pXfer->aToken[2], 1, isPriv); |
|
288
|
pXfer->nDeltaRcvd++; |
|
289
|
}else{ |
|
290
|
srcid = 0; |
|
291
|
pXfer->nFileRcvd++; |
|
292
|
} |
|
293
|
rid = content_put_ex(&content, blob_str(&pXfer->aToken[1]), srcid, |
|
294
|
szC, isPriv); |
|
295
|
Th_AppendToList(pzUuidList, pnUuidList, blob_str(&pXfer->aToken[1]), |
|
296
|
blob_size(&pXfer->aToken[1])); |
|
297
|
remote_has(rid); |
|
298
|
blob_reset(&content); |
|
299
|
} |
|
300
|
|
|
301
|
/* |
|
302
|
** The aToken[0..nToken-1] blob array is a parse of a "uvfile" line |
|
303
|
** message. This routine finishes parsing that message and adds the |
|
304
|
** unversioned file to the "unversioned" table. |
|
305
|
** |
|
306
|
** The file line is in one of the following two forms: |
|
307
|
** |
|
308
|
** uvfile NAME MTIME HASH SIZE FLAGS |
|
309
|
** uvfile NAME MTIME HASH SIZE FLAGS \n CONTENT |
|
310
|
** |
|
311
|
** If the 0x0001 bit of FLAGS is set, that means the file has been |
|
312
|
** deleted, SIZE is zero, the HASH is "-", and the "\n CONTENT" is omitted. |
|
313
|
** |
|
314
|
** SIZE is the number of bytes of CONTENT. The CONTENT is uncompressed. |
|
315
|
** HASH is the artifact hash of CONTENT. |
|
316
|
** |
|
317
|
** If the 0x0004 bit of FLAGS is set, that means the CONTENT is omitted. |
|
318
|
** The sender might have omitted the content because it is too big to |
|
319
|
** transmit, or because it is unchanged and this record exists purely |
|
320
|
** to update the MTIME. |
|
321
|
*/ |
|
322
|
static void xfer_accept_unversioned_file(Xfer *pXfer, int isWriter){ |
|
323
|
sqlite3_int64 mtime; /* The MTIME */ |
|
324
|
Blob *pHash; /* The HASH value */ |
|
325
|
int sz; /* The SIZE */ |
|
326
|
int flags; /* The FLAGS */ |
|
327
|
Blob content; /* The CONTENT */ |
|
328
|
Blob x; /* Compressed content */ |
|
329
|
Stmt q; /* SQL statements for comparison and insert */ |
|
330
|
int isDelete; /* HASH is "-" indicating this is a delete */ |
|
331
|
int nullContent; /* True of CONTENT is NULL */ |
|
332
|
int iStatus; /* Result from unversioned_status() */ |
|
333
|
|
|
334
|
pHash = &pXfer->aToken[3]; |
|
335
|
if( pXfer->nToken==5 |
|
336
|
|| !blob_is_filename(&pXfer->aToken[1]) |
|
337
|
|| !blob_is_int64(&pXfer->aToken[2], &mtime) |
|
338
|
|| (!blob_eq(pHash,"-") && !blob_is_hname(pHash)) |
|
339
|
|| !blob_is_int(&pXfer->aToken[4], &sz) |
|
340
|
|| !blob_is_int(&pXfer->aToken[5], &flags) |
|
341
|
|| (mtime<0 || sz<0 || flags<0) |
|
342
|
){ |
|
343
|
blob_appendf(&pXfer->err, "malformed uvfile line"); |
|
344
|
return; |
|
345
|
} |
|
346
|
blob_init(&content, 0, 0); |
|
347
|
blob_init(&x, 0, 0); |
|
348
|
if( sz>0 && (flags & 0x0005)==0 ){ |
|
349
|
blob_extract(pXfer->pIn, sz, &content); |
|
350
|
nullContent = 0; |
|
351
|
if( hname_verify_hash(&content, blob_buffer(pHash), blob_size(pHash))==0 ){ |
|
352
|
blob_appendf(&pXfer->err, "in uvfile line, HASH does not match CONTENT"); |
|
353
|
goto end_accept_unversioned_file; |
|
354
|
} |
|
355
|
}else{ |
|
356
|
nullContent = 1; |
|
357
|
} |
|
358
|
|
|
359
|
/* The isWriter flag must be true in order to land the new file */ |
|
360
|
if( !isWriter ){ |
|
361
|
blob_appendf(&pXfer->err,"Write permissions for unversioned files missing"); |
|
362
|
goto end_accept_unversioned_file; |
|
363
|
} |
|
364
|
|
|
365
|
/* Make sure we have a valid g.rcvid marker */ |
|
366
|
content_rcvid_init(0); |
|
367
|
|
|
368
|
/* Check to see if current content really should be overwritten. Ideally, |
|
369
|
** a uvfile card should never have been sent unless the overwrite should |
|
370
|
** occur. But do not trust the sender. Double-check. |
|
371
|
*/ |
|
372
|
iStatus = unversioned_status(blob_str(&pXfer->aToken[1]), mtime, |
|
373
|
blob_str(pHash)); |
|
374
|
if( iStatus>=3 ) goto end_accept_unversioned_file; |
|
375
|
|
|
376
|
/* Store the content */ |
|
377
|
isDelete = blob_eq(pHash, "-"); |
|
378
|
if( isDelete ){ |
|
379
|
db_prepare(&q, |
|
380
|
"UPDATE unversioned" |
|
381
|
" SET rcvid=:rcvid, mtime=:mtime, hash=NULL," |
|
382
|
" sz=0, encoding=0, content=NULL" |
|
383
|
" WHERE name=:name" |
|
384
|
); |
|
385
|
db_bind_int(&q, ":rcvid", g.rcvid); |
|
386
|
}else if( iStatus==2 ){ |
|
387
|
db_prepare(&q, "UPDATE unversioned SET mtime=:mtime WHERE name=:name"); |
|
388
|
}else{ |
|
389
|
db_prepare(&q, |
|
390
|
"REPLACE INTO unversioned(name,rcvid,mtime,hash,sz,encoding,content)" |
|
391
|
" VALUES(:name,:rcvid,:mtime,:hash,:sz,:encoding,:content)" |
|
392
|
); |
|
393
|
db_bind_int(&q, ":rcvid", g.rcvid); |
|
394
|
db_bind_text(&q, ":hash", blob_str(pHash)); |
|
395
|
db_bind_int(&q, ":sz", blob_size(&content)); |
|
396
|
if( !nullContent ){ |
|
397
|
blob_compress(&content, &x); |
|
398
|
if( blob_size(&x) < 0.8*blob_size(&content) ){ |
|
399
|
db_bind_blob(&q, ":content", &x); |
|
400
|
db_bind_int(&q, ":encoding", 1); |
|
401
|
}else{ |
|
402
|
db_bind_blob(&q, ":content", &content); |
|
403
|
db_bind_int(&q, ":encoding", 0); |
|
404
|
} |
|
405
|
}else{ |
|
406
|
db_bind_int(&q, ":encoding", 0); |
|
407
|
} |
|
408
|
} |
|
409
|
db_bind_text(&q, ":name", blob_str(&pXfer->aToken[1])); |
|
410
|
db_bind_int64(&q, ":mtime", mtime); |
|
411
|
db_step(&q); |
|
412
|
db_finalize(&q); |
|
413
|
db_unset("uv-hash", 0); |
|
414
|
|
|
415
|
end_accept_unversioned_file: |
|
416
|
blob_reset(&x); |
|
417
|
blob_reset(&content); |
|
418
|
} |
|
419
|
|
|
420
|
/* |
|
421
|
** Try to send a file as a delta against its parent. |
|
422
|
** If successful, return the number of bytes in the delta. |
|
423
|
** If we cannot generate an appropriate delta, then send |
|
424
|
** nothing and return zero. |
|
425
|
** |
|
426
|
** Never send a delta against a private artifact. |
|
427
|
*/ |
|
428
|
static int send_delta_parent( |
|
429
|
Xfer *pXfer, /* The transfer context */ |
|
430
|
int rid, /* record id of the file to send */ |
|
431
|
int isPrivate, /* True if rid is a private artifact */ |
|
432
|
Blob *pContent, /* The content of the file to send */ |
|
433
|
Blob *pUuid /* The HASH of the file to send */ |
|
434
|
){ |
|
435
|
static const char *const azQuery[] = { |
|
436
|
"SELECT pid FROM plink x" |
|
437
|
" WHERE cid=%d" |
|
438
|
" AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=pid)", |
|
439
|
|
|
440
|
"SELECT pid, min(mtime) FROM mlink, event ON mlink.mid=event.objid" |
|
441
|
" WHERE fid=%d" |
|
442
|
" AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=pid)" |
|
443
|
}; |
|
444
|
int i; |
|
445
|
Blob src, delta; |
|
446
|
int size = 0; |
|
447
|
int srcId = 0; |
|
448
|
|
|
449
|
for(i=0; srcId==0 && i<count(azQuery); i++){ |
|
450
|
srcId = db_int(0, azQuery[i] /*works-like:"%d"*/, rid); |
|
451
|
} |
|
452
|
if( srcId>0 |
|
453
|
&& (pXfer->syncPrivate || !content_is_private(srcId)) |
|
454
|
&& content_get(srcId, &src) |
|
455
|
){ |
|
456
|
char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", srcId); |
|
457
|
blob_delta_create(&src, pContent, &delta); |
|
458
|
size = blob_size(&delta); |
|
459
|
if( size>=(int)blob_size(pContent)-50 ){ |
|
460
|
size = 0; |
|
461
|
}else if( uuid_is_shunned(zUuid) ){ |
|
462
|
size = 0; |
|
463
|
}else{ |
|
464
|
if( isPrivate ) blob_append(pXfer->pOut, "private\n", -1); |
|
465
|
blob_appendf(pXfer->pOut, "file %b %s %d\n", pUuid, zUuid, size); |
|
466
|
blob_append(pXfer->pOut, blob_buffer(&delta), size); |
|
467
|
} |
|
468
|
blob_reset(&delta); |
|
469
|
free(zUuid); |
|
470
|
blob_reset(&src); |
|
471
|
} |
|
472
|
return size; |
|
473
|
} |
|
474
|
|
|
475
|
/* |
|
476
|
** Try to send a file as a native delta. |
|
477
|
** If successful, return the number of bytes in the delta. |
|
478
|
** If we cannot generate an appropriate delta, then send |
|
479
|
** nothing and return zero. |
|
480
|
** |
|
481
|
** Never send a delta against a private artifact. |
|
482
|
*/ |
|
483
|
static int send_delta_native( |
|
484
|
Xfer *pXfer, /* The transfer context */ |
|
485
|
int rid, /* record id of the file to send */ |
|
486
|
int isPrivate, /* True if rid is a private artifact */ |
|
487
|
Blob *pUuid /* The HASH of the file to send */ |
|
488
|
){ |
|
489
|
Blob src, delta; |
|
490
|
int size = 0; |
|
491
|
int srcId; |
|
492
|
|
|
493
|
srcId = db_int(0, "SELECT srcid FROM delta WHERE rid=%d", rid); |
|
494
|
if( srcId>0 |
|
495
|
&& (pXfer->syncPrivate || !content_is_private(srcId)) |
|
496
|
){ |
|
497
|
blob_zero(&src); |
|
498
|
db_blob(&src, "SELECT uuid FROM blob WHERE rid=%d", srcId); |
|
499
|
if( uuid_is_shunned(blob_str(&src)) ){ |
|
500
|
blob_reset(&src); |
|
501
|
return 0; |
|
502
|
} |
|
503
|
blob_zero(&delta); |
|
504
|
db_blob(&delta, "SELECT content FROM blob WHERE rid=%d", rid); |
|
505
|
blob_uncompress(&delta, &delta); |
|
506
|
if( isPrivate ) blob_append(pXfer->pOut, "private\n", -1); |
|
507
|
blob_appendf(pXfer->pOut, "file %b %b %d\n", |
|
508
|
pUuid, &src, blob_size(&delta)); |
|
509
|
blob_append(pXfer->pOut, blob_buffer(&delta), blob_size(&delta)); |
|
510
|
size = blob_size(&delta); |
|
511
|
blob_reset(&delta); |
|
512
|
blob_reset(&src); |
|
513
|
}else{ |
|
514
|
size = 0; |
|
515
|
} |
|
516
|
return size; |
|
517
|
} |
|
518
|
|
|
519
|
/* |
|
520
|
** Push an error message to alert the older client that the repository |
|
521
|
** has SHA3 content and cannot be synced or cloned. |
|
522
|
*/ |
|
523
|
static void xfer_cannot_send_sha3_error(Xfer *pXfer){ |
|
524
|
blob_appendf(pXfer->pOut, |
|
525
|
"error Fossil\\sversion\\s2.0\\sor\\slater\\srequired.\n" |
|
526
|
); |
|
527
|
} |
|
528
|
|
|
529
|
|
|
530
|
/* |
|
531
|
** Send the file identified by rid. |
|
532
|
** |
|
533
|
** The pUuid can be NULL in which case the correct hash is computed |
|
534
|
** from the rid. |
|
535
|
** |
|
536
|
** Try to send the file as a native delta if nativeDelta is true, or |
|
537
|
** as a parent delta if nativeDelta is false. |
|
538
|
** |
|
539
|
** It should never be the case that rid is a private artifact. But |
|
540
|
** as a precaution, this routine does check on rid and if it is private |
|
541
|
** this routine becomes a no-op. |
|
542
|
*/ |
|
543
|
static void send_file(Xfer *pXfer, int rid, Blob *pUuid, int nativeDelta){ |
|
544
|
Blob content, uuid; |
|
545
|
int size = 0; |
|
546
|
int isPriv = content_is_private(rid); |
|
547
|
|
|
548
|
if( isPriv && pXfer->syncPrivate==0 ){ |
|
549
|
if( pXfer->remoteDate>=20200413 && pUuid && blob_size(pUuid)>0 ){ |
|
550
|
/* If the artifact is private and we are not doing a private sync, |
|
551
|
** at least tell the other side that the artifact exists and is |
|
552
|
** known to be private. But only do this for newer clients since |
|
553
|
** older ones will throw an error if they get a private igot card |
|
554
|
** and private syncing is disallowed */ |
|
555
|
blob_appendf(pXfer->pOut, "igot %b 1\n", pUuid); |
|
556
|
pXfer->nIGotSent++; |
|
557
|
pXfer->nPrivIGot++; |
|
558
|
} |
|
559
|
return; |
|
560
|
} |
|
561
|
if( db_exists("SELECT 1 FROM onremote WHERE rid=%d", rid) ){ |
|
562
|
return; |
|
563
|
} |
|
564
|
blob_zero(&uuid); |
|
565
|
db_blob(&uuid, "SELECT uuid FROM blob WHERE rid=%d AND size>=0", rid); |
|
566
|
if( blob_size(&uuid)==0 ){ |
|
567
|
return; |
|
568
|
} |
|
569
|
if( blob_size(&uuid)>HNAME_LEN_SHA1 && pXfer->remoteVersion<20000 ){ |
|
570
|
xfer_cannot_send_sha3_error(pXfer); |
|
571
|
return; |
|
572
|
} |
|
573
|
if( pUuid ){ |
|
574
|
if( blob_compare(pUuid, &uuid)!=0 ){ |
|
575
|
blob_reset(&uuid); |
|
576
|
return; |
|
577
|
} |
|
578
|
}else{ |
|
579
|
pUuid = &uuid; |
|
580
|
} |
|
581
|
if( uuid_is_shunned(blob_str(pUuid)) ){ |
|
582
|
blob_reset(&uuid); |
|
583
|
return; |
|
584
|
} |
|
585
|
if( (pXfer->maxTime != -1 && time(NULL) >= pXfer->maxTime) || |
|
586
|
pXfer->mxSend<=(int)blob_size(pXfer->pOut) ){ |
|
587
|
const char *zFormat = isPriv ? "igot %b 1\n" : "igot %b\n"; |
|
588
|
blob_appendf(pXfer->pOut, zFormat /*works-like:"%b"*/, pUuid); |
|
589
|
pXfer->nIGotSent++; |
|
590
|
blob_reset(&uuid); |
|
591
|
return; |
|
592
|
} |
|
593
|
if( nativeDelta ){ |
|
594
|
size = send_delta_native(pXfer, rid, isPriv, pUuid); |
|
595
|
if( size ){ |
|
596
|
pXfer->nDeltaSent++; |
|
597
|
} |
|
598
|
} |
|
599
|
if( size==0 ){ |
|
600
|
content_get(rid, &content); |
|
601
|
|
|
602
|
if( !nativeDelta && blob_size(&content)>100 ){ |
|
603
|
size = send_delta_parent(pXfer, rid, isPriv, &content, pUuid); |
|
604
|
} |
|
605
|
if( size==0 ){ |
|
606
|
int size = blob_size(&content); |
|
607
|
if( isPriv ) blob_append(pXfer->pOut, "private\n", -1); |
|
608
|
blob_appendf(pXfer->pOut, "file %b %d\n", pUuid, size); |
|
609
|
blob_append(pXfer->pOut, blob_buffer(&content), size); |
|
610
|
pXfer->nFileSent++; |
|
611
|
}else{ |
|
612
|
pXfer->nDeltaSent++; |
|
613
|
} |
|
614
|
blob_reset(&content); |
|
615
|
} |
|
616
|
remote_has(rid); |
|
617
|
blob_reset(&uuid); |
|
618
|
#if 0 |
|
619
|
if( blob_buffer(pXfer->pOut)[blob_size(pXfer->pOut)-1]!='\n' ){ |
|
620
|
blob_append(pXfer->pOut, "\n", 1); |
|
621
|
} |
|
622
|
#endif |
|
623
|
} |
|
624
|
|
|
625
|
/* |
|
626
|
** Send the file identified by rid as a compressed artifact. Basically, |
|
627
|
** send the content exactly as it appears in the BLOB table using |
|
628
|
** a "cfile" card. |
|
629
|
*/ |
|
630
|
static void send_compressed_file(Xfer *pXfer, int rid){ |
|
631
|
const char *zContent; |
|
632
|
const char *zUuid; |
|
633
|
const char *zDelta; |
|
634
|
int szU; |
|
635
|
int szC; |
|
636
|
int rc; |
|
637
|
int isPrivate; |
|
638
|
int srcIsPrivate; |
|
639
|
static Stmt q1; |
|
640
|
Blob fullContent; |
|
641
|
|
|
642
|
isPrivate = content_is_private(rid); |
|
643
|
if( isPrivate && pXfer->syncPrivate==0 ) return; |
|
644
|
db_static_prepare(&q1, |
|
645
|
"SELECT uuid, size, content, delta.srcid IN private," |
|
646
|
" (SELECT uuid FROM blob WHERE rid=delta.srcid)" |
|
647
|
" FROM blob LEFT JOIN delta ON (blob.rid=delta.rid)" |
|
648
|
" WHERE blob.rid=:rid" |
|
649
|
" AND blob.size>=0" |
|
650
|
" AND NOT EXISTS(SELECT 1 FROM shun WHERE shun.uuid=blob.uuid)" |
|
651
|
); |
|
652
|
db_bind_int(&q1, ":rid", rid); |
|
653
|
rc = db_step(&q1); |
|
654
|
if( rc==SQLITE_ROW ){ |
|
655
|
zUuid = db_column_text(&q1, 0); |
|
656
|
szU = db_column_int(&q1, 1); |
|
657
|
szC = db_column_bytes(&q1, 2); |
|
658
|
zContent = db_column_raw(&q1, 2); |
|
659
|
srcIsPrivate = db_column_int(&q1, 3); |
|
660
|
zDelta = db_column_text(&q1, 4); |
|
661
|
if( isPrivate ) blob_append(pXfer->pOut, "private\n", -1); |
|
662
|
if( pXfer->remoteVersion<20000 && db_column_bytes(&q1,0)!=HNAME_LEN_SHA1 ){ |
|
663
|
xfer_cannot_send_sha3_error(pXfer); |
|
664
|
db_reset(&q1); |
|
665
|
return; |
|
666
|
} |
|
667
|
blob_appendf(pXfer->pOut, "cfile %s ", zUuid); |
|
668
|
if( !isPrivate && srcIsPrivate ){ |
|
669
|
content_get(rid, &fullContent); |
|
670
|
szU = blob_size(&fullContent); |
|
671
|
blob_compress(&fullContent, &fullContent); |
|
672
|
szC = blob_size(&fullContent); |
|
673
|
zContent = blob_buffer(&fullContent); |
|
674
|
zDelta = 0; |
|
675
|
} |
|
676
|
if( zDelta ){ |
|
677
|
blob_appendf(pXfer->pOut, "%s ", zDelta); |
|
678
|
pXfer->nDeltaSent++; |
|
679
|
}else{ |
|
680
|
pXfer->nFileSent++; |
|
681
|
} |
|
682
|
blob_appendf(pXfer->pOut, "%d %d\n", szU, szC); |
|
683
|
blob_append(pXfer->pOut, zContent, szC); |
|
684
|
if( blob_buffer(pXfer->pOut)[blob_size(pXfer->pOut)-1]!='\n' ){ |
|
685
|
blob_append(pXfer->pOut, "\n", 1); |
|
686
|
} |
|
687
|
if( !isPrivate && srcIsPrivate ){ |
|
688
|
blob_reset(&fullContent); |
|
689
|
} |
|
690
|
} |
|
691
|
db_reset(&q1); |
|
692
|
} |
|
693
|
|
|
694
|
/* |
|
695
|
** Send the unversioned file identified by zName by generating the |
|
696
|
** appropriate "uvfile" card. |
|
697
|
** |
|
698
|
** uvfile NAME MTIME HASH SIZE FLAGS \n CONTENT |
|
699
|
** |
|
700
|
** If the noContent flag is set, omit the CONTENT and set the 0x0004 |
|
701
|
** flag in FLAGS. |
|
702
|
*/ |
|
703
|
static void send_unversioned_file( |
|
704
|
Xfer *pXfer, /* Transfer context */ |
|
705
|
const char *zName, /* Name of unversioned file to be sent */ |
|
706
|
int noContent /* True to omit the content */ |
|
707
|
){ |
|
708
|
Stmt q1; |
|
709
|
|
|
710
|
if( (int)blob_size(pXfer->pOut)>=pXfer->mxSend ) noContent = 1; |
|
711
|
if( noContent ){ |
|
712
|
db_prepare(&q1, |
|
713
|
"SELECT mtime, hash, encoding, sz FROM unversioned WHERE name=%Q", |
|
714
|
zName |
|
715
|
); |
|
716
|
}else{ |
|
717
|
db_prepare(&q1, |
|
718
|
"SELECT mtime, hash, encoding, sz, content FROM unversioned" |
|
719
|
" WHERE name=%Q", |
|
720
|
zName |
|
721
|
); |
|
722
|
} |
|
723
|
if( db_step(&q1)==SQLITE_ROW ){ |
|
724
|
sqlite3_int64 mtime = db_column_int64(&q1, 0); |
|
725
|
const char *zHash = db_column_text(&q1, 1); |
|
726
|
if( pXfer->remoteVersion<20000 && db_column_bytes(&q1,1)>HNAME_LEN_SHA1 ){ |
|
727
|
xfer_cannot_send_sha3_error(pXfer); |
|
728
|
db_reset(&q1); |
|
729
|
return; |
|
730
|
} |
|
731
|
if( (int)blob_size(pXfer->pOut)>=pXfer->mxSend ){ |
|
732
|
/* If we have already reached the send size limit, send a (short) |
|
733
|
** uvigot card rather than a uvfile card. This only happens on the |
|
734
|
** server side. The uvigot card will provoke the client to resend |
|
735
|
** another uvgimme on the next cycle. */ |
|
736
|
blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n", |
|
737
|
zName, mtime, zHash, db_column_int(&q1,3)); |
|
738
|
}else{ |
|
739
|
blob_appendf(pXfer->pOut, "uvfile %s %lld", zName, mtime); |
|
740
|
if( zHash==0 ){ |
|
741
|
blob_append(pXfer->pOut, " - 0 1\n", -1); |
|
742
|
}else if( noContent ){ |
|
743
|
blob_appendf(pXfer->pOut, " %s %d 4\n", zHash, db_column_int(&q1,3)); |
|
744
|
}else{ |
|
745
|
Blob content; |
|
746
|
blob_init(&content, 0, 0); |
|
747
|
db_column_blob(&q1, 4, &content); |
|
748
|
if( db_column_int(&q1, 2) ){ |
|
749
|
blob_uncompress(&content, &content); |
|
750
|
} |
|
751
|
blob_appendf(pXfer->pOut, " %s %d 0\n", zHash, blob_size(&content)); |
|
752
|
blob_append(pXfer->pOut, blob_buffer(&content), blob_size(&content)); |
|
753
|
blob_reset(&content); |
|
754
|
} |
|
755
|
} |
|
756
|
} |
|
757
|
db_finalize(&q1); |
|
758
|
} |
|
759
|
|
|
760
|
/* |
|
761
|
** Send a gimme message for every phantom. |
|
762
|
** |
|
763
|
** Except: do not request shunned artifacts. And do not request |
|
764
|
** private artifacts if we are not doing a private transfer. |
|
765
|
*/ |
|
766
|
static void request_phantoms(Xfer *pXfer, int maxReq){ |
|
767
|
Stmt q; |
|
768
|
db_prepare(&q, |
|
769
|
"SELECT uuid FROM phantom CROSS JOIN blob USING(rid) /*scan*/" |
|
770
|
" WHERE NOT EXISTS(SELECT 1 FROM unk WHERE unk.uuid=blob.uuid)" |
|
771
|
" AND NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid) %s", |
|
772
|
(pXfer->syncPrivate ? "" : |
|
773
|
" AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)") |
|
774
|
); |
|
775
|
while( db_step(&q)==SQLITE_ROW && maxReq-- > 0 ){ |
|
776
|
const char *zUuid = db_column_text(&q, 0); |
|
777
|
blob_appendf(pXfer->pOut, "gimme %s\n", zUuid); |
|
778
|
pXfer->nGimmeSent++; |
|
779
|
} |
|
780
|
db_finalize(&q); |
|
781
|
} |
|
782
|
|
|
783
|
/* |
|
784
|
** Compute an hash on the tail of pMsg. Verify that it matches the |
|
785
|
** the hash given in pHash. Return non-zero for an error and 0 on success. |
|
786
|
** |
|
787
|
** The type of hash computed (SHA1, SHA3-256) is determined by |
|
788
|
** the length of the input hash in pHash. |
|
789
|
*/ |
|
790
|
static int check_tail_hash(Blob *pHash, Blob *pMsg){ |
|
791
|
Blob tail; |
|
792
|
int rc; |
|
793
|
blob_tail(pMsg, &tail); |
|
794
|
rc = hname_verify_hash(&tail, blob_buffer(pHash), blob_size(pHash)); |
|
795
|
blob_reset(&tail); |
|
796
|
return rc==HNAME_ERROR; |
|
797
|
} |
|
798
|
|
|
799
|
/* |
|
800
|
** Check the signature on an application/x-fossil payload received by |
|
801
|
** the HTTP server. The signature is a line of the following form: |
|
802
|
** |
|
803
|
** login LOGIN NONCE SIGNATURE |
|
804
|
** |
|
805
|
** The NONCE is the SHA1 hash of the remainder of the input. |
|
806
|
** SIGNATURE is the SHA1 checksum of the NONCE concatenated |
|
807
|
** with the sha1_shared_secret() encoding of the users password. |
|
808
|
** |
|
809
|
** SIGNATURE = sha1_sum( NONCE + sha1_shared_secret(PASSWORD) ); |
|
810
|
** |
|
811
|
** The parameters to this routine are ephemeral blobs holding the |
|
812
|
** LOGIN, NONCE and SIGNATURE. |
|
813
|
** |
|
814
|
** This routine attempts to locate the user and verify the signature. |
|
815
|
** If everything checks out, the USER.CAP column for the USER table |
|
816
|
** is consulted to set privileges in the global g variable. |
|
817
|
** |
|
818
|
** If anything fails to check out, no changes are made to privileges. |
|
819
|
** |
|
820
|
** Signature generation on the client side is handled by the |
|
821
|
** http_exchange() routine. |
|
822
|
** |
|
823
|
** Return non-zero for a login failure and zero for success. |
|
824
|
*/ |
|
825
|
static int check_login(Blob *pLogin, Blob *pNonce, Blob *pSig){ |
|
826
|
Stmt q; |
|
827
|
int rc = -1; |
|
828
|
char *zLogin = blob_terminate(pLogin); |
|
829
|
defossilize(zLogin); |
|
830
|
|
|
831
|
if( fossil_strcmp(zLogin, "nobody")==0 |
|
832
|
|| fossil_strcmp(zLogin, "anonymous")==0 |
|
833
|
){ |
|
834
|
return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */ |
|
835
|
} |
|
836
|
if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0 |
|
837
|
&& db_get_boolean("remote_user_ok",0) ){ |
|
838
|
return 0; /* Accept Basic Authorization */ |
|
839
|
} |
|
840
|
db_prepare(&q, |
|
841
|
"SELECT pw, cap, uid FROM user" |
|
842
|
" WHERE login=%Q" |
|
843
|
" AND login NOT IN ('anonymous','nobody','developer','reader')" |
|
844
|
" AND length(pw)>0", |
|
845
|
zLogin |
|
846
|
); |
|
847
|
if( db_step(&q)==SQLITE_ROW ){ |
|
848
|
int szPw; |
|
849
|
Blob pw, combined, hash; |
|
850
|
blob_zero(&pw); |
|
851
|
db_ephemeral_blob(&q, 0, &pw); |
|
852
|
szPw = blob_size(&pw); |
|
853
|
blob_zero(&combined); |
|
854
|
blob_copy(&combined, pNonce); |
|
855
|
blob_append(&combined, blob_buffer(&pw), szPw); |
|
856
|
sha1sum_blob(&combined, &hash); |
|
857
|
assert( blob_size(&hash)==40 ); |
|
858
|
rc = blob_constant_time_cmp(&hash, pSig); |
|
859
|
blob_reset(&hash); |
|
860
|
blob_reset(&combined); |
|
861
|
if( rc!=0 && szPw!=40 ){ |
|
862
|
/* If this server stores cleartext passwords and the password did not |
|
863
|
** match, then perhaps the client is sending SHA1 passwords. Try |
|
864
|
** again with the SHA1 password. |
|
865
|
*/ |
|
866
|
const char *zPw = db_column_text(&q, 0); |
|
867
|
char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0); |
|
868
|
blob_zero(&combined); |
|
869
|
blob_copy(&combined, pNonce); |
|
870
|
blob_append(&combined, zSecret, -1); |
|
871
|
fossil_free(zSecret); |
|
872
|
sha1sum_blob(&combined, &hash); |
|
873
|
rc = blob_constant_time_cmp(&hash, pSig); |
|
874
|
blob_reset(&hash); |
|
875
|
blob_reset(&combined); |
|
876
|
} |
|
877
|
if( rc==0 ){ |
|
878
|
const char *zCap; |
|
879
|
zCap = db_column_text(&q, 1); |
|
880
|
login_set_capabilities(zCap, 0); |
|
881
|
g.userUid = db_column_int(&q, 2); |
|
882
|
g.zLogin = mprintf("%b", pLogin); |
|
883
|
g.zNonce = mprintf("%b", pNonce); |
|
884
|
} |
|
885
|
} |
|
886
|
db_finalize(&q); |
|
887
|
return rc; |
|
888
|
} |
|
889
|
|
|
890
|
/* |
|
891
|
** Send the content of all files in the unsent table. |
|
892
|
** |
|
893
|
** This is really just an optimization. If you clear the |
|
894
|
** unsent table, all the right files will still get transferred. |
|
895
|
** It just might require an extra round trip or two. |
|
896
|
*/ |
|
897
|
static void send_unsent(Xfer *pXfer){ |
|
898
|
Stmt q; |
|
899
|
db_prepare(&q, "SELECT rid FROM unsent EXCEPT SELECT rid FROM private"); |
|
900
|
while( db_step(&q)==SQLITE_ROW ){ |
|
901
|
int rid = db_column_int(&q, 0); |
|
902
|
send_file(pXfer, rid, 0, 0); |
|
903
|
} |
|
904
|
db_finalize(&q); |
|
905
|
db_multi_exec("DELETE FROM unsent"); |
|
906
|
} |
|
907
|
|
|
908
|
/* |
|
909
|
** Check to see if the number of unclustered entries is greater than |
|
910
|
** 100 and if it is, form a new cluster. Unclustered phantoms do not |
|
911
|
** count toward the 100 total. And phantoms are never added to a new |
|
912
|
** cluster. |
|
913
|
*/ |
|
914
|
void create_cluster(void){ |
|
915
|
Blob cluster, cksum; |
|
916
|
Blob deleteWhere; |
|
917
|
Stmt q; |
|
918
|
int nUncl; |
|
919
|
int nRow = 0; |
|
920
|
int rid; |
|
921
|
|
|
922
|
#if 0 |
|
923
|
/* We should not ever get any private artifacts in the unclustered table. |
|
924
|
** But if we do (because of a bug) now is a good time to delete them. */ |
|
925
|
db_multi_exec( |
|
926
|
"DELETE FROM unclustered WHERE rid IN (SELECT rid FROM private)" |
|
927
|
); |
|
928
|
#endif |
|
929
|
|
|
930
|
nUncl = db_int(0, "SELECT count(*) FROM unclustered /*scan*/" |
|
931
|
" WHERE NOT EXISTS(SELECT 1 FROM phantom" |
|
932
|
" WHERE rid=unclustered.rid)"); |
|
933
|
if( nUncl>=100 ){ |
|
934
|
blob_zero(&cluster); |
|
935
|
blob_zero(&deleteWhere); |
|
936
|
db_prepare(&q, "SELECT uuid FROM unclustered, blob" |
|
937
|
" WHERE NOT EXISTS(SELECT 1 FROM phantom" |
|
938
|
" WHERE rid=unclustered.rid)" |
|
939
|
" AND unclustered.rid=blob.rid" |
|
940
|
" AND NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)" |
|
941
|
" ORDER BY 1"); |
|
942
|
while( db_step(&q)==SQLITE_ROW ){ |
|
943
|
blob_appendf(&cluster, "M %s\n", db_column_text(&q, 0)); |
|
944
|
nRow++; |
|
945
|
if( nRow>=800 && nUncl>nRow+100 ){ |
|
946
|
md5sum_blob(&cluster, &cksum); |
|
947
|
blob_appendf(&cluster, "Z %b\n", &cksum); |
|
948
|
blob_reset(&cksum); |
|
949
|
rid = content_put(&cluster); |
|
950
|
manifest_crosslink(rid, &cluster, MC_NONE); |
|
951
|
blob_reset(&cluster); |
|
952
|
nUncl -= nRow; |
|
953
|
nRow = 0; |
|
954
|
blob_append_sql(&deleteWhere, ",%d", rid); |
|
955
|
} |
|
956
|
} |
|
957
|
db_finalize(&q); |
|
958
|
db_multi_exec( |
|
959
|
"DELETE FROM unclustered WHERE rid NOT IN (0 %s)" |
|
960
|
" AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=unclustered.rid)", |
|
961
|
blob_sql_text(&deleteWhere) |
|
962
|
); |
|
963
|
blob_reset(&deleteWhere); |
|
964
|
if( nRow>0 ){ |
|
965
|
md5sum_blob(&cluster, &cksum); |
|
966
|
blob_appendf(&cluster, "Z %b\n", &cksum); |
|
967
|
blob_reset(&cksum); |
|
968
|
rid = content_put(&cluster); |
|
969
|
manifest_crosslink(rid, &cluster, MC_NONE); |
|
970
|
blob_reset(&cluster); |
|
971
|
} |
|
972
|
} |
|
973
|
} |
|
974
|
|
|
975
|
/* |
|
976
|
** Send igot messages for every private artifact |
|
977
|
*/ |
|
978
|
static int send_private(Xfer *pXfer){ |
|
979
|
int cnt = 0; |
|
980
|
Stmt q; |
|
981
|
if( pXfer->syncPrivate ){ |
|
982
|
db_prepare(&q, "SELECT uuid FROM private JOIN blob USING(rid)"); |
|
983
|
while( db_step(&q)==SQLITE_ROW ){ |
|
984
|
blob_appendf(pXfer->pOut, "igot %s 1\n", db_column_text(&q,0)); |
|
985
|
cnt++; |
|
986
|
} |
|
987
|
db_finalize(&q); |
|
988
|
} |
|
989
|
return cnt; |
|
990
|
} |
|
991
|
|
|
992
|
/* |
|
993
|
** Send an igot message for every entry in unclustered table. |
|
994
|
** Return the number of cards sent. |
|
995
|
** |
|
996
|
** Except: |
|
997
|
** * Do not send igot cards for shunned artifacts |
|
998
|
** * Do not send igot cards for phantoms |
|
999
|
** * Do not send igot cards for private artifacts |
|
1000
|
** * Do not send igot cards for any artifact that is in the |
|
1001
|
** ONREMOTE table, if that table exists. |
|
1002
|
** |
|
1003
|
** If the pXfer->resync flag is set, that means we are doing a "--verily" |
|
1004
|
** sync and all artifacts that don't meet the restrictions above should |
|
1005
|
** be sent. |
|
1006
|
*/ |
|
1007
|
static int send_unclustered(Xfer *pXfer){ |
|
1008
|
Stmt q; |
|
1009
|
int cnt = 0; |
|
1010
|
const char *zExtra; |
|
1011
|
if( db_table_exists("temp","onremote") ){ |
|
1012
|
zExtra = " AND NOT EXISTS(SELECT 1 FROM onremote WHERE rid=blob.rid)"; |
|
1013
|
}else{ |
|
1014
|
zExtra = ""; |
|
1015
|
} |
|
1016
|
if( pXfer->resync ){ |
|
1017
|
db_prepare(&q, |
|
1018
|
"SELECT uuid, rid FROM blob" |
|
1019
|
" WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)" |
|
1020
|
" AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)" |
|
1021
|
" AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)%s" |
|
1022
|
" AND blob.rid<=%d" |
|
1023
|
" ORDER BY blob.rid DESC", |
|
1024
|
zExtra /*safe-for-%s*/, pXfer->resync |
|
1025
|
); |
|
1026
|
}else{ |
|
1027
|
db_prepare(&q, |
|
1028
|
"SELECT uuid FROM unclustered JOIN blob USING(rid) /*scan*/" |
|
1029
|
" WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)" |
|
1030
|
" AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)" |
|
1031
|
" AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)%s", |
|
1032
|
zExtra /*safe-for-%s*/ |
|
1033
|
); |
|
1034
|
} |
|
1035
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1036
|
blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0)); |
|
1037
|
cnt++; |
|
1038
|
if( pXfer->resync && pXfer->mxSend<(int)blob_size(pXfer->pOut) ){ |
|
1039
|
pXfer->resync = db_column_int(&q, 1)-1; |
|
1040
|
} |
|
1041
|
} |
|
1042
|
db_finalize(&q); |
|
1043
|
if( cnt==0 ) pXfer->resync = 0; |
|
1044
|
return cnt; |
|
1045
|
} |
|
1046
|
|
|
1047
|
/* |
|
1048
|
** Send an igot message for every cluster artifact that is not a phantom, |
|
1049
|
** is not shunned, is not private, and that is not in the UNCLUSTERED table. |
|
1050
|
** Return the number of cards sent. |
|
1051
|
*/ |
|
1052
|
static int send_all_clusters(Xfer *pXfer){ |
|
1053
|
Stmt q; |
|
1054
|
int cnt = 0; |
|
1055
|
const char *zExtra; |
|
1056
|
if( db_table_exists("temp","onremote") ){ |
|
1057
|
zExtra = " AND NOT EXISTS(SELECT 1 FROM onremote WHERE rid=blob.rid)"; |
|
1058
|
}else{ |
|
1059
|
zExtra = ""; |
|
1060
|
} |
|
1061
|
db_prepare(&q, |
|
1062
|
"SELECT uuid" |
|
1063
|
" FROM tagxref JOIN blob ON tagxref.rid=blob.rid AND tagxref.tagid=%d" |
|
1064
|
" WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)" |
|
1065
|
" AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)" |
|
1066
|
" AND NOT EXISTS(SELECT 1 FROM unclustered WHERE rid=blob.rid)" |
|
1067
|
" AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)%s", |
|
1068
|
TAG_CLUSTER, zExtra /*safe-for-%s*/ |
|
1069
|
); |
|
1070
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1071
|
if( cnt==0 ) blob_appendf(pXfer->pOut, "# sending-clusters\n"); |
|
1072
|
blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0)); |
|
1073
|
cnt++; |
|
1074
|
} |
|
1075
|
db_finalize(&q); |
|
1076
|
if( cnt ) blob_appendf(pXfer->pOut, "# end-of-clusters\n"); |
|
1077
|
return cnt; |
|
1078
|
} |
|
1079
|
|
|
1080
|
/* |
|
1081
|
** Send an igot message for every artifact. |
|
1082
|
*/ |
|
1083
|
static void send_all(Xfer *pXfer){ |
|
1084
|
Stmt q; |
|
1085
|
db_prepare(&q, |
|
1086
|
"SELECT uuid FROM blob " |
|
1087
|
" WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)" |
|
1088
|
" AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)" |
|
1089
|
" AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)" |
|
1090
|
); |
|
1091
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1092
|
blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0)); |
|
1093
|
} |
|
1094
|
db_finalize(&q); |
|
1095
|
} |
|
1096
|
|
|
1097
|
/* |
|
1098
|
** pXfer is a "pragma uv-hash HASH" card. |
|
1099
|
** |
|
1100
|
** If HASH is different from the unversioned content hash on this server, |
|
1101
|
** then send a bunch of uvigot cards, one for each entry unversioned file |
|
1102
|
** on this server. |
|
1103
|
*/ |
|
1104
|
static void send_unversioned_catalog(Xfer *pXfer){ |
|
1105
|
Stmt uvq; |
|
1106
|
unversioned_schema(); |
|
1107
|
db_prepare(&uvq, |
|
1108
|
"SELECT name, mtime, hash, sz FROM unversioned" |
|
1109
|
); |
|
1110
|
while( db_step(&uvq)==SQLITE_ROW ){ |
|
1111
|
const char *zName = db_column_text(&uvq,0); |
|
1112
|
sqlite3_int64 mtime = db_column_int64(&uvq,1); |
|
1113
|
const char *zHash = db_column_text(&uvq,2); |
|
1114
|
int sz = db_column_int(&uvq,3); |
|
1115
|
if( zHash==0 ){ sz = 0; zHash = "-"; } |
|
1116
|
blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n", |
|
1117
|
zName, mtime, zHash, sz); |
|
1118
|
} |
|
1119
|
db_finalize(&uvq); |
|
1120
|
} |
|
1121
|
|
|
1122
|
/* |
|
1123
|
** Return a string that contains supplemental information about a |
|
1124
|
** "not authorized" error. The string might be empty if no additional |
|
1125
|
** information is available. |
|
1126
|
*/ |
|
1127
|
static char *whyNotAuth(void){ |
|
1128
|
if( g.useLocalauth && db_get_boolean("localauth",0)!=0 ){ |
|
1129
|
return "\\sbecause\\sthe\\s'localauth'\\ssetting\\sis\\senabled"; |
|
1130
|
} |
|
1131
|
return ""; |
|
1132
|
} |
|
1133
|
|
|
1134
|
/* |
|
1135
|
** Called when there is an attempt to transfer private content to and |
|
1136
|
** from a server without authorization. |
|
1137
|
*/ |
|
1138
|
static void server_private_xfer_not_authorized(void){ |
|
1139
|
@ error not\sauthorized\sto\ssync\sprivate\scontent%s(whyNotAuth()) |
|
1140
|
} |
|
1141
|
|
|
1142
|
/* |
|
1143
|
** Return the common TH1 code to evaluate prior to evaluating any other |
|
1144
|
** TH1 transfer notification scripts. |
|
1145
|
*/ |
|
1146
|
const char *xfer_common_code(void){ |
|
1147
|
return db_get("xfer-common-script", 0); |
|
1148
|
} |
|
1149
|
|
|
1150
|
/* |
|
1151
|
** Return the TH1 code to evaluate when a push is processed. |
|
1152
|
*/ |
|
1153
|
const char *xfer_push_code(void){ |
|
1154
|
return db_get("xfer-push-script", 0); |
|
1155
|
} |
|
1156
|
|
|
1157
|
/* |
|
1158
|
** Return the TH1 code to evaluate when a commit is processed. |
|
1159
|
*/ |
|
1160
|
const char *xfer_commit_code(void){ |
|
1161
|
return db_get("xfer-commit-script", 0); |
|
1162
|
} |
|
1163
|
|
|
1164
|
/* |
|
1165
|
** Return the TH1 code to evaluate when a ticket change is processed. |
|
1166
|
*/ |
|
1167
|
const char *xfer_ticket_code(void){ |
|
1168
|
return db_get("xfer-ticket-script", 0); |
|
1169
|
} |
|
1170
|
|
|
1171
|
/* |
|
1172
|
** Reset the CGI content, roll back any pending db transaction, and |
|
1173
|
** emit an "error" xfer message. The message text gets fossil-encoded |
|
1174
|
** by this function. This is only intended for use with |
|
1175
|
** fail-fast/fatal errors, not ones which can be skipped over. |
|
1176
|
*/ |
|
1177
|
static void xfer_fatal_error(const char *zMsg){ |
|
1178
|
cgi_reset_content(); |
|
1179
|
if( db_transaction_nesting_depth()>0 ){ |
|
1180
|
db_rollback_transaction(); |
|
1181
|
} |
|
1182
|
@ error %F(zMsg) |
|
1183
|
} |
|
1184
|
|
|
1185
|
/* |
|
1186
|
** Run the specified TH1 script, if any, and returns 1 on error. |
|
1187
|
*/ |
|
1188
|
int xfer_run_script( |
|
1189
|
const char *zScript, |
|
1190
|
const char *zUuidOrList, |
|
1191
|
int bIsList |
|
1192
|
){ |
|
1193
|
int rc = TH_OK; |
|
1194
|
if( !zScript ) return rc; |
|
1195
|
Th_FossilInit(TH_INIT_DEFAULT); |
|
1196
|
Th_Store(bIsList ? "uuids" : "uuid", zUuidOrList ? zUuidOrList : ""); |
|
1197
|
rc = Th_Eval(g.interp, 0, zScript, -1); |
|
1198
|
if( rc!=TH_OK ){ |
|
1199
|
fossil_error(1, "%s", Th_GetResult(g.interp, 0)); |
|
1200
|
} |
|
1201
|
return rc; |
|
1202
|
} |
|
1203
|
|
|
1204
|
/* |
|
1205
|
** Runs the pre-transfer TH1 script, if any, and returns its return code. |
|
1206
|
** This script may be run multiple times. If the script performs actions |
|
1207
|
** that cannot be redone, it should use an internal [if] guard similar to |
|
1208
|
** the following: |
|
1209
|
** |
|
1210
|
** if {![info exists common_done]} { |
|
1211
|
** # ... code here |
|
1212
|
** set common_done 1 |
|
1213
|
** } |
|
1214
|
*/ |
|
1215
|
int xfer_run_common_script(void){ |
|
1216
|
return xfer_run_script(xfer_common_code(), 0, 0); |
|
1217
|
} |
|
1218
|
|
|
1219
|
/* |
|
1220
|
** This routine makes a "syncwith:URL" entry in the CONFIG table to |
|
1221
|
** indicate that a sync is occurring with zUrl. |
|
1222
|
** |
|
1223
|
** Add a "syncfrom:URL" entry instead of "syncwith:URL" if bSyncFrom is true. |
|
1224
|
*/ |
|
1225
|
static void xfer_syncwith(const char *zUrl, int bSyncFrom){ |
|
1226
|
UrlData x; |
|
1227
|
memset(&x, 0, sizeof(x)); |
|
1228
|
url_parse_local(zUrl, URL_OMIT_USER, &x); |
|
1229
|
if( x.protocol && strncmp(x.protocol,"http",4)==0 |
|
1230
|
&& x.name && sqlite3_strlike("%localhost%", x.name, 0)!=0 |
|
1231
|
){ |
|
1232
|
db_unprotect(PROTECT_CONFIG); |
|
1233
|
db_multi_exec("REPLACE INTO config(name,value,mtime)" |
|
1234
|
"VALUES('sync%q:%q','{}',now())", |
|
1235
|
bSyncFrom ? "from" : "with", x.canonical); |
|
1236
|
db_protect_pop(); |
|
1237
|
} |
|
1238
|
url_unparse(&x); |
|
1239
|
} |
|
1240
|
|
|
1241
|
/* |
|
1242
|
** If this variable is set, disable login checks. Used for debugging |
|
1243
|
** only. |
|
1244
|
*/ |
|
1245
|
static int disableLogin = 0; |
|
1246
|
|
|
1247
|
/* |
|
1248
|
** Must be passed the version info from pragmas |
|
1249
|
** client-version/server-version cards. If the version info is "new |
|
1250
|
** enough" then the loginCardMode is ORd into the X-Fossil-Xfer-Login |
|
1251
|
** card flag, else this is a no-op. |
|
1252
|
*/ |
|
1253
|
static void xfer_xflc_check(int iRemoteVersion, int iDate, int iTime, |
|
1254
|
int fLoginCardMode){ |
|
1255
|
if( iRemoteVersion>=22700 |
|
1256
|
&& (iDate > 20250727 |
|
1257
|
|| (iDate == 20250727 && iTime >= 110500)) ){ |
|
1258
|
g.syncInfo.fLoginCardMode |= fLoginCardMode; |
|
1259
|
} |
|
1260
|
} |
|
1261
|
|
|
1262
|
/* |
|
1263
|
** The CGI/HTTP preprocessor always redirects requests with a content-type |
|
1264
|
** of application/x-fossil or application/x-fossil-debug to this page, |
|
1265
|
** regardless of what path was specified in the HTTP header. This allows |
|
1266
|
** clone clients to specify a URL that omits default pathnames, such |
|
1267
|
** as "http://fossil-scm.org/" instead of "http://fossil-scm.org/index.cgi". |
|
1268
|
** |
|
1269
|
** WEBPAGE: xfer raw-content loadavg-exempt |
|
1270
|
** |
|
1271
|
** This is the transfer handler on the server side. The transfer |
|
1272
|
** message has been uncompressed and placed in the g.cgiIn blob. |
|
1273
|
** Process this message and form an appropriate reply. |
|
1274
|
*/ |
|
1275
|
void page_xfer(void){ |
|
1276
|
int isPull = 0; |
|
1277
|
int isPush = 0; |
|
1278
|
int nErr = 0; |
|
1279
|
Xfer xfer; |
|
1280
|
int deltaFlag = 0; |
|
1281
|
int isClone = 0; |
|
1282
|
int nGimme = 0; |
|
1283
|
int size; |
|
1284
|
char *zNow; |
|
1285
|
int rc; |
|
1286
|
const char *zScript = 0; |
|
1287
|
char *zUuidList = 0; |
|
1288
|
int nUuidList = 0; |
|
1289
|
char **pzUuidList = 0; |
|
1290
|
int *pnUuidList = 0; |
|
1291
|
int uvCatalogSent = 0; |
|
1292
|
int bSendLinks = 0; |
|
1293
|
int nLogin = 0; |
|
1294
|
|
|
1295
|
if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){ |
|
1296
|
fossil_redirect_home(); |
|
1297
|
} |
|
1298
|
g.zLogin = "anonymous"; |
|
1299
|
login_set_anon_nobody_capabilities(); |
|
1300
|
login_check_credentials(); |
|
1301
|
cgi_check_for_malice(); |
|
1302
|
memset(&xfer, 0, sizeof(xfer)); |
|
1303
|
blobarray_zero(xfer.aToken, count(xfer.aToken)); |
|
1304
|
cgi_set_content_type(g.zContentType); |
|
1305
|
cgi_reset_content(); |
|
1306
|
if( db_schema_is_outofdate() ){ |
|
1307
|
@ error database\sschema\sis\sout-of-date\son\sthe\sserver. |
|
1308
|
return; |
|
1309
|
} |
|
1310
|
blob_zero(&xfer.err); |
|
1311
|
xfer.pIn = &g.cgiIn; |
|
1312
|
xfer.pOut = cgi_output_blob(); |
|
1313
|
xfer.mxSend = db_get_int("max-download", 5000000); |
|
1314
|
xfer.maxTime = db_get_int("max-download-time", 30); |
|
1315
|
if( xfer.maxTime<1 ) xfer.maxTime = 1; |
|
1316
|
xfer.maxTime += time(NULL); |
|
1317
|
g.xferPanic = 1; |
|
1318
|
|
|
1319
|
db_begin_write(); |
|
1320
|
db_multi_exec( |
|
1321
|
"CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);" |
|
1322
|
"CREATE TEMP TABLE unk(uuid TEXT PRIMARY KEY) WITHOUT ROWID;" |
|
1323
|
); |
|
1324
|
manifest_crosslink_begin(); |
|
1325
|
rc = xfer_run_common_script(); |
|
1326
|
if( rc==TH_ERROR ){ |
|
1327
|
cgi_reset_content(); |
|
1328
|
@ error common\sscript\sfailed:\s%F(g.zErrMsg) |
|
1329
|
nErr++; |
|
1330
|
} |
|
1331
|
zScript = xfer_push_code(); |
|
1332
|
if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */ |
|
1333
|
pzUuidList = &zUuidList; |
|
1334
|
pnUuidList = &nUuidList; |
|
1335
|
} |
|
1336
|
if( g.syncInfo.zLoginCard ){ |
|
1337
|
/* Login card received via HTTP Cookie header */ |
|
1338
|
blob_zero(&xfer.line); |
|
1339
|
blob_append(&xfer.line, g.syncInfo.zLoginCard, -1); |
|
1340
|
xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, |
|
1341
|
count(xfer.aToken)); |
|
1342
|
fossil_free( g.syncInfo.zLoginCard ); |
|
1343
|
g.syncInfo.zLoginCard = 0; |
|
1344
|
if( xfer.nToken==4 |
|
1345
|
&& blob_eq(&xfer.aToken[0], "login") ){ |
|
1346
|
goto handle_login_card; |
|
1347
|
} |
|
1348
|
} |
|
1349
|
while( blob_line(xfer.pIn, &xfer.line) ){ |
|
1350
|
if( blob_buffer(&xfer.line)[0]=='#' ) continue; |
|
1351
|
if( blob_size(&xfer.line)==0 ) continue; |
|
1352
|
xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); |
|
1353
|
|
|
1354
|
/* file HASH SIZE \n CONTENT |
|
1355
|
** file HASH DELTASRC SIZE \n CONTENT |
|
1356
|
** |
|
1357
|
** Server accepts a file from the client. |
|
1358
|
*/ |
|
1359
|
if( blob_eq(&xfer.aToken[0], "file") ){ |
|
1360
|
if( !isPush ){ |
|
1361
|
cgi_reset_content(); |
|
1362
|
@ error not\sauthorized\sto\swrite%s(whyNotAuth()) |
|
1363
|
nErr++; |
|
1364
|
break; |
|
1365
|
} |
|
1366
|
xfer_accept_file(&xfer, 0, pzUuidList, pnUuidList); |
|
1367
|
if( blob_size(&xfer.err) ){ |
|
1368
|
cgi_reset_content(); |
|
1369
|
@ error %T(blob_str(&xfer.err)) |
|
1370
|
nErr++; |
|
1371
|
break; |
|
1372
|
} |
|
1373
|
}else |
|
1374
|
|
|
1375
|
/* cfile HASH USIZE CSIZE \n CONTENT |
|
1376
|
** cfile HASH DELTASRC USIZE CSIZE \n CONTENT |
|
1377
|
** |
|
1378
|
** Server accepts a compressed file from the client. |
|
1379
|
*/ |
|
1380
|
if( blob_eq(&xfer.aToken[0], "cfile") ){ |
|
1381
|
if( !isPush ){ |
|
1382
|
cgi_reset_content(); |
|
1383
|
@ error not\sauthorized\sto\swrite%s(whyNotAuth()) |
|
1384
|
nErr++; |
|
1385
|
break; |
|
1386
|
} |
|
1387
|
xfer_accept_compressed_file(&xfer, pzUuidList, pnUuidList); |
|
1388
|
if( blob_size(&xfer.err) ){ |
|
1389
|
cgi_reset_content(); |
|
1390
|
@ error %T(blob_str(&xfer.err)) |
|
1391
|
nErr++; |
|
1392
|
break; |
|
1393
|
} |
|
1394
|
}else |
|
1395
|
|
|
1396
|
/* uvfile NAME MTIME HASH SIZE FLAGS \n CONTENT |
|
1397
|
** |
|
1398
|
** Server accepts an unversioned file from the client. |
|
1399
|
*/ |
|
1400
|
if( blob_eq(&xfer.aToken[0], "uvfile") ){ |
|
1401
|
xfer_accept_unversioned_file(&xfer, g.perm.WrUnver); |
|
1402
|
if( blob_size(&xfer.err) ){ |
|
1403
|
cgi_reset_content(); |
|
1404
|
@ error %T(blob_str(&xfer.err)) |
|
1405
|
fossil_print("%%%%%%%% xfer.err: '%s'\n", blob_str(&xfer.err)); |
|
1406
|
nErr++; |
|
1407
|
break; |
|
1408
|
} |
|
1409
|
}else |
|
1410
|
|
|
1411
|
/* gimme HASH |
|
1412
|
** |
|
1413
|
** Client is requesting a file from the server. Send it. |
|
1414
|
*/ |
|
1415
|
if( blob_eq(&xfer.aToken[0], "gimme") |
|
1416
|
&& xfer.nToken==2 |
|
1417
|
&& blob_is_hname(&xfer.aToken[1]) |
|
1418
|
){ |
|
1419
|
nGimme++; |
|
1420
|
remote_unk(&xfer.aToken[1]); |
|
1421
|
if( isPull ){ |
|
1422
|
int rid = rid_from_uuid(&xfer.aToken[1], 0, 0); |
|
1423
|
if( rid ){ |
|
1424
|
send_file(&xfer, rid, &xfer.aToken[1], deltaFlag); |
|
1425
|
} |
|
1426
|
} |
|
1427
|
}else |
|
1428
|
|
|
1429
|
/* uvgimme NAME |
|
1430
|
** |
|
1431
|
** Client is requesting an unversioned file from the server. Send it. |
|
1432
|
*/ |
|
1433
|
if( blob_eq(&xfer.aToken[0], "uvgimme") |
|
1434
|
&& xfer.nToken==2 |
|
1435
|
&& blob_is_filename(&xfer.aToken[1]) |
|
1436
|
){ |
|
1437
|
send_unversioned_file(&xfer, blob_str(&xfer.aToken[1]), 0); |
|
1438
|
}else |
|
1439
|
|
|
1440
|
/* igot HASH ?ISPRIVATE? |
|
1441
|
** |
|
1442
|
** Client announces that it has a particular file. If the ISPRIVATE |
|
1443
|
** argument exists and is "1", then the file is a private file. |
|
1444
|
*/ |
|
1445
|
if( xfer.nToken>=2 |
|
1446
|
&& blob_eq(&xfer.aToken[0], "igot") |
|
1447
|
&& blob_is_hname(&xfer.aToken[1]) |
|
1448
|
){ |
|
1449
|
if( isPush ){ |
|
1450
|
int rid = 0; |
|
1451
|
int isPriv = 0; |
|
1452
|
if( xfer.nToken==2 || blob_eq(&xfer.aToken[2],"1")==0 ){ |
|
1453
|
/* Client says the artifact is public */ |
|
1454
|
rid = rid_from_uuid(&xfer.aToken[1], 1, 0); |
|
1455
|
}else if( g.perm.Private ){ |
|
1456
|
/* Client says the artifact is private and the client has |
|
1457
|
** permission to push private content. Create a new phantom |
|
1458
|
** artifact that is marked private. */ |
|
1459
|
rid = rid_from_uuid(&xfer.aToken[1], 1, 1); |
|
1460
|
isPriv = 1; |
|
1461
|
}else{ |
|
1462
|
/* Client says the artifact is private and the client is unable |
|
1463
|
** or unwilling to send us the artifact. If we already hold the |
|
1464
|
** artifact here on the server as a phantom, make sure that |
|
1465
|
** phantom is marked as private so that we don't keep asking about |
|
1466
|
** it in subsequent sync requests. */ |
|
1467
|
rid = rid_from_uuid(&xfer.aToken[1], 0, 1); |
|
1468
|
isPriv = 1; |
|
1469
|
} |
|
1470
|
if( rid ){ |
|
1471
|
remote_has(rid); |
|
1472
|
if( isPriv ){ |
|
1473
|
content_make_private(rid); |
|
1474
|
}else{ |
|
1475
|
content_make_public(rid); |
|
1476
|
} |
|
1477
|
} |
|
1478
|
} |
|
1479
|
}else |
|
1480
|
|
|
1481
|
|
|
1482
|
/* pull SERVERCODE PROJECTCODE |
|
1483
|
** push SERVERCODE PROJECTCODE |
|
1484
|
** |
|
1485
|
** The client wants either send or receive. The server should |
|
1486
|
** verify that the project code matches. The server code is ignored. |
|
1487
|
*/ |
|
1488
|
if( xfer.nToken==3 |
|
1489
|
&& (blob_eq(&xfer.aToken[0], "pull") || blob_eq(&xfer.aToken[0], "push")) |
|
1490
|
&& blob_is_hname(&xfer.aToken[2]) |
|
1491
|
){ |
|
1492
|
const char *zPCode; |
|
1493
|
zPCode = db_get("project-code", 0); |
|
1494
|
if( zPCode==0 ){ |
|
1495
|
fossil_fatal("missing project code"); |
|
1496
|
} |
|
1497
|
if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){ |
|
1498
|
cgi_reset_content(); |
|
1499
|
@ error wrong\sproject |
|
1500
|
nErr++; |
|
1501
|
break; |
|
1502
|
} |
|
1503
|
login_check_credentials(); |
|
1504
|
if( blob_eq(&xfer.aToken[0], "pull") ){ |
|
1505
|
if( !g.perm.Read ){ |
|
1506
|
cgi_reset_content(); |
|
1507
|
@ error not\sauthorized\sto\sread%s(whyNotAuth()) |
|
1508
|
nErr++; |
|
1509
|
break; |
|
1510
|
} |
|
1511
|
isPull = 1; |
|
1512
|
}else{ |
|
1513
|
if( !g.perm.Write ){ |
|
1514
|
if( !isPull ){ |
|
1515
|
cgi_reset_content(); |
|
1516
|
@ error not\sauthorized\sto\swrite%s(whyNotAuth()) |
|
1517
|
nErr++; |
|
1518
|
}else{ |
|
1519
|
@ message pull\sonly\s-\snot\sauthorized\sto\spush%s(whyNotAuth()) |
|
1520
|
} |
|
1521
|
}else{ |
|
1522
|
isPush = 1; |
|
1523
|
} |
|
1524
|
} |
|
1525
|
}else |
|
1526
|
|
|
1527
|
/* clone ?PROTOCOL-VERSION? ?SEQUENCE-NUMBER? |
|
1528
|
** |
|
1529
|
** The client knows nothing. Tell all. |
|
1530
|
*/ |
|
1531
|
if( blob_eq(&xfer.aToken[0], "clone") ){ |
|
1532
|
int iVers; |
|
1533
|
login_check_credentials(); |
|
1534
|
if( !g.perm.Clone ){ |
|
1535
|
cgi_reset_content(); |
|
1536
|
@ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x")) |
|
1537
|
@ error not\sauthorized\sto\sclone%s(whyNotAuth()) |
|
1538
|
nErr++; |
|
1539
|
break; |
|
1540
|
} |
|
1541
|
if( db_get_boolean("uv-sync",0) && !uvCatalogSent ){ |
|
1542
|
@ pragma uv-pull-only |
|
1543
|
send_unversioned_catalog(&xfer); |
|
1544
|
uvCatalogSent = 1; |
|
1545
|
} |
|
1546
|
if( xfer.nToken==3 |
|
1547
|
&& blob_is_int(&xfer.aToken[1], &iVers) |
|
1548
|
&& iVers>=2 |
|
1549
|
){ |
|
1550
|
int seqno, max; |
|
1551
|
if( iVers>=3 ){ |
|
1552
|
cgi_set_content_type("application/x-fossil-uncompressed"); |
|
1553
|
} |
|
1554
|
blob_is_int(&xfer.aToken[2], &seqno); |
|
1555
|
if( seqno<=0 ){ |
|
1556
|
xfer_fatal_error("invalid clone sequence number"); |
|
1557
|
db_rollback_transaction(); |
|
1558
|
return; |
|
1559
|
} |
|
1560
|
max = db_int(0, "SELECT max(rid) FROM blob"); |
|
1561
|
while( xfer.mxSend>(int)blob_size(xfer.pOut) && seqno<=max){ |
|
1562
|
if( time(NULL) >= xfer.maxTime ) break; |
|
1563
|
if( iVers>=3 ){ |
|
1564
|
send_compressed_file(&xfer, seqno); |
|
1565
|
}else{ |
|
1566
|
send_file(&xfer, seqno, 0, 1); |
|
1567
|
} |
|
1568
|
seqno++; |
|
1569
|
} |
|
1570
|
if( seqno>max ) seqno = 0; |
|
1571
|
@ clone_seqno %d(seqno) |
|
1572
|
}else{ |
|
1573
|
isClone = 1; |
|
1574
|
isPull = 1; |
|
1575
|
deltaFlag = 1; |
|
1576
|
} |
|
1577
|
@ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x")) |
|
1578
|
}else |
|
1579
|
|
|
1580
|
/* login USER NONCE SIGNATURE |
|
1581
|
** |
|
1582
|
** The client has sent login credentials to the server. |
|
1583
|
** Validate the login. This has to happen before anything else. |
|
1584
|
** |
|
1585
|
** For many years, Fossil would accept multiple login cards with |
|
1586
|
** cumulative permissions. But that feature was never used. Hence |
|
1587
|
** it is now prohibited. Any login card after the first generates |
|
1588
|
** a fatal error. |
|
1589
|
*/ |
|
1590
|
if( blob_eq(&xfer.aToken[0], "login") |
|
1591
|
&& xfer.nToken==4 |
|
1592
|
){ |
|
1593
|
handle_login_card: |
|
1594
|
nLogin++; |
|
1595
|
if( disableLogin ){ |
|
1596
|
g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1; |
|
1597
|
}else if( nLogin > 1 ){ |
|
1598
|
cgi_reset_content(); |
|
1599
|
@ error multiple\slogin\cards |
|
1600
|
nErr++; |
|
1601
|
break; |
|
1602
|
}else{ |
|
1603
|
if( check_tail_hash(&xfer.aToken[2], xfer.pIn) |
|
1604
|
|| check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) |
|
1605
|
){ |
|
1606
|
cgi_reset_content(); |
|
1607
|
@ error login\sfailed |
|
1608
|
nErr++; |
|
1609
|
break; |
|
1610
|
} |
|
1611
|
} |
|
1612
|
}else |
|
1613
|
|
|
1614
|
/* reqconfig NAME |
|
1615
|
** |
|
1616
|
** Client is requesting a configuration value from the server |
|
1617
|
*/ |
|
1618
|
if( blob_eq(&xfer.aToken[0], "reqconfig") |
|
1619
|
&& xfer.nToken==2 |
|
1620
|
){ |
|
1621
|
if( g.perm.Read ){ |
|
1622
|
char *zName = blob_str(&xfer.aToken[1]); |
|
1623
|
if( zName[0]=='/' ){ |
|
1624
|
/* New style configuration transfer */ |
|
1625
|
int groupMask = configure_name_to_mask(&zName[1], 0); |
|
1626
|
if( !g.perm.Admin ) groupMask &= ~(CONFIGSET_USER|CONFIGSET_SCRIBER); |
|
1627
|
if( !g.perm.RdAddr ) groupMask &= ~CONFIGSET_ADDR; |
|
1628
|
configure_send_group(xfer.pOut, groupMask, 0); |
|
1629
|
} |
|
1630
|
} |
|
1631
|
}else |
|
1632
|
|
|
1633
|
/* config NAME SIZE \n CONTENT |
|
1634
|
** |
|
1635
|
** Client has sent a configuration value to the server. |
|
1636
|
** This is only permitted for high-privilege users. |
|
1637
|
*/ |
|
1638
|
if( blob_eq(&xfer.aToken[0],"config") && xfer.nToken==3 |
|
1639
|
&& blob_is_int(&xfer.aToken[2], &size) ){ |
|
1640
|
const char *zName = blob_str(&xfer.aToken[1]); |
|
1641
|
Blob content; |
|
1642
|
if( size<0 ){ |
|
1643
|
xfer_fatal_error("invalid config record"); |
|
1644
|
db_rollback_transaction(); |
|
1645
|
return; |
|
1646
|
} |
|
1647
|
blob_zero(&content); |
|
1648
|
blob_extract(xfer.pIn, size, &content); |
|
1649
|
if( !g.perm.Admin ){ |
|
1650
|
cgi_reset_content(); |
|
1651
|
@ error not\sauthorized\sto\spush\sconfiguration%s(whyNotAuth()) |
|
1652
|
nErr++; |
|
1653
|
break; |
|
1654
|
} |
|
1655
|
configure_receive(zName, &content, CONFIGSET_ALL); |
|
1656
|
blob_reset(&content); |
|
1657
|
blob_seek(xfer.pIn, 1, BLOB_SEEK_CUR); |
|
1658
|
}else |
|
1659
|
|
|
1660
|
|
|
1661
|
/* cookie TEXT |
|
1662
|
** |
|
1663
|
** A cookie contains an arbitrary-length argument that is server-defined. |
|
1664
|
** The argument must be encoded so as not to contain any whitespace. |
|
1665
|
** The server can optionally send a cookie to the client. The client |
|
1666
|
** might then return the same cookie back to the server on its next |
|
1667
|
** communication. The cookie might record information that helps |
|
1668
|
** the server optimize a push or pull. |
|
1669
|
** |
|
1670
|
** The client is not required to return a cookie. So the server |
|
1671
|
** must not depend on the cookie. The cookie should be an optimization |
|
1672
|
** only. The client might also send a cookie that came from a different |
|
1673
|
** server. So the server must be prepared to distinguish its own cookie |
|
1674
|
** from cookies originating from other servers. The client might send |
|
1675
|
** back several different cookies to the server. The server should be |
|
1676
|
** prepared to sift through the cookies and pick the one that it wants. |
|
1677
|
*/ |
|
1678
|
if( blob_eq(&xfer.aToken[0], "cookie") && xfer.nToken==2 ){ |
|
1679
|
/* Process the cookie */ |
|
1680
|
}else |
|
1681
|
|
|
1682
|
|
|
1683
|
/* private |
|
1684
|
** |
|
1685
|
** The client card indicates that the next "file" or "cfile" will contain |
|
1686
|
** private content. |
|
1687
|
*/ |
|
1688
|
if( blob_eq(&xfer.aToken[0], "private") ){ |
|
1689
|
if( !g.perm.Private ){ |
|
1690
|
server_private_xfer_not_authorized(); |
|
1691
|
}else{ |
|
1692
|
xfer.nextIsPrivate = 1; |
|
1693
|
} |
|
1694
|
}else |
|
1695
|
|
|
1696
|
/* pragma NAME VALUE... |
|
1697
|
** |
|
1698
|
** The client issues pragmas to try to influence the behavior of the |
|
1699
|
** server. These are requests only. Unknown pragmas are silently |
|
1700
|
** ignored. |
|
1701
|
*/ |
|
1702
|
if( blob_eq(&xfer.aToken[0], "pragma") && xfer.nToken>=2 ){ |
|
1703
|
|
|
1704
|
/* pragma send-private |
|
1705
|
** |
|
1706
|
** The client is requesting private artifacts. |
|
1707
|
** |
|
1708
|
** If the user has the "x" privilege (which must be set explicitly - |
|
1709
|
** it is not automatic with "a" or "s") then this pragma causes |
|
1710
|
** private information to be pulled in addition to public records. |
|
1711
|
*/ |
|
1712
|
if( blob_eq(&xfer.aToken[1], "send-private") ){ |
|
1713
|
login_check_credentials(); |
|
1714
|
if( !g.perm.Private ){ |
|
1715
|
server_private_xfer_not_authorized(); |
|
1716
|
}else{ |
|
1717
|
xfer.syncPrivate = 1; |
|
1718
|
} |
|
1719
|
}else |
|
1720
|
|
|
1721
|
/* pragma send-catalog |
|
1722
|
** |
|
1723
|
** The client wants to see igot cards for all known artifacts. |
|
1724
|
** This is used as part of "sync --verily" to help ensure that |
|
1725
|
** no artifacts have been missed on prior syncs. |
|
1726
|
*/ |
|
1727
|
if( blob_eq(&xfer.aToken[1], "send-catalog") ){ |
|
1728
|
xfer.resync = 0x7fffffff; |
|
1729
|
}else |
|
1730
|
|
|
1731
|
/* pragma client-version VERSION ?DATE? ?TIME? |
|
1732
|
** |
|
1733
|
** The client announces to the server what version of Fossil it |
|
1734
|
** is running. The DATE and TIME are a pure numeric ISO8601 time |
|
1735
|
** for the specific check-in of the client. |
|
1736
|
*/ |
|
1737
|
if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){ |
|
1738
|
xfer.remoteVersion = g.syncInfo.remoteVersion = |
|
1739
|
atoi(blob_str(&xfer.aToken[2])); |
|
1740
|
if( xfer.nToken>=5 ){ |
|
1741
|
xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
|
1742
|
xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
|
1743
|
@ pragma server-version %d(RELEASE_VERSION_NUMBER) \ |
|
1744
|
@ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME) |
|
1745
|
} |
|
1746
|
xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, |
|
1747
|
xfer.remoteTime, 0x04 ); |
|
1748
|
}else |
|
1749
|
|
|
1750
|
/* pragma uv-hash HASH |
|
1751
|
** |
|
1752
|
** The client wants to make sure that unversioned files are all synced. |
|
1753
|
** If the HASH does not match, send a complete catalog of |
|
1754
|
** "uvigot" cards. |
|
1755
|
*/ |
|
1756
|
if( blob_eq(&xfer.aToken[1], "uv-hash") |
|
1757
|
&& blob_is_hname(&xfer.aToken[2]) |
|
1758
|
){ |
|
1759
|
if( !uvCatalogSent |
|
1760
|
&& g.perm.Read |
|
1761
|
&& !blob_eq_str(&xfer.aToken[2], unversioned_content_hash(0),-1) |
|
1762
|
){ |
|
1763
|
if( g.perm.WrUnver ){ |
|
1764
|
@ pragma uv-push-ok |
|
1765
|
}else if( g.perm.Read ){ |
|
1766
|
@ pragma uv-pull-only |
|
1767
|
} |
|
1768
|
send_unversioned_catalog(&xfer); |
|
1769
|
} |
|
1770
|
uvCatalogSent = 1; |
|
1771
|
}else |
|
1772
|
|
|
1773
|
/* pragma ci-lock CHECKIN-HASH CLIENT-ID |
|
1774
|
** |
|
1775
|
** The client wants to make non-branch commit against the check-in |
|
1776
|
** identified by CHECKIN-HASH. The server will remember this and |
|
1777
|
** subsequent ci-lock requests from different clients will generate |
|
1778
|
** a ci-lock-fail pragma in the reply. |
|
1779
|
*/ |
|
1780
|
if( blob_eq(&xfer.aToken[1], "ci-lock") |
|
1781
|
&& xfer.nToken==4 |
|
1782
|
&& blob_is_hname(&xfer.aToken[2]) |
|
1783
|
){ |
|
1784
|
Stmt q; |
|
1785
|
sqlite3_int64 iNow = time(0); |
|
1786
|
sqlite3_int64 maxAge = db_get_int("lock-timeout",60); |
|
1787
|
int seenFault = 0; |
|
1788
|
db_prepare(&q, |
|
1789
|
"SELECT value->>'login'," |
|
1790
|
" mtime," |
|
1791
|
" value->>'clientid'," |
|
1792
|
" (SELECT rid FROM blob WHERE uuid=substr(name,9))," |
|
1793
|
" name" |
|
1794
|
" FROM config" |
|
1795
|
" WHERE name GLOB 'ci-lock-*'" |
|
1796
|
" AND json_valid(value)" |
|
1797
|
); |
|
1798
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1799
|
int x = db_column_int(&q,3); |
|
1800
|
const char *zName = db_column_text(&q,4); |
|
1801
|
if( db_column_int64(&q,1)<=iNow-maxAge || !is_a_leaf(x) ){ |
|
1802
|
/* check-in locks expire after maxAge seconds, or when the |
|
1803
|
** check-in is no longer a leaf */ |
|
1804
|
db_unprotect(PROTECT_CONFIG); |
|
1805
|
db_multi_exec("DELETE FROM config WHERE name=%Q", zName); |
|
1806
|
db_protect_pop(); |
|
1807
|
continue; |
|
1808
|
} |
|
1809
|
if( fossil_strcmp(zName+8, blob_str(&xfer.aToken[2]))==0 ){ |
|
1810
|
const char *zClientId = db_column_text(&q, 2); |
|
1811
|
const char *zLogin = db_column_text(&q,0); |
|
1812
|
sqlite3_int64 mtime = db_column_int64(&q, 1); |
|
1813
|
if( fossil_strcmp(zClientId, blob_str(&xfer.aToken[3]))!=0 ){ |
|
1814
|
@ pragma ci-lock-fail %F(zLogin) %lld(mtime) |
|
1815
|
} |
|
1816
|
seenFault = 1; |
|
1817
|
} |
|
1818
|
} |
|
1819
|
db_finalize(&q); |
|
1820
|
if( !seenFault ){ |
|
1821
|
db_unprotect(PROTECT_CONFIG); |
|
1822
|
db_multi_exec( |
|
1823
|
"REPLACE INTO config(name,value,mtime)" |
|
1824
|
"VALUES('ci-lock-%q',json_object('login',%Q,'clientid',%Q),now())", |
|
1825
|
blob_str(&xfer.aToken[2]), g.zLogin, |
|
1826
|
blob_str(&xfer.aToken[3]) |
|
1827
|
); |
|
1828
|
db_protect_pop(); |
|
1829
|
} |
|
1830
|
if( db_get_boolean("forbid-delta-manifests",0) ){ |
|
1831
|
@ pragma avoid-delta-manifests |
|
1832
|
} |
|
1833
|
}else |
|
1834
|
|
|
1835
|
/* pragma ci-unlock CLIENT-ID |
|
1836
|
** |
|
1837
|
** Remove any locks previously held by CLIENT-ID. Clients send this |
|
1838
|
** pragma with their own ID whenever they know that they no longer |
|
1839
|
** have any commits pending. |
|
1840
|
*/ |
|
1841
|
if( blob_eq(&xfer.aToken[1], "ci-unlock") |
|
1842
|
&& xfer.nToken==3 |
|
1843
|
&& blob_is_hname(&xfer.aToken[2]) |
|
1844
|
){ |
|
1845
|
db_unprotect(PROTECT_CONFIG); |
|
1846
|
db_multi_exec( |
|
1847
|
"DELETE FROM config" |
|
1848
|
" WHERE name GLOB 'ci-lock-*'" |
|
1849
|
" AND (NOT json_valid(value) OR value->>'clientid'==%Q)", |
|
1850
|
blob_str(&xfer.aToken[2]) |
|
1851
|
); |
|
1852
|
db_protect_pop(); |
|
1853
|
}else |
|
1854
|
|
|
1855
|
/* pragma client-url URL |
|
1856
|
** |
|
1857
|
** This pragma is an informational notification to the server that |
|
1858
|
** their relationship could, in theory, be inverted by having the |
|
1859
|
** server call the client at URL. |
|
1860
|
*/ |
|
1861
|
if( blob_eq(&xfer.aToken[1], "client-url") |
|
1862
|
&& xfer.nToken==3 |
|
1863
|
&& g.perm.Write |
|
1864
|
){ |
|
1865
|
xfer_syncwith(blob_str(&xfer.aToken[2]), 1); |
|
1866
|
}else |
|
1867
|
|
|
1868
|
/* pragma req-links |
|
1869
|
** |
|
1870
|
** The client sends this message to the server to ask the server |
|
1871
|
** to tell it about alternative repositories in the reply. |
|
1872
|
*/ |
|
1873
|
if( blob_eq(&xfer.aToken[1], "req-links") ){ |
|
1874
|
bSendLinks = 1; |
|
1875
|
}else |
|
1876
|
|
|
1877
|
/* pragma req-clusters |
|
1878
|
** |
|
1879
|
** This pragma requests that the server send igot cards for every |
|
1880
|
** cluster artifact that it knows about. |
|
1881
|
*/ |
|
1882
|
if( blob_eq(&xfer.aToken[1], "req-clusters") ){ |
|
1883
|
send_all_clusters(&xfer); |
|
1884
|
} |
|
1885
|
|
|
1886
|
}else |
|
1887
|
|
|
1888
|
/* Unknown message |
|
1889
|
*/ |
|
1890
|
{ |
|
1891
|
cgi_reset_content(); |
|
1892
|
@ error bad\scommand:\s%F(blob_str(&xfer.line)) |
|
1893
|
} |
|
1894
|
blobarray_reset(xfer.aToken, xfer.nToken); |
|
1895
|
blob_reset(&xfer.line); |
|
1896
|
} |
|
1897
|
if( isPush ){ |
|
1898
|
if( rc==TH_OK ){ |
|
1899
|
rc = xfer_run_script(zScript, zUuidList, 1); |
|
1900
|
if( rc==TH_ERROR ){ |
|
1901
|
cgi_reset_content(); |
|
1902
|
@ error push\sscript\sfailed:\s%F(g.zErrMsg) |
|
1903
|
nErr++; |
|
1904
|
} |
|
1905
|
} |
|
1906
|
request_phantoms(&xfer, 500); |
|
1907
|
} |
|
1908
|
if( zUuidList ){ |
|
1909
|
Th_Free(g.interp, zUuidList); |
|
1910
|
} |
|
1911
|
if( isClone && nGimme==0 ){ |
|
1912
|
/* The initial "clone" message from client to server contains no |
|
1913
|
** "gimme" cards. On that initial message, send the client an "igot" |
|
1914
|
** card for every artifact currently in the repository. This will |
|
1915
|
** cause the client to create phantoms for all artifacts, which will |
|
1916
|
** in turn make sure that the entire repository is sent efficiently |
|
1917
|
** and expeditiously. |
|
1918
|
*/ |
|
1919
|
send_all(&xfer); |
|
1920
|
if( xfer.syncPrivate ) send_private(&xfer); |
|
1921
|
}else if( isPull ){ |
|
1922
|
create_cluster(); |
|
1923
|
send_unclustered(&xfer); |
|
1924
|
if( xfer.syncPrivate ) send_private(&xfer); |
|
1925
|
} |
|
1926
|
hook_expecting_more_artifacts(xfer.nGimmeSent?60:0); |
|
1927
|
db_multi_exec("DROP TABLE onremote; DROP TABLE unk;"); |
|
1928
|
manifest_crosslink_end(MC_PERMIT_HOOKS); |
|
1929
|
|
|
1930
|
/* Send URLs for alternative repositories for the same project, |
|
1931
|
** if requested by the client. */ |
|
1932
|
if( bSendLinks && g.zBaseURL ){ |
|
1933
|
Stmt q; |
|
1934
|
db_prepare(&q, |
|
1935
|
"WITH remote(mtime, url, arg) AS (\n" |
|
1936
|
" SELECT mtime, substr(name,10), '{}' FROM config\n" |
|
1937
|
" WHERE name GLOB 'syncwith:http*'\n" |
|
1938
|
" UNION ALL\n" |
|
1939
|
" SELECT mtime, substr(name,10), '{}' FROM config\n" |
|
1940
|
" WHERE name GLOB 'syncfrom:http*'\n" |
|
1941
|
" UNION ALL\n" |
|
1942
|
" SELECT mtime, substr(name,9), '{\"type\":\"git\"}' FROM config\n" |
|
1943
|
" WHERE name GLOB 'gitpush:*'\n" |
|
1944
|
")\n" |
|
1945
|
"SELECT url, json_insert(arg,'$.src',%Q), max(mtime)\n" |
|
1946
|
" FROM remote WHERE mtime>unixepoch('now','-1 month')\n" |
|
1947
|
" GROUP BY url;", |
|
1948
|
g.zBaseURL |
|
1949
|
); |
|
1950
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1951
|
UrlData x; |
|
1952
|
const char *zUrl = db_column_text(&q, 0); |
|
1953
|
const char *zArg = db_column_text(&q, 1); |
|
1954
|
i64 iMtime = db_column_int64(&q, 2); |
|
1955
|
memset(&x, 0, sizeof(x)); |
|
1956
|
url_parse_local(zUrl, URL_OMIT_USER, &x); |
|
1957
|
if( x.name!=0 && sqlite3_strlike("%localhost%", x.name, 0)!=0 ){ |
|
1958
|
@ pragma link %F(x.canonical) %F(zArg) %lld(iMtime) |
|
1959
|
} |
|
1960
|
url_unparse(&x); |
|
1961
|
} |
|
1962
|
db_finalize(&q); |
|
1963
|
} |
|
1964
|
|
|
1965
|
/* Send the server timestamp last, in case prior processing happened |
|
1966
|
** to use up a significant fraction of our time window. |
|
1967
|
*/ |
|
1968
|
zNow = db_text(0, "SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%S', 'now')"); |
|
1969
|
@ # timestamp %s(zNow) errors %d(nErr) |
|
1970
|
fossil_free(zNow); |
|
1971
|
|
|
1972
|
db_commit_transaction(); |
|
1973
|
configure_rebuild(); |
|
1974
|
} |
|
1975
|
|
|
1976
|
/* |
|
1977
|
** COMMAND: test-xfer |
|
1978
|
** |
|
1979
|
** Usage: %fossil test-xfer ?OPTIONS? XFERFILE |
|
1980
|
** |
|
1981
|
** Pass the sync-protocol input file XFERFILE into the server-side sync |
|
1982
|
** protocol handler. Generate a reply on standard output. |
|
1983
|
** |
|
1984
|
** This command was original created to help debug the server side of |
|
1985
|
** sync messages. The XFERFILE is the uncompressed content of an |
|
1986
|
** "xfer" HTTP request from client to server. This command interprets |
|
1987
|
** that message and generates the content of an HTTP reply (without any |
|
1988
|
** encoding and without the HTTP reply headers) and writes that reply |
|
1989
|
** on standard output. |
|
1990
|
** |
|
1991
|
** One possible usages scenario is to capture some XFERFILE examples |
|
1992
|
** using a command like: |
|
1993
|
** |
|
1994
|
** fossil push http://bogus/ --httptrace |
|
1995
|
** |
|
1996
|
** The complete HTTP requests are stored in files named "http-request-N.txt". |
|
1997
|
** Find one of those requests, remove the HTTP header, and make other edits |
|
1998
|
** as necessary to generate an appropriate XFERFILE test case. Then run: |
|
1999
|
** |
|
2000
|
** fossil test-xfer xferfile.txt |
|
2001
|
** |
|
2002
|
** Options: |
|
2003
|
** --host HOSTNAME Supply a server hostname used to populate |
|
2004
|
** g.zBaseURL and similar. |
|
2005
|
*/ |
|
2006
|
void cmd_test_xfer(void){ |
|
2007
|
const char *zHost; |
|
2008
|
db_find_and_open_repository(0,0); |
|
2009
|
zHost = find_option("host",0,1); |
|
2010
|
verify_all_options(); |
|
2011
|
if( g.argc!=2 && g.argc!=3 ){ |
|
2012
|
usage("?MESSAGEFILE?"); |
|
2013
|
} |
|
2014
|
if( zHost==0 ) zHost = "localhost:8080"; |
|
2015
|
g.zBaseURL = mprintf("http://%s", zHost); |
|
2016
|
g.zHttpsURL = mprintf("https://%s", zHost); |
|
2017
|
g.zTop = mprintf(""); |
|
2018
|
blob_zero(&g.cgiIn); |
|
2019
|
blob_read_from_file(&g.cgiIn, g.argc==2 ? "-" : g.argv[2], ExtFILE); |
|
2020
|
disableLogin = 1; |
|
2021
|
page_xfer(); |
|
2022
|
fossil_print("%s", cgi_extract_content()); |
|
2023
|
} |
|
2024
|
|
|
2025
|
/* |
|
2026
|
** Format strings for progress reporting. |
|
2027
|
*/ |
|
2028
|
static const char zLabelFormat[] = "%-10s %10s %10s %10s %10s\n"; |
|
2029
|
static const char zValueFormat[] = "\r%-10s %10d %10d %10d %10d\n"; |
|
2030
|
static const char zBriefFormat[] = |
|
2031
|
"Round-trips: %d Artifacts sent: %d received: %d\r"; |
|
2032
|
|
|
2033
|
#if INTERFACE |
|
2034
|
/* |
|
2035
|
** Flag options for controlling client_sync() |
|
2036
|
*/ |
|
2037
|
#define SYNC_PUSH 0x00001 /* push content client to server */ |
|
2038
|
#define SYNC_PULL 0x00002 /* pull content server to client */ |
|
2039
|
#define SYNC_CLONE 0x00004 /* clone the repository */ |
|
2040
|
#define SYNC_PRIVATE 0x00008 /* Also transfer private content */ |
|
2041
|
#define SYNC_VERBOSE 0x00010 /* Extra diagnostics */ |
|
2042
|
#define SYNC_RESYNC 0x00020 /* --verily */ |
|
2043
|
#define SYNC_FROMPARENT 0x00040 /* Pull from the parent project */ |
|
2044
|
#define SYNC_UNVERSIONED 0x00100 /* Sync unversioned content */ |
|
2045
|
#define SYNC_UV_REVERT 0x00200 /* Copy server unversioned to client */ |
|
2046
|
#define SYNC_UV_TRACE 0x00400 /* Describe UV activities */ |
|
2047
|
#define SYNC_UV_DRYRUN 0x00800 /* Do not actually exchange files */ |
|
2048
|
#define SYNC_IFABLE 0x01000 /* Inability to sync is not fatal */ |
|
2049
|
#define SYNC_CKIN_LOCK 0x02000 /* Lock the current check-in */ |
|
2050
|
#define SYNC_NOHTTPCOMPRESS 0x04000 /* Do not compression HTTP messages */ |
|
2051
|
#define SYNC_ALLURL 0x08000 /* The --all flag - sync to all URLs */ |
|
2052
|
#define SYNC_SHARE_LINKS 0x10000 /* Request alternate repo links */ |
|
2053
|
#define SYNC_XVERBOSE 0x20000 /* Extra verbose. Network traffic */ |
|
2054
|
#define SYNC_PING 0x40000 /* Verify server is alive */ |
|
2055
|
#define SYNC_QUIET 0x80000 /* No output */ |
|
2056
|
#endif |
|
2057
|
|
|
2058
|
/* |
|
2059
|
** Floating-point absolute value |
|
2060
|
*/ |
|
2061
|
static double fossil_fabs(double x){ |
|
2062
|
return x>0.0 ? x : -x; |
|
2063
|
} |
|
2064
|
|
|
2065
|
/* |
|
2066
|
** Sync to the host identified in g.url.name and g.url.path. This |
|
2067
|
** routine is called by the client. |
|
2068
|
** |
|
2069
|
** Records are pushed to the server if pushFlag is true. Records |
|
2070
|
** are pulled if pullFlag is true. A full sync occurs if both are |
|
2071
|
** true. |
|
2072
|
*/ |
|
2073
|
int client_sync( |
|
2074
|
unsigned syncFlags, /* Mask of SYNC_* flags */ |
|
2075
|
unsigned configRcvMask, /* Receive these configuration items */ |
|
2076
|
unsigned configSendMask, /* Send these configuration items */ |
|
2077
|
const char *zAltPCode, /* Alternative project code (usually NULL) */ |
|
2078
|
int *pnRcvd /* Set to # received artifacts, if not NULL */ |
|
2079
|
){ |
|
2080
|
int go = 1; /* Loop until zero */ |
|
2081
|
int nCardSent = 0; /* Number of cards sent */ |
|
2082
|
int nCardRcvd = 0; /* Number of cards received */ |
|
2083
|
int nCycle = 0; /* Number of round trips to the server */ |
|
2084
|
int size; /* Size of a config value or uvfile */ |
|
2085
|
int origConfigRcvMask; /* Original value of configRcvMask */ |
|
2086
|
int nFileRecv = 0; /* Number of files received */ |
|
2087
|
int mxPhantomReq = 200; /* Max number of phantoms to request per comm */ |
|
2088
|
const char *zCookie; /* Server cookie */ |
|
2089
|
i64 nUncSent, nUncRcvd; /* Bytes sent and received (before compression) */ |
|
2090
|
i64 nSent, nRcvd; /* Bytes sent and received (after compression) */ |
|
2091
|
int cloneSeqno = 1; /* Sequence number for clones */ |
|
2092
|
Blob send; /* Text we are sending to the server */ |
|
2093
|
Blob recv; /* Reply we got back from the server */ |
|
2094
|
Xfer xfer; /* Transfer data */ |
|
2095
|
int pctDone; /* Percentage done with a message */ |
|
2096
|
int lastPctDone = -1; /* Last displayed pctDone */ |
|
2097
|
double rArrivalTime; /* Time at which a message arrived */ |
|
2098
|
const char *zSCode = db_get("server-code", "x"); |
|
2099
|
const char *zPCode = db_get("project-code", 0); |
|
2100
|
int nErr = 0; /* Number of errors */ |
|
2101
|
int nRoundtrip= 0; /* Number of HTTP requests */ |
|
2102
|
int nArtifactSent = 0; /* Total artifacts sent */ |
|
2103
|
int nArtifactRcvd = 0; /* Total artifacts received */ |
|
2104
|
int nPriorArtifact = 0; /* Artifacts received on prior round-trips */ |
|
2105
|
const char *zOpType = 0;/* Push, Pull, Sync, Clone */ |
|
2106
|
double rSkew = 0.0; /* Maximum time skew */ |
|
2107
|
int uvHashSent = 0; /* The "pragma uv-hash" message has been sent */ |
|
2108
|
int uvDoPush = 0; /* Generate uvfile messages to send to server */ |
|
2109
|
int uvPullOnly = 0; /* 1: pull-only. 2: pull-only warning issued */ |
|
2110
|
int nUvGimmeSent = 0; /* Number of uvgimme cards sent on this cycle */ |
|
2111
|
int nUvFileRcvd = 0; /* Number of uvfile cards received on this cycle */ |
|
2112
|
int nGimmeRcvd = 0; /* Number of gimme cards recevied on the prev cycle */ |
|
2113
|
sqlite3_int64 mtime; /* Modification time on a UV file */ |
|
2114
|
int autopushFailed = 0; /* Autopush following commit failed if true */ |
|
2115
|
const char *zCkinLock; /* Name of check-in to lock. NULL for none */ |
|
2116
|
const char *zClientId; /* A unique identifier for this check-out */ |
|
2117
|
unsigned int mHttpFlags;/* Flags for the http_exchange() subsystem */ |
|
2118
|
const int bOutIsTty = fossil_isatty(fossil_fileno(stdout)); |
|
2119
|
|
|
2120
|
if( pnRcvd ) *pnRcvd = 0; |
|
2121
|
if( db_get_boolean("dont-push", 0) ) syncFlags &= ~SYNC_PUSH; |
|
2122
|
if( (syncFlags & (SYNC_PUSH|SYNC_PULL|SYNC_CLONE|SYNC_UNVERSIONED|SYNC_PING)) |
|
2123
|
==0 |
|
2124
|
&& configRcvMask==0 |
|
2125
|
&& configSendMask==0 |
|
2126
|
){ |
|
2127
|
return 0; /* Nothing to do */ |
|
2128
|
} |
|
2129
|
|
|
2130
|
/* Compute an appropriate project code. zPCode is the project code |
|
2131
|
** for the local repository. zAltPCode will usually be NULL, but might |
|
2132
|
** also be an alternative project code to expect on the server. When |
|
2133
|
** zAltPCode is not NULL, that means we are doing a cross-project import - |
|
2134
|
** in other words, reading content from one project into a different |
|
2135
|
** project. |
|
2136
|
*/ |
|
2137
|
if( syncFlags & SYNC_FROMPARENT ){ |
|
2138
|
const char *zPX; |
|
2139
|
configRcvMask = 0; |
|
2140
|
configSendMask = 0; |
|
2141
|
syncFlags &= ~(SYNC_PUSH); |
|
2142
|
zPX = db_get("parent-project-code", 0); |
|
2143
|
if( zPX==0 || db_get("parent-project-name",0)==0 ){ |
|
2144
|
fossil_fatal("there is no parent project: set the 'parent-project-code'" |
|
2145
|
" and 'parent-project-name' config parameters in order" |
|
2146
|
" to pull from a parent project"); |
|
2147
|
} |
|
2148
|
if( zPX ){ |
|
2149
|
zAltPCode = zPX; |
|
2150
|
} |
|
2151
|
} |
|
2152
|
if( zAltPCode!=0 && zPCode!=0 && sqlite3_stricmp(zPCode, zAltPCode)==0 ){ |
|
2153
|
zAltPCode = 0; |
|
2154
|
} |
|
2155
|
|
|
2156
|
transport_stats(0, 0, 1); |
|
2157
|
socket_global_init(); |
|
2158
|
memset(&xfer, 0, sizeof(xfer)); |
|
2159
|
xfer.pIn = &recv; |
|
2160
|
xfer.pOut = &send; |
|
2161
|
xfer.mxSend = db_get_int("max-upload", 250000); |
|
2162
|
xfer.maxTime = -1; |
|
2163
|
xfer.remoteVersion = RELEASE_VERSION_NUMBER; |
|
2164
|
if( syncFlags & SYNC_PRIVATE ){ |
|
2165
|
g.perm.Private = 1; |
|
2166
|
xfer.syncPrivate = 1; |
|
2167
|
} |
|
2168
|
|
|
2169
|
blobarray_zero(xfer.aToken, count(xfer.aToken)); |
|
2170
|
blob_zero(&send); |
|
2171
|
blob_zero(&recv); |
|
2172
|
blob_zero(&xfer.err); |
|
2173
|
blob_zero(&xfer.line); |
|
2174
|
origConfigRcvMask = 0; |
|
2175
|
nUncSent = nUncRcvd = 0; |
|
2176
|
|
|
2177
|
/* Send the send-private pragma if we are trying to sync private data */ |
|
2178
|
if( syncFlags & SYNC_PRIVATE ){ |
|
2179
|
blob_append(&send, "pragma send-private\n", -1); |
|
2180
|
} |
|
2181
|
|
|
2182
|
/* Figure out which check-in to lock */ |
|
2183
|
if( syncFlags & SYNC_CKIN_LOCK ){ |
|
2184
|
int vid = db_lget_int("checkout",0); |
|
2185
|
zCkinLock = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", vid); |
|
2186
|
}else{ |
|
2187
|
zCkinLock = 0; |
|
2188
|
} |
|
2189
|
zClientId = g.localOpen ? db_lget("client-id", 0) : 0; |
|
2190
|
|
|
2191
|
/* When syncing unversioned files, create a TEMP table in which to store |
|
2192
|
** the names of files that need to be sent from client to server. |
|
2193
|
** |
|
2194
|
** The initial assumption is that all unversioned files need to be sent |
|
2195
|
** to the other side. But "uvigot" cards received back from the remote |
|
2196
|
** side will normally cause many of these entries to be removed since they |
|
2197
|
** do not really need to be sent. |
|
2198
|
*/ |
|
2199
|
if( (syncFlags & (SYNC_UNVERSIONED|SYNC_CLONE))!=0 ){ |
|
2200
|
unversioned_schema(); |
|
2201
|
db_multi_exec( |
|
2202
|
"CREATE TEMP TABLE IF NOT EXISTS uv_tosend(" |
|
2203
|
" name TEXT PRIMARY KEY," /* Name of file to send client->server */ |
|
2204
|
" mtimeOnly BOOLEAN" /* True to only send mtime, not content */ |
|
2205
|
") WITHOUT ROWID;" |
|
2206
|
"REPLACE INTO uv_tosend(name,mtimeOnly)" |
|
2207
|
" SELECT name, 0 FROM unversioned WHERE hash IS NOT NULL;" |
|
2208
|
); |
|
2209
|
} |
|
2210
|
|
|
2211
|
/* |
|
2212
|
** The request from the client always begin with a clone, pull, |
|
2213
|
** or push message. |
|
2214
|
*/ |
|
2215
|
blob_appendf(&send, "pragma client-version %d %d %d\n", |
|
2216
|
RELEASE_VERSION_NUMBER, MANIFEST_NUMERIC_DATE, |
|
2217
|
MANIFEST_NUMERIC_TIME); |
|
2218
|
if( syncFlags & SYNC_CLONE ){ |
|
2219
|
blob_appendf(&send, "clone 3 %d\n", cloneSeqno); |
|
2220
|
syncFlags &= ~(SYNC_PUSH|SYNC_PULL); |
|
2221
|
nCardSent++; |
|
2222
|
/* TBD: Request all transferable configuration values */ |
|
2223
|
content_enable_dephantomize(0); |
|
2224
|
zOpType = "Clone"; |
|
2225
|
}else if( syncFlags & SYNC_PULL ){ |
|
2226
|
blob_appendf(&send, "pull %s %s\n", zSCode, |
|
2227
|
zAltPCode ? zAltPCode : zPCode); |
|
2228
|
nCardSent++; |
|
2229
|
zOpType = (syncFlags & SYNC_PUSH)?"Sync":"Pull"; |
|
2230
|
if( (syncFlags & SYNC_RESYNC)!=0 && nCycle<2 ){ |
|
2231
|
blob_appendf(&send, "pragma send-catalog\n"); |
|
2232
|
nCardSent++; |
|
2233
|
} |
|
2234
|
} |
|
2235
|
if( syncFlags & SYNC_PUSH ){ |
|
2236
|
blob_appendf(&send, "push %s %s\n", zSCode, zPCode); |
|
2237
|
nCardSent++; |
|
2238
|
if( (syncFlags & SYNC_PULL)==0 ) zOpType = "Push"; |
|
2239
|
if( (syncFlags & SYNC_RESYNC)!=0 ) xfer.resync = 0x7fffffff; |
|
2240
|
} |
|
2241
|
if( syncFlags & SYNC_VERBOSE ){ |
|
2242
|
fossil_print(zLabelFormat /*works-like:"%s%s%s%s%d"*/, |
|
2243
|
"", "Bytes", "Cards", "Artifacts", "Deltas"); |
|
2244
|
} |
|
2245
|
|
|
2246
|
/* Send the client-url pragma on the first cycle if the client has |
|
2247
|
** a known public url. |
|
2248
|
*/ |
|
2249
|
if( zAltPCode==0 ){ |
|
2250
|
const char *zSelfUrl = public_url(); |
|
2251
|
if( zSelfUrl ){ |
|
2252
|
blob_appendf(&send, "pragma client-url %s\n", zSelfUrl); |
|
2253
|
} |
|
2254
|
} |
|
2255
|
|
|
2256
|
/* Request URLs of alternative repositories |
|
2257
|
*/ |
|
2258
|
if( zAltPCode==0 && (syncFlags & SYNC_SHARE_LINKS)!=0 ){ |
|
2259
|
blob_appendf(&send, "pragma req-links\n"); |
|
2260
|
} |
|
2261
|
|
|
2262
|
while( go ){ |
|
2263
|
int newPhantom = 0; |
|
2264
|
char *zRandomness; |
|
2265
|
db_begin_transaction(); |
|
2266
|
db_record_repository_filename(0); |
|
2267
|
db_multi_exec( |
|
2268
|
"CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);" |
|
2269
|
"CREATE TEMP TABLE unk(uuid TEXT PRIMARY KEY) WITHOUT ROWID;" |
|
2270
|
); |
|
2271
|
manifest_crosslink_begin(); |
|
2272
|
|
|
2273
|
|
|
2274
|
/* Client sends the most recently received cookie back to the server. |
|
2275
|
** Let the server figure out if this is a cookie that it cares about. |
|
2276
|
*/ |
|
2277
|
zCookie = db_get("cookie", 0); |
|
2278
|
if( zCookie ){ |
|
2279
|
blob_appendf(&send, "cookie %s\n", zCookie); |
|
2280
|
} |
|
2281
|
|
|
2282
|
/* Client sends gimme cards for phantoms |
|
2283
|
*/ |
|
2284
|
if( (syncFlags & SYNC_PULL)!=0 |
|
2285
|
|| ((syncFlags & SYNC_CLONE)!=0 && cloneSeqno==1) |
|
2286
|
){ |
|
2287
|
request_phantoms(&xfer, mxPhantomReq); |
|
2288
|
if( xfer.nGimmeSent>0 && nCycle==2 && (syncFlags & SYNC_PULL)!=0 ){ |
|
2289
|
blob_appendf(&send, "pragma req-clusters\n"); |
|
2290
|
} |
|
2291
|
} |
|
2292
|
if( syncFlags & SYNC_PUSH ){ |
|
2293
|
send_unsent(&xfer); |
|
2294
|
nCardSent += send_unclustered(&xfer); |
|
2295
|
if( syncFlags & SYNC_PRIVATE ) send_private(&xfer); |
|
2296
|
if( nGimmeRcvd>0 && nCycle==2 ){ |
|
2297
|
send_all_clusters(&xfer); |
|
2298
|
} |
|
2299
|
} |
|
2300
|
|
|
2301
|
/* Client sends configuration parameter requests. On a clone, delay sending |
|
2302
|
** this until the second cycle since the login card might fail on |
|
2303
|
** the first cycle. |
|
2304
|
*/ |
|
2305
|
if( configRcvMask && ((syncFlags & SYNC_CLONE)==0 || nCycle>0) ){ |
|
2306
|
const char *zName; |
|
2307
|
if( zOpType==0 ) zOpType = "Pull"; |
|
2308
|
zName = configure_first_name(configRcvMask); |
|
2309
|
while( zName ){ |
|
2310
|
blob_appendf(&send, "reqconfig %s\n", zName); |
|
2311
|
zName = configure_next_name(configRcvMask); |
|
2312
|
nCardSent++; |
|
2313
|
} |
|
2314
|
origConfigRcvMask = configRcvMask; |
|
2315
|
configRcvMask = 0; |
|
2316
|
} |
|
2317
|
|
|
2318
|
/* Client sends a request to sync unversioned files. |
|
2319
|
** On a clone, delay sending this until the second cycle since |
|
2320
|
** the login card might fail on the first cycle. |
|
2321
|
*/ |
|
2322
|
if( (syncFlags & SYNC_UNVERSIONED)!=0 |
|
2323
|
&& ((syncFlags & SYNC_CLONE)==0 || nCycle>0) |
|
2324
|
&& !uvHashSent |
|
2325
|
){ |
|
2326
|
blob_appendf(&send, "pragma uv-hash %s\n", unversioned_content_hash(0)); |
|
2327
|
nCardSent++; |
|
2328
|
uvHashSent = 1; |
|
2329
|
} |
|
2330
|
|
|
2331
|
/* On a "fossil config push", the client send configuration parameters |
|
2332
|
** being pushed up to the server */ |
|
2333
|
if( configSendMask ){ |
|
2334
|
if( zOpType==0 ) zOpType = "Push"; |
|
2335
|
nCardSent += configure_send_group(xfer.pOut, configSendMask, 0); |
|
2336
|
configSendMask = 0; |
|
2337
|
} |
|
2338
|
|
|
2339
|
/* Send unversioned files present here on the client but missing or |
|
2340
|
** obsolete on the server. |
|
2341
|
** |
|
2342
|
** Or, if the SYNC_UV_REVERT flag is set, delete the local unversioned |
|
2343
|
** files that do not exist on the server. |
|
2344
|
** |
|
2345
|
** This happens on the second exchange, since we do not know what files |
|
2346
|
** need to be sent until after the uvigot cards from the first exchange |
|
2347
|
** have been processed. |
|
2348
|
*/ |
|
2349
|
if( uvDoPush ){ |
|
2350
|
assert( (syncFlags & SYNC_UNVERSIONED)!=0 ); |
|
2351
|
if( syncFlags & SYNC_UV_DRYRUN ){ |
|
2352
|
uvDoPush = 0; |
|
2353
|
}else if( syncFlags & SYNC_UV_REVERT ){ |
|
2354
|
db_multi_exec( |
|
2355
|
"DELETE FROM unversioned" |
|
2356
|
" WHERE name IN (SELECT name FROM uv_tosend);" |
|
2357
|
"DELETE FROM uv_tosend;" |
|
2358
|
); |
|
2359
|
uvDoPush = 0; |
|
2360
|
}else{ |
|
2361
|
Stmt uvq; |
|
2362
|
int rc = SQLITE_OK; |
|
2363
|
db_prepare(&uvq, "SELECT name, mtimeOnly FROM uv_tosend"); |
|
2364
|
while( (rc = db_step(&uvq))==SQLITE_ROW ){ |
|
2365
|
const char *zName = db_column_text(&uvq, 0); |
|
2366
|
send_unversioned_file(&xfer, zName, db_column_int(&uvq,1)); |
|
2367
|
nCardSent++; |
|
2368
|
nArtifactSent++; |
|
2369
|
db_multi_exec("DELETE FROM uv_tosend WHERE name=%Q", zName); |
|
2370
|
if( syncFlags & SYNC_VERBOSE ){ |
|
2371
|
fossil_print("\rUnversioned-file sent: %s\n", zName); |
|
2372
|
} |
|
2373
|
if( (int)blob_size(xfer.pOut)>xfer.mxSend ) break; |
|
2374
|
} |
|
2375
|
db_finalize(&uvq); |
|
2376
|
if( rc==SQLITE_DONE ) uvDoPush = 0; |
|
2377
|
} |
|
2378
|
} |
|
2379
|
|
|
2380
|
/* Lock the current check-out */ |
|
2381
|
if( zCkinLock ){ |
|
2382
|
if( zClientId==0 ){ |
|
2383
|
zClientId = db_text(0, "SELECT lower(hex(randomblob(20)))"); |
|
2384
|
db_lset("client-id", zClientId); |
|
2385
|
} |
|
2386
|
blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId); |
|
2387
|
zCkinLock = 0; |
|
2388
|
}else if( zClientId ){ |
|
2389
|
blob_appendf(&send, "pragma ci-unlock %s\n", zClientId); |
|
2390
|
} |
|
2391
|
/* Append randomness to the end of the uplink message. This makes all |
|
2392
|
** messages unique so that the login-card nonce will always |
|
2393
|
** be unique. |
|
2394
|
*/ |
|
2395
|
zRandomness = db_text(0, "SELECT hex(randomblob(20))"); |
|
2396
|
blob_appendf(&send, "# %s\n", zRandomness); |
|
2397
|
fossil_free(zRandomness); |
|
2398
|
|
|
2399
|
if( (syncFlags & SYNC_VERBOSE)!=0 |
|
2400
|
&& (syncFlags & SYNC_XVERBOSE)==0 |
|
2401
|
){ |
|
2402
|
fossil_print("waiting for server..."); |
|
2403
|
} |
|
2404
|
fflush(stdout); |
|
2405
|
/* Exchange messages with the server */ |
|
2406
|
if( (syncFlags & SYNC_CLONE)!=0 && nCycle==0 ){ |
|
2407
|
/* Do not send a login card on the first round-trip of a clone */ |
|
2408
|
mHttpFlags = 0; |
|
2409
|
}else{ |
|
2410
|
mHttpFlags = HTTP_USE_LOGIN; |
|
2411
|
} |
|
2412
|
if( syncFlags & SYNC_NOHTTPCOMPRESS ){ |
|
2413
|
mHttpFlags |= HTTP_NOCOMPRESS; |
|
2414
|
} |
|
2415
|
if( syncFlags & SYNC_XVERBOSE ){ |
|
2416
|
mHttpFlags |= HTTP_VERBOSE; |
|
2417
|
} |
|
2418
|
if( syncFlags & SYNC_QUIET ){ |
|
2419
|
mHttpFlags |= HTTP_QUIET; |
|
2420
|
} |
|
2421
|
|
|
2422
|
/* Do the round-trip to the server */ |
|
2423
|
if( http_exchange(&send, &recv, mHttpFlags, MAX_REDIRECTS, 0) ){ |
|
2424
|
nErr++; |
|
2425
|
go = 2; |
|
2426
|
break; |
|
2427
|
} |
|
2428
|
|
|
2429
|
/* Remember the URL of the sync target in the config file on the |
|
2430
|
** first successful round-trip */ |
|
2431
|
if( nCycle==0 && db_is_writeable("repository") ){ |
|
2432
|
xfer_syncwith(g.url.canonical, 0); |
|
2433
|
} |
|
2434
|
|
|
2435
|
/* Output current stats */ |
|
2436
|
nRoundtrip++; |
|
2437
|
nArtifactSent += xfer.nFileSent + xfer.nDeltaSent; |
|
2438
|
if( syncFlags & SYNC_VERBOSE ){ |
|
2439
|
fossil_print(zValueFormat /*works-like:"%s%d%d%d%d"*/, "Sent:", |
|
2440
|
blob_size(&send), nCardSent+xfer.nGimmeSent+xfer.nIGotSent, |
|
2441
|
xfer.nFileSent, xfer.nDeltaSent); |
|
2442
|
}else if( syncFlags & SYNC_QUIET ){ |
|
2443
|
/* No-op */ |
|
2444
|
}else{ |
|
2445
|
if( bOutIsTty!=0 ){ |
|
2446
|
fossil_print(zBriefFormat /*works-like:"%d%d%d"*/, |
|
2447
|
nRoundtrip, nArtifactSent, nArtifactRcvd); |
|
2448
|
} |
|
2449
|
} |
|
2450
|
nCardSent = 0; |
|
2451
|
nCardRcvd = 0; |
|
2452
|
xfer.nFileSent = 0; |
|
2453
|
xfer.nDeltaSent = 0; |
|
2454
|
xfer.nGimmeSent = 0; |
|
2455
|
xfer.nIGotSent = 0; |
|
2456
|
xfer.nPrivIGot = 0; |
|
2457
|
|
|
2458
|
lastPctDone = -1; |
|
2459
|
nUncSent += blob_size(&send); |
|
2460
|
blob_reset(&send); |
|
2461
|
blob_appendf(&send, "pragma client-version %d %d %d\n", |
|
2462
|
RELEASE_VERSION_NUMBER, MANIFEST_NUMERIC_DATE, |
|
2463
|
MANIFEST_NUMERIC_TIME); |
|
2464
|
rArrivalTime = db_double(0.0, "SELECT julianday('now')"); |
|
2465
|
|
|
2466
|
/* Send the send-private pragma if we are trying to sync private data */ |
|
2467
|
if( syncFlags & SYNC_PRIVATE ){ |
|
2468
|
blob_append(&send, "pragma send-private\n", -1); |
|
2469
|
} |
|
2470
|
|
|
2471
|
/* Begin constructing the next message (which might never be |
|
2472
|
** sent) by beginning with the pull or push cards |
|
2473
|
*/ |
|
2474
|
if( syncFlags & SYNC_PULL ){ |
|
2475
|
blob_appendf(&send, "pull %s %s\n", zSCode, |
|
2476
|
zAltPCode ? zAltPCode : zPCode); |
|
2477
|
nCardSent++; |
|
2478
|
} |
|
2479
|
if( syncFlags & SYNC_PUSH ){ |
|
2480
|
blob_appendf(&send, "push %s %s\n", zSCode, zPCode); |
|
2481
|
nCardSent++; |
|
2482
|
} |
|
2483
|
go = 0; |
|
2484
|
nUvGimmeSent = 0; |
|
2485
|
nUvFileRcvd = 0; |
|
2486
|
nGimmeRcvd = 0; |
|
2487
|
nPriorArtifact = nArtifactRcvd; |
|
2488
|
|
|
2489
|
/* Process the reply that came back from the server */ |
|
2490
|
while( blob_line(&recv, &xfer.line) ){ |
|
2491
|
if( blob_buffer(&xfer.line)[0]=='#' ){ |
|
2492
|
const char *zLine = blob_buffer(&xfer.line); |
|
2493
|
if( memcmp(zLine, "# timestamp ", 12)==0 ){ |
|
2494
|
char zTime[20]; |
|
2495
|
double rDiff; |
|
2496
|
sqlite3_snprintf(sizeof(zTime), zTime, "%.19s", &zLine[12]); |
|
2497
|
rDiff = db_double(9e99, "SELECT julianday('%q') - %.17g", |
|
2498
|
zTime, rArrivalTime); |
|
2499
|
if( rDiff>9e98 || rDiff<-9e98 ) rDiff = 0.0; |
|
2500
|
if( rDiff*24.0*3600.0 >= -(blob_size(&recv)/5000.0 + 20) ){ |
|
2501
|
rDiff = 0.0; |
|
2502
|
} |
|
2503
|
if( fossil_fabs(rDiff)>fossil_fabs(rSkew) ) rSkew = rDiff; |
|
2504
|
} |
|
2505
|
nCardRcvd++; |
|
2506
|
continue; |
|
2507
|
} |
|
2508
|
xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); |
|
2509
|
nCardRcvd++; |
|
2510
|
if( (syncFlags & SYNC_VERBOSE)!=0 && recv.nUsed>0 ){ |
|
2511
|
pctDone = (recv.iCursor*100)/recv.nUsed; |
|
2512
|
if( pctDone!=lastPctDone ){ |
|
2513
|
fossil_print("\rprocessed: %d%% ", pctDone); |
|
2514
|
lastPctDone = pctDone; |
|
2515
|
fflush(stdout); |
|
2516
|
} |
|
2517
|
} |
|
2518
|
|
|
2519
|
/* file HASH SIZE \n CONTENT |
|
2520
|
** file HASH DELTASRC SIZE \n CONTENT |
|
2521
|
** |
|
2522
|
** Client receives a file transmitted from the server. |
|
2523
|
*/ |
|
2524
|
if( blob_eq(&xfer.aToken[0],"file") ){ |
|
2525
|
xfer_accept_file(&xfer, (syncFlags & SYNC_CLONE)!=0, 0, 0); |
|
2526
|
nArtifactRcvd++; |
|
2527
|
}else |
|
2528
|
|
|
2529
|
/* cfile HASH USIZE CSIZE \n CONTENT |
|
2530
|
** cfile HASH DELTASRC USIZE CSIZE \n CONTENT |
|
2531
|
** |
|
2532
|
** Client receives a compressed file transmitted from the server. |
|
2533
|
*/ |
|
2534
|
if( blob_eq(&xfer.aToken[0],"cfile") ){ |
|
2535
|
xfer_accept_compressed_file(&xfer, 0, 0); |
|
2536
|
nArtifactRcvd++; |
|
2537
|
}else |
|
2538
|
|
|
2539
|
/* uvfile NAME MTIME HASH SIZE FLAGS \n CONTENT |
|
2540
|
** |
|
2541
|
** Client accepts an unversioned file from the server. |
|
2542
|
*/ |
|
2543
|
if( blob_eq(&xfer.aToken[0], "uvfile") ){ |
|
2544
|
xfer_accept_unversioned_file(&xfer, 1); |
|
2545
|
nArtifactRcvd++; |
|
2546
|
nUvFileRcvd++; |
|
2547
|
if( syncFlags & SYNC_VERBOSE ){ |
|
2548
|
fossil_print("\rUnversioned-file received: %s\n", |
|
2549
|
blob_str(&xfer.aToken[1])); |
|
2550
|
} |
|
2551
|
}else |
|
2552
|
|
|
2553
|
/* gimme HASH |
|
2554
|
** |
|
2555
|
** Client receives an artifact request from the server. |
|
2556
|
** If the file is a manifest, assume that the server will also want |
|
2557
|
** to know all of the content artifacts associated with the manifest |
|
2558
|
** and send those too. |
|
2559
|
*/ |
|
2560
|
if( blob_eq(&xfer.aToken[0], "gimme") |
|
2561
|
&& xfer.nToken==2 |
|
2562
|
&& blob_is_hname(&xfer.aToken[1]) |
|
2563
|
){ |
|
2564
|
remote_unk(&xfer.aToken[1]); |
|
2565
|
if( syncFlags & SYNC_PUSH ){ |
|
2566
|
int rid = rid_from_uuid(&xfer.aToken[1], 0, 0); |
|
2567
|
if( rid ){ |
|
2568
|
send_file(&xfer, rid, &xfer.aToken[1], 0); |
|
2569
|
nGimmeRcvd++; |
|
2570
|
} |
|
2571
|
} |
|
2572
|
}else |
|
2573
|
|
|
2574
|
/* igot HASH ?PRIVATEFLAG? |
|
2575
|
** |
|
2576
|
** Server announces that it has a particular file. If this is |
|
2577
|
** not a file that we have and we are pulling, then create a |
|
2578
|
** phantom to cause this file to be requested on the next cycle. |
|
2579
|
** Always remember that the server has this file so that we do |
|
2580
|
** not transmit it by accident. |
|
2581
|
** |
|
2582
|
** If the PRIVATE argument exists and is 1, then the file is |
|
2583
|
** private. Pretend it does not exists if we are not pulling |
|
2584
|
** private files. |
|
2585
|
*/ |
|
2586
|
if( xfer.nToken>=2 |
|
2587
|
&& blob_eq(&xfer.aToken[0], "igot") |
|
2588
|
&& blob_is_hname(&xfer.aToken[1]) |
|
2589
|
){ |
|
2590
|
int rid; |
|
2591
|
int isPriv = xfer.nToken>=3 && blob_eq(&xfer.aToken[2],"1"); |
|
2592
|
rid = rid_from_uuid(&xfer.aToken[1], 0, 0); |
|
2593
|
if( rid>0 ){ |
|
2594
|
if( isPriv ){ |
|
2595
|
content_make_private(rid); |
|
2596
|
}else{ |
|
2597
|
content_make_public(rid); |
|
2598
|
} |
|
2599
|
}else if( isPriv && !g.perm.Private ){ |
|
2600
|
/* ignore private files */ |
|
2601
|
}else if( (syncFlags & (SYNC_PULL|SYNC_CLONE))!=0 ){ |
|
2602
|
rid = content_new(blob_str(&xfer.aToken[1]), isPriv); |
|
2603
|
if( rid ) newPhantom = 1; |
|
2604
|
} |
|
2605
|
remote_has(rid); |
|
2606
|
}else |
|
2607
|
|
|
2608
|
/* uvigot NAME MTIME HASH SIZE |
|
2609
|
** |
|
2610
|
** Server announces that it has a particular unversioned file. The |
|
2611
|
** server will only send this card if the client had previously sent |
|
2612
|
** a "pragma uv-hash" card with a hash that does not match. |
|
2613
|
** |
|
2614
|
** If the identified file needs to be transferred, then setup for the |
|
2615
|
** transfer. Generate a "uvgimme" card in the reply if the server |
|
2616
|
** version is newer than the client. Generate a "uvfile" card if |
|
2617
|
** the client version is newer than the server. If HASH is "-" |
|
2618
|
** (indicating that the file has been deleted) and MTIME is newer, |
|
2619
|
** then do the deletion. |
|
2620
|
*/ |
|
2621
|
if( xfer.nToken==5 |
|
2622
|
&& blob_eq(&xfer.aToken[0], "uvigot") |
|
2623
|
&& blob_is_filename(&xfer.aToken[1]) |
|
2624
|
&& blob_is_int64(&xfer.aToken[2], &mtime) |
|
2625
|
&& blob_is_int(&xfer.aToken[4], &size) |
|
2626
|
&& (blob_eq(&xfer.aToken[3],"-") || blob_is_hname(&xfer.aToken[3])) |
|
2627
|
){ |
|
2628
|
const char *zName = blob_str(&xfer.aToken[1]); |
|
2629
|
const char *zHash = blob_str(&xfer.aToken[3]); |
|
2630
|
int iStatus; |
|
2631
|
if( mtime<0 || size<0 ){ |
|
2632
|
xfer_fatal_error("invalid uvigot"); |
|
2633
|
return ++nErr; |
|
2634
|
} |
|
2635
|
iStatus = unversioned_status(zName, mtime, zHash); |
|
2636
|
if( (syncFlags & SYNC_UV_REVERT)!=0 ){ |
|
2637
|
if( iStatus==4 ) iStatus = 2; |
|
2638
|
if( iStatus==5 ) iStatus = 1; |
|
2639
|
} |
|
2640
|
if( syncFlags & (SYNC_UV_TRACE|SYNC_UV_DRYRUN) ){ |
|
2641
|
const char *zMsg = 0; |
|
2642
|
switch( iStatus ){ |
|
2643
|
case 0: |
|
2644
|
case 1: zMsg = "UV-PULL"; break; |
|
2645
|
case 2: zMsg = "UV-PULL-MTIME-ONLY"; break; |
|
2646
|
case 4: zMsg = "UV-PUSH-MTIME-ONLY"; break; |
|
2647
|
case 5: zMsg = "UV-PUSH"; break; |
|
2648
|
} |
|
2649
|
if( zMsg ) fossil_print("\r%s: %s\n", zMsg, zName); |
|
2650
|
if( syncFlags & SYNC_UV_DRYRUN ){ |
|
2651
|
iStatus = 99; /* Prevent any changes or reply messages */ |
|
2652
|
} |
|
2653
|
} |
|
2654
|
if( iStatus<=1 ){ |
|
2655
|
if( zHash[0]!='-' ){ |
|
2656
|
blob_appendf(xfer.pOut, "uvgimme %s\n", zName); |
|
2657
|
nCardSent++; |
|
2658
|
nUvGimmeSent++; |
|
2659
|
db_multi_exec("DELETE FROM unversioned WHERE name=%Q", zName); |
|
2660
|
}else if( iStatus==1 ){ |
|
2661
|
db_multi_exec( |
|
2662
|
"UPDATE unversioned" |
|
2663
|
" SET mtime=%lld, hash=NULL, sz=0, encoding=0, content=NULL" |
|
2664
|
" WHERE name=%Q", mtime, zName |
|
2665
|
); |
|
2666
|
db_unset("uv-hash", 0); |
|
2667
|
} |
|
2668
|
}else if( iStatus==2 ){ |
|
2669
|
db_multi_exec( |
|
2670
|
"UPDATE unversioned SET mtime=%lld WHERE name=%Q", mtime, zName |
|
2671
|
); |
|
2672
|
db_unset("uv-hash", 0); |
|
2673
|
} |
|
2674
|
if( iStatus>=4 && uvPullOnly==1 ){ |
|
2675
|
fossil_warning( |
|
2676
|
"Warning: uv-pull-only \n" |
|
2677
|
" Unable to push unversioned content because you lack\n" |
|
2678
|
" sufficient permission on the server\n" |
|
2679
|
); |
|
2680
|
uvPullOnly = 2; |
|
2681
|
} |
|
2682
|
if( iStatus<=3 || uvPullOnly ){ |
|
2683
|
db_multi_exec("DELETE FROM uv_tosend WHERE name=%Q", zName); |
|
2684
|
}else if( iStatus==4 ){ |
|
2685
|
db_multi_exec("UPDATE uv_tosend SET mtimeOnly=1 WHERE name=%Q",zName); |
|
2686
|
}else if( iStatus==5 ){ |
|
2687
|
db_multi_exec("REPLACE INTO uv_tosend(name,mtimeOnly) VALUES(%Q,0)", |
|
2688
|
zName); |
|
2689
|
} |
|
2690
|
}else |
|
2691
|
|
|
2692
|
/* push SERVERCODE PRODUCTCODE |
|
2693
|
** |
|
2694
|
** Should only happen in response to a clone. This message tells |
|
2695
|
** the client what product code to use for the new database. |
|
2696
|
*/ |
|
2697
|
if( blob_eq(&xfer.aToken[0],"push") |
|
2698
|
&& xfer.nToken==3 |
|
2699
|
&& (syncFlags & SYNC_CLONE)!=0 |
|
2700
|
&& blob_is_hname(&xfer.aToken[2]) |
|
2701
|
){ |
|
2702
|
if( zPCode==0 ){ |
|
2703
|
zPCode = mprintf("%b", &xfer.aToken[2]); |
|
2704
|
db_set("project-code", zPCode, 0); |
|
2705
|
} |
|
2706
|
if( cloneSeqno>0 ) blob_appendf(&send, "clone 3 %d\n", cloneSeqno); |
|
2707
|
nCardSent++; |
|
2708
|
}else |
|
2709
|
|
|
2710
|
/* config NAME SIZE \n CONTENT |
|
2711
|
** |
|
2712
|
** Client receive a configuration value from the server. |
|
2713
|
** |
|
2714
|
** The received configuration setting is silently ignored if it was |
|
2715
|
** not requested by a prior "reqconfig" sent from client to server. |
|
2716
|
*/ |
|
2717
|
if( blob_eq(&xfer.aToken[0],"config") && xfer.nToken==3 |
|
2718
|
&& blob_is_int(&xfer.aToken[2], &size) ){ |
|
2719
|
const char *zName = blob_str(&xfer.aToken[1]); |
|
2720
|
Blob content; |
|
2721
|
if( size<0 ){ |
|
2722
|
xfer_fatal_error("invalid config record"); |
|
2723
|
return ++nErr; |
|
2724
|
} |
|
2725
|
blob_zero(&content); |
|
2726
|
blob_extract(xfer.pIn, size, &content); |
|
2727
|
g.perm.Admin = g.perm.RdAddr = 1; |
|
2728
|
configure_receive(zName, &content, origConfigRcvMask); |
|
2729
|
nCardRcvd++; |
|
2730
|
nArtifactRcvd++; |
|
2731
|
blob_reset(&content); |
|
2732
|
blob_seek(xfer.pIn, 1, BLOB_SEEK_CUR); |
|
2733
|
}else |
|
2734
|
|
|
2735
|
|
|
2736
|
/* cookie TEXT |
|
2737
|
** |
|
2738
|
** The client reserves a cookie from the server. The client |
|
2739
|
** should remember this cookie and send it back to the server |
|
2740
|
** in its next query. |
|
2741
|
** |
|
2742
|
** Each cookie received overwrites the prior cookie from the |
|
2743
|
** same server. |
|
2744
|
*/ |
|
2745
|
if( blob_eq(&xfer.aToken[0], "cookie") && xfer.nToken==2 ){ |
|
2746
|
db_set("cookie", blob_str(&xfer.aToken[1]), 0); |
|
2747
|
}else |
|
2748
|
|
|
2749
|
|
|
2750
|
/* private |
|
2751
|
** |
|
2752
|
** The server tells the client that the next "file" or "cfile" will |
|
2753
|
** contain private content. |
|
2754
|
*/ |
|
2755
|
if( blob_eq(&xfer.aToken[0], "private") ){ |
|
2756
|
xfer.nextIsPrivate = 1; |
|
2757
|
}else |
|
2758
|
|
|
2759
|
|
|
2760
|
/* clone_seqno N |
|
2761
|
** |
|
2762
|
** When doing a clone, the server tries to send all of its artifacts |
|
2763
|
** in sequence. This card indicates the sequence number of the next |
|
2764
|
** blob that needs to be sent. If N<=0 that indicates that all blobs |
|
2765
|
** have been sent. |
|
2766
|
*/ |
|
2767
|
if( blob_eq(&xfer.aToken[0], "clone_seqno") && xfer.nToken==2 ){ |
|
2768
|
blob_is_int(&xfer.aToken[1], &cloneSeqno); |
|
2769
|
if( cloneSeqno<0 ){ |
|
2770
|
xfer_fatal_error("invalid clone_seqno"); |
|
2771
|
return ++nErr; |
|
2772
|
} |
|
2773
|
}else |
|
2774
|
|
|
2775
|
/* message MESSAGE |
|
2776
|
** |
|
2777
|
** A message is received from the server. Print it. |
|
2778
|
** Similar to "error" but does not stop processing. |
|
2779
|
*/ |
|
2780
|
if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){ |
|
2781
|
char *zMsg = blob_terminate(&xfer.aToken[1]); |
|
2782
|
defossilize(zMsg); |
|
2783
|
if( (syncFlags & SYNC_PUSH) && zMsg |
|
2784
|
&& sqlite3_strglob("pull only *", zMsg)==0 ){ |
|
2785
|
syncFlags &= ~SYNC_PUSH; |
|
2786
|
zMsg = 0; |
|
2787
|
} |
|
2788
|
if( zMsg && zMsg[0] ){ |
|
2789
|
fossil_force_newline(); |
|
2790
|
fossil_print("Server says: %s\n", zMsg); |
|
2791
|
} |
|
2792
|
}else |
|
2793
|
|
|
2794
|
/* pragma NAME VALUE... |
|
2795
|
** |
|
2796
|
** The server can send pragmas to try to convey meta-information to |
|
2797
|
** the client. These are informational only. Unknown pragmas are |
|
2798
|
** silently ignored. |
|
2799
|
*/ |
|
2800
|
if( blob_eq(&xfer.aToken[0], "pragma") && xfer.nToken>=2 ){ |
|
2801
|
/* pragma server-version VERSION ?DATE? ?TIME? |
|
2802
|
** |
|
2803
|
** The server announces to the server what version of Fossil it |
|
2804
|
** is running. The DATE and TIME are a pure numeric ISO8601 time |
|
2805
|
** for the specific check-in of the client. |
|
2806
|
*/ |
|
2807
|
if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){ |
|
2808
|
xfer.remoteVersion = g.syncInfo.remoteVersion = |
|
2809
|
atoi(blob_str(&xfer.aToken[2])); |
|
2810
|
if( xfer.nToken>=5 ){ |
|
2811
|
xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
|
2812
|
xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
|
2813
|
} |
|
2814
|
xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, |
|
2815
|
xfer.remoteTime, 0x08 ); |
|
2816
|
} |
|
2817
|
|
|
2818
|
/* pragma uv-pull-only |
|
2819
|
** pragma uv-push-ok |
|
2820
|
** |
|
2821
|
** If the server is unwilling to accept new unversioned content (because |
|
2822
|
** this client lacks the necessary permissions) then it sends a |
|
2823
|
** "uv-pull-only" pragma so that the client will know not to waste |
|
2824
|
** bandwidth trying to upload unversioned content. If the server |
|
2825
|
** does accept new unversioned content, it sends "uv-push-ok". |
|
2826
|
*/ |
|
2827
|
else if( syncFlags & SYNC_UNVERSIONED ){ |
|
2828
|
if( blob_eq(&xfer.aToken[1], "uv-pull-only") ){ |
|
2829
|
uvPullOnly = 1; |
|
2830
|
if( syncFlags & SYNC_UV_REVERT ) uvDoPush = 1; |
|
2831
|
}else if( blob_eq(&xfer.aToken[1], "uv-push-ok") ){ |
|
2832
|
uvDoPush = 1; |
|
2833
|
} |
|
2834
|
} |
|
2835
|
|
|
2836
|
/* pragma ci-lock-fail USER-HOLDING-LOCK LOCK-TIME |
|
2837
|
** |
|
2838
|
** The server generates this message when a "pragma ci-lock" |
|
2839
|
** is attempted on a check-in for which there is an existing |
|
2840
|
** lock. USER-HOLDING-LOCK is the name of the user who originated |
|
2841
|
** the lock, and LOCK-TIME is the timestamp (seconds since 1970) |
|
2842
|
** when the lock was taken. |
|
2843
|
*/ |
|
2844
|
else if( blob_eq(&xfer.aToken[1], "ci-lock-fail") && xfer.nToken==4 ){ |
|
2845
|
char *zUser = blob_terminate(&xfer.aToken[2]); |
|
2846
|
sqlite3_int64 mtime, iNow; |
|
2847
|
defossilize(zUser); |
|
2848
|
iNow = time(NULL); |
|
2849
|
if( blob_is_int64(&xfer.aToken[3], &mtime) && iNow>mtime ){ |
|
2850
|
if( mtime<0 ){ |
|
2851
|
xfer_fatal_error("invalid ci-lock-fail time"); |
|
2852
|
return ++nErr; |
|
2853
|
} |
|
2854
|
iNow = time(NULL); |
|
2855
|
fossil_print("\nParent check-in locked by %s %s ago\n", |
|
2856
|
zUser, human_readable_age((iNow+1-mtime)/86400.0)); |
|
2857
|
}else{ |
|
2858
|
fossil_print("\nParent check-in locked by %s\n", zUser); |
|
2859
|
} |
|
2860
|
g.ckinLockFail = fossil_strdup(zUser); |
|
2861
|
} |
|
2862
|
|
|
2863
|
/* pragma avoid-delta-manifests |
|
2864
|
** |
|
2865
|
** Discourage the use of delta manifests. The remote side sends |
|
2866
|
** this pragma when its forbid-delta-manifests setting is true. |
|
2867
|
*/ |
|
2868
|
else if( blob_eq(&xfer.aToken[1], "avoid-delta-manifests") ){ |
|
2869
|
g.bAvoidDeltaManifests = 1; |
|
2870
|
} |
|
2871
|
|
|
2872
|
/* pragma link URL ARG MTIME |
|
2873
|
** |
|
2874
|
** The server has sent the URL for a link to another repository. |
|
2875
|
** Record this as a link:URL entry in the config table. |
|
2876
|
*/ |
|
2877
|
else if( blob_eq(&xfer.aToken[1], "link") |
|
2878
|
&& xfer.nToken==5 |
|
2879
|
&& (syncFlags & SYNC_SHARE_LINKS)!=0 |
|
2880
|
){ |
|
2881
|
UrlData x; |
|
2882
|
char *zUrl = blob_str(&xfer.aToken[2]); |
|
2883
|
char *zArg = blob_str(&xfer.aToken[3]); |
|
2884
|
i64 iTime = strtoll(blob_str(&xfer.aToken[4]),0,0); |
|
2885
|
memset(&x, 0, sizeof(x)); |
|
2886
|
defossilize(zUrl); |
|
2887
|
defossilize(zArg); |
|
2888
|
url_parse_local(zUrl, URL_OMIT_USER, &x); |
|
2889
|
if( x.protocol |
|
2890
|
&& strncmp(x.protocol,"http",4)==0 |
|
2891
|
&& iTime>0 |
|
2892
|
){ |
|
2893
|
db_unprotect(PROTECT_CONFIG); |
|
2894
|
db_multi_exec( |
|
2895
|
"INSERT INTO config(name,value,mtime)\n" |
|
2896
|
" VALUES('link:%q',%Q,%lld)\n" |
|
2897
|
" ON CONFLICT DO UPDATE\n" |
|
2898
|
" SET value=excluded.value, mtime=excluded.mtime\n" |
|
2899
|
" WHERE mtime<excluded.mtime;", |
|
2900
|
zUrl, zArg, iTime |
|
2901
|
); |
|
2902
|
db_protect_pop(); |
|
2903
|
} |
|
2904
|
url_unparse(&x); |
|
2905
|
} |
|
2906
|
|
|
2907
|
}else |
|
2908
|
|
|
2909
|
/* error MESSAGE |
|
2910
|
** |
|
2911
|
** The server is reporting an error. The client will abandon |
|
2912
|
** the sync session. |
|
2913
|
** |
|
2914
|
** Except, when cloning we will sometimes get an error on the |
|
2915
|
** first message exchange because the project-code is unknown |
|
2916
|
** and so the login card on the request was invalid. The project-code |
|
2917
|
** is returned in the reply before the error card, so second and |
|
2918
|
** subsequent messages should be OK. Nevertheless, we need to ignore |
|
2919
|
** the error card on the first message of a clone. |
|
2920
|
** |
|
2921
|
** Also ignore "not authorized to write" errors if this is an |
|
2922
|
** autopush following a commit. |
|
2923
|
*/ |
|
2924
|
if( blob_eq(&xfer.aToken[0],"error") && xfer.nToken==2 ){ |
|
2925
|
char *zMsg = blob_terminate(&xfer.aToken[1]); |
|
2926
|
defossilize(zMsg); |
|
2927
|
if( (syncFlags & SYNC_IFABLE)!=0 |
|
2928
|
&& sqlite3_strlike("%not authorized to write%",zMsg,0)==0 ){ |
|
2929
|
autopushFailed = 1; |
|
2930
|
nErr++; |
|
2931
|
}else if( (syncFlags & SYNC_CLONE)==0 || nCycle>0 ){ |
|
2932
|
fossil_force_newline(); |
|
2933
|
fossil_print("Error: %s\n", zMsg); |
|
2934
|
blob_appendf(&xfer.err, "server says: %s\n", zMsg); |
|
2935
|
nErr++; |
|
2936
|
break; |
|
2937
|
} |
|
2938
|
}else |
|
2939
|
|
|
2940
|
/* Unknown message */ |
|
2941
|
if( xfer.nToken>0 ){ |
|
2942
|
if( blob_str(&xfer.aToken[0])[0]=='<' ){ |
|
2943
|
fossil_warning( |
|
2944
|
"server replies with HTML instead of fossil sync protocol:\n%b", |
|
2945
|
&recv |
|
2946
|
); |
|
2947
|
nErr++; |
|
2948
|
break; |
|
2949
|
} |
|
2950
|
blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.line); |
|
2951
|
} |
|
2952
|
|
|
2953
|
if( blob_size(&xfer.err) ){ |
|
2954
|
fossil_force_newline(); |
|
2955
|
fossil_warning("%b", &xfer.err); |
|
2956
|
nErr++; |
|
2957
|
break; |
|
2958
|
} |
|
2959
|
blobarray_reset(xfer.aToken, xfer.nToken); |
|
2960
|
blob_reset(&xfer.line); |
|
2961
|
} |
|
2962
|
origConfigRcvMask = 0; |
|
2963
|
if( nCardRcvd>0 && (syncFlags & SYNC_VERBOSE) ){ |
|
2964
|
fossil_print(zValueFormat /*works-like:"%s%d%d%d%d"*/, "Received:", |
|
2965
|
blob_size(&recv), nCardRcvd, |
|
2966
|
xfer.nFileRcvd, xfer.nDeltaRcvd + xfer.nDanglingFile); |
|
2967
|
}else if( syncFlags & SYNC_QUIET ){ |
|
2968
|
/* No-op */ |
|
2969
|
}else{ |
|
2970
|
if( bOutIsTty!=0 ){ |
|
2971
|
fossil_print(zBriefFormat /*works-like:"%d%d%d"*/, |
|
2972
|
nRoundtrip, nArtifactSent, nArtifactRcvd); |
|
2973
|
} |
|
2974
|
} |
|
2975
|
nUncRcvd += blob_size(&recv); |
|
2976
|
blob_reset(&recv); |
|
2977
|
nCycle++; |
|
2978
|
|
|
2979
|
/* Set go to 1 if we need to continue the sync/push/pull/clone for |
|
2980
|
** another round. Set go to 0 if it is time to quit. */ |
|
2981
|
nFileRecv = xfer.nFileRcvd + xfer.nDeltaRcvd + xfer.nDanglingFile; |
|
2982
|
if( (nFileRecv>0 || newPhantom) && db_exists("SELECT 1 FROM phantom") ){ |
|
2983
|
go = 1; |
|
2984
|
mxPhantomReq = nFileRecv*2; |
|
2985
|
if( mxPhantomReq<200 ) mxPhantomReq = 200; |
|
2986
|
}else if( xfer.nFileSent+xfer.nDeltaSent>0 || uvDoPush ){ |
|
2987
|
/* Go another round if files are queued to send */ |
|
2988
|
go = 1; |
|
2989
|
}else if( xfer.nPrivIGot>0 && nCycle==1 ){ |
|
2990
|
go = 1; |
|
2991
|
}else if( nUvGimmeSent>0 && (nUvFileRcvd>0 || nCycle<3) ){ |
|
2992
|
/* Continue looping as long as new uvfile cards are being received |
|
2993
|
** and uvgimme cards are being sent. */ |
|
2994
|
go = 1; |
|
2995
|
}else if( (syncFlags & SYNC_CLONE)!=0 ){ |
|
2996
|
if( nCycle==1 ){ |
|
2997
|
go = 1; /* go at least two rounds on a clone */ |
|
2998
|
}else if( nFileRecv>0 ){ |
|
2999
|
go = 1; |
|
3000
|
}else if( cloneSeqno>0 && nArtifactRcvd>nPriorArtifact ){ |
|
3001
|
/* Continue the clone until we see the clone_seqno 0" card or |
|
3002
|
** until we stop receiving artifacts */ |
|
3003
|
go = 1; |
|
3004
|
} |
|
3005
|
} |
|
3006
|
|
|
3007
|
nCardRcvd = 0; |
|
3008
|
xfer.nFileRcvd = 0; |
|
3009
|
xfer.nDeltaRcvd = 0; |
|
3010
|
xfer.nDanglingFile = 0; |
|
3011
|
db_multi_exec("DROP TABLE onremote; DROP TABLE unk;"); |
|
3012
|
if( go ){ |
|
3013
|
manifest_crosslink_end(MC_PERMIT_HOOKS); |
|
3014
|
}else{ |
|
3015
|
manifest_crosslink_end(MC_PERMIT_HOOKS); |
|
3016
|
content_enable_dephantomize(1); |
|
3017
|
} |
|
3018
|
db_end_transaction(0); |
|
3019
|
}; /* while(go) */ |
|
3020
|
transport_stats(&nSent, &nRcvd, 1); |
|
3021
|
if( pnRcvd ) *pnRcvd = nArtifactRcvd; |
|
3022
|
if( (rSkew*24.0*3600.0) > 10.0 ){ |
|
3023
|
fossil_warning("*** time skew *** server is fast by %s", |
|
3024
|
db_timespan_name(rSkew)); |
|
3025
|
g.clockSkewSeen = 1; |
|
3026
|
}else if( rSkew*24.0*3600.0 < -10.0 ){ |
|
3027
|
fossil_warning("*** time skew *** server is slow by %s", |
|
3028
|
db_timespan_name(-rSkew)); |
|
3029
|
g.clockSkewSeen = 1; |
|
3030
|
} |
|
3031
|
if( bOutIsTty==0 && (syncFlags & SYNC_QUIET)==0 ){ |
|
3032
|
fossil_print(zBriefFormat /*works-like:"%d%d%d"*/, |
|
3033
|
nRoundtrip, nArtifactSent, nArtifactRcvd); |
|
3034
|
fossil_force_newline(); |
|
3035
|
} |
|
3036
|
fossil_force_newline(); |
|
3037
|
if( g.zHttpCmd==0 ){ |
|
3038
|
if( syncFlags & SYNC_QUIET ){ |
|
3039
|
/* no-op */ |
|
3040
|
}else if( syncFlags & SYNC_VERBOSE ){ |
|
3041
|
fossil_print( |
|
3042
|
"%s done, wire bytes sent: %lld received: %lld remote: %s%s\n", |
|
3043
|
zOpType, nSent, nRcvd, |
|
3044
|
(g.url.name && g.url.name[0]!='\0') ? g.url.name : "", |
|
3045
|
(g.zIpAddr && g.zIpAddr[0]!='\0' |
|
3046
|
&& fossil_strcmp(g.zIpAddr, g.url.name)) |
|
3047
|
? mprintf(" (%s)", g.zIpAddr) : ""); |
|
3048
|
}else{ |
|
3049
|
fossil_print( |
|
3050
|
"%s done, wire bytes sent: %lld received: %lld remote: %s\n", |
|
3051
|
zOpType, nSent, nRcvd, g.zIpAddr); |
|
3052
|
} |
|
3053
|
} |
|
3054
|
if( syncFlags & SYNC_VERBOSE ){ |
|
3055
|
fossil_print( |
|
3056
|
"Uncompressed payload sent: %lld received: %lld\n", nUncSent, nUncRcvd); |
|
3057
|
} |
|
3058
|
blob_reset(&send); |
|
3059
|
blob_reset(&recv); |
|
3060
|
transport_close(&g.url); |
|
3061
|
transport_global_shutdown(&g.url); |
|
3062
|
if( nErr && go==2 ){ |
|
3063
|
db_multi_exec("DROP TABLE onremote; DROP TABLE unk;"); |
|
3064
|
manifest_crosslink_end(MC_PERMIT_HOOKS); |
|
3065
|
content_enable_dephantomize(1); |
|
3066
|
db_end_transaction(0); |
|
3067
|
} |
|
3068
|
if( nErr && autopushFailed ){ |
|
3069
|
fossil_warning( |
|
3070
|
"Warning: The check-in was successful and is saved locally but you\n" |
|
3071
|
" are not authorized to push the changes back to the server\n" |
|
3072
|
" at %s", |
|
3073
|
g.url.canonical |
|
3074
|
); |
|
3075
|
nErr--; |
|
3076
|
} |
|
3077
|
if( (syncFlags & SYNC_CLONE)==0 && g.rcvid && fossil_any_has_fork(g.rcvid) ){ |
|
3078
|
fossil_warning("***** WARNING: a fork has occurred *****\n" |
|
3079
|
"use \"fossil leaves -multiple\" for more details."); |
|
3080
|
} |
|
3081
|
return nErr; |
|
3082
|
} |
|
3083
|
|