First draft
[ntk/apt.git] / methods / ftp.cc
CommitLineData
30b30ec1
AL
1// -*- mode: cpp; mode: fold -*-
2// Description /*{{{*/
3// $Id: ftp.cc,v 1.1 1999/03/15 06:00:59 jgg Exp $
4/* ######################################################################
5
6 HTTP Aquire Method - This is the FTP aquire method for APT.
7
8 This is a very simple implementation that does not try to optimize
9 at all. Commands are sent syncronously with the FTP server (as the
10 rfc recommends, but it is not really necessary..) and no tricks are
11 done to speed things along.
12
13 ##################################################################### */
14 /*}}}*/
15// Include Files /*{{{*/
16#include <apt-pkg/fileutl.h>
17#include <apt-pkg/acquire-method.h>
18#include <apt-pkg/error.h>
19#include <apt-pkg/md5.h>
20#include "ftp.h"
21
22#include <sys/stat.h>
23#include <sys/time.h>
24#include <utime.h>
25#include <unistd.h>
26#include <signal.h>
27#include <stdio.h>
28#include <errno.h>
29#include <stdarg.h>
30
31// Internet stuff
32#include <netinet/in.h>
33#include <sys/socket.h>
34#include <arpa/inet.h>
35#include <netdb.h>
36
37 /*}}}*/
38
39unsigned long TimeOut = 120;
40URI Proxy;
41bool Debug;
42
43// FTPConn::FTPConn - Constructor /*{{{*/
44// ---------------------------------------------------------------------
45/* */
46FTPConn::FTPConn(URI Srv) : Len(0), ServerFd(-1), DataFd(-1),
47 DataListenFd(-1), ServerName(Srv)
48{
49 Debug = true;
50 memset(&PasvAddr,0,sizeof(PasvAddr));
51}
52 /*}}}*/
53// FTPConn::~FTPConn - Destructor /*{{{*/
54// ---------------------------------------------------------------------
55/* */
56FTPConn::~FTPConn()
57{
58 Close();
59}
60 /*}}}*/
61// FTPConn::Close - Close down the connection /*{{{*/
62// ---------------------------------------------------------------------
63/* Just tear down the socket and data socket */
64void FTPConn::Close()
65{
66 close(ServerFd);
67 ServerFd = -1;
68 close(DataFd);
69 DataFd = -1;
70 close(DataListenFd);
71 DataListenFd = -1;
72 memset(&PasvAddr,0,sizeof(PasvAddr));
73}
74 /*}}}*/
75// FTPConn::Open - Open a new connection /*{{{*/
76// ---------------------------------------------------------------------
77/* Connect to the server using a non-blocking connection and perform a
78 login. */
79string LastHost;
80in_addr LastHostA;
81bool FTPConn::Open()
82{
83 // Use the already open connection if possible.
84 if (ServerFd != -1)
85 return true;
86
87 Close();
88
89 // Determine the proxy setting
90 if (getenv("ftp_proxy") == 0)
91 {
92 string DefProxy = _config->Find("Acquire::ftp::Proxy");
93 string SpecificProxy = _config->Find("Acquire::ftp::Proxy::" + ServerName.Host);
94 if (SpecificProxy.empty() == false)
95 {
96 if (SpecificProxy == "DIRECT")
97 Proxy = "";
98 else
99 Proxy = SpecificProxy;
100 }
101 else
102 Proxy = DefProxy;
103 }
104 else
105 Proxy = getenv("ftp_proxy");
106
107 // Determine what host and port to use based on the proxy settings
108 int Port = 21;
109 string Host;
110 if (Proxy.empty() == true)
111 {
112 if (ServerName.Port != 0)
113 Port = ServerName.Port;
114 else
115 ServerName.Port = Port;
116 Host = ServerName.Host;
117 }
118 else
119 {
120 if (Proxy.Port != 0)
121 Port = Proxy.Port;
122 Host = Proxy.Host;
123 }
124
125 /* We used a cached address record.. Yes this is against the spec but
126 the way we have setup our rotating dns suggests that this is more
127 sensible */
128 if (LastHost != Host)
129 {
130// Owner->Status("Connecting to %s",Host.c_str());
131
132 // Lookup the host
133 hostent *Addr = gethostbyname(Host.c_str());
134 if (Addr == 0 || Addr->h_addr_list[0] == 0)
135 return _error->Error("Could not resolve '%s'",Host.c_str());
136 LastHost = Host;
137 LastHostA = *(in_addr *)(Addr->h_addr_list[0]);
138 }
139
140// Owner->Status("Connecting to %s (%s)",Host.c_str(),inet_ntoa(LastHostA));
141
142 // Get a socket
143 if ((ServerFd = socket(AF_INET,SOCK_STREAM,0)) < 0)
144 return _error->Errno("socket","Could not create a socket");
145
146 // Connect to the server
147 struct sockaddr_in server;
148 server.sin_family = AF_INET;
149 server.sin_port = htons(Port);
150 server.sin_addr = LastHostA;
151 SetNonBlock(ServerFd,true);
152 if (connect(ServerFd,(sockaddr *)&server,sizeof(server)) < 0 &&
153 errno != EINPROGRESS)
154 return _error->Errno("socket","Could not create a socket");
155 Peer = server;
156
157 /* This implements a timeout for connect by opening the connection
158 nonblocking */
159 if (WaitFd(ServerFd,true,TimeOut) == false)
160 return _error->Error("Could not connect, connection timed out");
161 unsigned int Err;
162 unsigned int Len = sizeof(Err);
163 if (getsockopt(ServerFd,SOL_SOCKET,SO_ERROR,&Err,&Len) != 0)
164 return _error->Errno("getsockopt","Failed");
165 if (Err != 0)
166 return _error->Error("Could not connect.");
167
168 return Login();
169}
170 /*}}}*/
171// FTPConn::Login - Login to the remote server /*{{{*/
172// ---------------------------------------------------------------------
173/* This performs both normal login and proxy login using a simples script
174 stored in the config file. */
175bool FTPConn::Login()
176{
177 unsigned int Tag;
178 string Msg;
179
180 // Setup the variables needed for authentication
181 string User = "anonymous";
182 string Pass = "apt_get_ftp_2.0@debian.linux.user";
183
184 // Fill in the user/pass
185 if (ServerName.User.empty() == false)
186 User = ServerName.User;
187 if (ServerName.Password.empty() == false)
188 Pass = ServerName.Password;
189
190 // Perform simple login
191 if (Proxy.empty() == true)
192 {
193 // Read the initial response
194 if (ReadResp(Tag,Msg) == false)
195 return false;
196 if (Tag >= 400)
197 return _error->Error("Server refused our connection and said: %s",Msg.c_str());
198
199 // Send the user
200 if (WriteMsg(Tag,Msg,"USER %s",User.c_str()) == false)
201 return false;
202 if (Tag >= 400)
203 return _error->Error("USER failed, server said: %s",Msg.c_str());
204
205 // Send the Password
206 if (WriteMsg(Tag,Msg,"PASS %s",Pass.c_str()) == false)
207 return false;
208 if (Tag >= 400)
209 return _error->Error("PASS failed, server said: %s",Msg.c_str());
210
211 // Enter passive mode
212 TryPassive = false;
213 if (_config->Exists("Acquire::FTP::Passive::" + ServerName.Host) == true &&
214 _config->FindB("Acquire::FTP::Passive::" + ServerName.Host,true) == true)
215 {
216 TryPassive = true;
217 }
218 else
219 {
220 if (_config->FindB("Acquire::FTP::Passive",true) == true)
221 TryPassive = true;
222 }
223 }
224 else
225 {
226 // Read the initial response
227 if (ReadResp(Tag,Msg) == false)
228 return false;
229 if (Tag >= 400)
230 return _error->Error("Server refused our connection and said: %s",Msg.c_str());
231
232 // Perform proxy script execution
233 Configuration::Item const *Opts = _config->Tree("Acquire::ftp::ProxyLogin");
234 if (Opts == 0 || Opts->Child == 0)
235 return _error->Error("A proxy server was specified but no login "
236 "script, Acquire::ftp::ProxyLogin is empty.");
237 Opts = Opts->Child;
238
239 // Iterate over the entire login script
240 for (; Opts != 0; Opts = Opts->Next)
241 {
242 if (Opts->Value.empty() == true)
243 continue;
244
245 // Substitute the variables into the command
246 char SitePort[20];
247 sprintf(SitePort,"%u",ServerName.Port);
248 string Tmp = Opts->Value;
249 Tmp = SubstVar(Tmp,"$(PROXY_USER)",Proxy.User);
250 Tmp = SubstVar(Tmp,"$(PROXY_PASS)",Proxy.Password);
251 Tmp = SubstVar(Tmp,"$(SITE_USER)",User);
252 Tmp = SubstVar(Tmp,"$(SITE_PASS)",Pass);
253 Tmp = SubstVar(Tmp,"$(SITE_PORT)",SitePort);
254 Tmp = SubstVar(Tmp,"$(SITE)",ServerName.Host);
255
256 // Send the command
257 if (WriteMsg(Tag,Msg,"%s",Tmp.c_str()) == false)
258 return false;
259 if (Tag >= 400)
260 return _error->Error("Login script command '%s' failed, server said: %s",Tmp.c_str(),Msg.c_str());
261 }
262
263 // Enter passive mode
264 TryPassive = false;
265 if (_config->Exists("Acquire::FTP::Passive::" + ServerName.Host) == true &&
266 _config->FindB("Acquire::FTP::Passive::" + ServerName.Host,true) == true)
267 {
268 TryPassive = true;
269 }
270 else
271 {
272 if (_config->Exists("Acquire::FTP::Proxy::Passive") == true &&
273 _config->FindB("Acquire::FTP::Proxy::Passive",true) == true)
274 TryPassive = true;
275 else
276 if (_config->FindB("Acquire::FTP::Passive",true) == true)
277 TryPassive = true;
278 }
279 }
280
281 // Binary mode
282 if (WriteMsg(Tag,Msg,"TYPE I") == false)
283 return false;
284 if (Tag >= 400)
285 return _error->Error("TYPE failed, server said: %s",Msg.c_str());
286
287 return true;
288}
289 /*}}}*/
290// FTPConn::ReadLine - Read a line from the server /*{{{*/
291// ---------------------------------------------------------------------
292/* This performs a very simple buffered read. */
293bool FTPConn::ReadLine(string &Text)
294{
295 // Suck in a line
296 while (Len < sizeof(Buffer))
297 {
298 // Scan the buffer for a new line
299 for (unsigned int I = 0; I != Len; I++)
300 {
301 // Escape some special chars
302 if (Buffer[I] == 0)
303 Buffer[I] = '?';
304
305 // End of line?
306 if (Buffer[I] != '\n')
307 continue;
308
309 I++;
310 Text = string(Buffer,I);
311 memmove(Buffer,Buffer+I,Len - I);
312 Len -= I;
313 return true;
314 }
315
316 // Wait for some data..
317 if (WaitFd(ServerFd,false,TimeOut) == false)
318 return _error->Error("Connection timeout");
319
320 // Suck it back
321 int Res = read(ServerFd,Buffer,sizeof(Buffer) - Len);
322 if (Res <= 0)
323 return _error->Errno("read","Read error");
324 Len += Res;
325 }
326
327 return _error->Error("A response overflowed the buffer.");
328}
329 /*}}}*/
330// FTPConn::ReadResp - Read a full response from the server /*{{{*/
331// ---------------------------------------------------------------------
332/* This reads a reply code from the server, it handles both p */
333bool FTPConn::ReadResp(unsigned int &Ret,string &Text)
334{
335 // Grab the first line of the response
336 string Msg;
337 if (ReadLine(Msg) == false)
338 return false;
339
340 // Get the ID code
341 char *End;
342 Ret = strtol(Msg.c_str(),&End,10);
343 if (End - Msg.c_str() != 3)
344 return _error->Error("Protocol corruption");
345
346 // All done ?
347 Text = Msg.c_str()+4;
348 if (*End == ' ')
349 {
350 if (Debug == true)
351 cout << "<- '" << QuoteString(Text,"") << "'" << endl;
352 return true;
353 }
354
355 if (*End != '-')
356 return _error->Error("Protocol corruption");
357
358 /* Okay, here we do the continued message trick. This is foolish, but
359 proftpd follows the protocol as specified and wu-ftpd doesn't, so
360 we filter. I wonder how many clients break if you use proftpd and
361 put a '- in the 3rd spot in the message? */
362 char Leader[4];
363 strncpy(Leader,Msg.c_str(),3);
364 Leader[3] = 0;
365 while (ReadLine(Msg) == true)
366 {
367 // Short, it must be using RFC continuation..
368 if (Msg.length() < 4)
369 {
370 Text += Msg;
371 continue;
372 }
373
374 // Oops, finished
375 if (strncmp(Msg.c_str(),Leader,3) == 0 && Msg[3] == ' ')
376 {
377 Text += Msg.c_str()+4;
378 break;
379 }
380
381 // This message has the wu-ftpd style reply code prefixed
382 if (strncmp(Msg.c_str(),Leader,3) == 0 && Msg[3] == '-')
383 {
384 Text += Msg.c_str()+4;
385 continue;
386 }
387
388 // Must be RFC style prefixing
389 Text += Msg;
390 }
391
392 if (Debug == true && _error->PendingError() == false)
393 cout << "<- '" << QuoteString(Text,"") << "'" << endl;
394
395 return !_error->PendingError();
396}
397 /*}}}*/
398// FTPConn::WriteMsg - Send a message to the server /*{{{*/
399// ---------------------------------------------------------------------
400/* Simple printf like function.. */
401bool FTPConn::WriteMsg(unsigned int &Ret,string &Text,const char *Fmt,...)
402{
403 va_list args;
404 va_start(args,Fmt);
405
406 // sprintf the description
407 char S[400];
408 vsnprintf(S,sizeof(S) - 4,Fmt,args);
409 strcat(S,"\r\n");
410
411 if (Debug == true)
412 cout << "-> '" << QuoteString(S,"") << "'" << endl;
413
414 // Send it off
415 unsigned long Len = strlen(S);
416 unsigned long Start = 0;
417 while (Len != 0)
418 {
419 if (WaitFd(ServerFd,true,TimeOut) == false)
420 return _error->Error("Connection timeout");
421
422 int Res = write(ServerFd,S + Start,Len);
423 if (Res <= 0)
424 return _error->Errno("write","Write Error");
425 Len -= Res;
426 Start += Res;
427 }
428
429 return ReadResp(Ret,Text);
430}
431 /*}}}*/
432// FTPConn::GoPasv - Enter Passive mode /*{{{*/
433// ---------------------------------------------------------------------
434/* Try to enter passive mode, the return code does not indicate if passive
435 mode could or could not be established, only if there was a fatal error.
436 Borrowed mostly from lftp. We have to enter passive mode every time
437 we make a data connection :| */
438bool FTPConn::GoPasv()
439{
440 // Try to enable pasv mode
441 unsigned int Tag;
442 string Msg;
443 if (WriteMsg(Tag,Msg,"PASV") == false)
444 return false;
445
446 // Unsupported function
447 string::size_type Pos = Msg.find('(');
448 if (Tag >= 400 || Pos == string::npos)
449 {
450 memset(&PasvAddr,0,sizeof(PasvAddr));
451 return true;
452 }
453
454 // Scan it
455 unsigned a0,a1,a2,a3,p0,p1;
456 if (sscanf(Msg.c_str() + Pos,"(%u,%u,%u,%u,%u,%u)",&a0,&a1,&a2,&a3,&p0,&p1) != 6)
457 {
458 memset(&PasvAddr,0,sizeof(PasvAddr));
459 return true;
460 }
461
462 // lftp used this horrid byte order manipulation.. Ik.
463 PasvAddr.sin_family = AF_INET;
464 unsigned char *a;
465 unsigned char *p;
466 a = (unsigned char *)&PasvAddr.sin_addr;
467 p = (unsigned char *)&PasvAddr.sin_port;
468
469 // Some evil servers return 0 to mean their addr
470 if (a0 == 0 && a1 == 0 && a2 == 0 && a3 == 0)
471 {
472 PasvAddr.sin_addr = Peer.sin_addr;
473 }
474 else
475 {
476 a[0] = a0;
477 a[1] = a1;
478 a[2] = a2;
479 a[3] = a3;
480 }
481
482 p[0] = p0;
483 p[1] = p1;
484
485 return true;
486}
487 /*}}}*/
488// FTPConn::Size - Return the size of a file /*{{{*/
489// ---------------------------------------------------------------------
490/* Grab the file size from the server, 0 means no size or empty file */
491unsigned long FTPConn::Size(const char *Path)
492{
493 // Query the size
494 unsigned int Tag;
495 string Msg;
496 if (WriteMsg(Tag,Msg,"SIZE %s",Path) == false)
497 return false;
498
499 char *End;
500 unsigned long Size = strtol(Msg.c_str(),&End,10);
501 if (Tag >= 400 || End == Msg.c_str())
502 return 0;
503 return Size;
504}
505 /*}}}*/
506// FTPConn::ModTime - Return the modification time of the file /*{{{*/
507// ---------------------------------------------------------------------
508/* Like Size no error is returned if the command is not supported. If the
509 command fails then time is set to the current time of day to fool
510 date checks. */
511bool FTPConn::ModTime(const char *Path, time_t &Time)
512{
513 Time = time(&Time);
514
515 // Query the mod time
516 unsigned int Tag;
517 string Msg;
518 if (WriteMsg(Tag,Msg,"MDTM %s",Path) == false)
519 return false;
520 if (Tag >= 400 || Msg.empty() == true || isdigit(Msg[0]) == 0)
521 return true;
522
523 // Parse it
524 struct tm tm;
525 memset(&tm,0,sizeof(tm));
526 if (sscanf(Msg.c_str(),"%4d%2d%2d%2d%2d%2d",&tm.tm_year,&tm.tm_mon,
527 &tm.tm_mday,&tm.tm_hour,&tm.tm_min,&tm.tm_sec) != 6)
528 return true;
529
530 tm.tm_year -= 1900;
531 tm.tm_mon--;
532
533 /* We use timegm from the GNU C library, libapt-pkg will provide this
534 symbol if it does not exist */
535 Time = timegm(&tm);
536 return true;
537}
538 /*}}}*/
539// FTPConn::CreateDataFd - Get a data connection /*{{{*/
540// ---------------------------------------------------------------------
541/* Create the data connection. Call FinalizeDataFd after this though.. */
542bool FTPConn::CreateDataFd()
543{
544 close(DataFd);
545 DataFd = -1;
546
547 // Attempt to enter passive mode.
548 if (TryPassive == true)
549 {
550 if (GoPasv() == false)
551 return false;
552
553 // Oops, didn't work out, don't bother trying again.
554 if (PasvAddr.sin_port == 0)
555 TryPassive = false;
556 }
557
558 // Passive mode?
559 if (PasvAddr.sin_port != 0)
560 {
561 // Get a socket
562 if ((DataFd = socket(AF_INET,SOCK_STREAM,0)) < 0)
563 return _error->Errno("socket","Could not create a socket");
564
565 // Connect to the server
566 SetNonBlock(DataFd,true);
567 if (connect(DataFd,(sockaddr *)&PasvAddr,sizeof(PasvAddr)) < 0 &&
568 errno != EINPROGRESS)
569 return _error->Errno("socket","Could not create a socket");
570
571 /* This implements a timeout for connect by opening the connection
572 nonblocking */
573 if (WaitFd(ServerFd,true,TimeOut) == false)
574 return _error->Error("Could not connect data socket, connection timed out");
575 unsigned int Err;
576 unsigned int Len = sizeof(Err);
577 if (getsockopt(ServerFd,SOL_SOCKET,SO_ERROR,&Err,&Len) != 0)
578 return _error->Errno("getsockopt","Failed");
579 if (Err != 0)
580 return _error->Error("Could not connect.");
581
582 return true;
583 }
584
585 // Port mode :<
586 if (DataListenFd == -1)
587 {
588 // Get a socket
589 if ((DataListenFd = socket(AF_INET,SOCK_STREAM,0)) < 0)
590 return _error->Errno("socket","Could not create a socket");
591
592 // Bind and listen
593 sockaddr_in Addr;
594 memset(&Addr,0,sizeof(Addr));
595 if (bind(DataListenFd,(sockaddr *)&Addr,sizeof(Addr)) < 0)
596 return _error->Errno("bind","Could not bind a socket");
597 if (listen(DataListenFd,1) < 0)
598 return _error->Errno("listen","Could not listen on the socket");
599 SetNonBlock(DataListenFd,true);
600 }
601
602 // Determine the name to send to the remote
603 sockaddr_in Addr;
604 sockaddr_in Addr2;
605 socklen_t Jnk = sizeof(Addr);
606 if (getsockname(DataListenFd,(sockaddr *)&Addr,&Jnk) < 0)
607 return _error->Errno("getsockname","Could not determine the socket's name");
608 Jnk = sizeof(Addr2);
609 if (getsockname(ServerFd,(sockaddr *)&Addr2,&Jnk) < 0)
610 return _error->Errno("getsockname","Could not determine the socket's name");
611
612 // This bit ripped from qftp
613 unsigned long badr = ntohl(*(unsigned long *)&Addr2.sin_addr);
614 unsigned long bp = ntohs(Addr.sin_port);
615
616 // Send the port command
617 unsigned int Tag;
618 string Msg;
619 if (WriteMsg(Tag,Msg,"PORT %d,%d,%d,%d,%d,%d",
620 (int) (badr >> 24) & 0xff, (int) (badr >> 16) & 0xff,
621 (int) (badr >> 8) & 0xff, (int) badr & 0xff,
622 (int) (bp >> 8) & 0xff, (int) bp & 0xff) == false)
623 return false;
624 if (Tag >= 400)
625 return _error->Error("Unable to send port command");
626
627 return true;
628}
629 /*}}}*/
630// FTPConn::Finalize - Complete the Data connection /*{{{*/
631// ---------------------------------------------------------------------
632/* If the connection is in port mode this waits for the other end to hook
633 up to us. */
634bool FTPConn::Finalize()
635{
636 // Passive mode? Do nothing
637 if (PasvAddr.sin_port != 0)
638 return true;
639
640 // Close any old socket..
641 close(DataFd);
642 DataFd = -1;
643
644 // Wait for someone to connect..
645 if (WaitFd(DataListenFd,false,TimeOut) == false)
646 return _error->Error("Data socket connect timed out");
647
648 // Accept the connection
649 struct sockaddr_in Addr;
650 socklen_t Len = sizeof(Addr);
651 DataFd = accept(DataListenFd,(struct sockaddr *)&Addr,&Len);
652 if (DataFd < 0)
653 return _error->Errno("accept","Unable to accept connection");
654
655 return true;
656}
657 /*}}}*/
658// FTPConn::Get - Get a file /*{{{*/
659// ---------------------------------------------------------------------
660/* This opens a data connection, sends REST and RETR and then
661 transfers the file over. */
662bool FTPConn::Get(const char *Path,FileFd &To,unsigned long Resume)
663{
664 if (CreateDataFd() == false)
665 return false;
666
667 unsigned int Tag;
668 string Msg;
669 if (Resume != 0)
670 {
671 if (WriteMsg(Tag,Msg,"REST %u",Resume) == false)
672 return false;
673 if (Tag >= 400)
674 Resume = 0;
675 }
676
677 if (To.Truncate(Resume) == false)
678 return false;
679
680 // Send the get command
681 if (WriteMsg(Tag,Msg,"RETR %s",Path) == false)
682 return false;
683
684 if (Tag >= 400)
685 return _error->Error("Unable to fetch file, server said '%s'",Msg.c_str());
686
687 // Finish off the data connection
688 if (Finalize() == false)
689 return false;
690
691 // Copy loop
692 unsigned char Buffer[4096];
693 while (1)
694 {
695 // Wait for some data..
696 if (WaitFd(DataFd,false,TimeOut) == false)
697 return _error->Error("Data socket connect timed out");
698
699 // Read the data..
700 int Res = read(DataFd,Buffer,sizeof(Buffer));
701 if (Res == 0)
702 break;
703 if (Res < 0)
704 {
705 if (errno == EAGAIN)
706 continue;
707 break;
708 }
709
710 if (To.Write(Buffer,Res) == false)
711 return false;
712 }
713
714 // All done
715 close(DataFd);
716 DataFd = -1;
717
718 // Read the closing message from the server
719 if (ReadResp(Tag,Msg) == false)
720 return false;
721 if (Tag >= 400)
722 return _error->Error("Data transfer failed, server said '%s'",Msg.c_str());
723 return true;
724}
725 /*}}}*/
726
727int main()
728{
729 FTPConn Con(URI("ftp://va.debian.org/debian/README"));
730 string Msg;
731 _config->Set("Acquire::FTP::Passive","false");
732
733 while (1)
734 {
735 if (Con.Open() == false)
736 break;
737 cout << "Size: " << Con.Size("/debian/README") << endl;
738
739 time_t Time;
740 Con.ModTime("/debian/README",Time);
741 cout << "Time: " << TimeRFC1123(Time) << endl;
742
743 {
744
745 FileFd F("t",FileFd::WriteEmpty);
746 Con.Get("/debian/README",F);
747 }
748
749 {
750
751 FileFd F("t3",FileFd::WriteEmpty);
752 Con.Get("/debian/README.pgp",F);
753 }
754
755 break;
756 }
757
758 _error->DumpErrors();
759 return 0;
760}