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;