https://issues.shibboleth.net/jira/browse/SSPCPP-255
[shibboleth/cpp-sp.git] / fastcgi / shibresponder.cpp
1 /*\r
2  *  Copyright 2001-2009 Internet2\r
3  * \r
4  * Licensed under the Apache License, Version 2.0 (the "License");\r
5  * you may not use this file except in compliance with the License.\r
6  * You may obtain a copy of the License at\r
7  *\r
8  *     http://www.apache.org/licenses/LICENSE-2.0\r
9  *\r
10  * Unless required by applicable law or agreed to in writing, software\r
11  * distributed under the License is distributed on an "AS IS" BASIS,\r
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
13  * See the License for the specific language governing permissions and\r
14  * limitations under the License.\r
15  */\r
16 \r
17 /* shibresponder.cpp - Shibboleth FastCGI Responder/Handler\r
18 \r
19    Andre Cruz\r
20 */\r
21 \r
22 // SAML Runtime\r
23 #include <saml/saml.h>\r
24 #include <shib-target/shib-target.h>\r
25 \r
26 #include <stdexcept>\r
27 #include <stdlib.h>\r
28 #ifdef HAVE_UNISTD_H\r
29 # include <unistd.h>\r
30 # include <sys/mman.h>\r
31 #endif\r
32 #include <fcgio.h>\r
33 \r
34 using namespace shibtarget;\r
35 using namespace saml;\r
36 using namespace std;\r
37 \r
38 typedef enum {\r
39     SHIB_RETURN_OK,\r
40     SHIB_RETURN_KO,\r
41     SHIB_RETURN_DONE\r
42 } shib_return_t;\r
43 \r
44 set<string> g_allowedSchemes;\r
45 \r
46 class ShibTargetFCGI : public ShibTarget\r
47 {\r
48     FCGX_Request* m_req;\r
49     char* m_body;\r
50     string m_cookie;\r
51     map<string, string> m_headers;\r
52 \r
53     void checkString(const string& s, const char* msg) {\r
54         string::const_iterator e = s.end();\r
55         for (string::const_iterator i=s.begin(); i!=e; ++i) {\r
56             if (iscntrl(*i))\r
57                 throw runtime_error(msg);\r
58         }\r
59     }\r
60 \r
61 public:\r
62     ShibTargetFCGI(FCGX_Request* req, char* post_data, const char* scheme=NULL, const char* hostname=NULL, int port=0)\r
63         : m_req(req), m_body(post_data) {\r
64 \r
65         const char* server_name_str = hostname;\r
66         if (!server_name_str || !*server_name_str)\r
67             server_name_str = FCGX_GetParam("SERVER_NAME", req->envp);\r
68 \r
69         int server_port = port;\r
70         if (!port) {\r
71             char* server_port_str = FCGX_GetParam("SERVER_PORT", req->envp);\r
72             server_port = strtol(server_port_str, &server_port_str, 10);\r
73             if (*server_port_str) {\r
74                 cerr << "can't parse SERVER_PORT (" << FCGX_GetParam("SERVER_PORT", req->envp) << ")" << endl;\r
75                 throw runtime_error("Unable to determine server port.");\r
76             }\r
77         }\r
78 \r
79         const char* server_scheme_str = scheme;\r
80         if (!server_scheme_str || !*server_scheme_str)\r
81             server_scheme_str = (server_port == 443 || server_port == 8443) ? "https" : "http";\r
82 \r
83         const char* request_uri_str = FCGX_GetParam("REQUEST_URI", req->envp);\r
84         const char* content_type_str = FCGX_GetParam("CONTENT_TYPE", req->envp);\r
85         const char* remote_addr_str = FCGX_GetParam("REMOTE_ADDR", req->envp);\r
86         const char* request_method_str = FCGX_GetParam("REQUEST_METHOD", req->envp);\r
87 \r
88 #ifdef _DEBUG\r
89         cerr << "server_name = " << server_name_str << endl\r
90              << "server_port = " << server_port << endl\r
91              << "request_uri_str = " << request_uri_str << endl\r
92              << "content_type = " << content_type_str << endl\r
93              << "remote_address = " << remote_addr_str << endl\r
94              << "request_method = " << request_method_str << endl;\r
95 #endif\r
96 \r
97         init(server_scheme_str,\r
98              server_name_str,\r
99              server_port,\r
100              request_uri_str,\r
101              content_type_str ? content_type_str : "",\r
102              remote_addr_str,\r
103              request_method_str\r
104              );\r
105     }\r
106 \r
107     ~ShibTargetFCGI() { }\r
108 \r
109     virtual void log(ShibLogLevel level, const string& msg) {\r
110         ShibTarget::log(level,msg);\r
111     \r
112         if (level == LogLevelError)\r
113             cerr << "shib: " << msg;\r
114     }\r
115   \r
116     virtual string getCookies(void) const {\r
117         char * cookie = FCGX_GetParam("HTTP_COOKIE", m_req->envp);\r
118         return cookie ? cookie : "";\r
119     }\r
120   \r
121     virtual void setCookie(const string& name, const string& value) {\r
122         m_cookie += "Set-Cookie: " + name + "=" + value + "\r\n";\r
123     }\r
124 \r
125     virtual string getArgs(void) {\r
126         char * args = FCGX_GetParam("QUERY_STRING", m_req->envp);\r
127         return args ? args : "";\r
128     }\r
129 \r
130     virtual string getPostData(void) {\r
131         return m_body ? m_body : "";\r
132     }\r
133 \r
134     virtual void clearHeader(const string &name) {\r
135         throw runtime_error("clearHeader not implemented by FastCGI responder.");\r
136     }\r
137   \r
138     virtual void setHeader(const string &name, const string &value) {\r
139         throw runtime_error("setHeader not implemented by FastCGI responder.");\r
140     }\r
141 \r
142     virtual string getHeader(const string &name) {\r
143         throw runtime_error("getHeader not implemented by FastCGI responder.");\r
144     }\r
145 \r
146     virtual void setRemoteUser(const string &user) {\r
147         throw runtime_error("setRemoteUser not implemented by FastCGI responder.");\r
148     }\r
149 \r
150     virtual string getRemoteUser(void) {\r
151         throw runtime_error("getRemoteUser not implemented by FastCGI responder.");\r
152     }\r
153 \r
154     virtual void* sendPage(\r
155         const string& msg,\r
156         int code=200,\r
157         const string& content_type="text/html",\r
158         const saml::Iterator<header_t>& headers=EMPTY(header_t)) {\r
159 \r
160         checkString(content_type, "Detected control character in a response header.");\r
161         string hdr = string ("Connection: close\r\nContent-type: ") + content_type + "\r\n" + m_cookie;\r
162         while (headers.hasNext()) {\r
163             const header_t& h=headers.next();\r
164             checkString(h.first, "Detected control character in a response header.");\r
165             checkString(h.second, "Detected control character in a response header.");\r
166             hdr += h.first + ": " + h.second + "\r\n";\r
167         }\r
168 \r
169         const char* codestr="Status: 200 OK";\r
170         switch (code) {\r
171             case 500:   codestr="Status: 500 Server Error"; break;\r
172             case 403:   codestr="Status: 403 Forbidden"; break;\r
173             case 404:   codestr="Status: 404 Not Found"; break;\r
174         }\r
175 \r
176         cout << codestr << "\r\n" << hdr << m_cookie << "\r\n" << msg;\r
177         return (void*)SHIB_RETURN_DONE;\r
178     }\r
179 \r
180     virtual void* sendRedirect(const string& url) {\r
181         checkString(url, "Detected control character in an attempted redirect.");\r
182         if (g_allowedSchemes.find(url.substr(0, url.find(':'))) == g_allowedSchemes.end())\r
183             throw runtime_error("Invalid scheme in attempted redirect.");\r
184         cout << "Status: 302 Please Wait" << "\r\n" << "Location: " << url << "\r\n" << m_cookie << "\r\n"\r
185             << "<HTML><BODY>Redirecting...</BODY></HTML>";\r
186         return (void*)SHIB_RETURN_DONE;\r
187     }\r
188 \r
189     virtual void* returnDecline(void) { \r
190         return (void*)SHIB_RETURN_KO;\r
191     }\r
192 \r
193     virtual void* returnOK(void) {\r
194         return (void*)SHIB_RETURN_OK;\r
195     }\r
196 };\r
197 \r
198 // Maximum number of bytes allowed to be read from stdin\r
199 static const unsigned long STDIN_MAX = 1000000;\r
200 \r
201 static long gstdin(FCGX_Request* request, char** content)\r
202 {\r
203     char* clenstr = FCGX_GetParam("CONTENT_LENGTH", request->envp);\r
204     unsigned long clen = STDIN_MAX;\r
205 \r
206     if (clenstr) {\r
207         clen = strtol(clenstr, &clenstr, 10);\r
208         if (*clenstr) {\r
209             cerr << "can't parse CONTENT_LENGTH (" << FCGX_GetParam("CONTENT_LENGTH", request->envp) << ")" << endl;\r
210             clen = STDIN_MAX;\r
211         }\r
212 \r
213         // *always* put a cap on the amount of data that will be read\r
214         if (clen > STDIN_MAX)\r
215             clen = STDIN_MAX;\r
216 \r
217         *content = new char[clen];\r
218 \r
219         cin.read(*content, clen);\r
220         clen = cin.gcount();\r
221     }\r
222     else {\r
223         // *never* read stdin when CONTENT_LENGTH is missing or unparsable\r
224         *content = 0;\r
225         clen = 0;\r
226     }\r
227 \r
228     // Chew up any remaining stdin - this shouldn't be necessary\r
229     // but is because mod_fastcgi doesn't handle it correctly.\r
230 \r
231     // ignore() doesn't set the eof bit in some versions of glibc++\r
232     // so use gcount() instead of eof()...\r
233     do cin.ignore(1024); while (cin.gcount() == 1024);\r
234 \r
235     return clen;\r
236 }\r
237 \r
238 static void print_ok() {\r
239     cout << "Status: 200 OK" << "\r\n\r\n";\r
240 }\r
241 \r
242 static void print_error(const char* msg) {\r
243     cout << "Status: 500 Server Error" << "\r\n\r\n" << msg;\r
244 }\r
245 \r
246 int main(void)\r
247 {\r
248     char* shib_config = getenv("SHIB_CONFIG");\r
249     char* shib_schema = getenv("SHIB_SCHEMA");\r
250     if ((shib_config == NULL) || (shib_schema == NULL)) {\r
251         cerr << "SHIB_CONFIG or SHIB_SCHEMA not set." << endl;\r
252         exit(1);\r
253     }\r
254     cerr << "SHIB_CONFIG = " << shib_config << endl\r
255          << "SHIB_SCHEMA = " << shib_schema << endl;\r
256 \r
257     string g_ServerScheme;\r
258     string g_ServerName;\r
259     int g_ServerPort=0;\r
260     ShibTargetConfig* g_Config;\r
261 \r
262     try {\r
263         g_Config = &ShibTargetConfig::getConfig();\r
264         g_Config->setFeatures(\r
265             ShibTargetConfig::Listener |\r
266             ShibTargetConfig::Metadata |\r
267             ShibTargetConfig::RequestMapper |\r
268             ShibTargetConfig::LocalExtensions |\r
269             ShibTargetConfig::Logging\r
270             );\r
271         if (!g_Config->init(shib_schema)) {\r
272             cerr << "failed to initialize Shibboleth libraries" << endl;\r
273             exit(1);\r
274         }\r
275         \r
276         if (!g_Config->load(shib_config)) {\r
277             cerr << "failed to load Shibboleth configuration" << endl;\r
278             exit(1);\r
279         }\r
280 \r
281         IConfig* conf=g_Config->getINI();\r
282         Locker locker(conf);\r
283         const IPropertySet* props=conf->getPropertySet("Local");\r
284         if (props) {\r
285             pair<bool,const char*> str=props->getString("allowedSchemes");\r
286             if (str.first) {\r
287                 string schemes=str.second;\r
288                 unsigned int j=0;\r
289                 for (unsigned int i=0;  i < schemes.length();  i++) {\r
290                     if (schemes.at(i)==' ') {\r
291                         g_allowedSchemes.insert(schemes.substr(j, i-j));\r
292                         j = i+1;\r
293                     }\r
294                 }\r
295                 g_allowedSchemes.insert(schemes.substr(j, schemes.length()-j));\r
296             }\r
297         }\r
298         if (g_allowedSchemes.empty()) {\r
299             g_allowedSchemes.insert("https");\r
300             g_allowedSchemes.insert("http");\r
301         }\r
302 \r
303     }\r
304     catch (exception& e) {\r
305         cerr << "exception while initializing Shibboleth configuration:" << e.what() << endl;\r
306         exit(1);\r
307     }\r
308 \r
309     // Load "authoritative" URL fields.\r
310     char* var = getenv("SHIBSP_SERVER_NAME");\r
311     if (var)\r
312         g_ServerName = var;\r
313     var = getenv("SHIBSP_SERVER_SCHEME");\r
314     if (var)\r
315         g_ServerScheme = var;\r
316     var = getenv("SHIBSP_SERVER_PORT");\r
317     if (var)\r
318         g_ServerPort = atoi(var);\r
319 \r
320     streambuf* cin_streambuf  = cin.rdbuf();\r
321     streambuf* cout_streambuf = cout.rdbuf();\r
322     streambuf* cerr_streambuf = cerr.rdbuf();\r
323 \r
324     FCGX_Request request;\r
325 \r
326     FCGX_Init();\r
327     FCGX_InitRequest(&request, 0, 0);\r
328     \r
329     cout << "Shibboleth initialization complete. Starting request loop." << endl;\r
330     while (FCGX_Accept_r(&request) == 0) {\r
331         // Note that the default bufsize (0) will cause the use of iostream\r
332         // methods that require positioning (such as peek(), seek(),\r
333         // unget() and putback()) to fail (in favour of more efficient IO).\r
334         fcgi_streambuf cin_fcgi_streambuf(request.in);\r
335         fcgi_streambuf cout_fcgi_streambuf(request.out);\r
336         fcgi_streambuf cerr_fcgi_streambuf(request.err);\r
337 \r
338         cin.rdbuf(&cin_fcgi_streambuf);\r
339         cout.rdbuf(&cout_fcgi_streambuf);\r
340         cerr.rdbuf(&cerr_fcgi_streambuf);\r
341 \r
342         // Although FastCGI supports writing before reading,\r
343         // many http clients (browsers) don't support it (so\r
344         // the connection deadlocks until a timeout expires!).\r
345         char* content;\r
346         gstdin(&request, &content);\r
347 \r
348         try {\r
349             saml::NDC ndc("FastCGI shibresponder");\r
350             ShibTargetFCGI stf(&request, content, g_ServerScheme.c_str(), g_ServerName.c_str(), g_ServerPort);\r
351           \r
352             pair<bool,void*> res = stf.doHandler();\r
353             if (res.first) {\r
354 #ifdef _DEBUG\r
355                 cerr << "shib: doHandler handled the request" << endl;\r
356 #endif\r
357                 switch((long)res.second) {\r
358                     case SHIB_RETURN_OK:\r
359                         print_ok();\r
360                         break;\r
361               \r
362                     case SHIB_RETURN_KO:\r
363                         cerr << "shib: doHandler failed to handle the request" << endl;\r
364                         print_error("<html><body>FastCGI Shibboleth responder should only be used for Shibboleth protocol requests.</body></html>");\r
365                         break;\r
366 \r
367                     case SHIB_RETURN_DONE:\r
368                         // response already handled\r
369                         break;\r
370               \r
371                     default:\r
372                         cerr << "shib: doHandler returned an unexpected result: " << (long)res.second << endl;\r
373                         print_error("<html><body>FastCGI Shibboleth responder returned an unexpected result.</body></html>");\r
374                         break;\r
375                 }\r
376             }\r
377             else {\r
378                 cerr << "shib: doHandler failed to handle request." << endl;\r
379                 print_error("<html><body>FastCGI Shibboleth responder failed to process request.</body></html>");\r
380             }          \r
381           \r
382         }\r
383         catch (exception& e) {\r
384             cerr << "shib: FastCGI responder caught an exception: " << e.what() << endl;\r
385             print_error("<html><body>FastCGI Shibboleth responder caught an exception, check log for details.</body></html>");\r
386         }\r
387 \r
388         delete[] content;\r
389 \r
390         // If the output streambufs had non-zero bufsizes and\r
391         // were constructed outside of the accept loop (i.e.\r
392         // their destructor won't be called here), they would\r
393         // have to be flushed here.\r
394     }\r
395 \r
396     cout << "Request loop ended." << endl;\r
397 \r
398     cin.rdbuf(cin_streambuf);\r
399     cout.rdbuf(cout_streambuf);\r
400     cerr.rdbuf(cerr_streambuf);\r
401 \r
402     if (g_Config)\r
403         g_Config->shutdown();\r
404  \r
405     return 0;\r
406 }\r