virtual void complete();
virtual store_client_t storeClientType() const;
virtual char const *getSerialisedMetaData();
+ /// Store a prepared error response. MemObject locks the reply object.
+ void storeErrorResponse(HttpReply *reply);
void replaceHttpReply(HttpReply *, bool andStartWriting = true);
void startWriting(); ///< pack and write reply headers and, maybe, body
/// whether we may start writing to disk (now or in the future)
static IOACB httpsAccept;
#endif
static CTCB clientLifetimeTimeout;
-static ClientSocketContext *parseHttpRequestAbort(ConnStateData * conn, const char *uri);
#if USE_IDENT
static IDCB clientIdentDone;
#endif
}
}
-static ClientSocketContext *
-parseHttpRequestAbort(ConnStateData * csd, const char *uri)
+ClientSocketContext *
+ConnStateData::abortRequestParsing(const char *const uri)
{
- ClientHttpRequest *http;
- ClientSocketContext *context;
- StoreIOBuffer tempBuffer;
- http = new ClientHttpRequest(csd);
- http->req_sz = csd->in.buf.length();
+ ClientHttpRequest *http = new ClientHttpRequest(this);
+ http->req_sz = in.buf.length();
http->uri = xstrdup(uri);
setLogUri (http, uri);
- context = new ClientSocketContext(csd->clientConnection, http);
+ ClientSocketContext *context = new ClientSocketContext(clientConnection, http);
+ StoreIOBuffer tempBuffer;
tempBuffer.data = context->reqbuf;
tempBuffer.length = HTTP_REQBUF_SZ;
clientStreamInit(&http->client_stream, clientGetMoreData, clientReplyDetach,
if (!url) {
hp->request_parse_status = Http::scBadRequest;
- return parseHttpRequestAbort(conn, "error:invalid-request");
+ return conn->abortRequestParsing("error:invalid-request");
}
#endif
} else if ( (size_t)hp->bufsiz >= Config.maxRequestHeaderSize && headersEnd(hp->buf, Config.maxRequestHeaderSize) == 0) {
debugs(33, 5, "parseHttpRequest: Too large request");
hp->request_parse_status = Http::scHeaderTooLarge;
- return parseHttpRequestAbort(csd, "error:request-too-large");
+ return csd->abortRequestParsing("error:request-too-large");
}
/* Attempt to parse the first line; this'll define the method, url, version and header begin */
}
if (r == -1) {
- return parseHttpRequestAbort(csd, "error:invalid-request");
+ return csd->abortRequestParsing("error:invalid-request");
}
/* Request line is valid here .. */
if (req_sz >= Config.maxRequestHeaderSize) {
debugs(33, 5, "parseHttpRequest: Too large request");
hp->request_parse_status = Http::scHeaderTooLarge;
- return parseHttpRequestAbort(csd, "error:request-too-large");
+ return csd->abortRequestParsing("error:request-too-large");
}
/* Set method_p */
/* XXX need a way to say "this many character length string" */
debugs(33, DBG_IMPORTANT, "WARNING: for request: " << hp->buf);
hp->request_parse_status = Http::scMethodNotAllowed;
- return parseHttpRequestAbort(csd, "error:method-not-allowed");
+ return csd->abortRequestParsing("error:method-not-allowed");
}
if (*method_p == Http::METHOD_NONE) {
/* XXX need a way to say "this many character length string" */
debugs(33, DBG_IMPORTANT, "clientParseRequestMethod: Unsupported method in request '" << hp->buf << "'");
hp->request_parse_status = Http::scMethodNotAllowed;
- return parseHttpRequestAbort(csd, "error:unsupported-request-method");
+ return csd->abortRequestParsing("error:unsupported-request-method");
}
/*
conn->consumeInput(byteCount);
}
-/// respond with ERR_TOO_BIG if request header exceeds request_header_max_size
-void
-ConnStateData::checkHeaderLimits()
-{
- if (in.buf.length() < Config.maxRequestHeaderSize)
- return; // can accumulte more header data
-
- debugs(33, 3, "Request header is too large (" << in.buf.length() << " > " <<
- Config.maxRequestHeaderSize << " bytes)");
-
- ClientSocketContext *context = parseHttpRequestAbort(this, "error:request-too-large");
- clientStreamNode *node = context->getClientReplyContext();
- clientReplyContext *repContext = dynamic_cast<clientReplyContext *>(node->data.getRaw());
- assert (repContext);
- repContext->setReplyToError(ERR_TOO_BIG,
- Http::scBadRequest, Http::METHOD_NONE, NULL,
- clientConnection->remote, NULL, NULL, NULL);
- context->registerWithConn();
- context->pullData();
-}
-
void
ConnStateData::clientAfterReadingRequests()
{
/**
* Attempt to parse one or more requests from the input buffer.
- * If a request is successfully parsed, even if the next request
- * is only partially parsed, it will return TRUE.
+ * Returns true after completing parsing of at least one request [header]. That
+ * includes cases where parsing ended with an error (e.g., a huge request).
*/
bool
ConnStateData::clientParseRequests()
break;
Http::ProtocolVersion http_ver;
- ClientSocketContext *context = parseOneRequest(http_ver);
-
- /* partial or incomplete request */
- if (!context) {
- // TODO: why parseHttpRequest can just return parseHttpRequestAbort
- // (which becomes context) but checkHeaderLimits cannot?
- checkHeaderLimits();
- break;
- }
-
- /* status -1 or 1 */
- if (context) {
- debugs(33, 5, HERE << clientConnection << ": parsed a request");
+ if (ClientSocketContext *context = parseOneRequest(http_ver)) {
+ debugs(33, 5, clientConnection << ": done parsing a request");
AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "clientLifetimeTimeout",
CommTimeoutCbPtrFun(clientLifetimeTimeout, context->http));
commSetConnTimeout(clientConnection, Config.Timeout.lifetime, timeoutCall);
+ context->registerWithConn();
+
processParsedRequest(context, http_ver);
parsed_req = true; // XXX: do we really need to parse everything right NOW ?
debugs(33, 3, HERE << "Not parsing new requests, as this request may need the connection");
break;
}
+ } else {
+ debugs(33, 5, clientConnection << ": not enough request data: " <<
+ in.buf.length() << " < " << Config.maxRequestHeaderSize);
+ Must(in.buf.length() < Config.maxRequestHeaderSize);
+ break;
}
}
void addContextToQueue(ClientSocketContext * context);
int getConcurrentRequestCount() const;
bool isOpen() const;
- void checkHeaderLimits();
// HttpControlMsgSink API
virtual void sendControlMsg(HttpControlMsg msg);
/// remove no longer needed leading bytes from the input buffer
void consumeInput(const size_t byteCount);
+ /* TODO: Make the methods below (at least) non-public when possible. */
+
+ /// stop parsing the request and create context for relaying error info
+ ClientSocketContext *abortRequestParsing(const char *const errUri);
+
protected:
void startDechunkingRequest();
void finishDechunkingRequest(bool withSuccess);
void clientPinnedConnectionRead(const CommIoCbParams &io);
/// parse input buffer prefix into a single transfer protocol request
+ /// return NULL to request more header bytes (after checking any limits)
+ /// use abortRequestParsing() to handle parsing errors w/o creating request
virtual ClientSocketContext *parseOneRequest(Http::ProtocolVersion &ver) = 0;
/// start processing a freshly parsed request
/* Now the caller reads to get this */
}
+void
+clientReplyContext::setReplyToReply(HttpReply *futureReply)
+{
+ Must(futureReply);
+ http->al->http.code = futureReply->sline.status();
+
+ HttpRequestMethod method;
+ if (http->request) { // nil on responses to unparsable requests
+ http->request->ignoreRange("responding with a Squid-generated reply");
+ method = http->request->method;
+ }
+
+ createStoreEntry(method, RequestFlags());
+
+ http->storeEntry()->storeErrorResponse(futureReply);
+ /* Now the caller reads to get futureReply */
+}
+
// Assumes that the entry contains an error response without Content-Range.
// To use with regular entries, make HTTP Range header removal conditional.
void clientReplyContext::setReplyToStoreEntry(StoreEntry *entry, const char *reason)
#endif
/// creates a store entry for the reply and appends err to it
void setReplyToError(const HttpRequestMethod& method, ErrorState *err);
+ /// creates a store entry for the reply and appends error reply to it
+ void setReplyToReply(HttpReply *reply);
void createStoreEntry(const HttpRequestMethod& m, RequestFlags flags);
void removeStoreReference(store_client ** scp, StoreEntry ** ep);
void removeClientStoreReference(store_client **scp, ClientHttpRequest *http);
}
}
- entry->lock("errorAppendEntry");
- entry->buffer();
- entry->replaceHttpReply( err->BuildHttpReply() );
- entry->flush();
- entry->complete();
- entry->negativeCache();
- entry->releaseRequest();
- entry->unlock("errorAppendEntry");
+ entry->storeErrorResponse(err->BuildHttpReply());
delete err;
}
ClientHttpRequest *const http = context->http;
assert(http != NULL);
- HttpRequest *const request = http->request;
- assert(request != NULL);
- debugs(33, 9, request);
-
- HttpHeader &header = request->header;
- assert(header.has(HDR_FTP_COMMAND));
- String &cmd = header.findEntry(HDR_FTP_COMMAND)->value;
- assert(header.has(HDR_FTP_ARGUMENTS));
- String ¶ms = header.findEntry(HDR_FTP_ARGUMENTS)->value;
- const bool fwd = !http->storeEntry() && handleRequest(cmd, params);
+ HttpRequest *const request = http->request;
+ Must(http->storeEntry() || request);
+ const bool mayForward = !http->storeEntry() && handleRequest(request);
if (http->storeEntry() != NULL) {
debugs(33, 4, "got an immediate response");
- assert(http->storeEntry() != NULL);
clientSetKeepaliveFlag(http);
context->pullData();
- } else if (fwd) {
+ } else if (mayForward) {
debugs(33, 4, "forwarding request to server side");
assert(http->storeEntry() == NULL);
clientProcessRequest(this, NULL /*parser*/, context.getRaw(),
void
Ftp::Server::processParsedRequest(ClientSocketContext *context, const Http::ProtocolVersion &)
{
+ Must(getConcurrentRequestCount() == 1);
+
// Process FTP request asynchronously to make sure FTP
// data connection accept callback is fired first.
CallJobHere(33, 4, CbcPointer<Server>(this),
return PathedCommands.find(cmd) != PathedCommands.end();
}
+/// creates a context filled with an error message for a given early error
+ClientSocketContext *
+Ftp::Server::earlyError(const EarlyErrorKind eek)
+{
+ /* Default values, to be updated by the switch statement below */
+ int scode = 421;
+ const char *reason = "Internal error";
+ const char *errUri = "error:ftp-internal-early-error";
+
+ switch (eek) {
+ case eekHugeRequest:
+ scode = 421;
+ reason = "Huge request";
+ errUri = "error:ftp-huge-request";
+ break;
+
+ case eekMissingLogin:
+ scode = 530;
+ reason = "Must login first";
+ errUri = "error:ftp-must-login-first";
+ break;
+
+ case eekMissingUsername:
+ scode = 501;
+ reason = "Missing username";
+ errUri = "error:ftp-missing-username";
+ break;
+
+ case eekMissingHost:
+ scode = 501;
+ reason = "Missing host";
+ errUri = "error:ftp-missing-host";
+ break;
+
+ case eekUnsupportedCommand:
+ scode = 502;
+ reason = "Unknown or unsupported command";
+ errUri = "error:ftp-unsupported-command";
+ break;
+
+ case eekInvalidUri:
+ scode = 501;
+ reason = "Invalid URI";
+ errUri = "error:ftp-invalid-uri";
+ break;
+
+ case eekMalformedCommand:
+ scode = 421;
+ reason = "Malformed command";
+ errUri = "error:ftp-malformed-command";
+ break;
+
+ // no default so that a compiler can check that we have covered all cases
+ }
+
+ ClientSocketContext *context = abortRequestParsing(errUri);
+ clientStreamNode *node = context->getClientReplyContext();
+ Must(node);
+ clientReplyContext *repContext = dynamic_cast<clientReplyContext *>(node->data.getRaw());
+
+ // We cannot relay FTP scode/reason via HTTP-specific ErrorState.
+ // TODO: When/if ErrorState can handle native FTP errors, use it instead.
+ HttpReply *reply = Ftp::HttpReplyWrapper(scode, reason, Http::scBadRequest, -1);
+ repContext->setReplyToReply(reply);
+ return context;
+}
+
/// Parses a single FTP request on the control connection.
-/// Returns NULL on errors and incomplete requests.
+/// Returns a new ClientSocketContext on valid requests and all errors.
+/// Returns NULL on incomplete requests that may still succeed given more data.
ClientSocketContext *
Ftp::Server::parseOneRequest(Http::ProtocolVersion &ver)
{
+ flags.readMore = false; // common for all but one case below
+
// OWS <command> [ RWS <parameter> ] OWS LF
// InlineSpaceChars are isspace(3) or RFC 959 Section 3.1.1.5.2, except
params.trim(bufWhiteSpace, false, true);
}
+ // Why limit command line and parameters size? Did not we just parse them?
+ // XXX: Our good old String cannot handle very long strings.
+ const SBuf::size_type tokenMax = min(
+ static_cast<SBuf::size_type>(32*1024), // conservative
+ static_cast<SBuf::size_type>(Config.maxRequestHeaderSize));
+ if (cmd.length() > tokenMax || params.length() > tokenMax) {
+ changeState(fssError, "huge req token");
+ quitAfterError(NULL);
+ return earlyError(eekHugeRequest);
+ }
+
// technically, we may skip multiple NLs below, but that is OK
if (!parsed || !tok.skipAll(CharacterSet::LF)) { // did not find terminating LF yet
// we need more data, but can we buffer more?
if (in.buf.length() >= Config.maxRequestHeaderSize) {
changeState(fssError, "huge req");
- writeEarlyReply(421, "Huge request");
- return NULL;
+ quitAfterError(NULL);
+ return earlyError(eekHugeRequest);
} else {
+ flags.readMore = true;
debugs(33, 5, "Waiting for more, up to " <<
(Config.maxRequestHeaderSize - in.buf.length()));
return NULL;
if (!transparent()) {
if (!master->clientReadGreeting) {
// the first command must be USER
- if (!pinning.pinned && cmd != cmdUser()) {
- writeEarlyReply(530, "Must login first");
- return NULL;
- }
+ if (!pinning.pinned && cmd != cmdUser())
+ return earlyError(eekMissingLogin);
}
// process USER request now because it sets FTP peer host name
- if (cmd == cmdUser() && !handleUserRequest(cmd, params))
- return NULL;
+ if (cmd == cmdUser()) {
+ if (ClientSocketContext *errCtx = handleUserRequest(cmd, params))
+ return errCtx;
+ }
}
- if (!Ftp::SupportedCommand(cmd)) {
- writeEarlyReply(502, "Unknown or unsupported command");
- return NULL;
- }
+ if (!Ftp::SupportedCommand(cmd))
+ return earlyError(eekUnsupportedCommand);
const HttpRequestMethod method =
cmd == cmdAppe() || cmd == cmdStor() || cmd == cmdStou() ?
HttpRequest *const request = HttpRequest::CreateFromUrlAndMethod(newUri, method);
if (!request) {
debugs(33, 5, "Invalid FTP URL: " << uri);
- writeEarlyReply(501, "Invalid host");
uri.clear();
safe_free(newUri);
- return NULL;
+ return earlyError(eekInvalidUri);
}
ver = Http::ProtocolVersion(Ftp::ProtocolVersion().major, Ftp::ProtocolVersion().minor);
clientReplyStatus, newServer, clientSocketRecipient,
clientSocketDetach, newClient, tempBuffer);
- Must(!getConcurrentRequestCount());
- result->registerWithConn();
result->flags.parsed_ok = 1;
- flags.readMore = false;
return result;
}
}
/// writes FTP response based on HTTP reply that is not an FTP-response wrapper
+/// for example, internally-generated Squid "errorpages" end up here (for now)
void
Ftp::Server::writeForwardedForeign(const HttpReply *reply)
{
}
bool
-Ftp::Server::handleRequest(String &cmd, String ¶ms)
+Ftp::Server::handleRequest(HttpRequest *request)
{
- HttpRequest *request = getCurrentContext()->http->request;
+ debugs(33, 9, request);
Must(request);
+ HttpHeader &header = request->header;
+ Must(header.has(HDR_FTP_COMMAND));
+ String &cmd = header.findEntry(HDR_FTP_COMMAND)->value;
+ Must(header.has(HDR_FTP_ARGUMENTS));
+ String ¶ms = header.findEntry(HDR_FTP_ARGUMENTS)->value;
+
if (do_debug(9, 2)) {
MemBuf mb;
Packer p;
}
/// Called to parse USER command, which is required to create an HTTP request
-/// wrapper. Thus, errors are handled with writeEarlyReply() here.
-bool
+/// wrapper. W/o request, the errors are handled by returning earlyError().
+ClientSocketContext *
Ftp::Server::handleUserRequest(const SBuf &cmd, SBuf ¶ms)
{
- if (params.isEmpty()) {
- writeEarlyReply(501, "Missing username");
- return false;
- }
+ if (params.isEmpty())
+ return earlyError(eekMissingUsername);
// find the [end of] user name
const SBuf::size_type eou = params.rfind('@');
- if (eou == SBuf::npos || eou + 1 >= params.length()) {
- writeEarlyReply(501, "Missing host");
- return false;
- }
+ if (eou == SBuf::npos || eou + 1 >= params.length())
+ return earlyError(eekMissingHost);
// Determine the intended destination.
host = params.substr(eou + 1, params.length());
resetLogin("URI reset");
}
- return true;
+ return NULL; // no early errors
}
bool
protected:
friend void StartListening();
+ // errors detected before it is possible to create an HTTP request wrapper
+ typedef enum {
+ eekHugeRequest,
+ eekMissingLogin,
+ eekMissingUsername,
+ eekMissingHost,
+ eekUnsupportedCommand,
+ eekInvalidUri,
+ eekMalformedCommand
+ } EarlyErrorKind;
+
/* ConnStateData API */
virtual ClientSocketContext *parseOneRequest(Http::ProtocolVersion &ver);
virtual void processParsedRequest(ClientSocketContext *context, const Http::ProtocolVersion &ver);
void calcUri(const SBuf *file);
void changeState(const Ftp::ServerState newState, const char *reason);
- bool handleUserRequest(const SBuf &cmd, SBuf ¶ms);
+ ClientSocketContext *handleUserRequest(const SBuf &cmd, SBuf ¶ms);
bool checkDataConnPost() const;
void replyDataWritingCheckpoint();
void maybeReadUploadData();
void writeForwardedReplyAndCall(const HttpReply *reply, AsyncCall::Pointer &call);
void writeReply(MemBuf &mb);
- bool handleRequest(String &cmd, String ¶ms);
+ ClientSocketContext *earlyError(const EarlyErrorKind eek);
+ bool handleRequest(HttpRequest *);
void setDataCommand();
bool checkDataConnPre();
void
Http::Server::processParsedRequest(ClientSocketContext *context, const Http::ProtocolVersion &ver)
{
- /* We have an initial client stream in place should it be needed */
- /* setup our private context */
- context->registerWithConn();
clientProcessRequest(this, &parser_, context, method_, ver);
}
#endif
+void
+StoreEntry::storeErrorResponse(HttpReply *reply)
+{
+ lock("StoreEntry::storeErrorResponse");
+ buffer();
+ replaceHttpReply(reply);
+ flush();
+ complete();
+ negativeCache();
+ releaseRequest();
+ unlock("StoreEntry::storeErrorResponse");
+}
+
/*
* Replace a store entry with
* a new reply. This eats the reply.
void ConnStateData::addContextToQueue(ClientSocketContext * context) STUB
int ConnStateData::getConcurrentRequestCount() const STUB_RETVAL(0)
bool ConnStateData::isOpen() const STUB_RETVAL(false)
-void ConnStateData::checkHeaderLimits() STUB
void ConnStateData::sendControlMsg(HttpControlMsg msg) STUB
int64_t ConnStateData::mayNeedToReadMoreBody() const STUB_RETVAL(0)
#if USE_AUTH