1 module hio.http.common; 2 3 import std.string; 4 import std.typecons: Tuple; 5 6 import std.bitmanip; 7 import std.uni; 8 9 import std.experimental.logger; 10 11 import ikod.containers.hashmap: HashMap; 12 import ikod.containers.unrolledlist: UnrolledList; 13 import nbuff: SmartPtr, Nbuff, NbuffChunk, smart_ptr; 14 15 import hio.http.http_parser; 16 17 package enum SchCode 18 { 19 HTTP = 0, 20 HTTPS = 1, 21 UNKNOWN 22 } 23 24 struct URL 25 { 26 private 27 { 28 SchCode _schemaCode = SchCode.UNKNOWN; 29 string _url; 30 string _schema; 31 string _host; 32 ushort _port; 33 string _path; 34 string _query; 35 string _fragment; 36 string _userinfo; 37 long _path_off = -1; 38 } 39 auto url() pure inout @nogc 40 { 41 return _url; 42 } 43 auto schema() pure inout @nogc 44 { 45 return _schema; 46 } 47 auto host() pure inout @nogc 48 { 49 return _host; 50 } 51 auto port() pure inout @nogc 52 { 53 return _port; 54 } 55 auto path() pure inout @nogc 56 { 57 return _path; 58 } 59 long path_off() pure inout @safe @nogc 60 { 61 return _path_off; 62 } 63 auto query() pure inout @nogc 64 { 65 return _query; 66 } 67 auto fragment() pure inout @nogc 68 { 69 return _fragment; 70 } 71 auto userinfo() pure inout @nogc 72 { 73 return _userinfo; 74 } 75 auto schemaCode() pure inout @nogc 76 { 77 return _schemaCode; 78 } 79 long pstrlen() @safe @nogc nothrow 80 { 81 // build path/query/frag lengh 82 long l = 0; 83 if ( _path.length == 0) 84 { 85 l += 1; 86 } 87 else 88 { 89 l += _path.length; 90 } 91 if ( _query.length > 0 ) 92 { 93 l += 1 + _query.length; 94 } 95 if ( _fragment.length > 0) 96 { 97 l += 1 + _fragment.length; 98 } 99 return l; 100 } 101 } 102 103 URL parse_url(string url) @safe 104 { 105 URL result; 106 static http_parser_url parser; 107 108 result._url = url; 109 http_parser_url_init(&parser); 110 auto l = http_parser_parse_url(&url[0], url.length, 0, &parser); 111 if ( l ) 112 { 113 return result; 114 } 115 if ( (1<<http_parser_url_fields.UF_PORT) & parser.field_set ) 116 { 117 result._port = parser.port; 118 } 119 if ( (1<<http_parser_url_fields.UF_SCHEMA) & parser.field_set ) 120 { 121 auto off = parser.field_data[http_parser_url_fields.UF_SCHEMA].off; 122 auto len = parser.field_data[http_parser_url_fields.UF_SCHEMA].len; 123 if ( off>=0 && len>=0 && off+len <= url.length ) 124 { 125 result._schema = url[off..off+len]; 126 if (result._schema.toLower == "http") 127 { 128 result._schemaCode = SchCode.HTTP; 129 if ( !result._port ) 130 { 131 result._port = 80; 132 } 133 } 134 else if (result._schema.toLower == "https") 135 { 136 result._schemaCode = SchCode.HTTPS; 137 if ( !result._port ) 138 { 139 result._port = 443; 140 } 141 } 142 } 143 } 144 if ( (1<<http_parser_url_fields.UF_HOST) & parser.field_set ) 145 { 146 auto off = parser.field_data[http_parser_url_fields.UF_HOST].off; 147 auto len = parser.field_data[http_parser_url_fields.UF_HOST].len; 148 if ( off>=0 && len>=0 && off+len <= url.length ) 149 { 150 result._host = url[off..off+len]; 151 } 152 } 153 if ( (1<<http_parser_url_fields.UF_PATH) & parser.field_set ) 154 { 155 auto off = parser.field_data[http_parser_url_fields.UF_PATH].off; 156 auto len = parser.field_data[http_parser_url_fields.UF_PATH].len; 157 if ( off>=0 && len>=0 && off+len <= url.length ) 158 { 159 result._path = url[off..off+len]; 160 } 161 result._path_off = off; 162 } 163 if ( (1<<http_parser_url_fields.UF_QUERY) & parser.field_set ) 164 { 165 auto off = parser.field_data[http_parser_url_fields.UF_QUERY].off; 166 auto len = parser.field_data[http_parser_url_fields.UF_QUERY].len; 167 if ( off>=0 && len>=0 && off+len <= url.length ) 168 { 169 result._query = url[off..off+len]; 170 } 171 } 172 if ( (1<<http_parser_url_fields.UF_FRAGMENT) & parser.field_set ) 173 { 174 auto off = parser.field_data[http_parser_url_fields.UF_FRAGMENT].off; 175 auto len = parser.field_data[http_parser_url_fields.UF_FRAGMENT].len; 176 if ( off>=0 && len>=0 && off+len <= url.length ) 177 { 178 result._fragment = url[off..off+len]; 179 } 180 } 181 if ( (1<<http_parser_url_fields.UF_USERINFO) & parser.field_set ) 182 { 183 auto off = parser.field_data[http_parser_url_fields.UF_USERINFO].off; 184 auto len = parser.field_data[http_parser_url_fields.UF_USERINFO].len; 185 if ( off>=0 && len>=0 && off+len <= url.length ) 186 { 187 result._userinfo = url[off..off+len]; 188 } 189 } 190 debug(hiohttp) tracef("url_parser: %s", parser); 191 return result; 192 } 193 194 unittest 195 { 196 URL u = parse_url("http://example.com/"); 197 assert(u._schema == "http"); 198 assert(u._host == "example.com"); 199 assert(u._path == "/"); 200 assert(u.port == 80); 201 u =parse_url("https://a:b@example.com/path?query#frag"); 202 assert(u._schema == "https"); 203 assert(u._host == "example.com"); 204 assert(u._path == "/path"); 205 assert(u._query == "query"); 206 assert(u._fragment == "frag"); 207 assert(u._userinfo == "a:b"); 208 assert(u.port == 443); 209 } 210 211 struct Header 212 { 213 string FieldName; 214 string FieldValue; 215 } 216 217 struct Method 218 { 219 private string _name; 220 string name() @safe @nogc nothrow 221 { 222 return _name; 223 } 224 } 225 226 struct _UH { 227 // flags for each important header, added by user using addHeaders 228 mixin(bitfields!( 229 bool, "Host", 1, 230 bool, "UserAgent", 1, 231 bool, "ContentLength", 1, 232 bool, "Connection", 1, 233 bool, "AcceptEncoding", 1, 234 bool, "ContentType", 1, 235 bool, "Cookie", 1, 236 uint, "", 1 237 )); 238 } 239 alias ErrorType = Tuple!(int, "code", string, "msg"); 240 enum AsyncHTTPErrors 241 { 242 None = ErrorType(0, "OK"), 243 Timeout = ErrorType(2, "Timedout"), 244 ResolveFailed 245 = ErrorType(3, "Can't resolve"), 246 ConnFailed 247 = ErrorType(4, "Connection failed"), 248 DataError 249 = ErrorType(5, "Error when receiving data"), 250 MaxRedirectsReached 251 = ErrorType(6, "Max num of redirects reached"), 252 } 253 254 package struct Request 255 { 256 private 257 { 258 Method _method; 259 HashMap!(string, Header) _user_headers; 260 _UH _user_headers_flags; 261 Nbuff _body; 262 } 263 ~this() 264 { 265 clear(); 266 } 267 this(Method m) 268 { 269 _method = m; 270 } 271 void method(string m) @safe @nogc nothrow 272 { 273 _method = Method(m); 274 } 275 void method(Method m) @safe @nogc nothrow 276 { 277 _method = m; 278 } 279 string method() @safe @nogc nothrow 280 { 281 return _method.name; 282 } 283 /// 284 void addHeader(Header h) @safe 285 { 286 // save header as user supplied 287 // use lower-case'd field as key in hashmap 288 // pro: fast replace, send header as user supplied 289 // contra: - extra `toLower` call 290 string field = h.FieldName.toLower(); 291 switch(field) 292 { 293 case "host": 294 _user_headers_flags.Host = true; 295 break; 296 case "useragent": 297 _user_headers_flags.UserAgent = true; 298 break; 299 case "accept-encoding": 300 _user_headers_flags.AcceptEncoding = true; 301 break; 302 case "content-length": 303 _user_headers_flags.ContentLength = true; 304 break; 305 case "content-type": 306 _user_headers_flags.ContentType = true; 307 break; 308 default: 309 break; 310 } 311 _user_headers.put(field, h); 312 } 313 auto user_headers() @safe @nogc nothrow 314 { 315 return _user_headers.byValue; 316 } 317 auto user_headers_flags() @safe @nogc nothrow 318 { 319 return _user_headers_flags; 320 } 321 void clear() 322 { 323 _user_headers.clear; 324 } 325 } 326 // alias Request = SmartPtr!_Request; 327 package struct MessageHeader 328 { 329 NbuffChunk field; 330 NbuffChunk value; 331 } 332 333 alias MessageHeaders = UnrolledList!MessageHeader; 334 335 ushort standard_port(string schema) @safe 336 { 337 switch(schema) 338 { 339 case "http": 340 return 80; 341 case "https": 342 return 443; 343 default: 344 return 0; 345 } 346 }