1 module hunt.http.client.HttpClientRequest;
2 
3 import hunt.http.client.CookieStore;
4 import hunt.http.client.InMemoryCookieStore;
5 
6 import hunt.http.AuthenticationScheme;
7 import hunt.http.Cookie;
8 import hunt.http.HttpBody;
9 import hunt.http.HttpHeader;
10 import hunt.http.HttpFields;
11 import hunt.http.HttpMetaData;
12 import hunt.http.HttpMethod;
13 import hunt.http.HttpRequest;
14 import hunt.http.HttpScheme;
15 import hunt.http.HttpVersion;
16 
17 import hunt.io.ByteBuffer;
18 import hunt.io.HeapByteBuffer;
19 import hunt.io.BufferUtils;
20 import hunt.Exceptions;
21 import hunt.logging;
22 import hunt.net.KeyCertOptions;
23 import hunt.net.PemKeyCertOptions;
24 import hunt.net.util.HttpURI;
25 
26 import std.array;
27 import std.algorithm;
28 import std.conv;
29 import std.file;
30 import std.path;
31 import std.string;
32 import std.range;
33 
34 version(WITH_HUNT_TRACE) {
35     import hunt.trace.Constrants;
36     import hunt.trace.Endpoint;
37     import hunt.trace.Span;
38     import hunt.trace.Tracer;
39     import hunt.trace.HttpSender;
40 }
41 
42 /**
43  * 
44  */
45 class HttpClientRequest : HttpRequest {
46 
47 	private Cookie[] _cookies;
48     private bool _isCookieStoreEnabled = true;
49 
50     // SSL/TLS settings
51     KeyCertOptions _keyCertOptions;
52     private bool _isCertificateAuth;
53 
54 	this(string method, string uri) {
55 		HttpFields fields = new HttpFields();
56 		super(method, new HttpURI(uri), HttpVersion.HTTP_1_1, fields);
57 	}
58 
59 	this(string method, string uri, HttpBody content) {
60 		HttpFields fields = new HttpFields();
61 		if(content !is null && !content.contentType().empty())
62 			fields.add(HttpHeader.CONTENT_TYPE, content.contentType());
63 
64 		super(method, new HttpURI(uri), HttpVersion.HTTP_1_1, fields,  
65 			content is null ? 0 : content.contentLength());
66         this.setBody(content);
67 	}
68 	
69 	this(string method, HttpURI uri, HttpFields fields, HttpBody content) {
70 		if(content !is null && !content.contentType().empty())
71 			fields.add(HttpHeader.CONTENT_TYPE, content.contentType());
72 
73 		super(method, uri, HttpVersion.HTTP_1_1, fields, 
74 			content is null ? 0 : content.contentLength());
75         this.setBody(content);
76 	}
77 	
78 	this(string method, HttpURI uri, HttpVersion ver, HttpFields fields, HttpBody content) {
79 		if(content !is null && !content.contentType().empty())
80 			fields.add(HttpHeader.CONTENT_TYPE, content.contentType());
81 			
82 		super(method, uri, ver, fields,  
83 			content is null ? 0 : content.contentLength());
84         this.setBody(content);
85 	}
86 
87 	this(HttpRequest request) {
88 		super(request);
89 	}
90 
91     bool isCookieStoreEnabled() {
92         return _isCookieStoreEnabled;
93     }
94 
95     void isCookieStoreEnabled(bool flag) {
96         _isCookieStoreEnabled = flag;
97     }
98 
99     bool isTracingEnabled() {
100         return _isTracingEnabled;
101     }
102 
103     void isTracingEnabled(bool flag) {
104         _isTracingEnabled = flag;
105     }
106 
107     void isCertificateAuth(bool flag) {
108         _isCertificateAuth = flag;
109     }
110 
111     bool isCertificateAuth() {
112         return _isCertificateAuth;
113     }
114 
115     KeyCertOptions getKeyCertOptions() {
116         return _keyCertOptions;
117     }
118 
119     void setKeyCertOptions(KeyCertOptions options) {
120         _keyCertOptions = options;
121     }
122 
123 	RequestBuilder newBuilder() {
124 		return new RequestBuilder(this);
125 	}
126 
127 version(WITH_HUNT_TRACE) {
128     private bool _isTracingEnabled = true;
129     private Span _span;
130     private string[string] tags;
131 
132     void startSpan() {
133         if(!isTracingEnabled()) return;
134         if(_span is null) {
135             warning("span is null");
136             return;
137         }
138 
139         HttpURI uri = getURI();
140         tags[HTTP_HOST] = uri.getHost();
141         tags[HTTP_URL] = uri.getPathQuery();
142         tags[HTTP_PATH] = uri.getPath();
143         tags[HTTP_REQUEST_SIZE] = getContentLength().to!string();
144         tags[HTTP_METHOD] = getMethod();
145 
146         if(_span !is null) {
147             _span.start();
148             getFields().put("b3", _span.defaultId());
149         }
150     }    
151     
152     void endTraceSpan(int status, string message) {
153         
154         if(!isTracingEnabled()) return;
155 
156         if(_span !is null) {
157             tags[HTTP_STATUS_CODE] = to!string(status);
158             // tags[HTTP_RESPONSE_SIZE] = to!string(response.getContentLength());
159 
160             traceSpanAfter(_span , tags, message);
161 
162             httpSender().sendSpans(_span);
163         }
164     }
165 } else {
166     private bool _isTracingEnabled = false;
167 }
168 
169     /**
170      * 
171      */
172     final static class Builder {
173 
174         private HttpURI _url;
175         private string _method;
176         private HttpFields _headers;
177         private HttpBody _requestBody;
178 
179         // SSL/TLS settings
180         private bool _isCertificateAuth = false;
181         private string _tlsCaFile;
182         private string _tlsCaPassworld;
183         private string _tlsCertificate;
184         private string _tlsPrivateKey;
185         private string _tlsCertPassword;
186         private string _tlsKeyPassword;
187 
188 
189         /** A mutable map of tags, or an immutable empty map if we don't have any. */
190         // Map<Class<?>, Object> tags = Collections.emptyMap();
191 
192         this() {
193             this._method = "GET";
194             this._headers = new HttpFields();
195         }
196 
197         this(Request request) {
198             this._url = request.getURI();
199             this._method = request.getMethod();
200             this._requestBody = request.getBody();
201         //   this.tags = request.tags.isEmpty()
202         //       ? Collections.emptyMap()
203         //       : new LinkedHashMap<>(request.tags);
204             this._headers = request.getFields();
205         }
206 
207         Builder url(HttpURI url) {
208             if (url is null) throw new NullPointerException("url is null");
209             this._url = url;
210             return this;
211         }
212 
213         /**
214          * Sets the URL target of this request.
215          *
216          * @throws IllegalArgumentException if {@code url} is not a valid HTTP or HTTPS URL. Avoid this
217          * exception by calling {@link HttpURI#parse}; it returns null for invalid URLs.
218          */
219         Builder url(string httpUri) {
220             if (httpUri is null) throw new NullPointerException("url is null");
221 
222             // Silently replace web socket URLs with HTTP URLs.
223             if (httpUri.length >= 3 && icmp(httpUri[0..3], "ws:") == 0) {
224                 httpUri = "http:" ~ httpUri[3 .. $];
225             } else if (httpUri.length >=4 && icmp(httpUri[0 .. 4], "wss:") == 0) {
226                 httpUri = "https:" ~ httpUri[4 .. $];
227             }
228 
229             return url(new HttpURI(httpUri));
230         }
231 
232         /**
233          * Sets the header named {@code name} to {@code value}. If this request already has any headers
234          * with that name, they are all replaced.
235          */
236         Builder header(string name, string value) {
237             _headers.put(name, value);
238             return this;
239         }
240 
241         /**
242          * Adds a header with {@code name} and {@code value}. Prefer this method for multiply-valued
243          * headers like "Cookie".
244          *
245          * <p>Note that for some headers including {@code Content-Length} and {@code Content-Encoding},
246          * OkHttp may replace {@code value} with a header derived from the request body.
247          */
248         Builder addHeader(string name, string value) {
249             _headers.add(name, value);
250             return this;
251         }
252 
253         /** Removes all headers named {@code name} on this builder. */
254         Builder removeHeader(string name) {
255             _headers.remove(name);
256             return this;
257         }
258 
259         /** Removes all headers on this builder and adds {@code headers}. */
260         Builder headers(HttpFields headers) {
261             _headers = headers;
262             return this;
263         }
264 
265         // Enable header authorization
266         Builder authorization(AuthenticationScheme scheme, string token) {
267             header("Authorization", scheme ~ " " ~ token);
268             return this;
269         }
270 
271         // Enable certificate authorization
272         // https://github.com/Hakky54/mutual-tls-ssl
273         Builder mutualTls(string certificate, string privateKey, string certPassword="", string keyPassword="") {
274             _isCertificateAuth = true;
275             _tlsCertificate = certificate;
276             _tlsPrivateKey = privateKey;
277             _tlsCertPassword = certPassword;
278             _tlsKeyPassword = keyPassword;
279             return this;
280         }
281 
282         // Certificate Authority (CA) certificate
283         Builder caCert(string caFile, string caPassword) {
284             _tlsCaFile = caFile;
285             _tlsCaPassworld = caPassword;
286             return this;
287         }
288           
289 
290         /**
291          * Sets this request's {@code Cache-Control} header, replacing any cache control headers already
292          * present. If {@code cacheControl} doesn't define any directives, this clears this request's
293          * cache-control headers.
294          */
295         // Builder cacheControl(CacheControl cacheControl) {
296         //   string value = cacheControl.toString();
297         //   if (value.isEmpty()) return removeHeader("Cache-Control");
298         //   return header("Cache-Control", value);
299         // }
300 
301         Builder get() {
302             return method("GET", null);
303         }
304 
305         Builder head() {
306             return method("HEAD", null);
307         }
308 
309         Builder post(HttpBody requestBody) {
310             return method("POST", requestBody);
311         }
312 
313         Builder del(HttpBody requestBody) {
314             return method("DELETE", requestBody);
315         }
316 
317         Builder del() {
318             return method("DELETE", null);
319         }
320 
321         Builder put(HttpBody requestBody) {
322             return method("PUT", requestBody);
323         }
324 
325         Builder patch(HttpBody requestBody) {
326             return method("PATCH", requestBody);
327         }
328 
329         Builder method(string method, HttpBody requestBody) {
330             if (method.empty) throw new NullPointerException("method is empty");
331 
332             if (requestBody !is null && !HttpMethod.permitsRequestBody(method)) {
333                 throw new IllegalArgumentException("method " ~ method ~ " must not have a request body.");
334             }
335             if (requestBody is null && HttpMethod.requiresRequestBody(method)) {
336                 throw new IllegalArgumentException("method " ~ method ~ " must have a request body.");
337             }
338             this._method = method;
339             this._requestBody = requestBody;
340             return this;
341         }
342 
343         /**
344          * Set the cookies.
345          *
346          * @param cookies The cookies.
347          * @return Builder
348          */
349         Builder cookies(Cookie[] cookies) {
350             _headers.put(HttpHeader.COOKIE, generateCookies(cookies));
351             return this;
352         }
353 		
354         Builder cookieStoreEnabled(bool flag) {
355             _isCookieStoreEnabled = flag;
356             return this;
357         }
358         private bool _isCookieStoreEnabled = true;
359 
360         // Trace
361         Builder enableTracing(bool flag) {
362             _isTracingEnabled = flag;
363             return this;
364         }
365         
366 version(WITH_HUNT_TRACE) {
367     
368         Builder withTracer(Tracer t) {
369             _tracer = t;
370             return this;
371         }
372 
373         Builder localServiceName(string name) {
374             _localServiceName = name;
375             return this;
376         }
377 
378         private Tracer _tracer;
379         private string _localServiceName;
380 
381         private bool _isTracingEnabled = true;
382 } else {
383         private bool _isTracingEnabled = false;
384 }
385 
386         /** Attaches {@code tag} to the request using {@code Object.class} as a key. */
387         // Builder tag(Object tag) {
388         //   return tag(Object.class, tag);
389         // }
390 
391         /**
392          * Attaches {@code tag} to the request using {@code type} as a key. Tags can be read from a
393          * request using {@link Request#tag}. Use null to remove any existing tag assigned for {@code
394          * type}.
395          *
396          * <p>Use this API to attach timing, debugging, or other application data to a request so that
397          * you may read it in interceptors, event listeners, or callbacks.
398          */
399         // <T> Builder tag(Class<? super T> type, T tag) {
400         //   if (type is null) throw new NullPointerException("type is null");
401 
402         //   if (tag is null) {
403         //     tags.remove(type);
404         //   } else {
405         //     if (tags.isEmpty()) tags = new LinkedHashMap<>();
406         //     tags.put(type, type.cast(tag));
407         //   }
408 
409         //   return this;
410         // }
411 
412         HttpClientRequest build() {
413             if (_url is null) throw new IllegalStateException("url is null");
414 
415             string basePath = dirName(thisExePath);
416 
417 			HttpClientRequest request = new HttpClientRequest(_method, _url, _headers, _requestBody);
418 			request._isCookieStoreEnabled = _isCookieStoreEnabled;
419             request._isTracingEnabled = _isTracingEnabled;
420             version(WITH_HUNT_TRACE) {
421                 if(_isTracingEnabled) {
422                     string spanName = _url.getPath();
423                     Span span;
424                     if(_tracer is null) {
425                         _tracer = new Tracer(spanName, KindOfClient);
426                         span = _tracer.root; 
427                     } else {
428                         span = _tracer.addSpan(spanName);
429                     }
430 
431                     span.localEndpoint = Span.buildLocalEndPoint(_localServiceName);
432 
433                     request.tracer = _tracer;
434                     request._span = span;
435                 }
436             }
437 
438             if(!_tlsCertificate.empty()) {
439                 PemKeyCertOptions options = new PemKeyCertOptions(buildPath(basePath, _tlsCertificate),
440                     buildPath(basePath, _tlsPrivateKey), _tlsCertPassword, _tlsKeyPassword);
441                 
442                 if(!_tlsCaFile.empty()) {
443                     options.setCaFile(buildPath(basePath, _tlsCaFile));
444                     options.setCaPassword(_tlsCaPassworld);
445                 }
446                 request.setKeyCertOptions(options);
447                 request.isCertificateAuth = _isCertificateAuth;
448             }
449 
450             return request;
451         }
452     }    	
453 }
454 
455 
456 alias Request = HttpClientRequest;
457 
458 alias RequestBuilder = HttpClientRequest.Builder;