|
| 1 | +#include <ydb/library/actors/core/actor.h> |
| 2 | +#include <ydb/library/actors/http/http.h> |
| 3 | +#include <ydb/mvp/core/mvp_log.h> |
| 4 | +#include <ydb/core/util/wildcard.h> |
| 5 | +#include "openid_connect.h" |
| 6 | +#include "oidc_protected_page.h" |
| 7 | + |
| 8 | +namespace NMVP { |
| 9 | +namespace NOIDC { |
| 10 | + |
| 11 | +THandlerSessionServiceCheck::THandlerSessionServiceCheck(const NActors::TActorId& sender, |
| 12 | + const NHttp::THttpIncomingRequestPtr& request, |
| 13 | + const NActors::TActorId& httpProxyId, |
| 14 | + const TOpenIdConnectSettings& settings) |
| 15 | + : Sender(sender) |
| 16 | + , Request(request) |
| 17 | + , HttpProxyId(httpProxyId) |
| 18 | + , Settings(settings) |
| 19 | + , ProtectedPageUrl(Request->URL.SubStr(1)) |
| 20 | +{} |
| 21 | + |
| 22 | +void THandlerSessionServiceCheck::Bootstrap(const NActors::TActorContext& ctx) { |
| 23 | + if (!CheckRequestedHost()) { |
| 24 | + ctx.Send(Sender, new NHttp::TEvHttpProxy::TEvHttpOutgoingResponse(CreateResponseForbiddenHost())); |
| 25 | + Die(ctx); |
| 26 | + return; |
| 27 | + } |
| 28 | + NHttp::THeaders headers(Request->Headers); |
| 29 | + IsAjaxRequest = DetectAjaxRequest(headers); |
| 30 | + TStringBuf authHeader = headers.Get(AUTH_HEADER_NAME); |
| 31 | + if (Request->Method == "OPTIONS" || IsAuthorizedRequest(authHeader)) { |
| 32 | + ForwardUserRequest(TString(authHeader), ctx); |
| 33 | + } else { |
| 34 | + StartOidcProcess(ctx); |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +void THandlerSessionServiceCheck::HandleProxy(NHttp::TEvHttpProxy::TEvHttpIncomingResponse::TPtr event, const NActors::TActorContext& ctx) { |
| 39 | + NHttp::THttpOutgoingResponsePtr httpResponse; |
| 40 | + if (event->Get()->Response != nullptr) { |
| 41 | + NHttp::THttpIncomingResponsePtr response = event->Get()->Response; |
| 42 | + LOG_DEBUG_S(ctx, EService::MVP, "Incoming response for protected resource: " << response->Status); |
| 43 | + if (NeedSendSecureHttpRequest(response)) { |
| 44 | + SendSecureHttpRequest(response, ctx); |
| 45 | + return; |
| 46 | + } |
| 47 | + NHttp::THeadersBuilder headers = GetResponseHeaders(response); |
| 48 | + TStringBuf contentType = headers.Get("Content-Type").NextTok(';'); |
| 49 | + if (contentType == "text/html") { |
| 50 | + TString newBody = FixReferenceInHtml(response->Body, response->GetRequest()->Host); |
| 51 | + httpResponse = Request->CreateResponse( response->Status, response->Message, headers, newBody); |
| 52 | + } else { |
| 53 | + httpResponse = Request->CreateResponse( response->Status, response->Message, headers, response->Body); |
| 54 | + } |
| 55 | + } else { |
| 56 | + static constexpr size_t MAX_LOGGED_SIZE = 1024; |
| 57 | + LOG_DEBUG_S(ctx, EService::MVP, "Can not process request to protected resource:\n" << event->Get()->Request->GetRawData().substr(0, MAX_LOGGED_SIZE)); |
| 58 | + httpResponse = CreateResponseForNotExistingResponseFromProtectedResource(event->Get()->GetError()); |
| 59 | + } |
| 60 | + ctx.Send(Sender, new NHttp::TEvHttpProxy::TEvHttpOutgoingResponse(httpResponse)); |
| 61 | + Die(ctx); |
| 62 | +} |
| 63 | + |
| 64 | +bool THandlerSessionServiceCheck::CheckRequestedHost() { |
| 65 | + size_t pos = ProtectedPageUrl.find('/'); |
| 66 | + if (pos == TString::npos) { |
| 67 | + return false; |
| 68 | + } |
| 69 | + TStringBuf scheme, host, uri; |
| 70 | + if (!NHttp::CrackURL(ProtectedPageUrl, scheme, host, uri)) { |
| 71 | + return false; |
| 72 | + } |
| 73 | + if (!scheme.empty() && (scheme != "http" && scheme != "https")) { |
| 74 | + return false; |
| 75 | + } |
| 76 | + RequestedPageScheme = scheme; |
| 77 | + auto it = std::find_if(Settings.AllowedProxyHosts.cbegin(), Settings.AllowedProxyHosts.cend(), [&host] (const TString& wildcard) { |
| 78 | + return NKikimr::IsMatchesWildcard(host, wildcard); |
| 79 | + }); |
| 80 | + return it != Settings.AllowedProxyHosts.cend(); |
| 81 | +} |
| 82 | + |
| 83 | +bool THandlerSessionServiceCheck::IsAuthorizedRequest(TStringBuf authHeader) { |
| 84 | + if (authHeader.empty()) { |
| 85 | + return false; |
| 86 | + } |
| 87 | + return to_lower(ToString(authHeader)).StartsWith(IAM_TOKEN_SCHEME_LOWER); |
| 88 | +} |
| 89 | + |
| 90 | +void THandlerSessionServiceCheck::ForwardUserRequest(TStringBuf authHeader, const NActors::TActorContext& ctx, bool secure) { |
| 91 | + LOG_DEBUG_S(ctx, EService::MVP, "Forward user request bypass OIDC"); |
| 92 | + NHttp::THttpOutgoingRequestPtr httpRequest = NHttp::THttpOutgoingRequest::CreateRequest(Request->Method, ProtectedPageUrl); |
| 93 | + ForwardRequestHeaders(httpRequest); |
| 94 | + if (!authHeader.empty()) { |
| 95 | + httpRequest->Set(AUTH_HEADER_NAME, authHeader); |
| 96 | + } |
| 97 | + if (Request->HaveBody()) { |
| 98 | + httpRequest->SetBody(Request->Body); |
| 99 | + } |
| 100 | + if (RequestedPageScheme.empty()) { |
| 101 | + httpRequest->Secure = secure; |
| 102 | + } |
| 103 | + ctx.Send(HttpProxyId, new NHttp::TEvHttpProxy::TEvHttpOutgoingRequest(httpRequest)); |
| 104 | +} |
| 105 | + |
| 106 | +TString THandlerSessionServiceCheck::FixReferenceInHtml(TStringBuf html, TStringBuf host, TStringBuf findStr) { |
| 107 | + TStringBuilder result; |
| 108 | + size_t n = html.find(findStr); |
| 109 | + if (n == TStringBuf::npos) { |
| 110 | + return TString(html); |
| 111 | + } |
| 112 | + size_t len = findStr.length() + 1; |
| 113 | + size_t pos = 0; |
| 114 | + while (n != TStringBuf::npos) { |
| 115 | + result << html.SubStr(pos, n + len - pos); |
| 116 | + if (html[n + len] == '/') { |
| 117 | + result << "/" << host; |
| 118 | + if (html[n + len + 1] == '\'' || html[n + len + 1] == '\"') { |
| 119 | + result << "/internal"; |
| 120 | + n++; |
| 121 | + } |
| 122 | + } |
| 123 | + pos = n + len; |
| 124 | + n = html.find(findStr, pos); |
| 125 | + } |
| 126 | + result << html.SubStr(pos); |
| 127 | + return result; |
| 128 | +} |
| 129 | + |
| 130 | +TString THandlerSessionServiceCheck::FixReferenceInHtml(TStringBuf html, TStringBuf host) { |
| 131 | + TStringBuf findString = "href="; |
| 132 | + auto result = FixReferenceInHtml(html, host, findString); |
| 133 | + findString = "src="; |
| 134 | + return FixReferenceInHtml(result, host, findString); |
| 135 | +} |
| 136 | + |
| 137 | +void THandlerSessionServiceCheck::ForwardRequestHeaders(NHttp::THttpOutgoingRequestPtr& request) const { |
| 138 | + static const TVector<TStringBuf> HEADERS_WHITE_LIST = { |
| 139 | + "Connection", |
| 140 | + "Accept-Language", |
| 141 | + "Cache-Control", |
| 142 | + "Sec-Fetch-Dest", |
| 143 | + "Sec-Fetch-Mode", |
| 144 | + "Sec-Fetch-Site", |
| 145 | + "Sec-Fetch-User", |
| 146 | + "Upgrade-Insecure-Requests", |
| 147 | + "Content-Type", |
| 148 | + "Origin" |
| 149 | + }; |
| 150 | + NHttp::THeadersBuilder headers(Request->Headers); |
| 151 | + for (const auto& header : HEADERS_WHITE_LIST) { |
| 152 | + if (headers.Has(header)) { |
| 153 | + request->Set(header, headers.Get(header)); |
| 154 | + } |
| 155 | + } |
| 156 | + request->Set("Accept-Encoding", "deflate"); |
| 157 | +} |
| 158 | + |
| 159 | +NHttp::THeadersBuilder THandlerSessionServiceCheck::GetResponseHeaders(const NHttp::THttpIncomingResponsePtr& response) { |
| 160 | + static const TVector<TStringBuf> HEADERS_WHITE_LIST = { |
| 161 | + "Content-Type", |
| 162 | + "Connection", |
| 163 | + "X-Worker-Name", |
| 164 | + "Set-Cookie", |
| 165 | + "Access-Control-Allow-Origin", |
| 166 | + "Access-Control-Allow-Credentials", |
| 167 | + "Access-Control-Allow-Headers", |
| 168 | + "Access-Control-Allow-Methods" |
| 169 | + }; |
| 170 | + NHttp::THeadersBuilder headers(response->Headers); |
| 171 | + NHttp::THeadersBuilder resultHeaders; |
| 172 | + for (const auto& header : HEADERS_WHITE_LIST) { |
| 173 | + if (headers.Has(header)) { |
| 174 | + resultHeaders.Set(header, headers.Get(header)); |
| 175 | + } |
| 176 | + } |
| 177 | + static const TString LOCATION_HEADER_NAME = "Location"; |
| 178 | + if (headers.Has(LOCATION_HEADER_NAME)) { |
| 179 | + resultHeaders.Set(LOCATION_HEADER_NAME, GetFixedLocationHeader(headers.Get(LOCATION_HEADER_NAME))); |
| 180 | + } |
| 181 | + return resultHeaders; |
| 182 | +} |
| 183 | + |
| 184 | +void THandlerSessionServiceCheck::SendSecureHttpRequest(const NHttp::THttpIncomingResponsePtr& response, const NActors::TActorContext& ctx) { |
| 185 | + NHttp::THttpOutgoingRequestPtr request = response->GetRequest(); |
| 186 | + LOG_DEBUG_S(ctx, EService::MVP, "Try to send request to HTTPS port"); |
| 187 | + NHttp::THeadersBuilder headers {request->Headers}; |
| 188 | + ForwardUserRequest(headers.Get(AUTH_HEADER_NAME), ctx, true); |
| 189 | +} |
| 190 | + |
| 191 | +TString THandlerSessionServiceCheck::GetFixedLocationHeader(TStringBuf location) { |
| 192 | + TStringBuf scheme, host, uri; |
| 193 | + NHttp::CrackURL(ProtectedPageUrl, scheme, host, uri); |
| 194 | + if (location.StartsWith("//")) { |
| 195 | + return TStringBuilder() << '/' << (scheme.empty() ? "" : TString(scheme) + "://") << location.SubStr(2); |
| 196 | + } else if (location.StartsWith('/')) { |
| 197 | + return TStringBuilder() << '/' |
| 198 | + << (scheme.empty() ? "" : TString(scheme) + "://") |
| 199 | + << host << location; |
| 200 | + } else { |
| 201 | + TStringBuf locScheme, locHost, locUri; |
| 202 | + NHttp::CrackURL(location, locScheme, locHost, locUri); |
| 203 | + if (!locScheme.empty()) { |
| 204 | + return TStringBuilder() << '/' << location; |
| 205 | + } |
| 206 | + } |
| 207 | + return TString(location); |
| 208 | +} |
| 209 | + |
| 210 | +NHttp::THttpOutgoingResponsePtr THandlerSessionServiceCheck::CreateResponseForbiddenHost() { |
| 211 | + NHttp::THeadersBuilder headers; |
| 212 | + headers.Set("Content-Type", "text/html"); |
| 213 | + SetCORS(Request, &headers); |
| 214 | + |
| 215 | + TStringBuf scheme, host, uri; |
| 216 | + NHttp::CrackURL(ProtectedPageUrl, scheme, host, uri); |
| 217 | + TStringBuilder html; |
| 218 | + html << "<html><head><title>403 Forbidden</title></head><body bgcolor=\"white\"><center><h1>"; |
| 219 | + html << "403 Forbidden host: " << host; |
| 220 | + html << "</h1></center></body></html>"; |
| 221 | + |
| 222 | + return Request->CreateResponse("403", "Forbidden", headers, html); |
| 223 | +} |
| 224 | + |
| 225 | +NHttp::THttpOutgoingResponsePtr THandlerSessionServiceCheck::CreateResponseForNotExistingResponseFromProtectedResource(const TString& errorMessage) { |
| 226 | + NHttp::THeadersBuilder headers; |
| 227 | + headers.Set("Content-Type", "text/html"); |
| 228 | + SetCORS(Request, &headers); |
| 229 | + |
| 230 | + TStringBuilder html; |
| 231 | + html << "<html><head><title>400 Bad Request</title></head><body bgcolor=\"white\"><center><h1>"; |
| 232 | + html << "400 Bad Request. Can not process request to protected resource: " << errorMessage; |
| 233 | + html << "</h1></center></body></html>"; |
| 234 | + return Request->CreateResponse("400", "Bad Request", headers, html); |
| 235 | +} |
| 236 | + |
| 237 | +} // NOIDC |
| 238 | +} // NMVP |
0 commit comments