1 module drocks.request;
2 
3 import std.stdio   : stderr, writeln;
4 import std.range     : join, isInputRange, ElementType;
5 import std.traits    : isIntegral;
6 import std.algorithm : map;
7 import std.socket    : InternetAddress, SocketException;
8 import std.typecons  : Tuple, tuple;
9 import std.conv      : to;
10 
11 public import drocks.exception : ClientException;
12 import drocks.sockhandler      : SockHandler;
13 import drocks.response         : Response;
14 import drocks.pair             : Pair;
15 
16 // Creates sockets and generates requests
17 struct Request
18 {
19 private:
20     InternetAddress _addr;
21     string _host;
22 
23 public:
24     this(string host, ushort port)
25     {
26         _host = host;
27         _addr = new InternetAddress(host, port);
28     }
29     @disable this ();
30 
31     // Generates a request and returns response
32     Response request(string req)
33     {
34         auto sock = new SockHandler();
35 
36         try {
37             sock.connect(_addr);
38             sock.send(req);
39         } catch (SocketException e) {
40             throw new ClientException(e);
41         }
42 
43         // Check response status
44         auto status = sock.receiveHeader();
45         enum string expectedStatus = "200 OK";
46         if( status.length < expectedStatus.length ){
47             throw new ClientException("Empty response");
48         }
49         
50         // Expected: status == "HTTP/1.1 200 OK"
51         if( !sock.isValid() || expectedStatus != status[$-expectedStatus.length..$] ){
52             throw new ClientException("Status error: " ~ status.idup);
53         }
54 
55         // skip headers
56         while (sock.receiveHeader().length) {}
57         
58         return Response(sock);
59     }
60 
61     //
62     // GET requests
63     //
64     Response httpGet(const string path)
65     {
66         string buf;
67         buf  = "GET /" ~ path  ~ " HTTP/1.1\r\n" ~
68                "Host:" ~ _host ~ "\r\n" ~
69                headsEnd;
70         return this.request(buf);
71     }
72 
73     Response httpGet(const string path, string data)
74     {
75         string buf;
76         buf  = "GET /" ~ path  ~ "?" ~ data ~ " HTTP/1.1\r\n" ~
77                "Host:" ~ _host ~ "\r\n" ~
78                headsEnd;
79         return this.request(buf);
80     }
81 
82     Response httpGet(T)(const string path, auto ref T data)
83         if(isIntegral!T)
84     {
85         return this.httpGet(path, data.to!string);
86     }
87 
88     Response httpGet(Range)(const string path, auto ref Range range)
89         if(isInputRange!Range && is(ElementType!Range == string))
90     {
91         return this.httpGet(path, range.join("&"));
92     }
93 
94     Response httpGet(Args...)(const string path, auto ref Args args)
95         if(Args.length > 1)
96     {
97         return this.httpGet(path, join(tuple(args), '&'));
98     }
99 
100     //
101     // POST requests
102     //
103     Response httpPost(const string path, string data)
104     {
105         string buf;
106         buf  = headsStartPost(path) ~
107                headsEndPost(data.length) ~
108                data;
109 
110         return this.request(buf);
111     }
112 
113     Response httpPost(T)(const string path, auto ref T data)
114         if(isIntegral!T)
115     {
116         return this.httpPost(path, data.to!string);
117     }
118 
119     Response httpPost(const string path)
120     {
121         return this.request( headsStartPost(path) ~ headsEnd );
122     }
123 
124     Response httpPost(Range)(const string path, auto ref Range range)
125         if(isInputRange!Range && is(ElementType!Range == string))
126     {
127         return this.httpPost(path, range.join("\n"));
128     }
129 
130     Response httpPost(Range)(const string path, auto ref Range range)
131         if( isInputRange!Range && is(ElementType!Range == Pair) )
132     {
133         string data = range
134             .map!( (const Pair x) {return x.serialize;})
135             .join("\n");
136 
137         return this.httpPost(path, data);
138     }
139 
140     Response httpPost(Range)(const string path, auto ref Range range)
141         if( isInputRange!Range && is(ElementType!Range: Tuple!(string, string)) )
142     {
143         return this.httpPost(path, range
144             .map!( (const Tuple!(string, string) x) {return Pair(x);})
145         );
146     }
147 
148     Response httpPost(Range)(const string path, auto ref Range range)
149         if(isInputRange!Range && isIntegral!(ElementType!Range))
150     {
151         return this.httpPost(path, range.map!"a.to!string");
152     }
153 
154     Response httpPost(Args...)(const string path, auto ref Args args)
155         if(Args.length > 1)
156     {
157         return this.httpPost(path, join(tuple(args), '\n'));
158     }
159 
160 
161 private:
162 
163     static string join(Args...)(auto ref Tuple!Args args, char c)
164         if(Args.length > 0)
165     {
166         static if(is(Args[0] == string)) {
167             string data = args[0];
168         } else {
169             string data = args[0].to!string;
170         }
171         
172         static foreach(enum ind; 1..Args.length) {
173             static if(is(Args[ind] == string)) {
174                 data ~= c ~ args[ind];
175             } else {
176                 data ~= c ~ args[ind].to!string;
177             }
178         }
179         return data;
180     }
181 
182     enum string  headsEnd = 
183         "Content-Type: charset=UTF-8\r\n" ~
184         "Connection: Close\r\n\r\n";
185 
186     string headsStartPost(const string path)
187     {
188         return 
189             "POST /" ~ path ~ " HTTP/1.1\r\n" ~
190             "Host:" ~ _host ~ "\r\n";
191     }
192     string headsEndPost(size_t len)
193     {
194         return 
195             "Content-Type:application/x-www-form-urlencoded; charset=UTF-8\r\n" ~
196             "Content-Length: " ~ len.to!string ~ "\r\n" ~
197             "Connection: Close\r\n\r\n";
198     }
199 
200 }