https://issues.shibboleth.net/jira/browse/SSPCPP-255
[shibboleth/cpp-sp.git] / fastcgi / shibauthorizer.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 /* shibauthorizer.cpp - Shibboleth FastCGI Authorizer\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 ShibTargetFCGIAuth : public ShibTarget\r
47 {\r
48     FCGX_Request* m_req;\r
49     string m_cookie;\r
50 \r
51     void checkString(const string& s, const char* msg) {\r
52         string::const_iterator e = s.end();\r
53         for (string::const_iterator i=s.begin(); i!=e; ++i) {\r
54             if (iscntrl(*i))\r
55                 throw runtime_error(msg);\r
56         }\r
57     }\r
58 \r
59 public:\r
60     map<string,string> m_headers;\r
61 \r
62     ShibTargetFCGIAuth(FCGX_Request* req, const char* scheme=NULL, const char* hostname=NULL, int port=0) : m_req(req) {\r
63         const char* server_name_str = hostname;\r
64         if (!server_name_str || !*server_name_str)\r
65             server_name_str = FCGX_GetParam("SERVER_NAME", req->envp);\r
66 \r
67         int server_port = port;\r
68         if (!port) {\r
69             char* server_port_str = FCGX_GetParam("SERVER_PORT", req->envp);\r
70             server_port = strtol(server_port_str, &server_port_str, 10);\r
71             if (*server_port_str) {\r
72                 cerr << "can't parse SERVER_PORT (" << FCGX_GetParam("SERVER_PORT", req->envp) << ")" << endl;\r
73                 throw runtime_error("Unable to determine server port.");\r
74             }\r
75         }\r
76 \r
77         const char* server_scheme_str = scheme;\r
78         if (!server_scheme_str || !*server_scheme_str)\r
79             server_scheme_str = (server_port == 443 || server_port == 8443) ? "https" : "http";\r
80 \r
81         const char* request_uri_str = FCGX_GetParam("REQUEST_URI", req->envp);\r
82         const char* content_type_str = FCGX_GetParam("CONTENT_TYPE", req->envp);\r
83         const char* remote_addr_str = FCGX_GetParam("REMOTE_ADDR", req->envp);\r
84         const char* request_method_str = FCGX_GetParam("REQUEST_METHOD", req->envp);\r
85 \r
86         init(server_scheme_str,\r
87              server_name_str,\r
88              server_port,\r
89              request_uri_str,\r
90              content_type_str ? content_type_str : "",\r
91              remote_addr_str,\r
92              request_method_str\r
93              );\r
94     }\r
95 \r
96     ~ShibTargetFCGIAuth() { }\r
97 \r
98     virtual void log(ShibLogLevel level, const string& msg) {\r
99         ShibTarget::log(level,msg);\r
100         if (level == LogLevelError)\r
101             cerr << "shib: " << msg;\r
102     }\r
103   \r
104     virtual string getCookies(void) const {\r
105         char* cookie = FCGX_GetParam("HTTP_COOKIE", m_req->envp);\r
106         return cookie ? cookie : "";\r
107     }\r
108   \r
109     virtual void setCookie(const string &name, const string &value) {\r
110         m_cookie += "Set-Cookie: " + name + "=" + value + "\r\n";\r
111     }\r
112 \r
113       virtual string getArgs(void) {\r
114         char* args = FCGX_GetParam("QUERY_STRING", m_req->envp);\r
115         return args ? args : "";\r
116     }\r
117 \r
118     virtual string getPostData(void) {\r
119         throw runtime_error("getPostData not implemented by FastCGI authorizer.");\r
120     }\r
121 \r
122     virtual void clearHeader(const string& name) {\r
123         // no need, since request headers turn into actual environment variables\r
124     }\r
125   \r
126     virtual void setHeader(const string& name, const string &value) {\r
127         m_headers[name] = value;\r
128     }\r
129 \r
130     virtual string getHeader(const string& name) {\r
131         if (m_headers.find(name) != m_headers.end())\r
132             return m_headers[name];\r
133         else\r
134             return "";\r
135     }\r
136 \r
137     virtual void setRemoteUser(const string& user) {\r
138         m_headers["REMOTE_USER"] = user;\r
139     }\r
140 \r
141     virtual string getRemoteUser(void) {\r
142         if (m_headers.find("REMOTE_USER") != m_headers.end())\r
143             return m_headers["REMOTE_USER"];\r
144         else {\r
145             char* remote_user = FCGX_GetParam("REMOTE_USER", m_req->envp);\r
146             if (remote_user)\r
147                 return remote_user;\r
148         }\r
149         return "";\r
150     }\r
151 \r
152     virtual void* sendPage(\r
153         const string& msg,\r
154         int code=200,\r
155         const string& content_type="text/html",\r
156         const saml::Iterator<header_t>& headers=EMPTY(header_t)) {\r
157 \r
158         checkString(content_type, "Detected control character in a response header.");\r
159         string hdr = m_cookie + "Connection: close\r\nContent-type: " + content_type + "\r\n";\r
160         while (headers.hasNext()) {\r
161             const header_t& h=headers.next();\r
162             checkString(h.first, "Detected control character in a response header.");\r
163             checkString(h.second, "Detected control character in a response header.");\r
164             hdr += h.first + ": " + h.second + "\r\n";\r
165         }\r
166 \r
167         // We can't return 200 OK here or else the filter is bypassed\r
168         // so custom Shib errors will get turned into a generic page.\r
169         const char* codestr="Status: 500 Server Error";\r
170         switch (code) {\r
171             case 403:   codestr="Status: 403 Forbidden"; break;\r
172             case 404:   codestr="Status: 404 Not Found"; break;\r
173         }\r
174 \r
175         cout << codestr << "\r\n" << hdr << "\r\n" << msg;\r
176         return (void*)SHIB_RETURN_DONE;\r
177     }\r
178 \r
179     virtual void* sendRedirect(const string& url) {\r
180         checkString(url, "Detected control character in an attempted redirect.");\r
181         if (g_allowedSchemes.find(url.substr(0, url.find(':'))) == g_allowedSchemes.end())\r
182             throw runtime_error("Invalid scheme in attempted redirect.");\r
183         cout << "Status: 302 Please Wait" << "\r\n"\r
184              << "Location: " << url << "\r\n"\r
185              <<  m_cookie << "\r\n"\r
186              << "<HTML><BODY>Redirecting...</BODY></HTML>";\r
187         return (void*)SHIB_RETURN_DONE;\r
188     }\r
189 \r
190     virtual void* returnDecline(void) { \r
191         return (void*)SHIB_RETURN_KO;\r
192     }\r
193 \r
194     virtual void* returnOK(void) {\r
195         return (void*)SHIB_RETURN_OK;\r
196     }\r
197 };\r
198 \r
199 static void print_ok(const map<string,string>& headers)\r
200 {\r
201     cout << "Status: 200 OK" << "\r\n";\r
202     for (map<string,string>::const_iterator iter = headers.begin(); iter != headers.end(); iter++) {\r
203         cout << "Variable-" << iter->first << ": " << iter->second << "\r\n";\r
204     }\r
205     cout << "\r\n";\r
206 }\r
207 \r
208 static void print_error(const char* msg)\r
209 {\r
210     cout << "Status: 500 Server Error" << "\r\n\r\n" << msg;\r
211 }\r
212 \r
213 int main(void)\r
214 {\r
215     char* shib_config = getenv("SHIB_CONFIG");\r
216     char* shib_schema = getenv("SHIB_SCHEMA");\r
217     if ((shib_config == NULL) || (shib_schema == NULL)) {\r
218       cerr << "SHIB_CONFIG or SHIB_SCHEMA not initialized!" << endl;\r
219       exit(1);\r
220     }\r
221     cerr << "SHIB_CONFIG = " << shib_config << endl\r
222          << "SHIB_SCHEMA = " << shib_schema << endl;\r
223 \r
224     string g_ServerScheme;\r
225     string g_ServerName;\r
226     int g_ServerPort = 0;\r
227     ShibTargetConfig* g_Config;\r
228 \r
229     try {\r
230         g_Config = &ShibTargetConfig::getConfig();\r
231         g_Config->setFeatures(\r
232             ShibTargetConfig::Listener |\r
233             ShibTargetConfig::Metadata |\r
234             ShibTargetConfig::AAP |\r
235             ShibTargetConfig::RequestMapper |\r
236             ShibTargetConfig::LocalExtensions |\r
237             ShibTargetConfig::Logging\r
238             );\r
239         if (!g_Config->init(shib_schema)) {\r
240             cerr << "failed to initialize Shibboleth libraries" << endl;\r
241             exit(1);\r
242         }\r
243         \r
244         if (!g_Config->load(shib_config)) {\r
245             cerr << "failed to load Shibboleth configuration" << endl;\r
246             exit(1);\r
247         }\r
248 \r
249         IConfig* conf=g_Config->getINI();\r
250         Locker locker(conf);\r
251         const IPropertySet* props=conf->getPropertySet("Local");\r
252         if (props) {\r
253             pair<bool,const char*> str=props->getString("allowedSchemes");\r
254             if (str.first) {\r
255                 string schemes=str.second;\r
256                 unsigned int j=0;\r
257                 for (unsigned int i=0;  i < schemes.length();  i++) {\r
258                     if (schemes.at(i)==' ') {\r
259                         g_allowedSchemes.insert(schemes.substr(j, i-j));\r
260                         j = i+1;\r
261                     }\r
262                 }\r
263                 g_allowedSchemes.insert(schemes.substr(j, schemes.length()-j));\r
264             }\r
265         }\r
266         if (g_allowedSchemes.empty()) {\r
267             g_allowedSchemes.insert("https");\r
268             g_allowedSchemes.insert("http");\r
269         }\r
270     }\r
271     catch (exception& e) {\r
272         cerr << "exception while initializing Shibboleth configuration: " << e.what() << endl;\r
273         exit(1);\r
274     }\r
275 \r
276 \r
277 \r
278     // Load "authoritative" URL fields.\r
279     char* var = getenv("SHIBSP_SERVER_NAME");\r
280     if (var)\r
281         g_ServerName = var;\r
282     var = getenv("SHIBSP_SERVER_SCHEME");\r
283     if (var)\r
284         g_ServerScheme = var;\r
285     var = getenv("SHIBSP_SERVER_PORT");\r
286     if (var)\r
287         g_ServerPort = atoi(var);\r
288 \r
289     streambuf* cout_streambuf = cout.rdbuf();\r
290     streambuf* cerr_streambuf = cerr.rdbuf();\r
291 \r
292     FCGX_Request request;\r
293 \r
294     FCGX_Init();\r
295     FCGX_InitRequest(&request, 0, 0);\r
296     \r
297     cout << "Shibboleth initialization complete. Starting request loop." << endl;\r
298     while (FCGX_Accept_r(&request) == 0)\r
299     {\r
300         // Note that the default bufsize (0) will cause the use of iostream\r
301         // methods that require positioning (such as peek(), seek(),\r
302         // unget() and putback()) to fail (in favour of more efficient IO).\r
303         fcgi_streambuf cout_fcgi_streambuf(request.out);\r
304         fcgi_streambuf cerr_fcgi_streambuf(request.err);\r
305 \r
306         cout.rdbuf(&cout_fcgi_streambuf);\r
307         cerr.rdbuf(&cerr_fcgi_streambuf);\r
308 \r
309         try {\r
310             saml::NDC ndc("FastCGI shibauthorizer");\r
311             ShibTargetFCGIAuth sta(&request, g_ServerScheme.c_str(), g_ServerName.c_str(), g_ServerPort);\r
312           \r
313             pair<bool,void*> res = sta.doCheckAuthN();\r
314             if (res.first) {\r
315 #ifdef _DEBUG\r
316                 cerr << "shib: doCheckAuthN handled the request" << endl;\r
317 #endif\r
318                 switch((long)res.second) {\r
319                     case SHIB_RETURN_OK:\r
320                         print_ok(sta.m_headers);\r
321                         continue;\r
322               \r
323                     case SHIB_RETURN_KO:\r
324                         print_ok(sta.m_headers);\r
325                         continue;\r
326 \r
327                     case SHIB_RETURN_DONE:\r
328                         continue;\r
329               \r
330                     default:\r
331                         cerr << "shib: doCheckAuthN returned an unexpected result: " << (long)res.second << endl;\r
332                         print_error("<html><body>FastCGI Shibboleth authorizer returned an unexpected result.</body></html>");\r
333                         continue;\r
334                 }\r
335             }\r
336           \r
337             res = sta.doExportAssertions();\r
338             if (res.first) {\r
339 #ifdef _DEBUG\r
340                 cerr << "shib: doExportAssertions handled request" << endl;\r
341 #endif\r
342                 switch((long)res.second) {\r
343                     case SHIB_RETURN_OK:\r
344                         print_ok(sta.m_headers);\r
345                         continue;\r
346               \r
347                     case SHIB_RETURN_KO:\r
348                         print_ok(sta.m_headers);\r
349                         continue;\r
350 \r
351                     case SHIB_RETURN_DONE:\r
352                         continue;\r
353               \r
354                     default:\r
355                         cerr << "shib: doExportAssertions returned an unexpected result: " << (long)res.second << endl;\r
356                         print_error("<html><body>FastCGI Shibboleth authorizer returned an unexpected result.</body></html>");\r
357                         continue;\r
358                 }\r
359             }\r
360 \r
361             res = sta.doCheckAuthZ();\r
362             if (res.first) {\r
363 #ifdef _DEBUG\r
364                 cerr << "shib: doCheckAuthZ handled request" << endl;\r
365 #endif\r
366                 switch((long)res.second) {\r
367                     case SHIB_RETURN_OK:\r
368                         print_ok(sta.m_headers);\r
369                         continue;\r
370               \r
371                     case SHIB_RETURN_KO:\r
372                         print_ok(sta.m_headers);\r
373                         continue;\r
374 \r
375                     case SHIB_RETURN_DONE:\r
376                         continue;\r
377               \r
378                     default:\r
379                         cerr << "shib: doCheckAuthZ returned an unexpected result: " << (long)res.second << endl;\r
380                         print_error("<html><body>FastCGI Shibboleth authorizer returned an unexpected result.</body></html>");\r
381                         continue;\r
382                 }\r
383             }\r
384 \r
385             print_ok(sta.m_headers);\r
386           \r
387         }\r
388         catch (exception& e) {\r
389             cerr << "shib: FastCGI authorizer caught an exception: " << e.what() << endl;\r
390             print_error("<html><body>FastCGI Shibboleth authorizer caught an exception, check log for details.</body></html>");\r
391         }\r
392 \r
393         // If the output streambufs had non-zero bufsizes and\r
394         // were constructed outside of the accept loop (i.e.\r
395         // their destructor won't be called here), they would\r
396         // have to be flushed here.\r
397     }\r
398     cout << "Request loop ended." << endl;\r
399 \r
400     cout.rdbuf(cout_streambuf);\r
401     cerr.rdbuf(cerr_streambuf);\r
402 \r
403     if (g_Config)\r
404         g_Config->shutdown();\r
405  \r
406     return 0;\r
407 }\r