2 * Copyright (C) 1996-2022 The Squid Software Foundation and contributors
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
9 /* DEBUG: section 33 Transfer protocol servers */
12 #include "acl/FilledChecklist.h"
13 #include "base/CharacterSet.h"
14 #include "base/RefCount.h"
15 #include "base/Subscription.h"
16 #include "client_side_reply.h"
17 #include "client_side_request.h"
18 #include "clientStream.h"
19 #include "comm/ConnOpener.h"
20 #include "comm/Read.h"
21 #include "comm/TcpAcceptor.h"
22 #include "comm/Write.h"
23 #include "errorpage.h"
25 #include "ftp/Elements.h"
26 #include "ftp/Parsing.h"
28 #include "http/one/RequestParser.h"
29 #include "http/Stream.h"
30 #include "HttpHdrCc.h"
32 #include "ipc/FdNotes.h"
33 #include "parser/Tokenizer.h"
34 #include "servers/forward.h"
35 #include "servers/FtpServer.h"
36 #include "SquidConfig.h"
37 #include "StatCounters.h"
43 CBDATA_NAMESPACED_CLASS_INIT(Ftp
, Server
);
47 static void PrintReply(MemBuf
&mb
, const HttpReply
*reply
, const char *const prefix
= "");
48 static bool SupportedCommand(const SBuf
&name
);
49 static bool CommandHasPathParameter(const SBuf
&cmd
);
52 Ftp::Server::Server(const MasterXaction::Pointer
&xact
):
53 AsyncJob("Ftp::Server"),
55 master(new MasterState
),
66 waitingForOrigin(false),
67 originDataDownloadAbortedOnError(false)
69 flags
.readMore
= false; // we need to announce ourselves first
73 Ftp::Server::~Server()
75 closeDataConnection();
79 Ftp::Server::pipelinePrefetchMax() const
81 return 0; // no support for concurrent FTP requests
85 Ftp::Server::idleTimeout() const
87 return Config
.Timeout
.ftpClientIdle
;
93 ConnStateData::start();
96 char buf
[MAX_IPSTRLEN
];
97 clientConnection
->local
.toUrl(buf
, MAX_IPSTRLEN
);
100 debugs(33, 5, "FTP transparent URL: " << uri
);
103 writeEarlyReply(220, "Service ready");
106 /// schedules another data connection read if needed
108 Ftp::Server::maybeReadUploadData()
113 const size_t availSpace
= sizeof(uploadBuf
) - uploadAvailSize
;
117 debugs(33, 4, dataConn
<< ": reading FTP data...");
119 typedef CommCbMemFunT
<Server
, CommIoCbParams
> Dialer
;
120 reader
= JobCallback(33, 5, Dialer
, this, Ftp::Server::readUploadData
);
121 comm_read(dataConn
, uploadBuf
+ uploadAvailSize
, availSpace
,
125 /// react to the freshly parsed request
127 Ftp::Server::doProcessRequest()
129 // zero pipelinePrefetchMax() ensures that there is only parsed request
130 Must(pipeline
.count() == 1);
131 Http::StreamPointer context
= pipeline
.front();
132 Must(context
!= nullptr);
134 ClientHttpRequest
*const http
= context
->http
;
135 assert(http
!= NULL
);
137 HttpRequest
*const request
= http
->request
;
138 Must(http
->storeEntry() || request
);
139 const bool mayForward
= !http
->storeEntry() && handleRequest(request
);
141 if (http
->storeEntry() != NULL
) {
142 debugs(33, 4, "got an immediate response");
143 clientSetKeepaliveFlag(http
);
145 } else if (mayForward
) {
146 debugs(33, 4, "forwarding request to server side");
147 assert(http
->storeEntry() == NULL
);
148 clientProcessRequest(this, Http1::RequestParserPointer(), context
.getRaw());
150 debugs(33, 4, "will resume processing later");
155 Ftp::Server::processParsedRequest(Http::StreamPointer
&)
157 Must(pipeline
.count() == 1);
159 // Process FTP request asynchronously to make sure FTP
160 // data connection accept callback is fired first.
161 CallJobHere(33, 4, CbcPointer
<Server
>(this),
162 Ftp::Server
, doProcessRequest
);
165 /// imports more upload data from the data connection
167 Ftp::Server::readUploadData(const CommIoCbParams
&io
)
169 debugs(33, 5, io
.conn
<< " size " << io
.size
);
170 Must(reader
!= NULL
);
173 assert(Comm::IsConnOpen(dataConn
));
174 assert(io
.conn
->fd
== dataConn
->fd
);
176 if (io
.flag
== Comm::OK
&& bodyPipe
!= NULL
) {
178 statCounter
.client_http
.kbytes_in
+= io
.size
;
180 char *const current_buf
= uploadBuf
+ uploadAvailSize
;
181 if (io
.buf
!= current_buf
)
182 memmove(current_buf
, io
.buf
, io
.size
);
183 uploadAvailSize
+= io
.size
;
185 } else if (io
.size
== 0) {
186 debugs(33, 5, io
.conn
<< " closed");
187 closeDataConnection();
188 if (uploadAvailSize
<= 0)
189 finishDechunkingRequest(true);
191 } else { // not Comm::Flags::OK or unexpected read
192 debugs(33, 5, io
.conn
<< " closed");
193 closeDataConnection();
194 finishDechunkingRequest(false);
199 /// shovel upload data from the internal buffer to the body pipe if possible
201 Ftp::Server::shovelUploadData()
203 assert(bodyPipe
!= NULL
);
205 debugs(33, 5, "handling FTP request data for " << clientConnection
);
206 const size_t putSize
= bodyPipe
->putMoreData(uploadBuf
,
209 uploadAvailSize
-= putSize
;
210 if (uploadAvailSize
> 0)
211 memmove(uploadBuf
, uploadBuf
+ putSize
, uploadAvailSize
);
214 if (Comm::IsConnOpen(dataConn
))
215 maybeReadUploadData();
216 else if (uploadAvailSize
<= 0)
217 finishDechunkingRequest(true);
221 Ftp::Server::noteMoreBodySpaceAvailable(BodyPipe::Pointer
)
223 if (!isOpen()) // if we are closing, nothing to do
230 Ftp::Server::noteBodyConsumerAborted(BodyPipe::Pointer ptr
)
232 if (!isOpen()) // if we are closing, nothing to do
235 ConnStateData::noteBodyConsumerAborted(ptr
);
236 closeDataConnection();
239 /// accept a new FTP control connection and hand it to a dedicated Server
241 Ftp::Server::AcceptCtrlConnection(const CommAcceptCbParams
¶ms
)
243 MasterXaction::Pointer xact
= params
.xaction
;
244 AnyP::PortCfgPointer s
= xact
->squidPort
;
246 // NP: it is possible the port was reconfigured when the call or accept() was queued.
248 if (params
.flag
!= Comm::OK
) {
249 // Its possible the call was still queued when the client disconnected
250 debugs(33, 2, s
->listenConn
<< ": FTP accept failure: " << xstrerr(params
.xerrno
));
254 debugs(33, 4, params
.conn
<< ": accepted");
255 fd_note(params
.conn
->fd
, "client ftp connect");
257 AsyncJob::Start(new Server(xact
));
261 Ftp::StartListening()
263 for (AnyP::PortCfgPointer s
= FtpPortList
; s
!= NULL
; s
= s
->next
) {
264 if (MAXTCPLISTENPORTS
== NHttpSockets
) {
265 debugs(1, DBG_IMPORTANT
, "Ignoring ftp_port lines exceeding the" <<
266 " limit of " << MAXTCPLISTENPORTS
<< " ports.");
270 // direct new connections accepted by listenConn to Accept()
271 typedef CommCbFunPtrCallT
<CommAcceptCbPtrFun
> AcceptCall
;
272 RefCount
<AcceptCall
> subCall
= commCbCall(5, 5, "Ftp::Server::AcceptCtrlConnection",
273 CommAcceptCbPtrFun(Ftp::Server::AcceptCtrlConnection
,
274 CommAcceptCbParams(NULL
)));
275 clientStartListeningOn(s
, subCall
, Ipc::fdnFtpSocket
);
282 for (AnyP::PortCfgPointer s
= FtpPortList
; s
!= NULL
; s
= s
->next
) {
283 if (s
->listenConn
!= NULL
) {
284 debugs(1, DBG_IMPORTANT
, "Closing FTP port " << s
->listenConn
->local
);
285 s
->listenConn
->close();
286 s
->listenConn
= NULL
;
292 Ftp::Server::notePeerConnection(Comm::ConnectionPointer conn
)
295 Http::StreamPointer context
= pipeline
.front();
296 Must(context
!= nullptr);
297 ClientHttpRequest
*const http
= context
->http
;
299 HttpRequest
*const request
= http
->request
;
300 Must(request
!= NULL
);
301 // make FTP peer connection exclusive to our request
302 pinBusyConnection(conn
, request
);
306 Ftp::Server::clientPinnedConnectionClosed(const CommCloseCbParams
&io
)
308 ConnStateData::clientPinnedConnectionClosed(io
);
310 // TODO: Keep the control connection open after fixing the reset
312 if (Comm::IsConnOpen(clientConnection
))
313 clientConnection
->close();
315 // TODO: If the server control connection is gone, reset state to login
316 // again. Resetting login alone is not enough: FtpRelay::sendCommand() will
317 // not re-login because FtpRelay::serverState() is not going to be
318 // fssConnected. Calling resetLogin() alone is also harmful because
319 // it does not reset correctly the client-to-squid control connection (eg
320 // respond if required with an error code, in all cases)
321 // resetLogin("control connection closure");
324 /// clear client and server login-related state after the old login is gone
326 Ftp::Server::resetLogin(const char *reason
)
328 debugs(33, 5, "will need to re-login due to " << reason
);
329 master
->clientReadGreeting
= false;
330 changeState(fssBegin
, reason
);
333 /// computes uri member from host and, if tracked, working dir with file name
335 Ftp::Server::calcUri(const SBuf
*file
)
337 // TODO: fill a class AnyP::Uri instead of string
340 if (port
->ftp_track_dirs
&& master
->workingDir
.length()) {
341 if (master
->workingDir
[0] != '/')
343 uri
.append(master
->workingDir
);
346 if (uri
[uri
.length() - 1] != '/')
349 if (port
->ftp_track_dirs
&& file
) {
350 static const CharacterSet
Slash("/", "/");
351 Parser::Tokenizer
tok(*file
);
353 uri
.append(tok
.remaining());
357 /// Starts waiting for a data connection. Returns listening port.
358 /// On errors, responds with an error and returns zero.
360 Ftp::Server::listenForDataConnection()
362 closeDataConnection();
364 Comm::ConnectionPointer conn
= new Comm::Connection
;
365 conn
->flags
= COMM_NONBLOCKING
;
366 conn
->local
= transparent() ? port
->s
: clientConnection
->local
;
368 const char *const note
= uri
.c_str();
369 comm_open_listener(SOCK_STREAM
, IPPROTO_TCP
, conn
, note
);
370 if (!Comm::IsConnOpen(conn
)) {
371 debugs(5, DBG_CRITICAL
, "ERROR: comm_open_listener failed for FTP data: " <<
372 conn
->local
<< " error: " << errno
);
373 writeCustomReply(451, "Internal error");
377 typedef CommCbMemFunT
<Server
, CommAcceptCbParams
> AcceptDialer
;
378 typedef AsyncCallT
<AcceptDialer
> AcceptCall
;
379 RefCount
<AcceptCall
> call
= static_cast<AcceptCall
*>(JobCallback(5, 5, AcceptDialer
, this, Ftp::Server::acceptDataConnection
));
380 Subscription::Pointer sub
= new CallSubscription
<AcceptCall
>(call
);
381 listener
= call
.getRaw();
382 dataListenConn
= conn
;
383 AsyncJob::Start(new Comm::TcpAcceptor(conn
, note
, sub
));
385 const unsigned int listeningPort
= comm_local_port(conn
->fd
);
386 conn
->local
.port(listeningPort
);
387 return listeningPort
;
391 Ftp::Server::acceptDataConnection(const CommAcceptCbParams
¶ms
)
393 if (params
.flag
!= Comm::OK
) {
394 // Its possible the call was still queued when the client disconnected
395 debugs(33, 2, dataListenConn
<< ": accept "
396 "failure: " << xstrerr(params
.xerrno
));
400 debugs(33, 4, "accepted " << params
.conn
);
401 fd_note(params
.conn
->fd
, "passive client ftp data");
403 if (!clientConnection
) {
404 debugs(33, 5, "late data connection?");
405 closeDataConnection(); // in case we are still listening
406 params
.conn
->close();
407 } else if (params
.conn
->remote
!= clientConnection
->remote
) {
408 debugs(33, 2, "rogue data conn? ctrl: " << clientConnection
->remote
);
409 params
.conn
->close();
410 // Some FTP servers close control connection here, but it may make
411 // things worse from DoS p.o.v. and no better from data stealing p.o.v.
413 closeDataConnection();
414 dataConn
= params
.conn
;
415 dataConn
->leaveOrphanage();
417 debugs(33, 7, "ready for data");
418 if (onDataAcceptCall
!= NULL
) {
419 AsyncCall::Pointer call
= onDataAcceptCall
;
420 onDataAcceptCall
= NULL
;
421 // If we got an upload request, start reading data from the client.
422 if (master
->serverState
== fssHandleUploadRequest
)
423 maybeReadUploadData();
425 Must(master
->serverState
== fssHandleDataRequest
);
428 mb
.appendf("150 Data connection opened.\r\n");
429 Comm::Write(clientConnection
, &mb
, call
);
435 Ftp::Server::closeDataConnection()
437 if (listener
!= NULL
) {
438 listener
->cancel("no longer needed");
442 if (Comm::IsConnOpen(dataListenConn
)) {
443 debugs(33, 5, "FTP closing client data listen socket: " <<
445 dataListenConn
->close();
447 dataListenConn
= NULL
;
449 if (reader
!= NULL
) {
450 // Comm::ReadCancel can deal with negative FDs
451 Comm::ReadCancel(dataConn
->fd
, reader
);
455 if (Comm::IsConnOpen(dataConn
)) {
456 debugs(33, 5, "FTP closing client data connection: " <<
463 /// Writes FTP [error] response before we fully parsed the FTP request and
464 /// created the corresponding HTTP request wrapper for that FTP request.
466 Ftp::Server::writeEarlyReply(const int code
, const char *msg
)
468 debugs(33, 7, code
<< ' ' << msg
);
469 assert(99 < code
&& code
< 1000);
473 mb
.appendf("%i %s\r\n", code
, msg
);
475 typedef CommCbMemFunT
<Server
, CommIoCbParams
> Dialer
;
476 AsyncCall::Pointer call
= JobCallback(33, 5, Dialer
, this, Ftp::Server::wroteEarlyReply
);
477 Comm::Write(clientConnection
, &mb
, call
);
479 flags
.readMore
= false;
481 // TODO: Create master transaction. Log it in wroteEarlyReply().
485 Ftp::Server::writeReply(MemBuf
&mb
)
487 debugs(9, 2, "FTP Client " << clientConnection
);
488 debugs(9, 2, "FTP Client REPLY:\n---------\n" << mb
.buf
<<
491 typedef CommCbMemFunT
<Server
, CommIoCbParams
> Dialer
;
492 AsyncCall::Pointer call
= JobCallback(33, 5, Dialer
, this, Ftp::Server::wroteReply
);
493 Comm::Write(clientConnection
, &mb
, call
);
497 Ftp::Server::writeCustomReply(const int code
, const char *msg
, const HttpReply
*reply
)
499 debugs(33, 7, code
<< ' ' << msg
);
500 assert(99 < code
&& code
< 1000);
502 const bool sendDetails
= reply
!= NULL
&&
503 reply
->header
.has(Http::HdrType::FTP_STATUS
) && reply
->header
.has(Http::HdrType::FTP_REASON
);
508 mb
.appendf("%i-%s\r\n", code
, msg
);
509 mb
.appendf(" Server reply:\r\n");
510 Ftp::PrintReply(mb
, reply
, " ");
511 mb
.appendf("%i \r\n", code
);
513 mb
.appendf("%i %s\r\n", code
, msg
);
519 Ftp::Server::changeState(const ServerState newState
, const char *reason
)
521 if (master
->serverState
== newState
) {
522 debugs(33, 3, "client state unchanged at " << master
->serverState
<<
523 " because " << reason
);
524 master
->serverState
= newState
;
526 debugs(33, 3, "client state was " << master
->serverState
<<
527 ", now " << newState
<< " because " << reason
);
528 master
->serverState
= newState
;
532 /// whether the given FTP command has a pathname parameter
534 Ftp::CommandHasPathParameter(const SBuf
&cmd
)
536 static std::set
<SBuf
> PathedCommands
;
537 if (!PathedCommands
.size()) {
538 PathedCommands
.insert(cmdMlst());
539 PathedCommands
.insert(cmdMlsd());
540 PathedCommands
.insert(cmdStat());
541 PathedCommands
.insert(cmdNlst());
542 PathedCommands
.insert(cmdList());
543 PathedCommands
.insert(cmdMkd());
544 PathedCommands
.insert(cmdRmd());
545 PathedCommands
.insert(cmdDele());
546 PathedCommands
.insert(cmdRnto());
547 PathedCommands
.insert(cmdRnfr());
548 PathedCommands
.insert(cmdAppe());
549 PathedCommands
.insert(cmdStor());
550 PathedCommands
.insert(cmdRetr());
551 PathedCommands
.insert(cmdSmnt());
552 PathedCommands
.insert(cmdCwd());
555 return PathedCommands
.find(cmd
) != PathedCommands
.end();
558 /// creates a context filled with an error message for a given early error
560 Ftp::Server::earlyError(const EarlyErrorKind eek
)
562 /* Default values, to be updated by the switch statement below */
564 const char *reason
= "Internal error";
565 const char *errUri
= "error:ftp-internal-early-error";
568 case EarlyErrorKind::HugeRequest
:
570 reason
= "Huge request";
571 errUri
= "error:ftp-huge-request";
574 case EarlyErrorKind::MissingLogin
:
576 reason
= "Must login first";
577 errUri
= "error:ftp-must-login-first";
580 case EarlyErrorKind::MissingUsername
:
582 reason
= "Missing username";
583 errUri
= "error:ftp-missing-username";
586 case EarlyErrorKind::MissingHost
:
588 reason
= "Missing host";
589 errUri
= "error:ftp-missing-host";
592 case EarlyErrorKind::UnsupportedCommand
:
594 reason
= "Unknown or unsupported command";
595 errUri
= "error:ftp-unsupported-command";
598 case EarlyErrorKind::InvalidUri
:
600 reason
= "Invalid URI";
601 errUri
= "error:ftp-invalid-uri";
604 case EarlyErrorKind::MalformedCommand
:
606 reason
= "Malformed command";
607 errUri
= "error:ftp-malformed-command";
610 // no default so that a compiler can check that we have covered all cases
613 Http::Stream
*context
= abortRequestParsing(errUri
);
614 clientStreamNode
*node
= context
->getClientReplyContext();
616 clientReplyContext
*repContext
= dynamic_cast<clientReplyContext
*>(node
->data
.getRaw());
619 // We cannot relay FTP scode/reason via HTTP-specific ErrorState.
620 // TODO: When/if ErrorState can handle native FTP errors, use it instead.
621 HttpReply
*reply
= Ftp::HttpReplyWrapper(scode
, reason
, Http::scBadRequest
, -1);
622 repContext
->setReplyToReply(reply
);
626 /// Parses a single FTP request on the control connection.
627 /// Returns a new Http::Stream on valid requests and all errors.
628 /// Returns NULL on incomplete requests that may still succeed given more data.
630 Ftp::Server::parseOneRequest()
632 flags
.readMore
= false; // common for all but one case below
634 // OWS <command> [ RWS <parameter> ] OWS LF
636 // InlineSpaceChars are isspace(3) or RFC 959 Section 3.1.1.5.2, except
637 // for the LF character that we must exclude here (but see FullWhiteSpace).
638 static const char * const InlineSpaceChars
= " \f\r\t\v";
639 static const CharacterSet InlineSpace
= CharacterSet("Ftp::Inline", InlineSpaceChars
);
640 static const CharacterSet FullWhiteSpace
= (InlineSpace
+ CharacterSet::LF
).rename("Ftp::FWS");
641 static const CharacterSet CommandChars
= FullWhiteSpace
.complement("Ftp::Command");
642 static const CharacterSet TailChars
= CharacterSet::LF
.complement("Ftp::Tail");
644 // This set is used to ignore empty commands without allowing an attacker
645 // to keep us endlessly busy by feeding us whitespace or empty commands.
646 static const CharacterSet
&LeadingSpace
= FullWhiteSpace
;
651 Parser::Tokenizer
tok(inBuf
);
653 (void)tok
.skipAll(LeadingSpace
); // leading OWS and empty commands
654 const bool parsed
= tok
.prefix(cmd
, CommandChars
); // required command
656 // note that the condition below will eat either RWS or trailing OWS
657 if (parsed
&& tok
.skipAll(InlineSpace
) && tok
.prefix(params
, TailChars
)) {
658 // now params may include trailing OWS
659 // TODO: Support right-trimming using CharacterSet in Tokenizer instead
660 static const SBuf
bufWhiteSpace(InlineSpaceChars
);
661 params
.trim(bufWhiteSpace
, false, true);
664 // Why limit command line and parameters size? Did not we just parse them?
665 // XXX: Our good old String cannot handle very long strings.
666 const SBuf::size_type tokenMax
= min(
667 static_cast<SBuf::size_type
>(32*1024), // conservative
668 static_cast<SBuf::size_type
>(Config
.maxRequestHeaderSize
));
669 if (cmd
.length() > tokenMax
|| params
.length() > tokenMax
) {
670 changeState(fssError
, "huge req token");
671 quitAfterError(NULL
);
672 return earlyError(EarlyErrorKind::HugeRequest
);
675 // technically, we may skip multiple NLs below, but that is OK
676 if (!parsed
|| !tok
.skipAll(CharacterSet::LF
)) { // did not find terminating LF yet
677 // we need more data, but can we buffer more?
678 if (inBuf
.length() >= Config
.maxRequestHeaderSize
) {
679 changeState(fssError
, "huge req");
680 quitAfterError(NULL
);
681 return earlyError(EarlyErrorKind::HugeRequest
);
683 flags
.readMore
= true;
684 debugs(33, 5, "Waiting for more, up to " <<
685 (Config
.maxRequestHeaderSize
- inBuf
.length()));
690 Must(parsed
&& cmd
.length());
691 consumeInput(tok
.parsedSize()); // TODO: Would delaying optimize copying?
693 debugs(33, 2, ">>ftp " << cmd
<< (params
.isEmpty() ? "" : " ") << params
);
695 cmd
.toUpper(); // this should speed up and simplify future comparisons
697 // interception cases do not need USER to calculate the uri
698 if (!transparent()) {
699 if (!master
->clientReadGreeting
) {
700 // the first command must be USER
701 if (!pinning
.pinned
&& cmd
!= cmdUser())
702 return earlyError(EarlyErrorKind::MissingLogin
);
705 // process USER request now because it sets FTP peer host name
706 if (cmd
== cmdUser()) {
707 if (Http::Stream
*errCtx
= handleUserRequest(cmd
, params
))
712 if (!Ftp::SupportedCommand(cmd
))
713 return earlyError(EarlyErrorKind::UnsupportedCommand
);
715 const HttpRequestMethod method
=
716 cmd
== cmdAppe() || cmd
== cmdStor() || cmd
== cmdStou() ?
717 Http::METHOD_PUT
: Http::METHOD_GET
;
719 const SBuf
*path
= (params
.length() && CommandHasPathParameter(cmd
)) ?
722 MasterXaction::Pointer mx
= new MasterXaction(XactionInitiator::initClient
);
723 mx
->tcpClient
= clientConnection
;
724 auto * const request
= HttpRequest::FromUrl(uri
, mx
, method
);
726 debugs(33, 5, "Invalid FTP URL: " << uri
);
728 return earlyError(EarlyErrorKind::InvalidUri
);
730 char *newUri
= xstrdup(uri
.c_str());
732 request
->flags
.ftpNative
= true;
733 request
->http_ver
= Http::ProtocolVersion(Ftp::ProtocolVersion().major
, Ftp::ProtocolVersion().minor
);
735 // Our fake Request-URIs are not distinctive enough for caching to work
736 request
->flags
.cachable
= false; // XXX: reset later by maybeCacheable()
737 request
->flags
.noCache
= true;
739 request
->header
.putStr(Http::HdrType::FTP_COMMAND
, cmd
.c_str());
740 request
->header
.putStr(Http::HdrType::FTP_ARGUMENTS
, params
.c_str()); // may be ""
741 if (method
== Http::METHOD_PUT
) {
742 request
->header
.putStr(Http::HdrType::EXPECT
, "100-continue");
743 request
->header
.putStr(Http::HdrType::TRANSFER_ENCODING
, "chunked");
746 ClientHttpRequest
*const http
= new ClientHttpRequest(this);
747 http
->req_sz
= tok
.parsedSize();
749 http
->initRequest(request
);
751 Http::Stream
*const result
=
752 new Http::Stream(clientConnection
, http
);
754 StoreIOBuffer tempBuffer
;
755 tempBuffer
.data
= result
->reqbuf
;
756 tempBuffer
.length
= HTTP_REQBUF_SZ
;
758 ClientStreamData newServer
= new clientReplyContext(http
);
759 ClientStreamData newClient
= result
;
760 clientStreamInit(&http
->client_stream
, clientGetMoreData
, clientReplyDetach
,
761 clientReplyStatus
, newServer
, clientSocketRecipient
,
762 clientSocketDetach
, newClient
, tempBuffer
);
764 result
->flags
.parsed_ok
= 1;
769 Ftp::Server::handleReply(HttpReply
*reply
, StoreIOBuffer data
)
771 // the caller guarantees that we are dealing with the current context only
772 Http::StreamPointer context
= pipeline
.front();
773 assert(context
!= nullptr);
775 static ReplyHandler handlers
[] = {
777 NULL
, // fssConnected
778 &Ftp::Server::handleFeatReply
, // fssHandleFeat
779 &Ftp::Server::handlePasvReply
, // fssHandlePasv
780 &Ftp::Server::handlePortReply
, // fssHandlePort
781 &Ftp::Server::handleDataReply
, // fssHandleDataRequest
782 &Ftp::Server::handleUploadReply
, // fssHandleUploadRequest
783 &Ftp::Server::handleEprtReply
,// fssHandleEprt
784 &Ftp::Server::handleEpsvReply
,// fssHandleEpsv
785 NULL
, // fssHandleCwd
786 NULL
, // fssHandlePass
787 NULL
, // fssHandleCdup
788 &Ftp::Server::handleErrorReply
// fssError
791 const Server
&server
= dynamic_cast<const Ftp::Server
&>(*context
->getConn());
792 if (const ReplyHandler handler
= handlers
[server
.master
->serverState
])
793 (this->*handler
)(reply
, data
);
795 writeForwardedReply(reply
);
796 } catch (const std::exception
&e
) {
798 throw TexcHere(e
.what());
803 Ftp::Server::handleFeatReply(const HttpReply
*reply
, StoreIOBuffer
)
805 if (pipeline
.front()->http
->request
->error
) {
806 writeCustomReply(502, "Server does not support FEAT", reply
);
811 HttpReply::Pointer featReply
= Ftp::HttpReplyWrapper(211, "End", Http::scNoContent
, 0);
812 HttpHeader
const &serverReplyHeader
= reply
->header
;
814 HttpHeaderPos pos
= HttpHeaderInitPos
;
815 bool hasEPRT
= false;
816 bool hasEPSV
= false;
817 int prependSpaces
= 1;
819 featReply
->header
.putStr(Http::HdrType::FTP_PRE
, "\"211-Features:\"");
820 const int scode
= serverReplyHeader
.getInt(Http::HdrType::FTP_STATUS
);
822 while (const HttpHeaderEntry
*e
= serverReplyHeader
.getEntry(&pos
)) {
823 if (e
->id
== Http::HdrType::FTP_PRE
) {
824 // assume RFC 2389 FEAT response format, quoted by Squid:
825 // <"> SP NAME [SP PARAMS] <">
826 // but accommodate MS servers sending four SPs before NAME
828 // command name ends with (SP parameter) or quote
829 static const CharacterSet
AfterFeatNameChars("AfterFeatName", " \"");
830 static const CharacterSet FeatNameChars
= AfterFeatNameChars
.complement("FeatName");
832 Parser::Tokenizer
tok(SBuf(e
->value
.termedBuf()));
833 if (!tok
.skip('"') || !tok
.skip(' '))
836 // optional spaces; remember their number to accommodate MS servers
837 prependSpaces
= 1 + tok
.skipAll(CharacterSet::SP
);
840 if (!tok
.prefix(cmd
, FeatNameChars
))
844 if (Ftp::SupportedCommand(cmd
)) {
845 featReply
->header
.addEntry(e
->clone());
848 if (cmd
== cmdEprt())
850 else if (cmd
== cmdEpsv())
854 } // else we got a FEAT error and will only report Squid-supported features
858 snprintf(buf
, sizeof(buf
), "\"%*s\"", prependSpaces
+ 4, "EPRT");
859 featReply
->header
.putStr(Http::HdrType::FTP_PRE
, buf
);
862 snprintf(buf
, sizeof(buf
), "\"%*s\"", prependSpaces
+ 4, "EPSV");
863 featReply
->header
.putStr(Http::HdrType::FTP_PRE
, buf
);
866 featReply
->header
.refreshMask();
868 writeForwardedReply(featReply
.getRaw());
872 Ftp::Server::handlePasvReply(const HttpReply
*reply
, StoreIOBuffer
)
874 const Http::StreamPointer
context(pipeline
.front());
875 assert(context
!= nullptr);
877 if (context
->http
->request
->error
) {
878 writeCustomReply(502, "Server does not support PASV", reply
);
882 const unsigned short localPort
= listenForDataConnection();
886 char addr
[MAX_IPSTRLEN
];
887 // remote server in interception setups and local address otherwise
888 const Ip::Address
&server
= transparent() ?
889 clientConnection
->local
: dataListenConn
->local
;
890 server
.toStr(addr
, MAX_IPSTRLEN
, AF_INET
);
891 addr
[MAX_IPSTRLEN
- 1] = '\0';
892 for (char *c
= addr
; *c
!= '\0'; ++c
) {
897 // In interception setups, we combine remote server address with a
898 // local port number and hope that traffic will be redirected to us.
899 // Do not use "227 =a,b,c,d,p1,p2" format or omit parens: some nf_ct_ftp
900 // versions block responses that use those alternative syntax rules!
903 mb
.appendf("227 Entering Passive Mode (%s,%i,%i).\r\n",
905 static_cast<int>(localPort
/ 256),
906 static_cast<int>(localPort
% 256));
907 debugs(9, 3, Raw("writing", mb
.buf
, mb
.size
));
912 Ftp::Server::handlePortReply(const HttpReply
*reply
, StoreIOBuffer
)
914 if (pipeline
.front()->http
->request
->error
) {
915 writeCustomReply(502, "Server does not support PASV (converted from PORT)", reply
);
919 writeCustomReply(200, "PORT successfully converted to PASV.");
925 Ftp::Server::handleErrorReply(const HttpReply
*reply
, StoreIOBuffer
)
927 if (!pinning
.pinned
) // we failed to connect to server
929 // 421: we will close due to fssError
930 writeErrorReply(reply
, 421);
934 Ftp::Server::handleDataReply(const HttpReply
*reply
, StoreIOBuffer data
)
936 if (reply
!= NULL
&& reply
->sline
.status() != Http::scOkay
) {
937 writeForwardedReply(reply
);
938 if (Comm::IsConnOpen(dataConn
)) {
939 debugs(33, 3, "closing " << dataConn
<< " on KO reply");
940 closeDataConnection();
946 // We got STREAM_COMPLETE (or error) and closed the client data conn.
947 debugs(33, 3, "ignoring FTP srv data response after clt data closure");
951 if (!checkDataConnPost()) {
952 writeCustomReply(425, "Data connection is not established.");
953 closeDataConnection();
957 debugs(33, 7, data
.length
);
959 if (data
.length
<= 0) {
960 replyDataWritingCheckpoint(); // skip the actual write call
965 mb
.init(data
.length
+ 1, data
.length
+ 1);
966 mb
.append(data
.data
, data
.length
);
968 typedef CommCbMemFunT
<Server
, CommIoCbParams
> Dialer
;
969 AsyncCall::Pointer call
= JobCallback(33, 5, Dialer
, this, Ftp::Server::wroteReplyData
);
970 Comm::Write(dataConn
, &mb
, call
);
972 pipeline
.front()->noteSentBodyBytes(data
.length
);
975 /// called when we are done writing a chunk of the response data
977 Ftp::Server::wroteReplyData(const CommIoCbParams
&io
)
979 if (io
.flag
== Comm::ERR_CLOSING
)
982 if (io
.flag
!= Comm::OK
) {
983 debugs(33, 3, "FTP reply data writing failed: " << xstrerr(io
.xerrno
));
984 userDataCompletionCheckpoint(426);
988 assert(pipeline
.front()->http
);
989 pipeline
.front()->http
->out
.size
+= io
.size
;
990 replyDataWritingCheckpoint();
993 /// ClientStream checks after (actual or skipped) reply data writing
995 Ftp::Server::replyDataWritingCheckpoint()
997 switch (pipeline
.front()->socketState()) {
999 debugs(33, 3, "Keep going");
1000 pipeline
.front()->pullData();
1002 case STREAM_COMPLETE
:
1003 debugs(33, 3, "FTP reply data transfer successfully complete");
1004 userDataCompletionCheckpoint(226);
1006 case STREAM_UNPLANNED_COMPLETE
:
1007 debugs(33, 3, "FTP reply data transfer failed: STREAM_UNPLANNED_COMPLETE");
1008 userDataCompletionCheckpoint(451);
1011 userDataCompletionCheckpoint(451);
1012 debugs(33, 3, "FTP reply data transfer failed: STREAM_FAILED");
1015 fatal("unreachable code");
1020 Ftp::Server::handleUploadReply(const HttpReply
*reply
, StoreIOBuffer
)
1022 writeForwardedReply(reply
);
1023 // note that the client data connection may already be closed by now
1027 Ftp::Server::writeForwardedReply(const HttpReply
*reply
)
1031 if (waitingForOrigin
) {
1032 Must(delayedReply
== NULL
);
1033 delayedReply
= reply
;
1037 const HttpHeader
&header
= reply
->header
;
1038 // adaptation and forwarding errors lack Http::HdrType::FTP_STATUS
1039 if (!header
.has(Http::HdrType::FTP_STATUS
)) {
1040 writeForwardedForeign(reply
); // will get to Ftp::Server::wroteReply
1044 typedef CommCbMemFunT
<Server
, CommIoCbParams
> Dialer
;
1045 AsyncCall::Pointer call
= JobCallback(33, 5, Dialer
, this, Ftp::Server::wroteReply
);
1046 writeForwardedReplyAndCall(reply
, call
);
1050 Ftp::Server::handleEprtReply(const HttpReply
*reply
, StoreIOBuffer
)
1052 if (pipeline
.front()->http
->request
->error
) {
1053 writeCustomReply(502, "Server does not support PASV (converted from EPRT)", reply
);
1057 writeCustomReply(200, "EPRT successfully converted to PASV.");
1059 // and wait for RETR
1063 Ftp::Server::handleEpsvReply(const HttpReply
*reply
, StoreIOBuffer
)
1065 if (pipeline
.front()->http
->request
->error
) {
1066 writeCustomReply(502, "Cannot connect to server", reply
);
1070 const unsigned short localPort
= listenForDataConnection();
1074 // In interception setups, we use a local port number and hope that data
1075 // traffic will be redirected to us.
1078 mb
.appendf("229 Entering Extended Passive Mode (|||%u|)\r\n", localPort
);
1080 debugs(9, 3, Raw("writing", mb
.buf
, mb
.size
));
1084 /// writes FTP error response with given status and reply-derived error details
1086 Ftp::Server::writeErrorReply(const HttpReply
*reply
, const int scode
)
1088 const HttpRequest
*request
= pipeline
.front()->http
->request
;
1095 mb
.appendf("%i-%s\r\n", scode
, errorPageName(request
->error
.category
));
1097 if (const auto &detail
= request
->error
.detail
) {
1098 mb
.appendf("%i-Error-Detail-Brief: " SQUIDSBUFPH
"\r\n", scode
, SQUIDSBUFPRINT(detail
->brief()));
1099 mb
.appendf("%i-Error-Detail-Verbose: " SQUIDSBUFPH
"\r\n", scode
, SQUIDSBUFPRINT(detail
->verbose(request
)));
1103 // XXX: Remove hard coded names. Use an error page template instead.
1104 const Adaptation::History::Pointer ah
= request
->adaptHistory();
1105 if (ah
!= NULL
) { // XXX: add adapt::<all_h but use lastMeta here
1106 const String info
= ah
->allMeta
.getByName("X-Response-Info");
1107 const String desc
= ah
->allMeta
.getByName("X-Response-Desc");
1109 mb
.appendf("%i-Information: %s\r\n", scode
, info
.termedBuf());
1111 mb
.appendf("%i-Description: %s\r\n", scode
, desc
.termedBuf());
1115 const char *reason
= "Lost Error";
1117 reason
= reply
->header
.has(Http::HdrType::FTP_REASON
) ?
1118 reply
->header
.getStr(Http::HdrType::FTP_REASON
):
1119 reply
->sline
.reason();
1122 mb
.appendf("%i %s\r\n", scode
, reason
); // error terminating line
1124 // TODO: errorpage.cc should detect FTP client and use
1125 // configurable FTP-friendly error templates which we should
1126 // write to the client "as is" instead of hiding most of the info
1131 /// writes FTP response based on HTTP reply that is not an FTP-response wrapper
1132 /// for example, internally-generated Squid "errorpages" end up here (for now)
1134 Ftp::Server::writeForwardedForeign(const HttpReply
*reply
)
1136 changeState(fssConnected
, "foreign reply");
1137 closeDataConnection();
1138 // 451: We intend to keep the control connection open.
1139 writeErrorReply(reply
, 451);
1143 Ftp::Server::writeControlMsgAndCall(HttpReply
*reply
, AsyncCall::Pointer
&call
)
1145 // the caller guarantees that we are dealing with the current context only
1146 // the caller should also make sure reply->header.has(Http::HdrType::FTP_STATUS)
1147 writeForwardedReplyAndCall(reply
, call
);
1152 Ftp::Server::writeForwardedReplyAndCall(const HttpReply
*reply
, AsyncCall::Pointer
&call
)
1154 assert(reply
!= NULL
);
1155 const HttpHeader
&header
= reply
->header
;
1157 // without status, the caller must use the writeForwardedForeign() path
1158 Must(header
.has(Http::HdrType::FTP_STATUS
));
1159 Must(header
.has(Http::HdrType::FTP_REASON
));
1160 const int scode
= header
.getInt(Http::HdrType::FTP_STATUS
);
1161 debugs(33, 7, "scode: " << scode
);
1163 // Status 125 or 150 implies upload or data request, but we still check
1164 // the state in case the server is buggy.
1165 if ((scode
== 125 || scode
== 150) &&
1166 (master
->serverState
== fssHandleUploadRequest
||
1167 master
->serverState
== fssHandleDataRequest
)) {
1168 if (checkDataConnPost()) {
1169 // If the data connection is ready, start reading data (here)
1170 // and forward the response to client (further below).
1171 debugs(33, 7, "data connection established, start data transfer");
1172 if (master
->serverState
== fssHandleUploadRequest
)
1173 maybeReadUploadData();
1175 // If we are waiting to accept the data connection, keep waiting.
1176 if (Comm::IsConnOpen(dataListenConn
)) {
1177 debugs(33, 7, "wait for the client to establish a data connection");
1178 onDataAcceptCall
= call
;
1179 // TODO: Add connect timeout for passive connections listener?
1180 // TODO: Remember server response so that we can forward it?
1182 // Either the connection was establised and closed after the
1183 // data was transferred OR we failed to establish an active
1184 // data connection and already sent the error to the client.
1185 // In either case, there is nothing more to do.
1186 debugs(33, 7, "done with data OR active connection failed");
1194 Ftp::PrintReply(mb
, reply
);
1196 debugs(9, 2, "FTP Client " << clientConnection
);
1197 debugs(9, 2, "FTP Client REPLY:\n---------\n" << mb
.buf
<<
1200 Comm::Write(clientConnection
, &mb
, call
);
1204 Ftp::PrintReply(MemBuf
&mb
, const HttpReply
*reply
, const char *const)
1206 const HttpHeader
&header
= reply
->header
;
1208 HttpHeaderPos pos
= HttpHeaderInitPos
;
1209 while (const HttpHeaderEntry
*e
= header
.getEntry(&pos
)) {
1210 if (e
->id
== Http::HdrType::FTP_PRE
) {
1212 if (httpHeaderParseQuotedString(e
->value
.rawBuf(), e
->value
.size(), &raw
))
1213 mb
.appendf("%s\r\n", raw
.termedBuf());
1217 if (header
.has(Http::HdrType::FTP_STATUS
)) {
1218 const char *reason
= header
.getStr(Http::HdrType::FTP_REASON
);
1219 mb
.appendf("%i %s\r\n", header
.getInt(Http::HdrType::FTP_STATUS
),
1220 (reason
? reason
: 0));
1225 Ftp::Server::wroteEarlyReply(const CommIoCbParams
&io
)
1227 if (io
.flag
== Comm::ERR_CLOSING
)
1230 if (io
.flag
!= Comm::OK
) {
1231 debugs(33, 3, "FTP reply writing failed: " << xstrerr(io
.xerrno
));
1236 Http::StreamPointer context
= pipeline
.front();
1237 if (context
!= nullptr && context
->http
) {
1238 context
->http
->out
.size
+= io
.size
;
1239 context
->http
->out
.headers_sz
+= io
.size
;
1242 flags
.readMore
= true;
1247 Ftp::Server::wroteReply(const CommIoCbParams
&io
)
1249 if (io
.flag
== Comm::ERR_CLOSING
)
1252 if (io
.flag
!= Comm::OK
) {
1253 debugs(33, 3, "FTP reply writing failed: " << xstrerr(io
.xerrno
));
1258 Http::StreamPointer context
= pipeline
.front();
1259 assert(context
->http
);
1260 context
->http
->out
.size
+= io
.size
;
1261 context
->http
->out
.headers_sz
+= io
.size
;
1263 if (master
->serverState
== fssError
) {
1264 debugs(33, 5, "closing on FTP server error");
1269 const clientStream_status_t socketState
= context
->socketState();
1270 debugs(33, 5, "FTP client stream state " << socketState
);
1271 switch (socketState
) {
1272 case STREAM_UNPLANNED_COMPLETE
:
1278 case STREAM_COMPLETE
:
1279 flags
.readMore
= true;
1280 changeState(fssConnected
, "Ftp::Server::wroteReply");
1282 finishDechunkingRequest(false);
1283 context
->finished();
1290 Ftp::Server::handleRequest(HttpRequest
*request
)
1292 debugs(33, 9, request
);
1295 HttpHeader
&header
= request
->header
;
1296 Must(header
.has(Http::HdrType::FTP_COMMAND
));
1297 String
&cmd
= header
.findEntry(Http::HdrType::FTP_COMMAND
)->value
;
1298 Must(header
.has(Http::HdrType::FTP_ARGUMENTS
));
1299 String
¶ms
= header
.findEntry(Http::HdrType::FTP_ARGUMENTS
)->value
;
1301 if (Debug::Enabled(9, 2)) {
1306 debugs(9, 2, "FTP Client " << clientConnection
);
1307 debugs(9, 2, "FTP Client REQUEST:\n---------\n" << mb
.buf
<<
1311 // TODO: When HttpHeader uses SBuf, change keys to SBuf
1312 typedef std::map
<const std::string
, RequestHandler
> RequestHandlers
;
1313 static RequestHandlers handlers
;
1314 if (!handlers
.size()) {
1315 handlers
["LIST"] = &Ftp::Server::handleDataRequest
;
1316 handlers
["NLST"] = &Ftp::Server::handleDataRequest
;
1317 handlers
["MLSD"] = &Ftp::Server::handleDataRequest
;
1318 handlers
["FEAT"] = &Ftp::Server::handleFeatRequest
;
1319 handlers
["PASV"] = &Ftp::Server::handlePasvRequest
;
1320 handlers
["PORT"] = &Ftp::Server::handlePortRequest
;
1321 handlers
["RETR"] = &Ftp::Server::handleDataRequest
;
1322 handlers
["EPRT"] = &Ftp::Server::handleEprtRequest
;
1323 handlers
["EPSV"] = &Ftp::Server::handleEpsvRequest
;
1324 handlers
["CWD"] = &Ftp::Server::handleCwdRequest
;
1325 handlers
["PASS"] = &Ftp::Server::handlePassRequest
;
1326 handlers
["CDUP"] = &Ftp::Server::handleCdupRequest
;
1329 RequestHandler handler
= NULL
;
1330 if (request
->method
== Http::METHOD_PUT
)
1331 handler
= &Ftp::Server::handleUploadRequest
;
1333 const RequestHandlers::const_iterator hi
= handlers
.find(cmd
.termedBuf());
1334 if (hi
!= handlers
.end())
1335 handler
= hi
->second
;
1339 debugs(9, 7, "forwarding " << cmd
<< " as is, no post-processing");
1343 return (this->*handler
)(cmd
, params
);
1346 /// Called to parse USER command, which is required to create an HTTP request
1347 /// wrapper. W/o request, the errors are handled by returning earlyError().
1349 Ftp::Server::handleUserRequest(const SBuf
&, SBuf
¶ms
)
1351 if (params
.isEmpty())
1352 return earlyError(EarlyErrorKind::MissingUsername
);
1354 // find the [end of] user name
1355 const SBuf::size_type eou
= params
.rfind('@');
1356 if (eou
== SBuf::npos
|| eou
+ 1 >= params
.length())
1357 return earlyError(EarlyErrorKind::MissingHost
);
1359 // Determine the intended destination.
1360 host
= params
.substr(eou
+ 1, params
.length());
1361 // If we can parse it as raw IPv6 address, then surround with "[]".
1362 // Otherwise (domain, IPv4, [bracketed] IPv6, garbage, etc), use as is.
1363 if (host
.find(':') != SBuf::npos
) {
1364 const Ip::Address
ipa(host
.c_str());
1365 if (!ipa
.isAnyAddr()) {
1366 char ipBuf
[MAX_IPSTRLEN
];
1367 ipa
.toHostStr(ipBuf
, MAX_IPSTRLEN
);
1372 // const SBuf login = params.substr(0, eou);
1373 params
.chop(0, eou
); // leave just the login part for the peer
1376 if (master
->clientReadGreeting
)
1379 master
->workingDir
.clear();
1382 if (!master
->clientReadGreeting
) {
1383 debugs(9, 3, "set URI to " << uri
);
1384 } else if (oldUri
.caseCmp(uri
) == 0) {
1385 debugs(9, 5, "kept URI as " << oldUri
);
1387 debugs(9, 3, "reset URI from " << oldUri
<< " to " << uri
);
1388 closeDataConnection();
1389 unpinConnection(true); // close control connection to peer
1390 resetLogin("URI reset");
1393 return NULL
; // no early errors
1397 Ftp::Server::handleFeatRequest(String
&, String
&)
1399 changeState(fssHandleFeat
, "handleFeatRequest");
1404 Ftp::Server::handlePasvRequest(String
&, String
¶ms
)
1407 setReply(500, "Bad PASV command");
1411 if (params
.size() > 0) {
1412 setReply(501, "Unexpected parameter");
1416 changeState(fssHandlePasv
, "handlePasvRequest");
1417 // no need to fake PASV request via setDataCommand() in true PASV case
1421 /// [Re]initializes dataConn for active data transfers. Does not connect.
1423 Ftp::Server::createDataConnection(Ip::Address cltAddr
)
1425 assert(clientConnection
!= NULL
);
1426 assert(!clientConnection
->remote
.isAnyAddr());
1428 if (cltAddr
!= clientConnection
->remote
) {
1429 debugs(33, 2, "rogue PORT " << cltAddr
<< " request? ctrl: " << clientConnection
->remote
);
1430 // Closing the control connection would not help with attacks because
1431 // the client is evidently able to connect to us. Besides, closing
1432 // makes retrials easier for the client and more damaging to us.
1433 setReply(501, "Prohibited parameter value");
1437 closeDataConnection();
1439 Comm::ConnectionPointer conn
= new Comm::Connection();
1440 conn
->flags
|= COMM_DOBIND
;
1442 if (clientConnection
->flags
& COMM_INTERCEPTION
) {
1443 // In the case of NAT interception conn->local value is not set
1444 // because the TCP stack will automatically pick correct source
1445 // address for the data connection. We must only ensure that IP
1446 // version matches client's address.
1447 conn
->local
.setAnyAddr();
1449 if (cltAddr
.isIPv4())
1450 conn
->local
.setIPv4();
1452 conn
->remote
= cltAddr
;
1454 // In the case of explicit-proxy the local IP of the control connection
1455 // is the Squid IP the client is knowingly talking to.
1457 // In the case of TPROXY the IP address of the control connection is
1458 // server IP the client is connecting to, it can be spoofed by Squid.
1460 // In both cases some clients may refuse to accept data connections if
1461 // these control connectin local-IP's are not used.
1462 conn
->setAddrs(clientConnection
->local
, cltAddr
);
1464 // Using non-local addresses in TPROXY mode requires appropriate socket option.
1465 if (clientConnection
->flags
& COMM_TRANSPARENT
)
1466 conn
->flags
|= COMM_TRANSPARENT
;
1469 // RFC 959 requires active FTP connections to originate from port 20
1470 // but that would preclude us from supporting concurrent transfers! (XXX?)
1471 conn
->local
.port(0);
1473 debugs(9, 3, "will actively connect from " << conn
->local
<< " to " <<
1477 uploadAvailSize
= 0;
1482 Ftp::Server::handlePortRequest(String
&, String
¶ms
)
1484 // TODO: Should PORT errors trigger closeDataConnection() cleanup?
1487 setReply(500, "Rejecting PORT after EPSV ALL");
1491 if (!params
.size()) {
1492 setReply(501, "Missing parameter");
1496 Ip::Address cltAddr
;
1497 if (!Ftp::ParseIpPort(params
.termedBuf(), NULL
, cltAddr
)) {
1498 setReply(501, "Invalid parameter");
1502 if (!createDataConnection(cltAddr
))
1505 changeState(fssHandlePort
, "handlePortRequest");
1507 return true; // forward our fake PASV request
1511 Ftp::Server::handleDataRequest(String
&, String
&)
1513 if (!checkDataConnPre())
1516 master
->userDataDone
= 0;
1517 originDataDownloadAbortedOnError
= false;
1519 changeState(fssHandleDataRequest
, "handleDataRequest");
1525 Ftp::Server::handleUploadRequest(String
&, String
&)
1527 if (!checkDataConnPre())
1530 if (Config
.accessList
.forceRequestBodyContinuation
) {
1531 ClientHttpRequest
*http
= pipeline
.front()->http
;
1532 HttpRequest
*request
= http
->request
;
1533 ACLFilledChecklist
bodyContinuationCheck(Config
.accessList
.forceRequestBodyContinuation
, request
, NULL
);
1534 bodyContinuationCheck
.al
= http
->al
;
1535 bodyContinuationCheck
.syncAle(request
, http
->log_uri
);
1536 if (bodyContinuationCheck
.fastCheck().allowed()) {
1537 request
->forcedBodyContinuation
= true;
1538 if (checkDataConnPost()) {
1539 // Write control Msg
1540 writeEarlyReply(150, "Data connection opened");
1541 maybeReadUploadData();
1543 // wait for acceptDataConnection but tell it to call wroteEarlyReply
1544 // after writing "150 Data connection opened"
1545 typedef CommCbMemFunT
<Server
, CommIoCbParams
> Dialer
;
1546 AsyncCall::Pointer call
= JobCallback(33, 5, Dialer
, this, Ftp::Server::wroteEarlyReply
);
1547 onDataAcceptCall
= call
;
1552 changeState(fssHandleUploadRequest
, "handleDataRequest");
1558 Ftp::Server::handleEprtRequest(String
&, String
¶ms
)
1560 debugs(9, 3, "Process an EPRT " << params
);
1563 setReply(500, "Rejecting EPRT after EPSV ALL");
1567 if (!params
.size()) {
1568 setReply(501, "Missing parameter");
1572 Ip::Address cltAddr
;
1573 if (!Ftp::ParseProtoIpPort(params
.termedBuf(), cltAddr
)) {
1574 setReply(501, "Invalid parameter");
1578 if (!createDataConnection(cltAddr
))
1581 changeState(fssHandleEprt
, "handleEprtRequest");
1583 return true; // forward our fake PASV request
1587 Ftp::Server::handleEpsvRequest(String
&, String
¶ms
)
1589 debugs(9, 3, "Process an EPSV command with params: " << params
);
1590 if (params
.size() <= 0) {
1591 // treat parameterless EPSV as "use the protocol of the ctrl conn"
1592 } else if (params
.caseCmp("ALL") == 0) {
1593 setReply(200, "EPSV ALL ok");
1596 } else if (params
.cmp("2") == 0) {
1597 if (!Ip::EnableIpv6
) {
1598 setReply(522, "Network protocol not supported, use (1)");
1601 } else if (params
.cmp("1") != 0) {
1602 setReply(501, "Unsupported EPSV parameter");
1606 changeState(fssHandleEpsv
, "handleEpsvRequest");
1608 return true; // forward our fake PASV request
1612 Ftp::Server::handleCwdRequest(String
&, String
&)
1614 changeState(fssHandleCwd
, "handleCwdRequest");
1619 Ftp::Server::handlePassRequest(String
&, String
&)
1621 changeState(fssHandlePass
, "handlePassRequest");
1626 Ftp::Server::handleCdupRequest(String
&, String
&)
1628 changeState(fssHandleCdup
, "handleCdupRequest");
1632 // Convert user PORT, EPRT, PASV, or EPSV data command to Squid PASV command.
1633 // Squid FTP client decides what data command to use with peers.
1635 Ftp::Server::setDataCommand()
1637 ClientHttpRequest
*const http
= pipeline
.front()->http
;
1638 assert(http
!= NULL
);
1639 HttpRequest
*const request
= http
->request
;
1640 assert(request
!= NULL
);
1641 HttpHeader
&header
= request
->header
;
1642 header
.delById(Http::HdrType::FTP_COMMAND
);
1643 header
.putStr(Http::HdrType::FTP_COMMAND
, "PASV");
1644 header
.delById(Http::HdrType::FTP_ARGUMENTS
);
1645 header
.putStr(Http::HdrType::FTP_ARGUMENTS
, "");
1646 debugs(9, 5, "client data command converted to fake PASV");
1649 /// check that client data connection is ready for future I/O or at least
1650 /// has a chance of becoming ready soon.
1652 Ftp::Server::checkDataConnPre()
1654 if (Comm::IsConnOpen(dataConn
))
1657 if (Comm::IsConnOpen(dataListenConn
)) {
1658 // We are still waiting for a client to connect to us after PASV.
1659 // Perhaps client's data conn handshake has not reached us yet.
1660 // After we talk to the server, checkDataConnPost() will recheck.
1661 debugs(33, 3, "expecting clt data conn " << dataListenConn
);
1665 if (!dataConn
|| dataConn
->remote
.isAnyAddr()) {
1666 debugs(33, 5, "missing " << dataConn
);
1667 // TODO: use client address and default port instead.
1668 setReply(425, "Use PORT or PASV first");
1672 // active transfer: open a data connection from Squid to client
1673 typedef CommCbMemFunT
<Server
, CommConnectCbParams
> Dialer
;
1674 AsyncCall::Pointer callback
= JobCallback(17, 3, Dialer
, this, Ftp::Server::connectedForData
);
1675 const auto cs
= new Comm::ConnOpener(dataConn
->cloneProfile(), callback
,
1676 Config
.Timeout
.connect
);
1677 dataConnWait
.start(cs
, callback
);
1681 /// Check that client data connection is ready for immediate I/O.
1683 Ftp::Server::checkDataConnPost() const
1685 if (!Comm::IsConnOpen(dataConn
)) {
1686 debugs(33, 3, "missing client data conn: " << dataConn
);
1692 /// Done establishing a data connection to the user.
1694 Ftp::Server::connectedForData(const CommConnectCbParams
¶ms
)
1696 dataConnWait
.finish();
1698 if (params
.flag
!= Comm::OK
) {
1699 setReply(425, "Cannot open data connection.");
1700 Http::StreamPointer context
= pipeline
.front();
1701 Must(context
->http
);
1702 Must(context
->http
->storeEntry() != NULL
);
1703 // TODO: call closeDataConnection() to reset data conn processing?
1705 // Finalize the details and start owning the supplied connection.
1706 assert(params
.conn
);
1708 assert(!dataConn
->isOpen());
1709 dataConn
= params
.conn
;
1710 // XXX: Missing comm_add_close_handler() to track external closures.
1712 Must(Comm::IsConnOpen(params
.conn
));
1713 fd_note(params
.conn
->fd
, "active client ftp data");
1720 Ftp::Server::setReply(const int code
, const char *msg
)
1722 Http::StreamPointer context
= pipeline
.front();
1723 ClientHttpRequest
*const http
= context
->http
;
1724 assert(http
!= NULL
);
1725 assert(http
->storeEntry() == NULL
);
1727 HttpReply
*const reply
= Ftp::HttpReplyWrapper(code
, msg
, Http::scNoContent
, 0);
1729 clientStreamNode
*const node
= context
->getClientReplyContext();
1730 clientReplyContext
*const repContext
=
1731 dynamic_cast<clientReplyContext
*>(node
->data
.getRaw());
1732 assert(repContext
!= NULL
);
1734 RequestFlags reqFlags
;
1735 reqFlags
.cachable
= false; // force releaseRequest() in storeCreateEntry()
1736 reqFlags
.noCache
= true;
1737 repContext
->createStoreEntry(http
->request
->method
, reqFlags
);
1738 http
->storeEntry()->replaceHttpReply(reply
);
1742 Ftp::Server::callException(const std::exception
&e
)
1744 debugs(33, 2, "FTP::Server job caught: " << e
.what());
1745 closeDataConnection();
1746 unpinConnection(true);
1747 if (Comm::IsConnOpen(clientConnection
))
1748 clientConnection
->close();
1749 AsyncJob::callException(e
);
1753 Ftp::Server::startWaitingForOrigin()
1755 if (!isOpen()) // if we are closing, nothing to do
1758 debugs(33, 5, "waiting for Ftp::Client data transfer to end");
1759 waitingForOrigin
= true;
1763 Ftp::Server::stopWaitingForOrigin(int originStatus
)
1765 Must(waitingForOrigin
);
1766 waitingForOrigin
= false;
1768 if (!isOpen()) // if we are closing, nothing to do
1771 // if we have already decided how to respond, respond now
1773 HttpReply::Pointer reply
= delayedReply
;
1774 delayedReply
= nullptr;
1775 writeForwardedReply(reply
.getRaw());
1776 return; // do not completeDataDownload() after an earlier response
1779 if (master
->serverState
!= fssHandleDataRequest
)
1782 // completeDataDownload() could be waitingForOrigin in fssHandleDataRequest
1783 // Depending on which side has finished downloading first, either trust
1784 // master->userDataDone status or set originDataDownloadAbortedOnError:
1785 if (master
->userDataDone
) {
1786 // We finished downloading before Ftp::Client. Most likely, the
1787 // adaptation shortened the origin response or we hit an error.
1788 // Our status (stored in master->userDataDone) is more informative.
1789 // Use master->userDataDone; avoid originDataDownloadAbortedOnError.
1790 completeDataDownload();
1792 debugs(33, 5, "too early to write the response");
1793 // Ftp::Client naturally finished downloading before us. Set
1794 // originDataDownloadAbortedOnError to overwrite future
1795 // master->userDataDone and relay Ftp::Client error, if there was
1796 // any, to the user.
1797 originDataDownloadAbortedOnError
= (originStatus
>= 400);
1801 void Ftp::Server::userDataCompletionCheckpoint(int finalStatusCode
)
1803 Must(!master
->userDataDone
);
1804 master
->userDataDone
= finalStatusCode
;
1807 finishDechunkingRequest(false);
1809 if (waitingForOrigin
) {
1810 // The completeDataDownload() is not called here unconditionally
1811 // because we want to signal the FTP user that we are not fully
1812 // done processing its data stream, even though all data bytes
1813 // have been sent or received already.
1814 debugs(33, 5, "Transferring from FTP server is not complete");
1818 // Adjust our reply if the server aborted with an error before we are done.
1819 if (master
->userDataDone
== 226 && originDataDownloadAbortedOnError
) {
1820 debugs(33, 5, "Transferring from FTP server terminated with an error, adjust status code");
1821 master
->userDataDone
= 451;
1823 completeDataDownload();
1826 void Ftp::Server::completeDataDownload()
1828 writeCustomReply(master
->userDataDone
, master
->userDataDone
== 226 ? "Transfer complete" : "Server error; transfer aborted");
1829 closeDataConnection();
1832 /// Whether Squid FTP Relay supports a named feature (e.g., a command).
1834 Ftp::SupportedCommand(const SBuf
&name
)
1836 static std::set
<SBuf
> BlockList
;
1837 if (BlockList
.empty()) {
1838 /* Add FTP commands that Squid cannot relay correctly. */
1840 // We probably do not support AUTH TLS.* and AUTH SSL,
1841 // but let's disclaim all AUTH support to KISS, for now.
1842 BlockList
.insert(cmdAuth());
1845 // we claim support for all commands that we do not know about
1846 return BlockList
.find(name
) == BlockList
.end();