-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathreq.go
647 lines (584 loc) · 17.3 KB
/
req.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
// Copyright 2017 Blues Inc. All rights reserved.
// Use of this source code is governed by licenses granted by the
// copyright holder including that found in the LICENSE file.
package notelib
import (
"context"
"fmt"
"math"
"strings"
"github.com/blues/note-go/note"
"github.com/blues/note-go/notecard"
)
// Request performs a local operation using the JSON API
func (box *Notebox) Request(ctx context.Context, endpointID string, reqJSON []byte) (rspJSON []byte) {
var err error
req := notecard.Request{}
rsp := notecard.Request{}
// Debug
if debugRequest {
logDebug(ctx, ">> %s", string(reqJSON))
}
// Unmarshal the incoming request
err = note.JSONUnmarshal(reqJSON, &req)
if err != nil {
rsp.Err = fmt.Sprintf("unknown request: %s", err)
rspJSON, _ = note.JSONMarshal(rsp)
if debugRequest {
logDebug(ctx, "<< %s", string(rspJSON))
}
return
}
// Extract the request ID, which will be used to correlate requests with responses
rsp.RequestID = req.RequestID
// Handle legacy which used "files." instead of "file.". This was changed 2019-11-18 and
// can be removed after we ship because the compat was just until we distributed new notecard fw.
req.Req = strings.Replace(req.Req, "files.", "file.", -1)
// Dispatch based on request type
switch req.Req {
default:
rsp.Err = fmt.Sprintf("unknown request type: %s", req.Req)
case notecard.ReqFileSet:
fallthrough
case notecard.ReqFileAdd:
if req.FileInfo == nil || len(*req.FileInfo) == 0 {
rsp.Err = "no notefiles were specified"
break
}
err = box.VerifyAccess(note.ACResourceNotefiles, note.ACActionCreate)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
for notefileID, notefileInfo := range *req.FileInfo {
if !NotefileIDIsReservedWithExceptions(notefileID) || req.Allow {
err = box.AddNotefile(ctx, notefileID, ¬efileInfo)
if err != nil && rsp.Err == "" {
rsp.Err = fmt.Sprintf("error adding notefile: %s", err)
break
}
}
}
case notecard.ReqFileDelete:
if req.Files == nil || len(*req.Files) == 0 {
rsp.Err = "no notefiles were specified"
break
}
err = box.VerifyAccess(note.ACResourceNotefiles, note.ACActionDelete)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
deleteFiles := *req.Files
for i := range deleteFiles {
if !NotefileIDIsReservedWithExceptions(deleteFiles[i]) || req.Allow {
err = box.DeleteNotefile(ctx, deleteFiles[i])
if err != nil && rsp.Err == "" {
rsp.Err = fmt.Sprintf("error deleting %s: %s", deleteFiles[i], err)
break
}
}
}
case notecard.ReqNoteAdd:
if req.NotefileID == "" {
req.NotefileID = note.HubDefaultInboundNotefile
}
// Check for access to add a note
err = box.VerifyAccess(note.ACResourceNotefile+req.NotefileID, note.ACActionCreate)
if err != nil {
// Preset assuming access failure
rsp.Err = fmt.Sprintf("%s", err)
// Special-case access check for notefiles with "add only" access control
var info note.NotefileInfo
info, err = box.GetNotefileInfo(req.NotefileID)
if err != nil || !info.AnonAddAllowed {
break
}
}
// Make sure that the file exists
if NotefileIDIsReservedWithExceptions(req.NotefileID) && !req.Allow {
rsp.Err = "reserved notefile name"
break
}
if !box.NotefileExists(req.NotefileID) {
err = box.AddNotefile(ctx, req.NotefileID, nil)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
}
// Check the noteID
isQueue, toHub, _, _, _, _ := NotefileAttributesFromID(req.NotefileID)
if req.NoteID == "" {
if !isQueue {
rsp.Err = "note ID is required when using a database notefile"
break
}
} else {
if isQueue {
rsp.Err = "note ID should not be specified when using a queue notefile"
break
}
}
// Add the note
xnote := note.Note{}
annotateNoteWithWhereWhen(&xnote, req)
if req.Payload != nil {
xnote.Payload = *req.Payload
}
if req.Body != nil {
xnote.Body = *req.Body
}
if !req.Allow {
err = box.checkAddNoteLimits(ctx, req.NotefileID, xnote)
}
if err == nil {
err = box.AddNoteWithHistory(ctx, endpointID, req.NotefileID, req.NoteID, xnote)
}
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
// If this is an outbound queue, purge ALL tombstones from the
// file that would normally have been purged during sync/merge
if isQueue && toHub {
openfile, file, err := box.OpenNotefile(ctx, req.NotefileID)
if err == nil {
file.PurgeTombstones("*")
openfile.Close(ctx)
}
}
case notecard.ReqNoteUpdate:
if req.NotefileID == "" {
rsp.Err = "no notefile specified"
break
}
if NotefileIDIsReservedWithExceptions(req.NotefileID) && !req.Allow {
rsp.Err = "reserved notefile name"
break
}
if !box.NotefileExists(req.NotefileID) {
err = box.AddNotefile(ctx, req.NotefileID, nil)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
}
if req.NoteID == "" {
rsp.Err = "no note ID specified"
break
}
err = box.VerifyAccess(note.ACResourceNotefile+req.NotefileID, note.ACActionUpdate)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
var xnote note.Note
xnote, err = box.GetNote(ctx, req.NotefileID, req.NoteID)
if err != nil {
annotateNoteWithWhereWhen(&xnote, req)
if req.Payload != nil {
xnote.Payload = *req.Payload
}
if req.Body != nil {
xnote.Body = *req.Body
}
err = nil
if !req.Allow {
err = box.checkAddNoteLimits(ctx, req.NotefileID, xnote)
}
if err == nil {
err = box.AddNoteWithHistory(ctx, endpointID, req.NotefileID, req.NoteID, xnote)
}
if err != nil {
rsp.Err = fmt.Sprintf("error adding note: %s", err)
break
}
break
}
annotateNoteWithWhereWhen(&xnote, req)
if req.Payload != nil {
xnote.Payload = *req.Payload
} else {
xnote.Payload = nil
}
if req.Body != nil {
xnote.Body = *req.Body
} else {
xnote.Body = nil
}
err = box.UpdateNote(ctx, endpointID, req.NotefileID, req.NoteID, xnote)
if err != nil {
rsp.Err = fmt.Sprintf("error updating note: %s", err)
break
}
case notecard.ReqNoteDelete:
err = box.VerifyAccess(note.ACResourceNotefile+req.NotefileID, note.ACActionDelete)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
if req.NotefileID == "" {
rsp.Err = "no notefile specified"
break
}
if NotefileIDIsReservedWithExceptions(req.NotefileID) && !req.Allow {
rsp.Err = "reserved notefile name"
break
}
if req.NoteID == "" {
rsp.Err = "no note ID specified"
break
}
err = box.DeleteNote(ctx, endpointID, req.NotefileID, req.NoteID)
if err != nil {
rsp.Err = fmt.Sprintf("error deleting note: %s", err)
break
}
case notecard.ReqNoteGet:
if req.NotefileID == "" {
rsp.Err = "no notefile specified"
break
}
isQueue, toHub, _, _, _, _ := NotefileAttributesFromID(req.NotefileID)
if req.NoteID == "" {
if !isQueue || !toHub {
rsp.Err = "no note ID specified"
break
}
}
if isQueue || req.Delete {
err = box.VerifyAccess(note.ACResourceNotefile+req.NotefileID, note.ACActionRead+note.ACActionAnd+note.ACActionDelete)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
} else {
err = box.VerifyAccess(note.ACResourceNotefile+req.NotefileID, note.ACActionRead)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
}
// Handle queues by getting the LRU note
if isQueue && toHub && req.NoteID == "" {
openfile, file, err2 := box.OpenNotefile(ctx, req.NotefileID)
if err2 != nil {
rsp.Err = fmt.Sprintf("%s", err2)
break
}
req.NoteID, err = file.GetLeastRecentNoteID()
if err != nil {
openfile.Close(ctx)
rsp.Err = fmt.Sprintf("%s", err)
break
}
openfile.Close(ctx)
}
// Get the note
var xnote note.Note
xnote, err = box.GetNote(ctx, req.NotefileID, req.NoteID)
if err != nil {
rsp.Err = fmt.Sprintf("error getting note: %s", err)
break
}
rsp.NoteID = req.NoteID
if !isQueue && xnote.Deleted {
if !req.Deleted {
rsp.Err = fmt.Sprintf("note has been deleted: %s "+note.ErrNoteNoExist, req.NoteID)
break
}
rsp.Deleted = true
}
if xnote.Body != nil {
rsp.Body = &xnote.Body
}
if len(xnote.Payload) != 0 {
payload := xnote.GetPayload()
rsp.Payload = &payload
}
if xnote.Histories != nil && len(*xnote.Histories) > 0 {
histories := *xnote.Histories
if histories[0].When < 1483228800 || histories[0].When > math.MaxUint32 { // before 1/1/2017 or can't fit into a uint32
rsp.Time = 0
} else {
rsp.Time = histories[0].When
}
}
if req.Delete {
_ = box.DeleteNote(ctx, endpointID, req.NotefileID, req.NoteID)
}
case notecard.ReqFileGetL:
fallthrough
case notecard.ReqFileChanges:
fallthrough
case notecard.ReqFileChangesPending:
var notefiles []string
changes := 0
total := 0
showOnlyTotal := false
// Check access
err = box.VerifyAccess(note.ACResourceNotefiles, note.ACActionRead)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
// Special way of requesting to see if there is stuff pending to be uploaded
if req.Req == notecard.ReqFileChangesPending {
req.Pending = true
}
if req.Pending {
req.TrackerID = "^"
}
// If no tracker, generate the entire list of notefiles
tracker := req.TrackerID
if tracker == "" {
// Use the special reserved name, which (for internal use only) means "no tracker"
showOnlyTotal = true
tracker = ReservedIDDelimiter
notefiles = box.Notefiles(false)
} else {
// Handle special case of "^" meaning things to be downloaded
if req.TrackerID == "^" {
req.TrackerID = note.DefaultDeviceEndpointID
} else {
// Make sure that it's a valid tracker in that it's not one of our known endpoints
if req.TrackerID == note.DefaultDeviceEndpointID || req.TrackerID == note.DefaultHubEndpointID {
rsp.Err = fmt.Sprintf("cannot use this reserved tracker name: %s", req.TrackerID)
break
}
}
// Get the changed notefiles for that tracker
notefiles = box.GetChangedNotefiles(ctx, req.TrackerID)
}
// Prune the list of notefiles based on whether or not the user's perception is
// that they should ever even exist on the notehub. The obvious things we're trying to skim off
// are .qo, .qos, and anything ending in 'x' which is a local-only file
newNotefiles := []string{}
for _, file := range notefiles {
isQueue, syncToHub, syncFromHub, _, _, _ := NotefileAttributesFromID(file)
if !(isQueue && syncToHub) && (syncToHub || syncFromHub) {
newNotefiles = append(newNotefiles, file)
}
}
notefiles = newNotefiles
// Prune the list of notefiles based on the input
if req.Files != nil && len(*req.Files) != 0 {
newNotefiles := []string{}
filterFiles := *req.Files
for i := range filterFiles {
for j := range notefiles {
if notefiles[j] == filterFiles[i] {
newNotefiles = append(newNotefiles, notefiles[j])
break
}
}
}
notefiles = newNotefiles
}
// Generate the list of notefiles in the response data structure
fileinfoArray := map[string]note.NotefileInfo{}
for i := range notefiles {
notefileID := notefiles[i]
// If this is the notebox's notefile, omit it
if notefileID == box.EndpointID() {
continue
}
// If we're getting a list of all notefiles, skip the reserved notefiles
// entirely. Otherwise, if we have a tracker, allow the exceptions.
if tracker == ReservedIDDelimiter {
if NotefileIDIsReserved(notefileID) && !req.Allow {
continue
}
} else {
if NotefileIDIsReservedWithExceptions(notefileID) && !req.Allow {
continue
}
}
// Get the number of pending changes. Note that if we didn't supply a tracker,
// we supply all the results even if there are 0 changes (0 notes) in the notefile.
openfile, file, err := box.OpenNotefile(ctx, notefileID)
if err != nil {
rsp.Err = fmt.Sprintf("error opening notefile %s to get changes: %s", notefileID, err)
break
}
// Use a special internal "count only" mode of this call, for efficiency
_, _, totalChanges, totalNotes, _, _, err := file.GetChanges(tracker, false, -1)
if err != nil {
rsp.Err = fmt.Sprintf("cannot get changes for %s: %s", notefileID, err)
break
} else {
// Append it
fileinfo := note.NotefileInfo{}
if !showOnlyTotal {
fileinfo.Changes = totalChanges
}
fileinfo.Total = totalNotes
changes += totalChanges
total += totalNotes
fileinfoArray[notefileID] = fileinfo
}
openfile.Close(ctx)
}
// Done
if len(fileinfoArray) != 0 {
rsp.FileInfo = &fileinfoArray
if !showOnlyTotal {
rsp.Changes = int32(changes)
}
rsp.Total = int32(total)
}
// Return whether or not pending
rsp.Pending = req.Pending && rsp.Changes > 0
case notecard.ReqNotesGetL:
fallthrough
case notecard.ReqNoteChanges:
showTotalOnly := false
updateTracker := true
if req.NotefileID == "" {
rsp.Err = "no notefile specified"
break
}
// Check access
if req.Delete {
err = box.VerifyAccess(note.ACResourceNotefile+req.NotefileID, note.ACActionRead+note.ACActionAnd+note.ACActionDelete)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
} else {
err = box.VerifyAccess(note.ACResourceNotefile+req.NotefileID, note.ACActionRead)
if err != nil {
rsp.Err = fmt.Sprintf("%s", err)
break
}
}
// Make sure that a tracker name is specified
if req.TrackerID == "" {
showTotalOnly = true
updateTracker = false
req.TrackerID = ReservedIDDelimiter
}
// If we're debugging and we want to see the changes for the specified tracker
// BUT we don't want to update the tracker just yet.
if req.Pending {
updateTracker = false
}
// Make sure that it's a valid tracker in that it's not one of our known endpoints
if req.TrackerID == note.DefaultDeviceEndpointID || req.TrackerID == note.DefaultHubEndpointID {
rsp.Err = fmt.Sprintf("cannot use this reserved tracker name: %s", req.TrackerID)
break
}
if NotefileIDIsReservedWithExceptions(req.NotefileID) && !req.Allow {
rsp.Err = "reserved notefile name"
break
}
// Open the notefile
openfile, file, err := box.OpenNotefile(ctx, req.NotefileID)
if err != nil {
rsp.Err = fmt.Sprintf("error opening notefile: %s", err)
break
}
// If the flag was set, clear the tracker to make sure we get all notes
if (req.Start || req.Reset) && updateTracker {
file.ClearTracker(req.TrackerID)
}
// Get the changed notes for that tracker, up to the specified (or default) max
chgfile, _, totalChanges, totalNotes, since, until, err := file.GetChanges(req.TrackerID, req.Deleted, int(req.Max))
if err != nil {
if req.Stop && updateTracker {
_ = file.DeleteTracker(req.TrackerID)
}
openfile.Close(ctx)
rsp.Err = fmt.Sprintf("cannot get list of changed notes: %s", err)
break
}
// Update the change tracker because we're confident that we'll return successfully
if updateTracker {
_ = file.UpdateChangeTracker(req.TrackerID, since, until)
}
// If the flag was set, delete the tracker because it is no longer needed
if req.Stop && updateTracker {
_ = file.DeleteTracker(req.TrackerID)
}
// We'll need to know if it's a queue
isQueue, _, _, _, _, _ := NotefileAttributesFromID(req.NotefileID)
// Generate the list of notes in the response data structure
noteIDs := chgfile.NoteIDs(true)
infolist := map[string]note.Info{}
for i := range noteIDs {
xnote, err := chgfile.GetNote(noteIDs[i])
if err != nil {
if rsp.Err == "" {
rsp.Err = fmt.Sprintf("error opening note: %s", err)
}
break
}
// Get the info from the note
info := note.Info{}
if !isQueue && xnote.Deleted {
info.Deleted = true
}
if xnote.Body != nil {
info.Body = &xnote.Body
}
if len(xnote.Payload) != 0 {
payload := xnote.GetPayload()
info.Payload = &payload
}
// Add it to the list to be returned
infolist[noteIDs[i]] = info
// Delete it as a side-effect, if desired
if req.Delete {
_ = file.DeleteNote(endpointID, noteIDs[i])
}
}
if len(infolist) > 0 {
rsp.Notes = &infolist
if !showTotalOnly {
rsp.Changes = int32(totalChanges)
}
}
rsp.Total = int32(totalNotes)
// Close the notefile
openfile.Close(ctx)
}
// Marshal a response
rspJSON, _ = note.JSONMarshal(rsp)
// Append a \n so that the requester can recognize end-of-response
rspJSON = []byte(string(rspJSON) + "\n")
// Debug
if debugRequest {
logDebug(ctx, "<< %s", string(rspJSON))
}
// Done
return
}
// Define an invalid time so that we force a history to be added with a 0 time.
// (see packet.go and elsewhere)
const InvalidTime = int64(0xffffffff)
// Annotate a note structure with optional location and tower info from a request
func annotateNoteWithWhereWhen(xnote *note.Note, req notecard.Request) {
if req.Time != 0 || req.Latitude != 0 || req.Longitude != 0 {
if req.Time == InvalidTime {
req.Time = 0
}
newHistory := note.History{}
newHistory.When = req.Time
newHistory.Where = req.LocationOLC
newHistory.WhereWhen = req.LocationTime
newHistories := []note.History{}
newHistories = append(newHistories, newHistory)
xnote.Histories = &newHistories
}
if req.Tower != nil {
xnote.Tower = req.Tower
}
}
// ErrorResponse creates a simple JSON response given an error
func ErrorResponse(err error) (response []byte) {
rsp := notecard.Request{}
rsp.Err = fmt.Sprintf("%s", err)
response, _ = note.JSONMarshal(&rsp)
return
}