ObjFW  Check-in [c35ba05bbc]

Overview
Comment:OFHTTPClient: Add support for passing a context
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: c35ba05bbce42cc28e96988fe97dccb4d78da8b38e5bcecdc9530fe8e548b9d8
User & Date: js on 2017-09-25 00:02:44
Other Links: manifest | tags
Context
2017-09-25
21:31
TableGenerator: Use new async OFHTTPClient API check-in: adb82c292c user: js tags: trunk
00:02
OFHTTPClient: Add support for passing a context check-in: c35ba05bbc user: js tags: trunk
00:02
OFObject: Add -[performSelector*] with 4 objects check-in: f714793eb4 user: js tags: trunk
Changes

Modified src/OFHTTPClient.h from [e0785471fb] to [c9323a5119].

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
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







+
+



-
+
+








+
+



-
+
+













+
+



-
+
+









+
+




-
+
+







@protocol OFHTTPClientDelegate <OFObject>
/*!
 * @brief A callback which is called when an OFHTTPClient performed a request.
 *
 * @param client The OFHTTPClient which performed the request
 * @param request The request the OFHTTPClient performed
 * @param response The response to the request performed
 * @param context The context object that was passed to
 *		  @ref asyncPerformRequest:context:
 */
-      (void)client: (OFHTTPClient *)client
  didPerformRequest: (OFHTTPRequest *)request
	   response: (OFHTTPResponse *)response;
	   response: (OFHTTPResponse *)response
	    context: (nullable id)context;

/*!
 * @brief A callback which is called when an OFHTTPClient encountered an
 *	  exception while performing a request.
 *
 * @param client The client which encountered an exception
 * @param exception The exception the client encountered
 * @param request The request during which the client encountered the exception
 * @param context The context object that was passed to
 *		  @ref asyncPerformRequest:context:
 */
-	   (void)client: (OFHTTPClient *)client
  didEncounterException: (id)exception
	     forRequest: (OFHTTPRequest *)request;
	     forRequest: (OFHTTPRequest *)request
		context: (nullable id)context;

@optional
/*!
 * @brief A callback which is called when an OFHTTPClient creates a socket.
 *
 * This is useful if the connection is using HTTPS and the server requires a
 * client certificate. This callback can then be used to tell the TLS socket
 * about the certificate. Another use case is to tell the socket about a SOCKS5
 * proxy it should use for this connection.
 *
 * @param client The OFHTTPClient that created a socket
 * @param socket The socket created by the OFHTTPClient
 * @param request The request for which the socket was created
 * @param context The context object that was passed to
 *		  @ref asyncPerformRequest:context:
 */
-    (void)client: (OFHTTPClient *)client
  didCreateSocket: (OF_KINDOF(OFTCPSocket *))socket
       forRequest: (OFHTTPRequest *)request;
       forRequest: (OFHTTPRequest *)request
	  context: (nullable id)context;

/*!
 * @brief A callback which is called when an OFHTTPClient received headers.
 *
 * @param client The OFHTTPClient which received the headers
 * @param headers The headers received
 * @param statusCode The status code received
 * @param request The request for which the headers and status code have been
 *		  received
 * @param context The context object that was passed to
 *		  @ref asyncPerformRequest:context:
 */
-      (void)client: (OFHTTPClient *)client
  didReceiveHeaders: (OFDictionary OF_GENERIC(OFString *, OFString *) *)headers
	 statusCode: (int)statusCode
	    request: (OFHTTPRequest *)request;
	    request: (OFHTTPRequest *)request
	    context: (nullable id)context;

/*!
 * @brief A callback which is called when an OFHTTPClient wants to follow a
 *	  redirect.
 *
 * If you want to get the headers and data for each redirect, set the number of
 * redirects to 0 and perform a new OFHTTPClient for each redirect. However,
107
108
109
110
111
112
113


114
115
116
117
118
119
120


121
122
123
124
125
126
127
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

134
135
136
137
138
139
140
141
142







+
+






-
+
+







 * @param statusCode The status code for the redirection
 * @param request The request for which the OFHTTPClient wants to redirect.
 *		  You are allowed to change the request's headers from this
 *		  callback and they will be used when following the redirect
 *		  (e.g. to set the cookies for the new URL), however, keep in
 *		  mind that this will change the request you originally passed.
 * @param response The response indicating the redirect
 * @param context The context object that was passed to
 *		  @ref asyncPerformRequest:context:
 * @return A boolean whether the OFHTTPClient should follow the redirect
 */
-	  (bool)client: (OFHTTPClient *)client
  shouldFollowRedirect: (OFURL *)URL
	    statusCode: (int)statusCode
	       request: (OFHTTPRequest *)request
	      response: (OFHTTPResponse *)response;
	      response: (OFHTTPResponse *)response
	       context: (nullable id)context;
@end

/*!
 * @class OFHTTPClient OFHTTPClient.h ObjFW/OFHTTPClient.h
 *
 * @brief A class for performing HTTP requests.
 */
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
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







+
+
+

-
+
+








+


-
+
+








 *
 * @return A new, autoreleased OFHTTPClient
 */
+ (instancetype)client;

/*!
 * @brief Asynchronously performs the specified HTTP request.
 *
 * @param request The request to perform
 * @param context A context object to be passed to the delegate
 */
- (void)asyncPerformRequest: (OFHTTPRequest *)request;
- (void)asyncPerformRequest: (OFHTTPRequest *)request
		    context: (nullable id)context;

/*!
 * @brief Asynchronously performs the specified HTTP request.
 *
 * @param request The request to perform
 * @param redirects The maximum number of redirects after which no further
 *		    attempt is done to follow the redirect, but instead the
 *		    redirect is treated as an OFHTTPResponse
 * @param context A context object to be passed to the delegate
 */
- (void)asyncPerformRequest: (OFHTTPRequest *)request
		  redirects: (unsigned int)redirects;
		  redirects: (unsigned int)redirects
		    context: (nullable id)context;

/*!
 * @brief Closes connections that are still open due to keep-alive.
 */
- (void)close;
@end

OF_ASSUME_NONNULL_END

Modified src/OFHTTPClient.m from [3d75f15cfb] to [6841c42cc1].

45
46
47
48
49
50
51

52
53
54
55
56
57
58
59
60


61
62
63
64
65
66
67
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







+








-
+
+







#import "OFWriteFailedException.h"

@interface OFHTTPClientRequestHandler: OFObject
{
	OFHTTPClient *_client;
	OFHTTPRequest *_request;
	unsigned int _redirects;
	id _context;
	bool _firstLine;
	OFString *_version;
	int _status;
	OFMutableDictionary OF_GENERIC(OFString *, OFString *) *_serverHeaders;
}

- initWithClient: (OFHTTPClient *)client
	 request: (OFHTTPRequest *)request
       redirects: (unsigned int)redirects;
       redirects: (unsigned int)redirects
	 context: (id)context;
- (void)start;
@end

@interface OFHTTPClientResponse: OFHTTPResponse
{
	OFTCPSocket *_socket;
	bool _hasContentLength, _chunked, _keepAlive, _atEndOfStream;
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
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







+







+














+







	}
}

@implementation OFHTTPClientRequestHandler
- initWithClient: (OFHTTPClient *)client
	 request: (OFHTTPRequest *)request
       redirects: (unsigned int)redirects
	 context: (id)context
{
	self = [super init];

	@try {
		_client = [client retain];
		_request = [request retain];
		_redirects = redirects;
		_context = [context retain];
		_firstLine = true;
		_serverHeaders = [[OFMutableDictionary alloc] init];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	[_client release];
	[_request release];
	[_context release];
	[_version release];
	[_serverHeaders release];

	[super dealloc];
}

- (void)closeAndReconnect
305
306
307
308
309
310
311
312
313


314
315
316
317
318


319
320
321
322
323
324
325
310
311
312
313
314
315
316


317
318
319
320
321
322

323
324
325
326
327
328
329
330
331







-
-
+
+




-
+
+







	    [location hasPrefix: @"https://"])) {
		OFURL *newURL;
		bool follow;

		newURL = [OFURL URLWithString: location
				relativeToURL: URL];

		if ([_client->_delegate respondsToSelector: @selector(
		    client:shouldFollowRedirect:statusCode:request:response:)])
		if ([_client->_delegate respondsToSelector: @selector(client:
		    shouldFollowRedirect:statusCode:request:response:context:)])
			follow = [_client->_delegate client: _client
				       shouldFollowRedirect: newURL
						 statusCode: _status
						    request: _request
						   response: response];
						   response: response
						    context: _context];
		else {
			of_http_request_method_t method = [_request method];

			/*
			 * 301, 302 and 307 should only redirect with user
			 * confirmation if the request method is not GET or
			 * HEAD. Asking the delegate and getting true returned
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
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







-
+
+












-
+



+








			[newRequest setURL: newURL];
			[newRequest setHeaders: newHeaders];

			_client->_inProgress = false;

			[_client asyncPerformRequest: newRequest
					   redirects: _redirects - 1];
					   redirects: _redirects - 1
					     context: _context];
			return;
		}
	}

	if (_status / 100 != 2)
		@throw [OFHTTPRequestFailedException
		    exceptionWithRequest: _request
				response: response];

	_client->_inProgress = false;

	[_client->_delegate performSelector: @selector(client:didPerformRequest:
						 response:)
						 response:context:)
				 withObject: _client
				 withObject: _request
				 withObject: response
				 withObject: _context
				 afterDelay: 0];
}

- (bool)handleFirstLine: (OFString *)line
{
	/*
	 * It's possible that the write succeeds on a connection that is
434
435
436
437
438
439
440
441
442


443
444
445
446


447
448
449
450
451
452
453
442
443
444
445
446
447
448


449
450
451
452
453

454
455
456
457
458
459
460
461
462







-
-
+
+



-
+
+








	if (line == nil)
		@throw [OFInvalidServerReplyException exception];

	if ([line length] == 0) {
		[_serverHeaders makeImmutable];

		if ([_client->_delegate respondsToSelector:
		    @selector(client:didReceiveHeaders:statusCode:request:)])
		if ([_client->_delegate respondsToSelector: @selector(client:
		    didReceiveHeaders:statusCode:request:context:)])
			[_client->_delegate client: _client
				 didReceiveHeaders: _serverHeaders
					statusCode: _status
					   request: _request];
					   request: _request
					   context: _context];

		[self performSelector: @selector(createResponseWithSocket:)
			   withObject: socket
			   afterDelay: 0];

		return false;
	}
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
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







-
+
+













-
+
+













-
+
+







	if (exception != nil) {
		if ([exception isKindOfClass:
		    [OFInvalidEncodingException class]])
			exception = [OFInvalidServerReplyException exception];

		[_client->_delegate client: _client
		     didEncounterException: exception
				forRequest: _request];
				forRequest: _request
				   context: _context];
		return false;
	}

	@try {
		if (_firstLine) {
			_firstLine = false;
			return [self handleFirstLine: line];
		} else
			return [self handleServerHeader: line
						 socket: socket];
	} @catch (id e) {
		[_client->_delegate client: _client
		     didEncounterException: e
				forRequest: _request];
				forRequest: _request
				   context: _context];
		return false;
	}
}

- (size_t)socket: (OFTCPSocket *)socket
    didWriteBody: (const void **)body
	  length: (size_t)length
	 context: (id)context
       exception: (id)exception
{
	if (exception != nil) {
		[_client->_delegate client: _client
		     didEncounterException: exception
				forRequest: _request];
				forRequest: _request
				   context: _context];
		return 0;
	}

	[socket asyncReadLineWithTarget: self
			       selector: @selector(socket:didReadLine:context:
					      exception:)
				context: nil];
555
556
557
558
559
560
561
562


563
564
565
566
567
568
569
567
568
569
570
571
572
573

574
575
576
577
578
579
580
581
582







-
+
+







			/* In case a keep-alive connection timed out */
			[self closeAndReconnect];
			return false;
		}

		[_client->_delegate client: _client
		     didEncounterException: exception
				forRequest: _request];
				forRequest: _request
				   context: _context];
		return 0;
	}

	if ((body = [_request body]) != nil) {
		[socket asyncWriteBuffer: [body items]
				  length: [body count] * [body itemSize]
				  target: self
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
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







-
+
+









-
+


-
+
+















-
+
+







				  target: self
				selector: @selector(socket:didWriteRequest:
					      length:context:exception:)
				 context: requestString];
	} @catch (id e) {
		[_client->_delegate client: _client
		     didEncounterException: e
				forRequest: _request];
				forRequest: _request
				   context: _context];
		return;
	}
}

- (void)socketDidConnect: (OFTCPSocket *)socket
		 context: (id)context
	       exception: (id)exception
{
	if ([_client->_delegate respondsToSelector:
	    @selector(client:didCreateSocket:forRequest:)])
	    @selector(client:didCreateSocket:forRequest:context:)])
		[_client->_delegate client: _client
			   didCreateSocket: socket
				forRequest: _request];
				forRequest: _request
				   context: _context];

	[self performSelector: @selector(handleSocket:)
		   withObject: socket
		   afterDelay: 0];
}

- (bool)throwAwayContent: (OFHTTPClientResponse *)response
		  buffer: (char *)buffer
		  length: (size_t)length
		 context: (OFTCPSocket *)socket
	       exception: (id)exception
{
	if (exception != nil) {
		[_client->_delegate client: _client
		     didEncounterException: exception
				forRequest: _request];
				forRequest: _request
				   context: _context];
		return false;
	}

	if ([response isAtEndOfStream]) {
		[self freeMemory: buffer];

		[_client->_lastResponse release];
684
685
686
687
688
689
690
691

692
693
694
695
696
697
698
700
701
702
703
704
705
706

707
708
709
710
711
712
713
714







-
+







			[_client->_lastResponse
			    asyncReadIntoBuffer: buffer
					 length: 512
					 target: self
				       selector: @selector(throwAwayContent:
						     buffer:length:context:
						     exception:)
				   context: socket];
					context: socket];
		} else {
			[_client->_lastResponse release];
			_client->_lastResponse = nil;

			[self performSelector: @selector(handleSocket:)
				   withObject: socket
				   afterDelay: 0];
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
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







+


-
+
+




+

















-
+
+







{
	[self close];

	[super dealloc];
}

- (void)asyncPerformRequest: (OFHTTPRequest *)request
		    context: (id)context
{
	[self asyncPerformRequest: request
			redirects: 10];
			redirects: 10
			  context: context];
}

- (void)asyncPerformRequest: (OFHTTPRequest *)request
		  redirects: (unsigned int)redirects
		    context: (id)context
{
	void *pool = objc_autoreleasePoolPush();
	OFURL *URL = [request URL];
	OFString *scheme = [URL scheme];

	if (![scheme isEqual: @"http"] && ![scheme isEqual: @"https"])
		@throw [OFUnsupportedProtocolException exceptionWithURL: URL];

	if (_inProgress)
		/* TODO: Find a better exception */
		@throw [OFAlreadyConnectedException exception];

	_inProgress = true;

	[[[[OFHTTPClientRequestHandler alloc]
	    initWithClient: self
		   request: request
		 redirects: redirects] autorelease] start];
		 redirects: redirects
		   context: context] autorelease] start];

	objc_autoreleasePoolPop(pool);
}

- (void)close
{
	[_socket release];

Modified tests/OFHTTPClientTests.m from [1220c3f5ba] to [76843d6944].

90
91
92
93
94
95
96

97
98
99
100
101
102
103
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104







+







}
@end

@implementation TestsAppDelegate (OFHTTPClientTests)
-      (void)client: (OFHTTPClient *)client
  didPerformRequest: (OFHTTPRequest *)request
	   response: (OFHTTPResponse *)response_
	    context: (id)context
{
	response = [response_ retain];

	[[OFRunLoop mainRunLoop] stop];
}

- (void)HTTPClientTests
121
122
123
124
125
126
127
128


129
130
131
132
133
134
135
122
123
124
125
126
127
128

129
130
131
132
133
134
135
136
137







-
+
+







	URL = [OFURL URLWithString:
	    [OFString stringWithFormat: @"http://127.0.0.1:%" @PRIu16 "/foo",
					server->_port]];

	TEST(@"-[asyncPerformRequest:]",
	    (client = [OFHTTPClient client]) && R([client setDelegate: self]) &&
	    R(request = [OFHTTPRequest requestWithURL: URL]) &&
	    R([client asyncPerformRequest: request]))
	    R([client asyncPerformRequest: request
				  context: nil]))

	[[OFRunLoop mainRunLoop] runUntilDate:
	    [OFDate dateWithTimeIntervalSinceNow: 2]];
	[response autorelease];

	TEST(@"Asynchronous handling of requests", response != nil)

Modified utils/ofhttp/OFHTTP.m from [7ea7ca97c5] to [e7a88133ce].

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
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







+











+







	[self performSelector: @selector(downloadNextURL)
		   afterDelay: 0];
}

-    (void)client: (OFHTTPClient *)client
  didCreateSocket: (OF_KINDOF(OFTCPSocket *))socket
	  request: (OFHTTPRequest *)request
	  context: (id)context
{
	if (_insecure && [socket respondsToSelector:
	    @selector(setCertificateVerificationEnabled:)])
		[socket setCertificateVerificationEnabled: false];
}

-	  (bool)client: (OFHTTPClient *)client
  shouldFollowRedirect: (OFURL *)URL
	    statusCode: (int)statusCode
	       request: (OFHTTPRequest *)request
	      response: (OFHTTPResponse *)response
	       context: (id)context
{
	if (!_quiet)
		[of_stdout writeFormat: @" ➜ %d\n", statusCode];

	if (_verbose) {
		void *pool = objc_autoreleasePoolPush();
		OFDictionary OF_GENERIC(OFString *, OFString *) *headers =
537
538
539
540
541
542
543

544
545
546
547
548
549
550
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553







+








	return true;
}

-	   (void)client: (OFHTTPClient *)client
  didEncounterException: (id)e
	     forRequest: (OFHTTPRequest *)request
		context: (id)context
{
	if ([e isKindOfClass: [OFAddressTranslationFailedException class]]) {
		if (!_quiet)
			[of_stdout writeString: @"\n"];

		[of_stderr writeLine:
		    OF_LOCALIZED(@"download_failed_address_translation",
624
625
626
627
628
629
630

631
632
633
634
635
636

637
638
639
640
641
642
643
627
628
629
630
631
632
633
634
635
636
637
638


639
640
641
642
643
644
645
646







+




-
-
+







	[self performSelector: @selector(downloadNextURL)
		   afterDelay: 0];
}

-      (void)client: (OFHTTPClient *)client
  didPerformRequest: (OFHTTPRequest *)request
	   response: (OFHTTPResponse *)response
	    context: (id)context
{
	OFDictionary OF_GENERIC(OFString *, OFString *) *headers;
	OFString *lengthString, *type;

	/* Was a request to retrieve the file name */
	if (_detectFileName && _currentFileName == nil) {
	if ([context isEqual: @"detectFileName"]) {
		_currentFileName = [fileNameFromContentDisposition(
		    [[response headers] objectForKey: @"Content-Disposition"])
		    copy];
		_detectedFileName = true;

		if (!_quiet)
			[of_stdout writeFormat: @" ➜ %d\n",
904
905
906
907
908
909
910
911


912
913
914
915
916
917
918
907
908
909
910
911
912
913

914
915
916
917
918
919
920
921
922







-
+
+







		if (!_quiet)
			[of_stdout writeFormat: @"⠒ %@", [URL string]];

		request = [OFHTTPRequest requestWithURL: URL];
		[request setHeaders: clientHeaders];
		[request setMethod: OF_HTTP_REQUEST_METHOD_HEAD];

		[_HTTPClient asyncPerformRequest: request];
		[_HTTPClient asyncPerformRequest: request
					 context: @"detectFileName"];
		return;
	}

	_detectedFileName = false;

	if (!_quiet)
		[of_stdout writeFormat: @"⇣ %@", [URL string]];
945
946
947
948
949
950
951
952


953
954
955
956
957
958
959
949
950
951
952
953
954
955

956
957
958
959
960
961
962
963
964







-
+
+







	}

	request = [OFHTTPRequest requestWithURL: URL];
	[request setHeaders: clientHeaders];
	[request setMethod: _method];
	[request setBody: _body];

	[_HTTPClient asyncPerformRequest: request];
	[_HTTPClient asyncPerformRequest: request
				 context: nil];
	return;

next:
	[self performSelector: @selector(downloadNextURL)
		   afterDelay: 0];
}
@end