1 module fastcgi; 2 3 import std.socket; 4 import std.conv; 5 import std.algorithm; 6 import std.range; 7 import std.experimental.logger; 8 import std.random; 9 import std.datetime; 10 import std.utf; 11 import std.typecons; 12 13 struct FastCGIHeader { 14 ubyte _version; 15 ubyte type; 16 ushort requestId; 17 ushort contentLength; 18 ubyte paddingLength; 19 ubyte reserved; 20 string content; 21 } 22 23 final class FastCGIClient { 24 private { 25 static immutable FCGI_VERSION = 1; 26 static immutable FCGI_ROLE_RESPONDER = 1; 27 static immutable FCGI_ROLE_AUTHORIZER = 2; 28 static immutable FCGI_ROLE_FILTER = 3; 29 static immutable FCGI_TYPE_BEGIN = 1; 30 static immutable FCGI_TYPE_ABORT = 2; 31 static immutable FCGI_TYPE_END = 3; 32 static immutable FCGI_TYPE_PARAMS = 4; 33 static immutable FCGI_TYPE_STDIN = 5; 34 static immutable FCGI_TYPE_STDOUT = 6; 35 static immutable FCGI_TYPE_STDERR = 7; 36 static immutable FCGI_TYPE_DATA = 8; 37 static immutable FCGI_TYPE_GETVALUES = 9; 38 static immutable FCGI_TYPE_GETVALUES_RESULT = 10; 39 static immutable FCGI_TYPE_UNKOWNTYPE = 11; 40 static immutable FCGI_HEADER_SIZE = 8; 41 } 42 43 enum FCGI_STATE_SEND = 1; 44 enum FCGI_STATE_ERROR = 2; 45 enum FCGI_STATE_SUCCESS = 3; 46 47 private { 48 string host; 49 ushort port; 50 Duration timeout; 51 int keepalive; 52 Socket sock; 53 string[string][ushort] requests; 54 } 55 56 this(string host, ushort port, int timeout, bool keepalive) { 57 this.host = host; 58 this.port = port; 59 this.timeout = seconds(timeout); 60 this.keepalive = keepalive ? 1 : 0; 61 this.sock = null; 62 } 63 64 bool connect() { 65 this.sock = new Socket(AddressFamily.INET, SocketType.STREAM); 66 67 this.sock.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, this.timeout); 68 try { 69 this.sock.connect(new InternetAddress(this.host, this.port)); 70 } catch (SocketException msg) { 71 error(msg.toString()); 72 return false; 73 } 74 return true; 75 } 76 77 bool ensureConnection() { 78 if (!this.isConnected()) { 79 if (!this.connect()) { 80 error("Failed to establish connection!"); 81 return false; 82 } 83 } 84 return true; 85 } 86 87 bool isConnected() { 88 return this.sock !is null; 89 } 90 91 void closeConnection() { 92 if (this.isConnected()) { 93 this.sock.close(); 94 this.sock = null; 95 } 96 } 97 98 ubyte[] encodeFastCGIRecord(ubyte fcgi_type, ubyte[] content, ushort requestid) { 99 auto length = content.length; 100 return [ 101 cast(ubyte) FCGI_VERSION, 102 cast(ubyte) fcgi_type, 103 cast(ubyte) ((requestid >> 8) & 0xFF), 104 cast(ubyte) (requestid & 0xFF), 105 cast(ubyte) ((length >> 8) & 0xFF), 106 cast(ubyte) (length & 0xFF), 107 cast(ubyte) 0, 108 cast(ubyte) 0 109 ] ~ content; 110 } 111 112 ubyte[] encodeNameValueParams(string name, string value) { 113 auto nLen = name.length; 114 auto vLen = value.length; 115 ubyte[] record; 116 if (nLen < 128) { 117 record ~= cast(ubyte) nLen; 118 } else { 119 record ~= cast(ubyte) (((nLen >> 24) & 0xFF) | 0x80); 120 record ~= cast(ubyte) ((nLen >> 16) & 0xFF); 121 record ~= cast(ubyte) ((nLen >> 8) & 0xFF); 122 record ~= cast(ubyte) (nLen & 0xFF); 123 } 124 if (vLen < 128) { 125 record ~= cast(ubyte) vLen; 126 } else { 127 record ~= cast(ubyte) (((vLen >> 24) & 0xFF) | 0x80); 128 record ~= cast(ubyte) ((vLen >> 16) & 0xFF); 129 record ~= cast(ubyte) ((vLen >> 8) & 0xFF); 130 record ~= cast(ubyte) (vLen & 0xFF); 131 } 132 return record ~ cast(ubyte[]) name.toUTF8 ~ cast(ubyte[]) value.toUTF8; 133 } 134 135 FastCGIHeader decodeFastCGIHeader(ubyte[] stream) { 136 FastCGIHeader header; 137 138 header._version = stream[0]; 139 header.type = stream[1]; 140 header.requestId = (cast(ushort) stream[2] << 8) | stream[3]; 141 header.contentLength = (cast(ushort) stream[4] << 8) | stream[5]; 142 header.paddingLength = stream[6]; 143 header.reserved = stream[7]; 144 145 return header; 146 } 147 148 Nullable!FastCGIHeader decodeFastCGIRecord() { 149 Nullable!FastCGIHeader ret; 150 151 ubyte[FCGI_HEADER_SIZE] headerBuff; 152 auto headerLen = this.sock.receive(headerBuff[]); 153 154 if (headerLen != FCGI_HEADER_SIZE) { 155 return ret; 156 } 157 158 auto header = decodeFastCGIHeader(headerBuff[]); 159 if (header.contentLength > 0) { 160 ubyte[] buffer; buffer.length = header.contentLength; 161 auto bytesRead = this.sock.receive(buffer); 162 if (bytesRead != header.contentLength) { 163 error("Failed to read the expected content length."); 164 return ret; 165 } 166 header.content = cast(string)buffer; 167 } 168 169 if (header.paddingLength > 0) { 170 auto dummyBuffer = new ubyte[](header.paddingLength); 171 auto skipped = this.sock.receive(dummyBuffer); 172 } 173 174 return Nullable!FastCGIHeader(header); 175 } 176 177 178 string request(string[string] nameValuePairs, string post) { 179 180 if (!this.ensureConnection()) { 181 error("connect failure! please check your fasctcgi-server !!"); 182 return ""; 183 } 184 185 auto requestId = cast(ushort) uniform(1, (1 << 16) - 1); 186 this.requests[requestId] = ["state": FCGI_STATE_SEND.to!string, "response": ""]; 187 ubyte[] request; 188 ubyte[] beginFCGIRecordContent = [ 189 cast(ubyte) 0, 190 cast(ubyte) FCGI_ROLE_RESPONDER, 191 cast(ubyte) this.keepalive, 192 cast(ubyte) 0, 193 cast(ubyte) 0, 194 cast(ubyte) 0, 195 cast(ubyte) 0, 196 cast(ubyte) 0 197 ]; 198 request ~= this.encodeFastCGIRecord(FCGI_TYPE_BEGIN, beginFCGIRecordContent, requestId); 199 ubyte[] paramsRecord; 200 if (!nameValuePairs.empty) { 201 foreach (name, value; nameValuePairs) { 202 paramsRecord ~= this.encodeNameValueParams(name, value); 203 } 204 } 205 206 if (!paramsRecord.empty) { 207 request ~= this.encodeFastCGIRecord(FCGI_TYPE_PARAMS, paramsRecord, requestId); 208 } 209 request ~= this.encodeFastCGIRecord(FCGI_TYPE_PARAMS, null, requestId); 210 211 if (!post.empty) { 212 request ~= this.encodeFastCGIRecord(FCGI_TYPE_STDIN.to!ubyte, cast(ubyte[])post.toUTF8, requestId); 213 } 214 request ~= this.encodeFastCGIRecord(FCGI_TYPE_STDIN, null, requestId); 215 216 this.sock.send(request); 217 this.requests[requestId]["state"] = FCGI_STATE_SEND.to!string; 218 return this.waitForResponse(requestId); 219 } 220 221 string waitForResponse(ushort requestId) { 222 while (true) { 223 auto response = this.decodeFastCGIRecord(); 224 if (response.isNull) { 225 break; 226 } 227 if (response.get.type == FCGI_TYPE_STDOUT || response.get.type == FCGI_TYPE_STDERR) { 228 if (response.get.type == FCGI_TYPE_STDERR) { 229 this.requests[requestId]["state"] = FCGI_STATE_ERROR.to!string; 230 } 231 if (requestId == response.get.requestId) { 232 this.requests[requestId]["response"] ~= response.get.content; 233 } 234 } 235 if (response.get.type == FCGI_STATE_SUCCESS) { 236 // 237 } 238 } 239 this.closeConnection(); 240 return this.requests[requestId]["response"]; 241 } 242 243 override string toString() { 244 return "fastcgi connect host:" ~ this.host ~ " port:" ~ to!string(this.port); 245 } 246 }