void HTTPDigestCredentials::updateAuthParams(const HTTPRequest& request) { MD5Engine engine; const std::string& qop = _requestAuthParams.get(QOP_PARAM, DEFAULT_QOP); const std::string& realm = _requestAuthParams.getRealm(); const std::string& nonce = _requestAuthParams.get(NONCE_PARAM); _requestAuthParams.set(URI_PARAM, request.getURI()); if (qop.empty()) { const std::string ha1 = digest(engine, _username, realm, _password); const std::string ha2 = digest(engine, request.getMethod(), request.getURI()); _requestAuthParams.set(RESPONSE_PARAM, digest(engine, ha1, nonce, ha2)); } else if (icompare(qop, AUTH_PARAM) == 0) { const std::string& cnonce = _requestAuthParams.get(CNONCE_PARAM); const std::string ha1 = digest(engine, _username, realm, _password); const std::string ha2 = digest(engine, request.getMethod(), request.getURI()); const std::string nc = formatNonceCounter(updateNonceCounter(nonce)); _requestAuthParams.set(NC_PARAM, nc); _requestAuthParams.set(RESPONSE_PARAM, digest(engine, ha1, nonce, nc, cnonce, qop, ha2)); } }
void HTMLForm::load(const HTTPRequest& request, std::istream& requestBody, PartHandler& handler) { if (request.getMethod() == HTTPRequest::HTTP_POST) { std::string mediaType; NameValueCollection params; MessageHeader::splitParameters(request.getContentType(), mediaType, params); _encoding = mediaType; if (_encoding == ENCODING_MULTIPART) { _boundary = params["boundary"]; readMultipart(requestBody, handler); } else { readUrl(requestBody); } } else { URI uri(request.getURI()); std::istringstream istr(uri.getRawQuery()); readUrl(istr); } }
void HTTPRequestTest::testRead1() { std::string s("GET / HTTP/1.0\r\n\r\n"); std::istringstream istr(s); HTTPRequest request; request.read(istr); assert (request.getMethod() == HTTPRequest::HTTP_GET); assert (request.getURI() == "/"); assert (request.getVersion() == HTTPMessage::HTTP_1_0); assert (request.empty()); assert (istr.get() == -1); }
TEST(HTTPRequest, testRead1) { std::string s("GET / HTTP/1.0\r\n\r\n"); std::istringstream istr(s); HTTPRequest request; request.read(istr); EXPECT_TRUE (request.getMethod() == HTTPRequest::HTTP_GET); EXPECT_TRUE (request.getURI() == "/"); EXPECT_TRUE (request.getVersion() == HTTPMessage::HTTP_1_0); EXPECT_TRUE (request.empty()); EXPECT_TRUE (istr.get() == -1); }
bool HTTPDigestCredentials::verifyAuthParams(const HTTPRequest& request, const HTTPAuthenticationParams& params) const { const std::string& nonce = params.get(NONCE_PARAM); const std::string& realm = params.getRealm(); const std::string& qop = params.get(QOP_PARAM, DEFAULT_QOP); std::string response; MD5Engine engine; if (qop.empty()) { const std::string ha1 = digest(engine, _username, realm, _password); const std::string ha2 = digest(engine, request.getMethod(), request.getURI()); response = digest(engine, ha1, nonce, ha2); } else if (icompare(qop, AUTH_PARAM) == 0) { const std::string& cnonce = params.get(CNONCE_PARAM); const std::string& nc = params.get(NC_PARAM); const std::string ha1 = digest(engine, _username, realm, _password); const std::string ha2 = digest(engine, request.getMethod(), request.getURI()); response = digest(engine, ha1, nonce, nc, cnonce, qop, ha2); } return response == params.get(RESPONSE_PARAM); }
void HTTPRequestTest::testRead2() { std::string s("HEAD /index.html HTTP/1.1\r\nConnection: Keep-Alive\r\nHost: localhost\r\nUser-Agent: Poco\r\n\r\n"); std::istringstream istr(s); HTTPRequest request; request.read(istr); assert (request.getMethod() == HTTPRequest::HTTP_HEAD); assert (request.getURI() == "/index.html"); assert (request.getVersion() == HTTPMessage::HTTP_1_1); assert (request.size() == 3); assert (request["Connection"] == "Keep-Alive"); assert (request["Host"] == "localhost"); assert (request["User-Agent"] == "Poco"); assert (istr.get() == -1); }
void HTTPRequestTest::testRead4() { std::string s("POST /test.cgi HTTP/1.1\r\nConnection: Close\r\nContent-Length: 100 \r\nContent-Type: text/plain\r\nHost: localhost:8000\r\nUser-Agent: Poco\r\n\r\n"); std::istringstream istr(s); HTTPRequest request; request.read(istr); assert (request.getMethod() == HTTPRequest::HTTP_POST); assert (request.getURI() == "/test.cgi"); assert (request.getVersion() == HTTPMessage::HTTP_1_1); assert (request.size() == 5); assert (request["Connection"] == "Close"); assert (request["Host"] == "localhost:8000"); assert (request["User-Agent"] == "Poco"); assert (request.getContentType() == "text/plain"); assert (request.getContentLength() == 100); assert (istr.get() == -1); }
std::string FileServerRequestHandler::getRequestPathname(const HTTPRequest& request) { Poco::URI requestUri(request.getURI()); // avoid .'s and ..'s requestUri.normalize(); std::string path(requestUri.getPath()); Poco::RegularExpression gitHashRe("/([0-9a-f]+)/"); std::string gitHash; if (gitHashRe.extract(path, gitHash)) { // Convert version back to a real file name. Poco::replaceInPlace(path, std::string("/loleaflet" + gitHash), std::string("/loleaflet/dist/")); } return path; }
void HTMLForm::prepareSubmit(HTTPRequest& request) { if (request.getMethod() == HTTPRequest::HTTP_POST) { if (_encoding == ENCODING_URL) { request.setContentType(_encoding); request.setChunkedTransferEncoding(false); Poco::CountingOutputStream ostr; writeUrl(ostr); request.setContentLength(ostr.chars()); } else { _boundary = MultipartWriter::createBoundary(); std::string ct(_encoding); ct.append("; boundary=\""); ct.append(_boundary); ct.append("\""); request.setContentType(ct); } if (request.getVersion() == HTTPMessage::HTTP_1_0) { request.setKeepAlive(false); request.setChunkedTransferEncoding(false); } else if (_encoding != ENCODING_URL) { request.setChunkedTransferEncoding(true); } } else { std::string uri = request.getURI(); std::ostringstream ostr; writeUrl(ostr); uri.append("?"); uri.append(ostr.str()); request.setURI(uri); } }
void FileServerRequestHandler::preprocessFile(const HTTPRequest& request, Poco::MemoryInputStream& message, const std::shared_ptr<StreamSocket>& socket) { const auto host = ((LOOLWSD::isSSLEnabled() || LOOLWSD::isSSLTermination()) ? "wss://" : "ws://") + (LOOLWSD::ServerName.empty() ? request.getHost() : LOOLWSD::ServerName); const Poco::URI::QueryParameters params = Poco::URI(request.getURI()).getQueryParameters(); // Is this a file we read at startup - if not; its not for serving. const std::string relPath = getRequestPathname(request); LOG_DBG("Preprocessing file: " << relPath); std::string preprocess = *getUncompressedFile(relPath); HTMLForm form(request, message); const std::string accessToken = form.get("access_token", ""); const std::string accessTokenTtl = form.get("access_token_ttl", ""); LOG_TRC("access_token=" << accessToken << ", access_token_ttl=" << accessTokenTtl); const std::string accessHeader = form.get("access_header", ""); LOG_TRC("access_header=" << accessHeader); // Escape bad characters in access token. // This is placed directly in javascript in loleaflet.html, we need to make sure // that no one can do anything nasty with their clever inputs. std::string escapedAccessToken, escapedAccessHeader; Poco::URI::encode(accessToken, "'", escapedAccessToken); Poco::URI::encode(accessHeader, "'", escapedAccessHeader); unsigned long tokenTtl = 0; if (!accessToken.empty()) { if (!accessTokenTtl.empty()) { try { tokenTtl = std::stoul(accessTokenTtl); } catch (const std::exception& exc) { LOG_ERR("access_token_ttl must be represented as the number of milliseconds since January 1, 1970 UTC, when the token will expire"); } } else { LOG_INF("WOPI host did not pass optional access_token_ttl"); } } Poco::replaceInPlace(preprocess, std::string("%ACCESS_TOKEN%"), escapedAccessToken); Poco::replaceInPlace(preprocess, std::string("%ACCESS_TOKEN_TTL%"), std::to_string(tokenTtl)); Poco::replaceInPlace(preprocess, std::string("%ACCESS_HEADER%"), escapedAccessHeader); Poco::replaceInPlace(preprocess, std::string("%HOST%"), host); Poco::replaceInPlace(preprocess, std::string("%VERSION%"), std::string(LOOLWSD_VERSION_HASH)); Poco::replaceInPlace(preprocess, std::string("%SERVICE_ROOT%"), LOOLWSD::ServiceRoot); static const std::string linkCSS("<link rel=\"stylesheet\" href=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.css\">"); static const std::string scriptJS("<script src=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.js\"></script>"); std::string brandCSS(Poco::format(linkCSS, LOOLWSD::ServiceRoot, std::string(BRANDING))); std::string brandJS(Poco::format(scriptJS, LOOLWSD::ServiceRoot, std::string(BRANDING))); const auto& config = Application::instance().config(); #if ENABLE_SUPPORT_KEY const std::string keyString = config.getString("support_key", ""); SupportKey key(keyString); if (!key.verify() || key.validDaysRemaining() <= 0) { brandCSS = Poco::format(linkCSS, LOOLWSD::ServiceRoot, std::string(BRANDING_UNSUPPORTED)); brandJS = Poco::format(scriptJS, LOOLWSD::ServiceRoot, std::string(BRANDING_UNSUPPORTED)); } #endif Poco::replaceInPlace(preprocess, std::string("<!--%BRANDING_CSS%-->"), brandCSS); Poco::replaceInPlace(preprocess, std::string("<!--%BRANDING_JS%-->"), brandJS); // Customization related to document signing. std::string documentSigningDiv; const std::string documentSigningURL = config.getString("per_document.document_signing_url", ""); if (!documentSigningURL.empty()) { documentSigningDiv = "<div id=\"document-signing-bar\"></div>"; } Poco::replaceInPlace(preprocess, std::string("<!--%DOCUMENT_SIGNING_DIV%-->"), documentSigningDiv); Poco::replaceInPlace(preprocess, std::string("%DOCUMENT_SIGNING_URL%"), documentSigningURL); const auto loleafletLogging = config.getString("loleaflet_logging", "false"); Poco::replaceInPlace(preprocess, std::string("%LOLEAFLET_LOGGING%"), loleafletLogging); const std::string outOfFocusTimeoutSecs= config.getString("per_view.out_of_focus_timeout_secs", "60"); Poco::replaceInPlace(preprocess, std::string("%OUT_OF_FOCUS_TIMEOUT_SECS%"), outOfFocusTimeoutSecs); const std::string idleTimeoutSecs= config.getString("per_view.idle_timeout_secs", "900"); Poco::replaceInPlace(preprocess, std::string("%IDLE_TIMEOUT_SECS%"), idleTimeoutSecs); const std::string mimeType = "text/html"; std::ostringstream oss; oss << "HTTP/1.1 200 OK\r\n" << "Date: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n" << "Last-Modified: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n" << "User-Agent: " << WOPI_AGENT_STRING << "\r\n" << "Cache-Control:max-age=11059200\r\n" << "ETag: \"" LOOLWSD_VERSION_HASH "\"\r\n" << "Content-Length: " << preprocess.size() << "\r\n" << "Content-Type: " << mimeType << "\r\n" << "X-Content-Type-Options: nosniff\r\n" << "X-XSS-Protection: 1; mode=block\r\n" << "Referrer-Policy: no-referrer\r\n"; // Document signing: if endpoint URL is configured, whitelist that for // iframe purposes. std::ostringstream cspOss; cspOss << "Content-Security-Policy: default-src 'none'; " << "frame-src 'self' blob: " << documentSigningURL << "; " << "connect-src 'self' " << host << "; " << "script-src 'unsafe-inline' 'self'; " << "style-src 'self' 'unsafe-inline'; " << "font-src 'self' data:; " << "object-src blob:; "; // Frame ancestors: Allow loolwsd host, wopi host and anything configured. std::string configFrameAncestor = config.getString("net.frame_ancestors", ""); std::string frameAncestors = configFrameAncestor; Poco::URI uriHost(host); if (uriHost.getHost() != configFrameAncestor) frameAncestors += " " + uriHost.getHost() + ":*"; for (const auto& param : params) { if (param.first == "WOPISrc") { std::string wopiFrameAncestor; Poco::URI::decode(param.second, wopiFrameAncestor); Poco::URI uriWopiFrameAncestor(wopiFrameAncestor); // Remove parameters from URL wopiFrameAncestor = uriWopiFrameAncestor.getHost(); if (wopiFrameAncestor != uriHost.getHost() && wopiFrameAncestor != configFrameAncestor) { frameAncestors += " " + wopiFrameAncestor + ":*"; LOG_TRC("Picking frame ancestor from WOPISrc: " << wopiFrameAncestor); } break; } } if (!frameAncestors.empty()) { LOG_TRC("Allowed frame ancestors: " << frameAncestors); // X-Frame-Options supports only one ancestor, ignore that //(it's deprecated anyway and CSP works in all major browsers) cspOss << "img-src 'self' data: " << frameAncestors << "; " << "frame-ancestors " << frameAncestors; } else { LOG_TRC("Denied all frame ancestors"); cspOss << "img-src 'self' data: none;"; } cspOss << "\r\n"; // Append CSP to response headers too oss << cspOss.str(); // Setup HTTP Public key pinning if ((LOOLWSD::isSSLEnabled() || LOOLWSD::isSSLTermination()) && config.getBool("ssl.hpkp[@enable]", false)) { size_t i = 0; std::string pinPath = "ssl.hpkp.pins.pin[" + std::to_string(i) + "]"; std::ostringstream hpkpOss; bool keysPinned = false; while (config.has(pinPath)) { const std::string pin = config.getString(pinPath, ""); if (!pin.empty()) { hpkpOss << "pin-sha256=\"" << pin << "\"; "; keysPinned = true; } pinPath = "ssl.hpkp.pins.pin[" + std::to_string(++i) + "]"; } if (keysPinned && config.getBool("ssl.hpkp.max_age[@enable]", false)) { int maxAge = 1000; // seconds try { maxAge = config.getInt("ssl.hpkp.max_age", maxAge); } catch (Poco::SyntaxException& exc) { LOG_WRN("Invalid value of HPKP's max-age directive found in config file. Defaulting to " << maxAge); } hpkpOss << "max-age=" << maxAge << "; "; } if (keysPinned && config.getBool("ssl.hpkp.report_uri[@enable]", false)) { const std::string reportUri = config.getString("ssl.hpkp.report_uri", ""); if (!reportUri.empty()) { hpkpOss << "report-uri=" << reportUri << "; "; } } if (!hpkpOss.str().empty()) { if (config.getBool("ssl.hpkp[@report_only]", false)) { // Only send validation failure reports to reportUri while still allowing UAs to // connect to the server oss << "Public-Key-Pins-Report-Only: " << hpkpOss.str() << "\r\n"; } else { oss << "Public-Key-Pins: " << hpkpOss.str() << "\r\n"; } } } oss << "\r\n" << preprocess; socket->send(oss.str()); LOG_DBG("Sent file: " << relPath << ": " << preprocess); }
void FileServerRequestHandler::handleRequest(const HTTPRequest& request, Poco::MemoryInputStream& message, const std::shared_ptr<StreamSocket>& socket) { try { bool noCache = false; #if ENABLE_DEBUG noCache = true; #endif Poco::Net::HTTPResponse response; Poco::URI requestUri(request.getURI()); LOG_TRC("Fileserver request: " << requestUri.toString()); requestUri.normalize(); // avoid .'s and ..'s std::string path(requestUri.getPath()); if (path.find("loleaflet/" LOOLWSD_VERSION_HASH "/") == std::string::npos) { LOG_WRN("client - server version mismatch, disabling browser cache."); noCache = true; } std::vector<std::string> requestSegments; requestUri.getPathSegments(requestSegments); const std::string relPath = getRequestPathname(request); // Is this a file we read at startup - if not; its not for serving. if (requestSegments.size() < 1 || FileHash.find(relPath) == FileHash.end()) throw Poco::FileNotFoundException("Invalid URI request: [" + requestUri.toString() + "]."); const auto& config = Application::instance().config(); const std::string loleafletHtml = config.getString("loleaflet_html", "loleaflet.html"); const std::string endPoint = requestSegments[requestSegments.size() - 1]; if (endPoint == loleafletHtml) { preprocessFile(request, message, socket); return; } if (request.getMethod() == HTTPRequest::HTTP_GET) { if (endPoint == "admin.html" || endPoint == "adminSettings.html" || endPoint == "adminHistory.html" || endPoint == "adminAnalytics.html") { preprocessAdminFile(request, socket); return; } if (endPoint == "admin-bundle.js" || endPoint == "admin-localizations.js") { noCache = true; if (!LOOLWSD::AdminEnabled) throw Poco::FileAccessDeniedException("Admin console disabled"); if (!FileServerRequestHandler::isAdminLoggedIn(request, response)) throw Poco::Net::NotAuthenticatedException("Invalid admin login"); // Ask UAs to block if they detect any XSS attempt response.add("X-XSS-Protection", "1; mode=block"); // No referrer-policy response.add("Referrer-Policy", "no-referrer"); } // Do we have an extension. const std::size_t extPoint = endPoint.find_last_of('.'); if (extPoint == std::string::npos) throw Poco::FileNotFoundException("Invalid file."); const std::string fileType = endPoint.substr(extPoint + 1); std::string mimeType; if (fileType == "js") mimeType = "application/javascript"; else if (fileType == "css") mimeType = "text/css"; else if (fileType == "html") mimeType = "text/html"; else if (fileType == "png") mimeType = "image/png"; else if (fileType == "svg") mimeType = "image/svg+xml"; else mimeType = "text/plain"; auto it = request.find("If-None-Match"); if (it != request.end()) { // if ETags match avoid re-sending the file. if (!noCache && it->second == "\"" LOOLWSD_VERSION_HASH "\"") { // TESTME: harder ... - do we even want ETag support ? std::ostringstream oss; Poco::DateTime now; Poco::DateTime later(now.utcTime(), int64_t(1000)*1000 * 60 * 60 * 24 * 128); oss << "HTTP/1.1 304 Not Modified\r\n" << "Date: " << Poco::DateTimeFormatter::format( now, Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n" << "Expires: " << Poco::DateTimeFormatter::format( later, Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n" << "User-Agent: " << WOPI_AGENT_STRING << "\r\n" << "Cache-Control: max-age=11059200\r\n" << "\r\n"; socket->send(oss.str()); socket->shutdown(); return; } } response.set("User-Agent", HTTP_AGENT_STRING); response.set("Date", Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT)); bool gzip = request.hasToken("Accept-Encoding", "gzip"); const std::string *content; #if ENABLE_DEBUG if (std::getenv("LOOL_SERVE_FROM_FS")) { // Useful to not serve from memory sometimes especially during loleaflet development // Avoids having to restart loolwsd everytime you make a change in loleaflet const std::string filePath = Poco::Path(LOOLWSD::FileServerRoot, relPath).absolute().toString(); HttpHelper::sendFile(socket, filePath, mimeType, response, noCache); return; } #endif if (gzip) { response.set("Content-Encoding", "gzip"); content = getCompressedFile(relPath); } else content = getUncompressedFile(relPath); if (!noCache) { // 60 * 60 * 24 * 128 (days) = 11059200 response.set("Cache-Control", "max-age=11059200"); response.set("ETag", "\"" LOOLWSD_VERSION_HASH "\""); } response.setContentType(mimeType); response.add("X-Content-Type-Options", "nosniff"); std::ostringstream oss; response.write(oss); const std::string header = oss.str(); LOG_TRC("#" << socket->getFD() << ": Sending " << (!gzip ? "un":"") << "compressed : file [" << relPath << "]: " << header); socket->send(header); socket->send(*content); } } catch (const Poco::Net::NotAuthenticatedException& exc) { LOG_ERR("FileServerRequestHandler::NotAuthenticated: " << exc.displayText()); sendError(401, request, socket, "", "", "WWW-authenticate: Basic realm=\"online\"\r\n"); } catch (const Poco::FileAccessDeniedException& exc) { LOG_ERR("FileServerRequestHandler: " << exc.displayText()); sendError(403, request, socket, "403 - Access denied!", "You are unable to access"); } catch (const Poco::FileNotFoundException& exc) { LOG_WRN("FileServerRequestHandler: " << exc.displayText()); sendError(404, request, socket, "404 - file not found!", "There seems to be a problem locating"); } }