1 /** 2 3 Introduces a simple but under-powered HTTP client. 4 5 Consider using $(WEB vibed.org,vibe.d) if you need something better. 6 7 */ 8 module gfm.net.httpclient; 9 10 import std.socketstream, 11 std.stream, 12 std.socket, 13 std.string, 14 std.conv, 15 std.stdio; 16 17 import gfm.net.uri; 18 19 /// The exception type for HTTP errors. 20 class HTTPException : Exception 21 { 22 public 23 { 24 this(string msg) 25 { 26 super(msg); 27 } 28 } 29 } 30 31 /// Results of a HTTP request. 32 class HTTPResponse 33 { 34 int statusCode; /// HTTP status code received. 35 string[string] headers; /// HTTP headers received. 36 ubyte[] content; /// Request body. 37 } 38 39 40 enum HTTPMethod 41 { 42 OPTIONS, 43 GET, 44 HEAD, 45 POST, 46 PUT, 47 DELETE, 48 TRACE, 49 CONNECT 50 } 51 52 /** 53 54 Minimalistic HTTP client. 55 At this point it only support simple GET requests which return the 56 whole response body. 57 58 Bugs: We might need to pool TCP connections later on. 59 */ 60 class HTTPClient 61 { 62 public 63 { 64 /// Creates a HTTP client with user-specified User-Agent. 65 this(string userAgent = "gfm-http-client") 66 { 67 buffer.length = 4096; 68 _userAgent = userAgent; 69 } 70 71 ~this() 72 { 73 close(); 74 } 75 76 void close() 77 { 78 if (_socket !is null) 79 { 80 _socket.close(); 81 _socket = null; 82 } 83 } 84 85 /// Perform a HTTP GET request. 86 HTTPResponse GET(URI uri) 87 { 88 return request(HTTPMethod.GET, uri, defaultHeaders(uri)); 89 } 90 91 /// Perform a HTTP HEAD request (same as GET but without content). 92 HTTPResponse HEAD(URI uri) 93 { 94 return request(HTTPMethod.HEAD, uri, defaultHeaders(uri)); 95 } 96 97 /// Performs a HTTP request. 98 /// Requested URI can be "*", an absolute URI, an absolute path, or an authority 99 /// depending on the method. 100 /// Throws: $(D HTTPException) on error. 101 HTTPResponse request(HTTPMethod method, URI uri, string[string] headers) 102 { 103 checkURI(uri); 104 auto res = new HTTPResponse(); 105 106 107 try 108 { 109 connectTo(uri); 110 assert(_socket !is null); 111 112 string request = format("%s %s HTTP/1.0\r\n", to!string(method), uri.toString()); 113 114 foreach (header; headers.byKey()) 115 { 116 request ~= format("%s: %s\r\n", header, headers[header]); 117 } 118 request ~= "\r\n"; 119 120 auto scope ss = new SocketStream(_socket); 121 ss.writeString(request); 122 123 // parse status line 124 auto line = ss.readLine(); 125 if (line.length < 12 || line[0..5] != "HTTP/" || line[6] != '.') 126 throw new HTTPException("Cannot parse HTTP status line"); 127 128 if (line[5] != '1' || (line[7] != '0' && line[7] != '1')) 129 throw new HTTPException("Unsupported HTTP version"); 130 131 // parse error code 132 res.statusCode = 0; 133 for (int i = 0; i < 3; ++i) 134 { 135 char c = line[9 + i]; 136 if (c >= '0' && c <= '9') 137 res.statusCode = res.statusCode * 10 + (c - '0'); 138 else 139 throw new HTTPException("Expected digit in HTTP status code"); 140 } 141 142 // parse headers 143 while(true) 144 { 145 auto headerLine = ss.readLine(); 146 147 if (headerLine.length == 0) 148 break; 149 150 sizediff_t colonIdx = indexOf(headerLine, ':'); 151 if (colonIdx == -1) 152 throw new HTTPException("Cannot parse HTTP header: missing colon"); 153 154 string key = headerLine[0..colonIdx].idup; 155 156 // trim leading spaces and tabs 157 sizediff_t valueStart = colonIdx + 1; 158 for ( ; valueStart <= headerLine.length; ++valueStart) 159 { 160 char c = headerLine[valueStart]; 161 if (c != ' ' && c != '\t') 162 break; 163 } 164 165 // trim trailing spaces and tabs 166 sizediff_t valueEnd = headerLine.length; 167 for ( ; valueEnd > valueStart; --valueEnd) 168 { 169 char c = headerLine[valueEnd - 1]; 170 if (c != ' ' && c != '\t') 171 break; 172 } 173 174 string value = headerLine[valueStart..valueEnd].idup; 175 res.headers[key] = value; 176 } 177 178 while (!ss.eof()) 179 { 180 int read = cast(int)( ss.readBlock(buffer.ptr, buffer.length)); 181 res.content ~= buffer[0..read]; 182 } 183 184 return res; 185 } 186 catch (Exception e) 187 { 188 throw new HTTPException(e.msg); 189 } 190 } 191 } 192 193 private 194 { 195 TcpSocket _socket; 196 ubyte[] buffer; 197 string _userAgent; 198 199 void connectTo(URI uri) 200 { 201 if (_socket !is null) 202 { 203 _socket.close(); 204 _socket = null; 205 } 206 _socket = new TcpSocket(uri.resolveAddress()); 207 } 208 209 static checkURI(URI uri) 210 { 211 if (uri.scheme() != "http") 212 throw new HTTPException(format("'%' is not an HTTP absolute url", uri.toString())); 213 } 214 215 string[string] defaultHeaders(URI uri) 216 { 217 string hostName = uri.hostName(); 218 auto headers = ["Host": hostName, 219 "User-Agent": _userAgent]; 220 return headers; 221 } 222 } 223 } 224