-
Notifications
You must be signed in to change notification settings - Fork 0
/
HttpCommand.aplc
1461 lines (1328 loc) · 67.7 KB
/
HttpCommand.aplc
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
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
:Class HttpCommand
⍝ General HTTP Commmand utility
⍝ Documentation is found at https://dyalog.github.io/HttpCommand/
⎕ML←⎕IO←1
∇ r←Version
⍝ Return the current version
:Access public shared
r←'HttpCommand' '5.4.4' '2023-10-31'
∇
⍝ Request-related fields
:field public Command←'get' ⍝ HTTP command (method)
:field public URL←'' ⍝ requested resource
:field public Params←'' ⍝ request parameters
:field public Headers←0 2⍴⊂'' ⍝ request headers - name, value
:field public ContentType←'' ⍝ request content-type
:field public Cookies←⍬ ⍝ request cookies - vector of namespaces
:field public Auth←'' ⍝ authentication string
:field public AuthType←'' ⍝ authentication type
:field public BaseURL←'' ⍝ base URL to use when making multiple requests to the same host
⍝ Proxy-related fields - only used if connecting through a proxy server
:field public ProxyURL←'' ⍝ address of the proxy server
:field public ProxyAuth←'' ⍝ authentication string, if any, for the proxy server
:field public ProxyAuthType←'' ⍝ authentication type, if any, for the proxy server
:field public ProxyHeaders←0 2⍴⊂'' ⍝ any headers that the proxy server might need
⍝ Conga-related fields
:field public BufferSize←200000 ⍝ Conga buffersize
:field public WaitTime←5000 ⍝ Timeout in ms on Conga Wait call
:field public Cert←⍬ ⍝ X509 instance if using HTTPS
:field public SSLFlags←32 ⍝ SSL/TLS flags - 32 = accept cert without checking it
:field public Priority←'NORMAL:!CTYPE-OPENPGP' ⍝ GnuTLS priority string
:field public PublicCertFile←'' ⍝ if not using an X509 instance, this is the client public certificate file
:field public PrivateKeyFile←'' ⍝ if not using an X509 instance, this is the client private key file
:field public shared LDRC ⍝ HttpCommand-set reference to Conga after CongaRef has been resolved
:field public shared CongaPath←'' ⍝ path to user-supplied conga workspace (assumes shared libraries are in the same path)
:field public shared CongaRef←'' ⍝ user-supplied reference to Conga library
:field public shared CongaVersion←'' ⍝ Conga [major minor build]
⍝ Operational fields
:field public SuppressHeaders←0 ⍝ set to 1 to suppress HttpCommand-supplied default request headers
:field public MaxPayloadSize←¯1 ⍝ set to ≥0 to take effect
:field public Timeout←10 ⍝ seconds to wait for a response before timing out, negative means reset timeout if any activity
:field public RequestOnly←¯1 ⍝ set to 1 if you only want to return the generated HTTP request, but not actually send it
:field public OutFile←'' ⍝ name of file to send payload to (format is same as ⎕NPUT right argument)
:field public MaxRedirections←10 ⍝ set to 0 if you don't want to follow any redirected references, ¯1 for unlimited
:field public KeepAlive←1 ⍝ default to not close client connection
:field public TranslateData←0 ⍝ set to 1 to translate XML or JSON response data
:field public UseZip←0 ⍝ zip request payload (0-no, 1-use gzip, 2-use deflate)
:field public ZipLevel←1 ⍝ default compression level (0-9)
:field public shared Debug←0 ⍝ set to 1 to disable trapping, 2 to stop just before creating client
:field public readonly shared ValidFormUrlEncodedChars←'&=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~*+~%'
:field Client←'' ⍝ Conga client ID
:field ConxProps←'' ⍝ when a client is made, its connection properties are saved so that if either changes, we close the previous client
:field origCert←¯1 ⍝ used to check if Cert changed between calls
∇ make
⍝ No argument constructor
:Access public
:Implements constructor
∇
∇ make1 args;settings;invalid
⍝ Constructor arguments - [Command URL Params Headers Cert SSLFlags Priority]
:Access public
:Implements constructor
→0⍴⍨0∊⍴args
args←(eis⍣({9.1≠⎕NC⊂,'⍵'}⊃args)⊢args)
:Select {⊃⎕NC⊂,'⍵'}⊃args
:Case 2.1 ⍝ array
Command URL Params Headers Cert SSLFlags Priority←7↑args,(⍴args)↓Command URL Params Headers Cert SSLFlags Priority
:Case 9.1 ⍝ namespace
:If 0∊⍴invalid←(settings←args.⎕NL ¯2.1 ¯9.1)~(⎕NEW⊃⊃⎕CLASS ⎕THIS).⎕NL ¯2.2
args{⍎⍵,'←⍺⍎⍵'}¨settings
:Else ⋄ ('Invalid HttpCommand setting(s): ',,⍕invalid)⎕SIGNAL 11
:EndIf
:Else ⋄ 'Invalid constructor argument'⎕SIGNAL 11
:EndSelect
∇
∇ {ns}←initResult ns
⍝ initialize the namespace result
:Access shared
ns.(Command URL rc msg HttpVersion HttpStatus HttpMessage Headers Data PeerCert Redirections Cookies OutFile Elapsed BytesWritten)←'' '' ¯1 '' ''⍬''(0 2⍴⊂'')''⍬(0⍴⊂'')⍬'' 0 ¯1
ns.GetHeader←{⎕IO←⎕ML←1 ⋄ ⍺←Headers ⋄ ⍺{1<|≡⍵:⍺∘∇¨⍵ ⋄ (⍺[;2],⊂'')⊃⍨⍺[;1](⍳{(⍵⍵ ⍺)⍺⍺(⍵⍵ ⍵)}{2::0(819⌶)⍵ ⋄ ¯3 ⎕C ⍵})⊆,⍵}⍵} ⍝ return header value or '' if not found
ns.⎕FX'∇r←IsOK' 'r←0 2≡rc,⌊.01×HttpStatus' '∇'
∇
∇ Goodbye
:Implements destructor
{}{0::'' ⋄ LDRC.Names'.'⊣LDRC.Close ⍵}⍣(~0∊⍴Client)⊢Client
∇
∇ r←Config
⍝ Returns current configuration
:Access public
r←↑{6::⍵'not set' ⋄ ⍵(⍎⍵)}¨(⎕THIS⍎'⎕NL ¯2.2')~⊂'ValidFormUrlEncodedChars'
∇
∇ r←Run
⍝ Attempt to run the HTTP command
:Access public
RequestOnly←0⌈RequestOnly
Result←initResult #.⎕NS''
:Trap Debug↓0
r←(Cert SSLFlags Priority PublicCertFile PrivateKeyFile)HttpCmd Command URL Params Headers
:Else ⍝ :Trap
r←Result
r.(rc msg)←¯1('Unexpected ',⊃{⍺,' at ',⍵}/2↑⎕DMX.DM)
:EndTrap
setDisplayFormat r
exit:
∇
∇ r←Show;ro
⍝ Show the request to be sent to the server
:Access public
ro←RequestOnly
RequestOnly←1
r←Run
RequestOnly←ro
∇
∇ {r}←setDisplayFormat r;rc;msg;stat;data
⍝ set the display format for the namespace result for most HttpCommand commands
:If 9.1=nameClass r
rc←'rc: ',⍕r.rc
msg←' | msg: ',⍕r.msg
stat←' | HTTP Status: ',(⍕r.HttpStatus),' "',r.HttpMessage,'"'
data←' | ',{¯1≠r.BytesWritten:(⍕r.BytesWritten),' bytes written to ',r.OutFile ⋄ '≢Data: ',(⍕≢⍵),(9.1=nameClass ⍵)/' (namespace)'}r.Data
r.⎕DF 1⌽'][',rc,msg,stat,data
:EndIf
∇
∇ r←{requestOnly}Get args
⍝ Shared method to perform an HTTP GET request
⍝ args - [URL Params Headers Cert SSLFlags Priority]
:Access public shared
:If 0=⎕NC'requestOnly' ⋄ requestOnly←¯1 ⋄ :EndIf
:If 2.1=nameClass⊃args ⋄ args←((⊂'GET'),eis args) ⋄ :EndIf
→∆EXIT⍴⍨9.1=nameClass r←requestOnly New args
r←r.Run
∆EXIT:
∇
∇ r←{requestOnly}Do args
⍝ Shared method to perform any HTTP request
⍝ args - [Command URL Params Headers Cert SSLFlags Priority]
:Access public shared
:If 0=⎕NC'requestOnly' ⋄ requestOnly←¯1 ⋄ :EndIf
→∆EXIT⍴⍨9.1=nameClass r←requestOnly New args
r←r.Run
∆EXIT:
∇
∇ r←{requestOnly}New args
⍝ Shared method to create new HttpCommand
⍝ args - [Command URL Params Headers Cert SSLFlags Priority]
⍝ requestOnly - initial setting for RequestOnly
:Access public shared
:If 0=⎕NC'requestOnly' ⋄ requestOnly←¯1 ⋄ :EndIf
r←''
:Trap Debug↓0
:If 0∊⍴args
r←##.⎕NEW ⎕THIS
:Else
r←##.⎕NEW ⎕THIS(eis⍣(9.1≠nameClass⊃args)⊢args)
:EndIf
r.RequestOnly←requestOnly
:Else
r←initResult #.⎕NS''
r.(rc msg)←¯1 ⎕DMX.EM
setDisplayFormat r
→∆EXIT
:EndTrap
∆EXIT:
∇
∇ r←{requestOnly}GetJSON args;cmd
⍝ Shared method to perform an HTTP request with JSON data as the request and response payloads
⍝ args - [Command URL Params Headers Cert SSLFlags Priority]
:Access public shared
:If 0=⎕NC'requestOnly' ⋄ requestOnly←¯1 ⋄ :EndIf
→∆EXIT⍴⍨9.1=nameClass cmd←requestOnly New args
:If 0∊⍴cmd.Command ⋄ cmd.Command←(1+0∊⍴cmd.Params)⊃'POST' 'GET' ⋄ :EndIf
:If ~(⊂lc cmd.Command)∊'get' 'head'
:If 0∊⍴cmd.ContentType ⋄ cmd.ContentType←'application/json;charset=utf-8' ⋄ :EndIf
:If ~0∊⍴cmd.Params
:Trap Debug↓0
cmd.Params←JSONexport cmd.Params
:Else
→∆DONE⊣r.(rc msg)←¯1 'Could not convert parameters to JSON format'
:EndTrap
:EndIf
:EndIf
r←cmd.Run
→cmd.RequestOnly⍴∆EXIT
:If r.rc=0
→∆DONE⍴⍨204=r.HttpStatus ⍝ exit if "no content" HTTP status
:If ¯1=r.BytesWritten ⍝ if not writing to file
:If ∨/'application/json'⍷lc r.Headers getHeader'content-type'
JSONimport r
:Else ⋄ →∆DONE⊣r.(rc msg)←¯2 'Response content-type is not application/json'
:EndIf
:EndIf
:EndIf
∆DONE: ⍝ reset ⎕DF if messages have changed
setDisplayFormat r
∆EXIT:
∇
∇ r←{ro}Fix args;z;url;target
⍝ retrieve and fix APL code loads the latest version from GitHub
⍝ args is:
⍝ [1] URL of code to fix - if the URL has 'github' (but not 'raw.githubusercontent.com') in it, we do some gratuitous massaging
⍝ [2] (optional) reference to namespace in which to fix the code (default ##)
⍝ example: HttpCommand.Fix 'github/Dyalog/Jarvis/Source/Jarvis.dyalog' #.
:Access public shared
(url target)←2↑(,⊆args),##
:If 0=⎕NC'ro' ⋄ ro←0 ⋄ :EndIf
r←z←ro Get{ ⍝ convert url if necessary
~∨/'github'⍷⍵:⍵ ⍝ if not github just
∨/'raw.githubusercontent.com'⍷⍵:⍵ ⍝ already refers to
t←'/'(≠⊆⊢)⍵
i←⍸<\∨/¨'github'∘⍷¨t
'https://raw.githubusercontent.com',∊'/',¨(2↑i↓t),(⊂'master'),(2+i)↓t
}url
→ro⍴0
:If z.rc≠0
r←z.(rc msg)
:ElseIf z.HttpStatus≠200
r←¯1(⍕z)
:Else
:Trap 0
r←0(⍕target{0::⍺.⎕FX ⍵ ⋄ ⍺.⎕FIX ⍵}{⍵⊆⍨~⍵∊⎕UCS 13 10 65279}z.Data)
:Else
r←¯1('Could not ⎕FIX file: ',2↓∊': '∘,¨⎕DMX.(EM Message))
:EndTrap
:EndIf
∇
∇ r←Init
:Access Public
r←(Initialize initResult ⎕NS'').(rc msg)
r[1]×←~0∊⍴2⊃r ⍝ set to 0 if no error message from Conga initialization
∇
∇ r←Initialize r;ref;root;nc;n;ns;congaCopied;class;path
⍝↓↓↓ Check if LDRC exists (VALUE ERROR (6) if not), and is LDRC initialized? (NONCE ERROR (16) if not)
r.msg←''
:Hold 'HttpCommandInit'
:If {6 16 999::1 ⋄ ''≡LDRC:1 ⋄ 0⊣LDRC.Describe'.'}''
LDRC←''
:If ~0∊⍴CongaRef ⍝ did the user supply a reference to Conga?
:If 0∊⍴LDRC←r ResolveCongaRef CongaRef
r.msg,⍨←'Could not initialize Conga using CongaRef "',(⍕CongaRef),'" due to '
→∆END
:EndIf
:Else
:For root :In ## #
ref nc←root{1↑¨⍵{(×⍵)∘/¨⍺ ⍵}⍺.⎕NC ⍵}ns←'Conga' 'DRC'
:If 9=⊃⌊nc ⋄ :Leave ⋄ :EndIf
:EndFor
:If 9=⊃⌊nc
:If 0∊⍴LDRC←r ResolveCongaRef(root⍎∊ref)
→∆END⊣r.msg,⍨←'Could not initialize Conga from "',(∊(⍕root)'.'ref),'" due to '
:EndIf
→∆COPY↓⍨{999::0 ⋄ 1⊣LDRC.Describe'.'}'' ⍝ it's possible that Conga was saved in a semi-initialized state
:Else
∆COPY:
class←⊃⊃⎕CLASS ⎕THIS
:If ~0∊⍴CongaPath
CongaPath←∊1 ⎕NPARTS CongaPath,'/'
→∆END↓⍨0∊⍴r.msg←(~⎕NEXISTS CongaPath)/'CongaPath "',CongaPath,'" does not exist'
→∆END↓⍨0∊⍴r.msg←(1≠1 ⎕NINFO CongaPath)/'CongaPath "',CongaPath,'" is not a folder'
:EndIf
congaCopied←0
:For n :In ns
:For path :In (1+0∊⍴CongaPath)⊃(⊂CongaPath)((dyalogRoot,'ws/')'') ⍝ if CongaPath specifiec, use it exclusively
:Trap Debug↓0
n class.⎕CY path,'conga'
LDRC←r ResolveCongaRef(class⍎n)
:If 0∊⍴LDRC
r.msg,⍨←n,' was copied from "',path,'conga", but encountered '
→∆END
:EndIf
→∆COPIED⊣congaCopied←1
:EndTrap
:EndFor
:EndFor
→∆END↓⍨0∊⍴r.msg←(~congaCopied)/'neither Conga nor DRC were successfully copied'
∆COPIED:
:EndIf
:EndIf
:EndIf
CongaVersion←LDRC.Version
LDRC.X509Cert.LDRC←LDRC ⍝ reset X509Cert.LDRC reference
:If 0≠⊃LDRC.SetProp'.' 'EventMode' 1
r.msg←'Unable to set EventMode on Conga root'
:EndIf
∆END:
:EndHold
∇
∇ LDRC←r ResolveCongaRef CongaRef;failed;z
⍝ Attempt to resolve what CongaRef refers to
⍝ CongaRef can be a charvec, reference to the Conga or DRC namespaces, or reference to an iConga instance
⍝ LDRC is '' if Conga could not be initialized, otherwise it's a reference to the the Conga.LIB instance or the DRC namespace
LDRC←'' ⋄ failed←0
:Select nameClass CongaRef ⍝ what is it?
:Case 9.1 ⍝ namespace? e.g. CongaRef←DRC or Conga
∆TRY:
:Trap Debug↓0
:If 2 3≢⌊CongaRef.⎕NC'DllVer' 'Init'
r.msg←'it does not refer to a valid Conga interface'
→∆EXIT⊣LDRC←''
:EndIf
:If ∨/'.Conga'⍷⍕CongaRef ⍝ Conga?
LDRC←CongaPath CongaRef.Init'HttpCommand'
:ElseIf 0≡⊃CongaRef.Init CongaPath ⍝ DRC?
LDRC←CongaRef
:Else ⍝ should never get to here, but... (paranoia)
r.msg←'it does not refer to a valid Conga interface'
→∆EXIT⊣LDRC←''
:End
:Else ⍝ if HttpCommand is reloaded and re-executed in rapid succession, Conga initialization may fail, so we try twice
:If failed
→∆EXIT⊣LDRC←''⊣r.msg←∊{⍺,(~0∊⍴⍵)/': ',⍵}/⎕DMX.(EM Message)
:Else
→∆TRY⊣failed←1
:EndIf
:EndTrap
:Case 9.2 ⍝ instance? e.g. CongaRef←Conga.Init ''
:If 3=⌊|CongaRef.⎕NC⊂'Clt' ⍝ if it looks like a valid Conga reference
LDRC←CongaRef ⍝ an instance is already initialized
:EndIf
:Case 2.1 ⍝ variable? e.g. CongaRef←'#.Conga'
:Trap Debug↓0
:If 9≠z←⎕NC⍕CongaRef
→∆EXIT⊣r.msg←'CongaRef ',(1+z=0)⊃'is invalid' 'was not found'
:EndIf
LDRC←r ResolveCongaRef(⍎∊⍕CongaRef)
:Else
r.msg←∊{⍺,(~0∊⍴⍵)/': ',⍵}/⎕DMX.(EM Message)
:EndTrap
:EndSelect
∆EXIT:
∇
∇ (rc secureParams)←CreateSecureParams certs;cert;flags;priority;public;private;nmt;msg;t
⍝ certs is:
⍝ cert - X509Cert instance or (PublicCertFile PrivateKeyFile)
⍝ flags - SSL flags
⍝ priority - GnuTLS priority
⍝ public - PublicCertFile
⍝ private - PrivateKeyFile
certs←,⊆certs
(cert flags priority public private)←5↑certs,(≢certs)↓'' 0 'NORMAL:!CTYPE-OPENPGP' '' ''
LDRC.X509Cert.LDRC←LDRC ⍝ make sure the X509 instance points to the right LDRC
:If 0∊⍴cert ⍝ if X509 (or public private) not supplied
∆CHECK:
⍝ if cert is empty, check PublicCertFile and PrivateKeyFile
:If ∨/nmt←(~0∊⍴)¨public private ⍝ either file name not empty?
:If ∧/nmt ⍝ if so, both need to be non-empty
:If ∨/t←{0::1 ⋄ ~⎕NEXISTS ⍵}¨public private ⍝ either file not exist?
→∆FAIL⊣msg←'Not found',4↓∊t/'PublicCertFile' 'PrivateKeyFile'{' and ',⍺,' "',(∊⍕⍵),'"'}¨public private
:EndIf
:Trap Debug↓0
cert←⊃LDRC.X509Cert.ReadCertFromFile public
:Else
→∆FAIL⊣msg←'Unable to decode PublicCertFile "',(∊⍕public),'" as certificate'
:EndTrap
cert.KeyOrigin←'DER'private
:Else
→∆FAIL⊣msg←(⊃nmt/'PublicCertFile' 'PrivateKeyFile'),' is empty' ⍝ both must be specified
:EndIf
:Else
cert←⎕NEW LDRC.X509Cert
:EndIf
:ElseIf 2=⍴cert ⍝ 2-element vector of public/private file names?
public private←cert
→∆CHECK
:ElseIf {0::1 ⋄ 'X509Cert'≢{⊃⊢/'.'(≠⊆⊢)⍵}⍕⎕CLASS ⍵}cert
→∆FAIL⊣msg←'Invalid certificate parameter'
:EndIf
secureParams←('x509'cert)('SSLValidation'flags)('Priority'priority)
→rc←0
∆FAIL:(rc secureParams)←¯1 msg ⍝ failure
∇
∇ {r}←certs HttpCmd args;url;parms;hdrs;urlparms;p;b;secure;port;host;path;auth;req;err;done;data;datalen;rc;donetime;ind;len;obj;evt;dat;z;msg;timedOut;certfile;keyfile;simpleChar;defaultPort;cookies;domain;t;replace;outFile;toFile;startSize;options;congaPath;progress;starttime;outTn;secureParams;ct;forceClose;headers;cmd;file;protocol;conx;proxied;proxy;cert;noCT;simpleParms;noContentLength;connectionClose;tmpFile;tmpTn;redirected;encoding;compType;isutf8
⍝ issue an HTTP command
⍝ certs - X509Cert|(PublicCertFile PrivateKeyFile) SSLValidation Priority PublicCertFile PrivateKeyFile
⍝ args - [1] HTTP method
⍝ [2] URL in format [HTTP[S]://][user:pass@]url[:port][/path[?query_string]]
⍝ {3} parameters is using POST - either a namespace or URL-encoded string
⍝ {4} HTTP headers in form {↑}(('hdr1' 'val1')('hdr2' 'val2'))
⍝ {5} cookies in form {↑}(('cookie1' 'val1')('cookie2' 'val2'))
⍝ Makes secure connection if left arg provided or URL begins with https:
⍝ Result: namespace containing (conga return code) (HTTP Status) (HTTP headers) (HTTP body) [PeerCert if secure]
args←,⊆args
(cmd url parms headers cookies)←args,(⍴args)↓'' ''(⎕NS'')'' ''
:If 0∊⍴cmd ⋄ cmd←'GET' ⋄ :EndIf
r←Result
toFile←redirected←outTn←tmpTn←0 ⍝ initial settings
tmpFile←''
⍝ Do some cursory parameter checking
→∆END↓⍨0∊⍴r.msg←'No URL specified'/⍨0∊⍴url ⍝ exit early if no URL
→∆END↓⍨0∊⍴r.msg←'URL is not a simple character vector'/⍨~isSimpleChar url
→∆END↓⍨0∊⍴r.msg←'Cookies are not character'/⍨(0∊⍴cookies)⍱1↑isChar cookies
→∆END↓⍨0∊⍴r.msg←'Headers are not character'/⍨(0∊⍴headers)⍱1↑isChar headers
:If ~RequestOnly ⍝ don't bother initializing Conga if only returning request
→∆END↓⍨0∊⍴(Initialize r).msg
:EndIf
url←,url
url←BaseURL makeURL url
cmd←uc,cmd
∆GET:
⍝ do header initialization here because we may return here on a redirect
:Trap 7
hdrs←makeHeaders headers
:Else
→∆END⊣r.msg←'Improper header format'
:EndTrap
conx←ConxProps ConnectionProperties r.URL←url
→∆END↓⍨0∊⍴r.msg←conx.msg
(protocol secure auth host port path urlparms defaultPort)←conx.(protocol secure auth host port path urlparms defaultPort)
secure∨←⍲/{0∊⍴⍵}¨certs[1 4] ⍝ we're also secure if we have a cert or a PublicCertFile
:If proxied←~0∊⍴ProxyURL
:If CongaVersion(~atLeast)3 4 1626 ⍝ Conga build that introduced proxy support
→∆END⊣r.msg←'Conga version 3.4.1626 or later is required to use a proxy'
:EndIf
proxy←ConnectionProperties ProxyURL
→∆END↓⍨0∊⍴r.msg←proxy.msg
proxy.headers←makeHeaders ProxyHeaders
:EndIf
r.(Secure Host Port Path)←secure(lc host)port({{'/',¯1↓⍵/⍨⌽∨\'/'=⌽⍵}⍵↓⍨'/'=⊃⍵}path)
:If ~SuppressHeaders
hdrs←'Host'(hdrs addHeader)host,((~defaultPort)/':',⍕port)
hdrs←'User-Agent'(hdrs addHeader)deb'Dyalog-',1↓∊'/',¨2↑Version
hdrs←'Accept'(hdrs addHeader)'*/*'
hdrs←'Accept-Encoding'(hdrs addHeader)'gzip, deflate'
:If ~0∊⍴Auth
:If (1<|≡Auth)∨':'∊Auth ⍝ (userid password) or userid:password
:AndIf ('basic'≡lc AuthType)∨0∊⍴AuthType
Auth←Base64Encode ¯1↓∊(,⊆Auth),¨':'
AuthType←'Basic'
:EndIf
hdrs←'Authorization'(hdrs setHeader)deb AuthType,' ',⍕Auth
:EndIf
:If '∘???∘'≡hdrs getHeader'cookie' ⍝ if the user has specified a cookie header, it takes precedence
:AndIf ~0∊⍴cookies←r applyCookies Cookies
hdrs←'Cookie'(hdrs addHeader)formatCookies cookies
:EndIf
:If ~0∊⍴auth
hdrs←'Authorization'(hdrs addHeader)auth
:EndIf
:If proxied
:If ~0∊⍴ProxyAuth
:If (1<|≡ProxyAuth)∨':'∊ProxyAuth ⍝ (userid password) or userid:password
:AndIf ('basic'≡lc ProxyAuthType)∨0∊⍴ProxyAuthType
ProxyAuth←Base64Encode ¯1↓∊(,⊆ProxyAuth),¨':'
ProxyAuthType←'Basic'
:EndIf
proxy.headers←'Proxy-Authorization'(proxy.headers setHeader)deb ProxyAuthType,' ',⍕ProxyAuth
:EndIf
:If ~0∊⍴proxy.auth
proxy.headers←'Proxy-Authorization'(proxy.headers addHeader)proxy.auth
:EndIf
:EndIf
:EndIf
noCT←(0∊⍴ContentType)∧('∘???∘'≡hdrs getHeader'content-type') ⍝ no content-type specified
:If noCT⍲0∊⍴parms ⍝ do we have any parameters or a content-type
simpleParms←{2≠⎕NC'⍵':0 ⋄ 1≥|≡⍵}parms ⍝ simple vector or scalar and not a reference
:If (⊆cmd)∊'GET' 'HEAD' ⍝ if the command is GET or HEAD
:AndIf noCT
⍝ params needs to be URLEncoded and will be appended to the query string
:If simpleParms
parms←∊⍕parms ⍝ deal with possible numeric
parms←UrlEncode⍣(~∧/parms∊HttpCommand.ValidFormUrlEncodedChars)⊢parms ⍝ URLEncode if necessary
:Else ⍝ parms is a namespace or a name/value pairs array
parms←UrlEncode parms
:EndIf
urlparms,←(0∊⍴urlparms)↓'&',parms
parms←''
:Else ⍝ not a GET or HEAD command or content-type has been specified
:If ~SuppressHeaders
:If noCT ⍝ no content-type specified, try to work out what it should be
:If simpleParms ⍝ if parms is simple
:If (isJSON parms)∨isNum parms ⍝ and looks like JSON or is numeric
hdrs←'Content-Type'(hdrs addHeader)'application/json;charset=utf-8'
:Else
hdrs←'Content-Type'(hdrs addHeader)'application/x-www-form-urlencoded'
:EndIf
:Else ⍝ not simpleParms
hdrs←'Content-Type'(hdrs addHeader)'application/json;charset=utf-8'
:EndIf
:ElseIf ~0∊⍴ContentType ⍝ ContentType has been specified
hdrs←'Content-Type'(hdrs setHeader)ContentType ⍝ it overrides a pre-existing content-type header
:EndIf
:EndIf
simpleChar←{1<≢⍴⍵:0 ⋄ (⎕DR ⍵)∊80 82}parms
:Select ⊃';'(≠⊆⊢)lc hdrs getHeader'Content-Type'
:Case 'application/x-www-form-urlencoded'
:If ~simpleChar ⍝ if not simple character...
:OrIf ~∧/parms∊ValidFormUrlEncodedChars ⍝ or not valid URL-encoded
parms←UrlEncode parms ⍝ encode it
:EndIf
:Case 'application/json'
:If ~isJSON parms ⍝ if it's not already JSON
parms←JSONexport parms ⍝ JSONify it
:Else
parms←SafeJSON parms
:EndIf
:Else
parms←∊⍕parms
:EndSelect
:Select UseZip
:Case 1 ⍝ gzip
:Trap 0
parms←toChar 2⊃3 ZipLevel Zipper sint parms
hdrs←'Content-Encoding'(hdrs setHeader)'gzip'
:Else
r.msg←'gzip encoding on request payload failed'
:EndTrap
:Case 2 ⍝ deflate
:Trap 0
parms←toChar 2⊃2 ZipLevel Zipper sint parms
hdrs←'Content-Encoding'(hdrs setHeader)'deflate'
:Else
r.msg←'deflate encoding on request payload failed'
:EndTrap
:EndSelect
:If RequestOnly>SuppressHeaders ⍝ Conga supplies content-length, but for RequestOnly we need to insert it
hdrs←'Content-Length'(hdrs addHeader)⍴parms
:EndIf
:EndIf
:EndIf
hdrs⌿⍨←~0∊¨≢¨hdrs[;2] ⍝ remove any headers with empty values
:If RequestOnly
r←cmd,' ',(path,(0∊⍴urlparms)↓'?',urlparms),' HTTP/1.1',(∊{NL,⍺,': ',∊⍕⍵}/hdrs),NL,NL,parms
→∆EXIT
:EndIf
(outFile replace)←2↑{⍵,(≢⍵)↓'' 0}eis OutFile
:If 0=outTn ⍝ if we don't already have an output file tied
:If toFile←~0∊⍴outFile ⍝ if we're directing the response payload to file
:Trap Debug↓0
outFile←1 ⎕NPARTS outFile
:If ~⎕NEXISTS⊃outFile
→∆END⊣r.msg←'Output file folder "',(⊃outFile),'" does not exist'
:EndIf
:If 0∊⍴∊1↓outFile ⍝ no file name specified, try to use the name from the URL
:If ~0∊⍴file←∊1↓1 ⎕NPARTS path
outFile←(⊃outFile),file
:Else ⍝ no file name specified and none in the URL
→∆END⊣r.msg←'No file name specified in OutFile or URL'
:EndIf
:EndIf
:If ⎕NEXISTS outFile←∊outFile
:If (0=replace)∧0≠2 ⎕NINFO outFile
→∆END⊣r.msg←'Output file "',outFile,'" is not empty'
:Else
outTn←outFile ⎕NTIE 0
{}0 ⎕NRESIZE⍣(1=replace)⊢outTn
:EndIf
:Else
outTn←outFile ⎕NCREATE 0
:EndIf
startSize←⎕NSIZE outTn
r.OutFile←outFile
tmpFile←tempFolder,'/',(∊1↓1 ⎕NPARTS outFile) ⍝ create temporary file to work with
tmpTn←tmpFile(⎕NCREATE⍠'Unique' 1)0 ⍝ create with a unique name
tmpFile←∊1 ⎕NPARTS ⎕NNAMES[⎕NNUMS⍳tmpTn;] ⍝ save the name for ⎕NDELETE later
:Else
→∆END⊣r.msg←({⍺,(~0∊⍴⍵)/' (',⍵,')'}/⎕DMX.(EM Message)),' occurred while trying to initialize output file "',(⍕outFile),'"'
:EndTrap
:EndIf
:EndIf
secureParams←''
:If secure
:AndIf 0≠⊃(rc secureParams)←CreateSecureParams certs
→∆END⊣r.(rc msg)←rc secureParams
:EndIf
:If proxied
proxy.secureParams←''
:If proxy.secure
:AndIf 0≠⊃(rc proxy.secureParams)←CreateSecureParams'' 0
→∆END⊣r.(rc msg)←rc('PROXY: ',proxy.secureParams)
:EndIf
:EndIf
stopIf Debug=2
:If ~0∊⍴Client ⍝ do we have a client already?
:If 0∊⍴ConxProps ⍝ should never happen (have a client but no connection properties)
Client←'' ⍝ reset client
:ElseIf ConxProps.(Host Port Secure certs)≢r.(Host Port Secure),⊂certs ⍝ something's changed, reset
⍝ don't set message for same domain
r.msg←(ConxProps.Host≢over{lc ¯2↑'.'(≠⊆⊢)⍵}r.Host)/'Connection properties changed, connection reset'
{}{0::'' ⋄ LDRC.Close ⍵}Client
Client←ConxProps←''
:ElseIf 'Timeout'≢3⊃LDRC.Wait Client 0 ⍝ nothing changed, make sure client is alive
Client←ConxProps←'' ⍝ connection dropped, reset
:EndIf
:EndIf
starttime←⎕AI[3]
donetime←⌊starttime+1000×|Timeout ⍝ time after which we'll time out
:If 0∊⍴Client
options←''
:If CongaVersion atLeast 3 3
options←⊂'Options'LDRC.Options.DecodeHttp
:EndIf
:If ~proxied
:If 0≠⊃(err Client)←2↑rc←LDRC.Clt''host port'http'BufferSize,secureParams,options
Client←''
→∆END⊣r.(rc msg)←err('Conga client creation failed ',,⍕1↓rc)
:EndIf
:Else ⍝ proxied
forceClose←1 ⍝ any error forces client to close, forceClose gets reset later if no proxy connection errors
⍝ connect to proxy
:If 0≠⊃(err Client)←2↑rc←LDRC.Clt''proxy.host proxy.port'http'BufferSize proxy.secureParams,options
Client←''
→∆END⊣r.(rc msg)←err('Conga proxy client creation failed ',,⍕1↓rc)
:EndIf
⍝ connect to proxied host
:If 0≠err←⊃rc←LDRC.Send Client('CONNECT'(host,':',⍕port)'HTTP/1.1'proxy.headers'')
→∆END⊣r.(rc msg)←err('Proxy CONNECT failed: ',⍕1↓rc)
:EndIf
:If 0≠err←⊃rc←LDRC.Wait Client 1000
→∆END⊣r.(rc msg)←err('Proxy CONNECT wait failed: ',∊⍕1↓rc)
:Else
(err obj evt dat)←4↑rc
:If evt≢'HTTPHeader'
→∆END⊣r.(rc msg)←err('Proxy CONNECT did not respond with HTTPHeader event: ',∊⍕1↓rc)
:EndIf
:If '200'≢2⊃dat
r.(msg HttpStatus HttpMessage Headers)←(⊂'Proxy CONNECT response failed'),1↓dat
r.HttpStatus←⊃toInt r.HttpStatus
datalen←⊃toInt{0∊⍴⍵:'¯1' ⋄ ⍵}r.GetHeader'Content-Length' ⍝ ¯1 if no content length not specified
→(datalen≠0)↓∆END,∆LISTEN
:EndIf
:EndIf
⍝ if secure, upgrade to SSL
:If proxied∧secure
cert←1 2⊃secureParams
:AndIf 0≠err←⊃rc←LDRC.SetProp Client'StartTLS'(cert.AsArg,('SSLValidation' 0)('Address'host))
→∆END⊣r.(rc msg)←err('Proxy failed to upgrade to secure connection: ',∊⍕1↓rc)
:EndIf
:EndIf
:If CongaVersion(~atLeast)3 3
:AndIf 0≠err←⊃rc←LDRC.SetProp Client'DecodeBuffers' 15 ⍝ set to decode HTTP messages
→∆END⊣r.(rc msg)←err('Could not set DecodeBuffers on Conga client "',Client,'": ',,⍕1↓rc)
:EndIf
:EndIf
(ConxProps←⎕NS'').(Host Port Secure certs)←r.(Host Port Secure),⊂certs ⍝ preserve connection settings for subsequent calls
:If 0=⊃rc←LDRC.Send Client(cmd(path,(0∊⍴urlparms)↓'?',urlparms)'HTTP/1.1'hdrs parms)
∆LISTEN:
forceClose←~KeepAlive
(timedOut done data progress noContentLength connectionClose)←0 0 ⍬ 0 0 0
:Trap 1000 ⍝ in case break is pressed while listening
:While ~done
:If ~done←0≠err←1⊃rc←LDRC.Wait Client WaitTime
(err obj evt dat)←4↑rc
:Select evt
:Case 'HTTPHeader'
:If 1=≡dat
→∆END⊣r.(rc Data msg)←¯1 dat'Conga failed to parse the response HTTP header' ⍝ HTTP header parsing failed?
:Else
r.(HttpVersion HttpStatus HttpMessage Headers)←4↑dat
r.HttpStatus←toInt r.HttpStatus
redirected←3=⌊0.01×r.HttpStatus
datalen←⊃toInt{0∊⍴⍵:'¯1' ⋄ ⍵}r.GetHeader'Content-Length' ⍝ ¯1 if no content length not specified
connectionClose←'close'≡lc r.GetHeader'Connection'
noContentLength←datalen=¯1
done←(cmd≡'HEAD')∨(0=datalen)∨204=r.HttpStatus
→∆END⍴⍨forceClose←r CheckPayloadSize datalen ⍝ we have a payload size limit
:EndIf
:Case 'HTTPBody'
→∆END⍴⍨forceClose←r CheckPayloadSize(≢data)+≢dat
:If toFile>redirected ⍝ don't write redirect response payload to file
→∆END⍴⍨forceClose←r CheckPayloadSize(⎕NSIZE tmpTn)+≢dat
dat ⎕NAPPEND tmpTn
⎕NUNTIE ⍬
:Else
data,←dat
:EndIf
done←~noContentLength ⍝ if not content-length specified and not chunked - keep listening
:Case 'HTTPChunk'
:If 1=≡dat
→∆END⊣r.(Data msg)←dat'Conga failed to parse the response HTTP chunk' ⍝ HTTP chunk parsing failed?
:ElseIf toFile>redirected ⍝ don't write redirect response payload to file
→∆END⍴⍨forceClose←r CheckPayloadSize(⎕NSIZE tmpTn)+≢1⊃dat
(1⊃dat)⎕NAPPEND tmpTn
⎕NUNTIE ⍬
:Else
→∆END⍴⍨forceClose←r CheckPayloadSize(≢data)+≢1⊃dat
data,←1⊃dat
:EndIf
:Case 'HTTPTrailer'
:If 2≠≢⍴dat
→∆END⊣r.(Data msg)←dat'Conga failed to parse the response HTTP trailer' ⍝ HTTP trailer parsing failed?
:Else
r.Headers⍪←dat ⋄ done←1
:EndIf
:Case 'HTTPFail'
data,←dat
r.Data←data
r.msg←'Conga could not parse the HTTP reponse'
→∆END
:Case 'HTTPError'
data,←dat
r.Data←data
:If noContentLength∧connectionClose
r.(rc msg)←0 ''
done←1
:Else
rc.msg←'Response payload not completely received'
→∆END
:EndIf
:Case 'BlockLast' ⍝ BlockLast included for pre-Conga v3.4 compatibility for RFC7230 (Sec 3.3.3 item 7)
→∆END⍴⍨forceClose←r CheckPayloadSize(≢data)+≢dat
:If toFile<redirected
→∆END⍴⍨forceClose←r CheckPayloadSize(⎕NSIZE tmpTn)+≢dat
dat ⎕NAPPEND tmpTn
⎕NUNTIE ⍬
:Else
data,←dat
:EndIf
done←1
:Case 'Timeout'
timedOut←⊃(done donetime progress)←Client checkTimeOut donetime progress
:Case 'Error'
:Select ⊃r.rc←4⊃rc
:Case 1135
r.msg←'Response header size exceeds BufferSize (',(⍕BufferSize),')'
:Else
r.msg←'Conga error processing your request: ',,⍕r.rc
:EndSelect
→∆END⊣forceClose←1
:Case 'Closed'
r.msg←'Socket closed by server'
done←forceClose←1
:If 0∊⍴r.HttpStatus
→∆END⊣r.rc←4⊃rc ⍝ set return code if closed before receiving HTTPHeader event
:EndIf
:Else
→∆END⊣r.msg←'Unhandled Conga event type: ',evt ⍝ This shouldn't happen
:EndSelect
:Else
r.msg←'Conga wait error ',,⍕rc ⍝ some other error (very unlikely)
:EndIf
:EndWhile
:EndTrap
r.Elapsed←⎕AI[3]-starttime
:If timedOut
forceClose←1
r.(rc msg)←100 'Request timed out before server responded'
r.Data←data ⍝ return any partial payload
→∆END
:EndIf
forceClose∨←connectionClose ⍝ if there's a 'Connection: close' header
:If 0=err
ct←lc r.GetHeader'content-type'
isutf8←0<≢'charset\s*=\s*utf-8'⎕S'&'⍠1⊢ct←lc r.GetHeader'content-type'
isutf8∨←(∨/'application/json'⍷ct)∧~∨/'charset'⍷ct ⍝ application/json defaults to UTF-8 unless other charset specified
encoding←lc r.GetHeader'content-encoding' ⍝ response payload compressed?
compType←¯2 ¯3 0['deflate' 'gzip'⍳⊂encoding]
:If toFile≤redirected
:Trap Debug↓0 ⍝ If any errors occur, abandon conversion
:If ~0∊⍴data
:If ~0∊⍴encoding
:If 0≠compType
data←256|compType Zipper 83 ⎕DR data ⍝ unzip
data←⎕UCS data ⍝ try to translate
:Else
r.msg←'Unhandled content-encoding: ',compType,', could not decode response payload'
:EndIf
:EndIf
:EndIf
:If isutf8
data←'UTF-8'⎕UCS ⎕UCS data
data←(65279=⎕UCS⊃data)↓data ⍝ drop off BOM, if any
:EndIf
:Else
r.rc←⎕DMX.EN
r.msg←⎕DMX.EM,' occurred during response payload conversion (Data was not converted)'
r.Data←data
→∆END
:EndTrap
:If TranslateData=1
:If ∨/∊'text/xml' 'application/xml'⍷¨⊂ct
r{0::⍺.(rc Data msg)←¯2 ⍵'Could not translate XML payload' ⋄ ⍺.Data←⎕XML ⍵}data
:ElseIf ∨/'application/json'⍷ct
r.Data←data
JSONimport r
:Else
r.Data←data
:EndIf
:Else
r.Data←data
:EndIf
:Else ⍝ toFile and not redirected
:If ~0∊⍴encoding ⍝ content-encoding header?
:If 0≠compType
:If 0≠z←compType UnzipFile tmpTn
r.msg←(⎕EM z),' occurred when attempting to decompress response payload'
:EndIf
:Else
r.msg←'Unhandled content-encoding: ',compType,', could not decode response payload'
:EndIf
:EndIf
r.BytesWritten←⎕NSIZE tmpTn
(⎕NREAD tmpTn,83,(r.BytesWritten),0)⎕NAPPEND outTn
:EndIf ⍝ ~toFile
r.Cookies←parseCookies r.Headers r.Host(extractPath r.Path)
Cookies←Cookies updateCookies r.Cookies
:If (r.HttpStatus∊301 302 303 307 308)>0=MaxRedirections ⍝ if redirected and allowing redirections
:If MaxRedirections<.=¯1,≢r.Redirections ⋄ →∆END⊣r.(rc msg)←¯1('Too many redirections (',(⍕MaxRedirections),')')
:Else
:If ''≢url←r.GetHeader'location' ⍝ if we were redirected use the "location" header field for the URL
:If '/'=⊃url ⋄ url,⍨←host ⋄ :EndIf ⍝ if a relative redirection, use the current host
r.Redirections,←t←#.⎕NS''
t.(URL HttpVersion HttpStatus HttpMessage Headers Data)←r.(URL HttpVersion HttpStatus HttpMessage Headers Data)
{}LDRC.Close Client
cmd←(1+303=r.HttpStatus)⊃cmd'GET' ⍝ 303 (See Other) is always followed by a 'GET'. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303
→∆GET
:Else
r.msg←'Redirection detected, but no "location" header supplied.' ⍝ should never happen from a properly functioning server
:EndIf
:EndIf
:EndIf
:If secure
:AndIf 0=⊃z←LDRC.GetProp Client'PeerCert'
r.PeerCert←2⊃z
:EndIf
:EndIf
:Else
:If 1081=⊃rc ⍝ 1081 could be due to an error in Conga that fails on long URLs, so try sending request as a character vector
:If 0=⊃rc←LDRC.Send Client(cmd,' ',(path,(0∊⍴urlparms)↓'?',urlparms),' HTTP/1.1',(⎕UCS 13 10),(∊': '(⎕UCS 13 10),⍨¨⍤1⊢hdrs),(⎕UCS 13 10),parms)
→∆LISTEN
:EndIf
:EndIf
r.msg←'Conga error while attempting to send request: ',,⍕1↓rc
:EndIf
r.rc←1⊃rc ⍝ set the return code to the Conga return code
∆END:
⎕NUNTIE tmpTn,outTn
{0:: ⋄ ⎕NDELETE ⍵}tmpFile
Client←{0::'' ⋄ KeepAlive>forceClose:⍵ ⋄ ''⊣LDRC.Close ⍵}Client
∆EXIT:
∇
∇ rc←r CheckPayloadSize size
⍝ checks if payload exceeds MaxPayloadSize
rc←0
:If MaxPayloadSize≠¯1
:AndIf size>MaxPayloadSize
r.(rc msg)←¯1 'Payload length exceeds MaxPayloadSize'
rc←1
:EndIf
∇
∇ (timedOut donetime progress)←obj checkTimeOut(donetime progress);tmp;snap
⍝ check if request has timed out
→∆EXIT↓⍨timedOut←⎕AI[3]>donetime ⍝ exit unless donetime hasn't passed
→∆EXIT↓⍨Timeout<0 ⍝ if Timeout<0, reset donetime if there's progress
→∆EXIT↓⍨0=⊃tmp←LDRC.Tree obj ⍝ look at the current state of the connection
snap←2 2⊃tmp ⍝ second element shoulf contain the state
:If ~0∊⍴snap ⍝ if we have any...
snap←(⊂∘⍋⌷⊢)↑(↑2 2⊃tmp)[;1] ⍝ ...progress should be in elements [4 5]
:EndIf
→∆EXIT⍴⍨progress≡snap ⍝ exit if nothing further received
(timedOut donetime progress)←0(donetime+WaitTime)snap ⍝ reset ticker
∆EXIT:
∇
∇ {r}←type UnzipFile tn;data
:Access public shared
⍝ Unzip an output file
⍝ type is compression type: ¯2 for gzip, ¯3 for deflate
⍝ tn is the tie number of the file to unzip
⍝ r is 0 for success or ⎕EN
:Trap 0
data←⎕NREAD tn 83,(⎕NSIZE tn),0
data←⎕UCS 256|type Zipper data
0 ⎕NRESIZE tn
data ⎕NAPPEND tn
⎕NUNTIE ⍬
r←0
:Else
r←⎕EN
:EndTrap
∇
NL←⎕UCS 13 10
toChar←{(⎕DR'')⎕DR ⍵}
fromutf8←{0::(⎕AV,'?')[⎕AVU⍳⍵] ⋄ 'UTF-8'⎕UCS ⍵} ⍝ Turn raw UTF-8 input into text
utf8←{3=10|⎕DR ⍵: 256|⍵ ⋄ 'UTF-8' ⎕UCS ⍵}
sint←{⎕IO←0 ⋄ 83=⎕DR ⍵:⍵ ⋄ 0 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 ¯127 ¯126 ¯125 ¯124 ¯123 ¯122 ¯121 ¯120 ¯119 ¯118 ¯117 ¯116 ¯115 ¯114 ¯113 ¯112 ¯111 ¯110 ¯109 ¯108 ¯107 ¯106 ¯105 ¯104 ¯103 ¯102 ¯101 ¯100 ¯99 ¯98 ¯97 ¯96 ¯95 ¯94 ¯93 ¯92 ¯91 ¯90 ¯89 ¯88 ¯87 ¯86 ¯85 ¯84 ¯83 ¯82 ¯81 ¯80 ¯79 ¯78 ¯77 ¯76 ¯75 ¯74 ¯73 ¯72 ¯71 ¯70 ¯69 ¯68 ¯67 ¯66 ¯65 ¯64 ¯63 ¯62 ¯61 ¯60 ¯59 ¯58 ¯57 ¯56 ¯55 ¯54 ¯53 ¯52 ¯51 ¯50 ¯49 ¯48 ¯47 ¯46 ¯45 ¯44 ¯43 ¯42 ¯41 ¯40 ¯39 ¯38 ¯37 ¯36 ¯35 ¯34 ¯33 ¯32 ¯31 ¯30 ¯29 ¯28 ¯27 ¯26 ¯25 ¯24 ¯23 ¯22 ¯21 ¯20 ¯19 ¯18 ¯17 ¯16 ¯15 ¯14 ¯13 ¯12 ¯11 ¯10 ¯9 ¯8 ¯7 ¯6 ¯5 ¯4 ¯3 ¯2 ¯1[utf8 ⍵]}
lc←{2::0(819⌶)⍵ ⋄ ¯3 ⎕C ⍵} ⍝ lower case conversion
uc←{2::1(819⌶)⍵ ⋄ 1 ⎕C ⍵} ⍝ upper case conversion
ci←{(lc ⍺)⍺⍺ lc ⍵} ⍝ case insensitive operator
deb←' '∘(1↓,(/⍨)1(⊢∨⌽)0,≠) ⍝ delete extraneous blanks
dlb←{(+/∧\' '=⍵)↓⍵} ⍝ delete leading blanks
dltb←{(⌽dlb)⍣2⊢⍵} ⍝ delete leading and trailing blanks
iotaz←((≢⊣)(≥×⊢)⍳)
nameClass←{⎕NC⊂,'⍵'} ⍝ name class of argument
splitOnFirst←{(⍺↑⍨¯1+p)(⍺↓⍨p←⌊/⍺⍳⍵)} ⍝ split ⍺ on first occurrence of ⍵ (removing first ⍵)
splitOn←≠⊆⊣ ⍝ split ⍺ on all ⍵ (removing ⍵)
h2d←{⎕IO←0 ⋄ 16⊥'0123456789abcdef'⍳lc ⍵} ⍝ hex to decimal
d2h←{⎕IO←0 ⋄ '0123456789ABCDEF'[16(⊥⍣¯1)⍵]} ⍝ decimal to hex
getchunklen←{¯1=len←¯1+⊃(NL⍷⍵)/⍳⍴⍵:¯1 ¯1 ⋄ chunklen←h2d len↑⍵ ⋄ (⍴⍵)<len+chunklen+4:¯1 ¯1 ⋄ len chunklen}
toInt←{0∊⍴⍵:⍬ ⋄ ~3 5∊⍨10|⎕DR t←1⊃2⊃⎕VFI ⍕⍵:⍬ ⋄ t≠⌊t:⍬ ⋄ t} ⍝ simple char to int
fmtHeaders←{0∊⍴⍵:'' ⋄ (firstCaps¨⍵[;1])(,∘⍕¨⍵[;2])} ⍝ formatted HTTP headers
firstCaps←{1↓uc@(¯1↓0,'-'∘=)lc '-',⍵} ⍝ capitalize first letters e.g. Content-Encoding
getHeader←{⍺{1<|≡⍵:⍺∘∇¨⍵ ⋄ (⍺[;2],⊂'∘???∘')⊃⍨⍺[;1](⍳{(⍵⍵ ⍺)⍺⍺(⍵⍵ ⍵)}lc)⊆,⍵}⍵} ⍝ return header value(s) or '∘???∘' if not found
tableGet←{⍺[;2]/⍨⍺[;1](≡ ci)¨⊂⍵}
endsWith←{∧/⍵=⍺↑⍨-≢⍵}
beginsWith←{∧/⍵=⍺↑⍨≢⍵}
extractPath←{⍵↑⍨1⌈¯1+⊢/⍸'/'=⍵}∘,
isChar←{1≥|≡⍵:0 2∊⍨10|⎕DR {⊃⍣(0∊⍴⍵)⊢⍵}⍵ ⋄ ∧/∇¨⍵}
isSimpleChar←{1≥|≡⍵: isChar ⍵ ⋄ 0}
isNum←{1 3 5 7∊⍨10|⎕DR ⍵}
over←{(⍵⍵ ⍺)⍺⍺(⍵⍵ ⍵)}
isJSON←{~0 2∊⍨10|⎕DR ⍵:0 ⋄ ~(⊃⍵)∊'-{["',⎕D:0 ⋄ {0::0 ⋄1⊣0 ⎕JSON ⍵}⍵} ⍝ test for JSONableness fails on APL that looks like JSON (e.g. '"abc"')
stopIf←{1∊⍵:-⎕TRAP←0 'C' '⎕←''Stopped for debugging... (Press Ctrl-Enter)''' ⋄ shy←0} ⍝ faster alternative to setting ⎕STOP
seconds←{⍵÷86400} ⍝ convert seconds to fractional day (for cookie max-age)
atLeast←{a←(≢⍵)↑⍺ ⋄ ⊃((~∧\⍵=a)/a>⍵),1} ⍝ checks if ⍺ is at least version ⍵
Zipper←219⌶
tempFolder←739⌶0
makeURL←{ ⍝ build URL from BaseURL (⍺) and URL (⍵)
~0∊⍴'^https?\:\/\/'⎕S 3⍠('IC' 1)⊢⍵:⍵ ⍝ URL begins with http:// or https://
0∊⍴⍺:⍵ ⍝ no BaseURL
t←'/'=⊃⍵ ⍝ URL begins with '/'?
'/'=⊃⌽⍺:⍺,t↓⍵ ⍝ BaseURL ends with '/'
⍺,t↓'/',⍵ ⍝ insert '/' if not already there
}
∇ r←makeHeaders w
r←{