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 @safe pure nothrow this(string message, string file =__FILE__, size_t line = __LINE__, Throwable next = null)
25 {
26 super(message, file, line, next);
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