1161764Sobrien/* $NetBSD: cmds.c,v 1.24 2006/02/01 14:20:12 christos Exp $ */ 279968Sobrien 379968Sobrien/* 4133936Sobrien * Copyright (c) 1999-2004 The NetBSD Foundation, Inc. 579968Sobrien * All rights reserved. 679968Sobrien * 779968Sobrien * This code is derived from software contributed to The NetBSD Foundation 879968Sobrien * by Luke Mewburn. 979968Sobrien * 1079968Sobrien * Redistribution and use in source and binary forms, with or without 1179968Sobrien * modification, are permitted provided that the following conditions 1279968Sobrien * are met: 1379968Sobrien * 1. Redistributions of source code must retain the above copyright 1479968Sobrien * notice, this list of conditions and the following disclaimer. 1579968Sobrien * 2. Redistributions in binary form must reproduce the above copyright 1679968Sobrien * notice, this list of conditions and the following disclaimer in the 1779968Sobrien * documentation and/or other materials provided with the distribution. 1879968Sobrien * 3. All advertising materials mentioning features or use of this software 1979968Sobrien * must display the following acknowledgement: 2079968Sobrien * This product includes software developed by the NetBSD 2179968Sobrien * Foundation, Inc. and its contributors. 2279968Sobrien * 4. Neither the name of The NetBSD Foundation nor the names of its 2379968Sobrien * contributors may be used to endorse or promote products derived 2479968Sobrien * from this software without specific prior written permission. 2579968Sobrien * 2679968Sobrien * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS 2779968Sobrien * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 2879968Sobrien * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 2979968Sobrien * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS 3079968Sobrien * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 3179968Sobrien * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 3279968Sobrien * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 3379968Sobrien * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 3479968Sobrien * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 3579968Sobrien * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 3679968Sobrien * POSSIBILITY OF SUCH DAMAGE. 3779968Sobrien */ 3879968Sobrien 3979968Sobrien/* 4079968Sobrien * Copyright (c) 1985, 1988, 1990, 1992, 1993, 1994 4179968Sobrien * The Regents of the University of California. All rights reserved. 4279968Sobrien * 4379968Sobrien * Redistribution and use in source and binary forms, with or without 4479968Sobrien * modification, are permitted provided that the following conditions 4579968Sobrien * are met: 4679968Sobrien * 1. Redistributions of source code must retain the above copyright 4779968Sobrien * notice, this list of conditions and the following disclaimer. 4879968Sobrien * 2. Redistributions in binary form must reproduce the above copyright 4979968Sobrien * notice, this list of conditions and the following disclaimer in the 5079968Sobrien * documentation and/or other materials provided with the distribution. 51133936Sobrien * 3. Neither the name of the University nor the names of its contributors 5279968Sobrien * may be used to endorse or promote products derived from this software 5379968Sobrien * without specific prior written permission. 5479968Sobrien * 5579968Sobrien * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 5679968Sobrien * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 5779968Sobrien * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 5879968Sobrien * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 5979968Sobrien * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 6079968Sobrien * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 6179968Sobrien * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 6279968Sobrien * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 6379968Sobrien * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 6479968Sobrien * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 6579968Sobrien * SUCH DAMAGE. 6679968Sobrien */ 6779968Sobrien 6879968Sobrien/* 6979968Sobrien * Copyright (C) 1997 and 1998 WIDE Project. 7079968Sobrien * All rights reserved. 7179968Sobrien * 7279968Sobrien * Redistribution and use in source and binary forms, with or without 7379968Sobrien * modification, are permitted provided that the following conditions 7479968Sobrien * are met: 7579968Sobrien * 1. Redistributions of source code must retain the above copyright 7679968Sobrien * notice, this list of conditions and the following disclaimer. 7779968Sobrien * 2. Redistributions in binary form must reproduce the above copyright 7879968Sobrien * notice, this list of conditions and the following disclaimer in the 7979968Sobrien * documentation and/or other materials provided with the distribution. 8079968Sobrien * 3. Neither the name of the project nor the names of its contributors 8179968Sobrien * may be used to endorse or promote products derived from this software 8279968Sobrien * without specific prior written permission. 8379968Sobrien * 8479968Sobrien * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND 8579968Sobrien * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 8679968Sobrien * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 8779968Sobrien * ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE 8879968Sobrien * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 8979968Sobrien * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 9079968Sobrien * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 9179968Sobrien * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 9279968Sobrien * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 9379968Sobrien * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 9479968Sobrien * SUCH DAMAGE. 9579968Sobrien */ 9679968Sobrien 9779968Sobrien 98108746Sobrien#include <sys/cdefs.h> 99108746Sobrien#ifndef lint 100161764Sobrien__RCSID("$NetBSD: cmds.c,v 1.24 2006/02/01 14:20:12 christos Exp $"); 101108746Sobrien#endif /* not lint */ 102108746Sobrien 103108746Sobrien#include <sys/param.h> 104108746Sobrien#include <sys/stat.h> 105108746Sobrien 106108746Sobrien#include <arpa/ftp.h> 107108746Sobrien 108108746Sobrien#include <dirent.h> 109108746Sobrien#include <errno.h> 110108746Sobrien#include <stdio.h> 111108746Sobrien#include <stdlib.h> 112108746Sobrien#include <string.h> 113108746Sobrien#include <tzfile.h> 114108746Sobrien#include <unistd.h> 115108746Sobrien#include <ctype.h> 116108746Sobrien 117108746Sobrien#ifdef KERBEROS5 118108746Sobrien#include <krb5/krb5.h> 119108746Sobrien#endif 120108746Sobrien 12179968Sobrien#include "extern.h" 12279968Sobrien 12392282Sobrientypedef enum { 12492282Sobrien FE_MLSD = 1<<0, /* if op is MLSD (MLST otherwise ) */ 12592282Sobrien FE_ISCURDIR = 1<<1, /* if name is the current directory */ 12692282Sobrien} factflag_t; 12792282Sobrien 12879968Sobrientypedef struct { 12979968Sobrien const char *path; /* full pathname */ 13079968Sobrien const char *display; /* name to display */ 13179968Sobrien struct stat *stat; /* stat of path */ 13279968Sobrien struct stat *pdirstat; /* stat of path's parent dir */ 13392282Sobrien factflag_t flags; /* flags */ 13479968Sobrien} factelem; 13579968Sobrien 13679968Sobrienstatic void ack(const char *); 13779968Sobrienstatic void base64_encode(const char *, size_t, char *, int); 13879968Sobrienstatic void fact_type(const char *, FILE *, factelem *); 13979968Sobrienstatic void fact_size(const char *, FILE *, factelem *); 14079968Sobrienstatic void fact_modify(const char *, FILE *, factelem *); 14179968Sobrienstatic void fact_perm(const char *, FILE *, factelem *); 14279968Sobrienstatic void fact_unique(const char *, FILE *, factelem *); 14379968Sobrienstatic int matchgroup(gid_t); 14479968Sobrienstatic void mlsname(FILE *, factelem *); 14579968Sobrienstatic void replydirname(const char *, const char *); 14679968Sobrien 14779968Sobrienstruct ftpfact { 14879968Sobrien const char *name; /* name of fact */ 14979968Sobrien int enabled; /* if fact is enabled */ 15079968Sobrien void (*display)(const char *, FILE *, factelem *); 15179968Sobrien /* function to display fact */ 15279968Sobrien}; 15379968Sobrien 15479968Sobrienstruct ftpfact facttab[] = { 15579968Sobrien { "Type", 1, fact_type }, 15679968Sobrien#define FACT_TYPE 0 15779968Sobrien { "Size", 1, fact_size }, 15879968Sobrien { "Modify", 1, fact_modify }, 15979968Sobrien { "Perm", 1, fact_perm }, 16079968Sobrien { "Unique", 1, fact_unique }, 16179968Sobrien /* "Create" */ 16279968Sobrien /* "Lang" */ 16379968Sobrien /* "Media-Type" */ 16479968Sobrien /* "CharSet" */ 16579968Sobrien}; 16679968Sobrien 16779968Sobrien#define FACTTABSIZE (sizeof(facttab) / sizeof(struct ftpfact)) 16879968Sobrien 169110242Sobrienstatic char cached_path[MAXPATHLEN + 1] = "/"; 170110242Sobrienstatic void discover_path(char *, const char *); 17179968Sobrien 17279968Sobrienvoid 17379968Sobriencwd(const char *path) 17479968Sobrien{ 17579968Sobrien 17679968Sobrien if (chdir(path) < 0) 17779968Sobrien perror_reply(550, path); 17879968Sobrien else { 17979968Sobrien show_chdir_messages(250); 18079968Sobrien ack("CWD"); 181110242Sobrien if (getcwd(cached_path, MAXPATHLEN) == NULL) { 182110242Sobrien discover_path(cached_path, path); 183110242Sobrien } 18479968Sobrien } 18579968Sobrien} 18679968Sobrien 18779968Sobrienvoid 18879968Sobriendelete(const char *name) 18979968Sobrien{ 19079968Sobrien char *p = NULL; 19179968Sobrien 19279968Sobrien if (remove(name) < 0) { 19379968Sobrien p = strerror(errno); 19479968Sobrien perror_reply(550, name); 19579968Sobrien } else 19679968Sobrien ack("DELE"); 19779968Sobrien logxfer("delete", -1, name, NULL, NULL, p); 19879968Sobrien} 19979968Sobrien 20079968Sobrienvoid 20179968Sobrienfeat(void) 20279968Sobrien{ 20379968Sobrien int i; 20479968Sobrien 20579968Sobrien reply(-211, "Features supported"); 20679968Sobrien cprintf(stdout, " MDTM\r\n"); 20779968Sobrien cprintf(stdout, " MLST "); 20879968Sobrien for (i = 0; i < FACTTABSIZE; i++) 20979968Sobrien cprintf(stdout, "%s%s;", facttab[i].name, 21079968Sobrien facttab[i].enabled ? "*" : ""); 21179968Sobrien cprintf(stdout, "\r\n"); 21279968Sobrien cprintf(stdout, " REST STREAM\r\n"); 21379968Sobrien cprintf(stdout, " SIZE\r\n"); 21479968Sobrien cprintf(stdout, " TVFS\r\n"); 21579968Sobrien reply(211, "End"); 21679968Sobrien} 21779968Sobrien 21879968Sobrienvoid 21979968Sobrienmakedir(const char *name) 22079968Sobrien{ 22179968Sobrien char *p = NULL; 22279968Sobrien 22379968Sobrien if (mkdir(name, 0777) < 0) { 22479968Sobrien p = strerror(errno); 22579968Sobrien perror_reply(550, name); 22679968Sobrien } else 22779968Sobrien replydirname(name, "directory created."); 22879968Sobrien logxfer("mkdir", -1, name, NULL, NULL, p); 22979968Sobrien} 23079968Sobrien 23179968Sobrienvoid 23279968Sobrienmlsd(const char *path) 23379968Sobrien{ 23479968Sobrien struct dirent *dp; 23579968Sobrien struct stat sb, pdirstat; 23679968Sobrien factelem f; 23779968Sobrien FILE *dout; 23879968Sobrien DIR *dirp; 23979968Sobrien char name[MAXPATHLEN]; 24079968Sobrien int hastypefact; 24179968Sobrien 24279968Sobrien hastypefact = facttab[FACT_TYPE].enabled; 24379968Sobrien if (path == NULL) 24479968Sobrien path = "."; 24579968Sobrien if (stat(path, &pdirstat) == -1) { 24679968Sobrien mlsdperror: 24779968Sobrien perror_reply(550, path); 24879968Sobrien return; 24979968Sobrien } 25079968Sobrien if (! S_ISDIR(pdirstat.st_mode)) { 25179968Sobrien errno = ENOTDIR; 25279968Sobrien perror_reply(501, path); 25379968Sobrien return; 25479968Sobrien } 25592282Sobrien if ((dirp = opendir(path)) == NULL) 25692282Sobrien goto mlsdperror; 25792282Sobrien 25879968Sobrien dout = dataconn("MLSD", (off_t)-1, "w"); 25979968Sobrien if (dout == NULL) 26079968Sobrien return; 26179968Sobrien 26292282Sobrien memset(&f, 0, sizeof(f)); 26379968Sobrien f.stat = &sb; 26492282Sobrien f.flags |= FE_MLSD; 26579968Sobrien while ((dp = readdir(dirp)) != NULL) { 26679968Sobrien snprintf(name, sizeof(name), "%s/%s", path, dp->d_name); 26779968Sobrien if (ISDOTDIR(dp->d_name)) { /* special case curdir: */ 26879968Sobrien if (! hastypefact) 26979968Sobrien continue; 27079968Sobrien f.pdirstat = NULL; /* require stat of parent */ 27179968Sobrien f.display = path; /* set name to real name */ 27292282Sobrien f.flags |= FE_ISCURDIR; /* flag name is curdir */ 27379968Sobrien } else { 27479968Sobrien if (ISDOTDOTDIR(dp->d_name)) { 27579968Sobrien if (! hastypefact) 27679968Sobrien continue; 27779968Sobrien f.pdirstat = NULL; 27879968Sobrien } else 27979968Sobrien f.pdirstat = &pdirstat; /* cache parent stat */ 28079968Sobrien f.display = dp->d_name; 28192282Sobrien f.flags &= ~FE_ISCURDIR; 28279968Sobrien } 28379968Sobrien if (stat(name, &sb) == -1) 28479968Sobrien continue; 28579968Sobrien f.path = name; 28679968Sobrien mlsname(dout, &f); 28779968Sobrien } 28879968Sobrien (void)closedir(dirp); 28979968Sobrien 29079968Sobrien if (ferror(dout) != 0) 29179968Sobrien perror_reply(550, "Data connection"); 29279968Sobrien else 29379968Sobrien reply(226, "MLSD complete."); 29479968Sobrien closedataconn(dout); 29579968Sobrien total_xfers_out++; 29679968Sobrien total_xfers++; 29779968Sobrien} 29879968Sobrien 29979968Sobrienvoid 30079968Sobrienmlst(const char *path) 30179968Sobrien{ 30279968Sobrien struct stat sb; 30379968Sobrien factelem f; 30479968Sobrien 30579968Sobrien if (path == NULL) 30679968Sobrien path = "."; 30779968Sobrien if (stat(path, &sb) == -1) { 30879968Sobrien perror_reply(550, path); 30979968Sobrien return; 31079968Sobrien } 31179968Sobrien reply(-250, "MLST %s", path); 31292282Sobrien memset(&f, 0, sizeof(f)); 31379968Sobrien f.path = path; 31479968Sobrien f.display = path; 31579968Sobrien f.stat = &sb; 31679968Sobrien f.pdirstat = NULL; 31779968Sobrien CPUTC(' ', stdout); 31879968Sobrien mlsname(stdout, &f); 31979968Sobrien reply(250, "End"); 32079968Sobrien} 32179968Sobrien 32279968Sobrien 32379968Sobrienvoid 32479968Sobrienopts(const char *command) 32579968Sobrien{ 32679968Sobrien struct tab *c; 32779968Sobrien char *ep; 32879968Sobrien 32979968Sobrien if ((ep = strchr(command, ' ')) != NULL) 33079968Sobrien *ep++ = '\0'; 33179968Sobrien c = lookup(cmdtab, command); 33279968Sobrien if (c == NULL) { 333108746Sobrien reply(502, "Unknown command '%s'.", command); 33479968Sobrien return; 33579968Sobrien } 33679968Sobrien if (! CMD_IMPLEMENTED(c)) { 337108746Sobrien reply(502, "%s command not implemented.", c->name); 33879968Sobrien return; 33979968Sobrien } 34079968Sobrien if (! CMD_HAS_OPTIONS(c)) { 34179968Sobrien reply(501, "%s command does not support persistent options.", 34279968Sobrien c->name); 34379968Sobrien return; 34479968Sobrien } 34579968Sobrien 34679968Sobrien /* special case: MLST */ 34779968Sobrien if (strcasecmp(command, "MLST") == 0) { 34879968Sobrien int enabled[FACTTABSIZE]; 34979968Sobrien int i, onedone; 35079968Sobrien size_t len; 35179968Sobrien char *p; 35279968Sobrien 35379968Sobrien for (i = 0; i < sizeof(enabled) / sizeof(int); i++) 35479968Sobrien enabled[i] = 0; 35579968Sobrien if (ep == NULL || *ep == '\0') 35679968Sobrien goto displaymlstopts; 35779968Sobrien 35879968Sobrien /* don't like spaces, and need trailing ; */ 35979968Sobrien len = strlen(ep); 36079968Sobrien if (strchr(ep, ' ') != NULL || ep[len - 1] != ';') { 36179968Sobrien badmlstopt: 36279968Sobrien reply(501, "Invalid MLST options"); 36379968Sobrien return; 36479968Sobrien } 36579968Sobrien ep[len - 1] = '\0'; 36679968Sobrien while ((p = strsep(&ep, ";")) != NULL) { 36779968Sobrien if (*p == '\0') 36879968Sobrien goto badmlstopt; 36979968Sobrien for (i = 0; i < FACTTABSIZE; i++) 37079968Sobrien if (strcasecmp(p, facttab[i].name) == 0) { 37179968Sobrien enabled[i] = 1; 37279968Sobrien break; 37379968Sobrien } 37479968Sobrien } 37579968Sobrien 37679968Sobrien displaymlstopts: 37779968Sobrien for (i = 0; i < FACTTABSIZE; i++) 37879968Sobrien facttab[i].enabled = enabled[i]; 37979968Sobrien cprintf(stdout, "200 MLST OPTS"); 38079968Sobrien for (i = onedone = 0; i < FACTTABSIZE; i++) { 38179968Sobrien if (facttab[i].enabled) { 38279968Sobrien cprintf(stdout, "%s%s;", onedone ? "" : " ", 38379968Sobrien facttab[i].name); 38479968Sobrien onedone++; 38579968Sobrien } 38679968Sobrien } 38779968Sobrien cprintf(stdout, "\r\n"); 38879968Sobrien fflush(stdout); 38979968Sobrien return; 39079968Sobrien } 39179968Sobrien 39279968Sobrien /* default cases */ 39379968Sobrien if (ep != NULL && *ep != '\0') 394161764Sobrien REASSIGN(c->options, ftpd_strdup(ep)); 39579968Sobrien if (c->options != NULL) 39679968Sobrien reply(200, "Options for %s are '%s'.", c->name, 39779968Sobrien c->options); 39879968Sobrien else 39979968Sobrien reply(200, "No options defined for %s.", c->name); 40079968Sobrien} 40179968Sobrien 40279968Sobrienvoid 40379968Sobrienpwd(void) 40479968Sobrien{ 40579968Sobrien char path[MAXPATHLEN]; 40679968Sobrien 407110242Sobrien if (getcwd(path, sizeof(path) - 1) == NULL) { 408110242Sobrien if (chdir(cached_path) < 0) { 409110242Sobrien reply(550, "Can't get the current directory: %s.", 410110242Sobrien strerror(errno)); 411110242Sobrien return; 412110242Sobrien } 413110242Sobrien (void)strlcpy(path, cached_path, MAXPATHLEN); 414110242Sobrien } 415110242Sobrien replydirname(path, "is the current directory."); 41679968Sobrien} 41779968Sobrien 41879968Sobrienvoid 41979968Sobrienremovedir(const char *name) 42079968Sobrien{ 42179968Sobrien char *p = NULL; 42279968Sobrien 42379968Sobrien if (rmdir(name) < 0) { 42479968Sobrien p = strerror(errno); 42579968Sobrien perror_reply(550, name); 42679968Sobrien } else 42779968Sobrien ack("RMD"); 42879968Sobrien logxfer("rmdir", -1, name, NULL, NULL, p); 42979968Sobrien} 43079968Sobrien 43179968Sobrienchar * 43279968Sobrienrenamefrom(const char *name) 43379968Sobrien{ 43479968Sobrien struct stat st; 43579968Sobrien 43679968Sobrien if (stat(name, &st) < 0) { 43779968Sobrien perror_reply(550, name); 43879968Sobrien return (NULL); 43979968Sobrien } 44079968Sobrien reply(350, "File exists, ready for destination name"); 441161764Sobrien return (ftpd_strdup(name)); 44279968Sobrien} 44379968Sobrien 44479968Sobrienvoid 44579968Sobrienrenamecmd(const char *from, const char *to) 44679968Sobrien{ 44779968Sobrien char *p = NULL; 44879968Sobrien 44979968Sobrien if (rename(from, to) < 0) { 45079968Sobrien p = strerror(errno); 45179968Sobrien perror_reply(550, "rename"); 45279968Sobrien } else 45379968Sobrien ack("RNTO"); 45479968Sobrien logxfer("rename", -1, from, to, NULL, p); 45579968Sobrien} 45679968Sobrien 45779968Sobrienvoid 45879968Sobriensizecmd(const char *filename) 45979968Sobrien{ 46079968Sobrien switch (type) { 46179968Sobrien case TYPE_L: 46279968Sobrien case TYPE_I: 46379968Sobrien { 46479968Sobrien struct stat stbuf; 46579968Sobrien if (stat(filename, &stbuf) < 0 || !S_ISREG(stbuf.st_mode)) 46679968Sobrien reply(550, "%s: not a plain file.", filename); 46779968Sobrien else 46879968Sobrien reply(213, ULLF, (ULLT)stbuf.st_size); 46979968Sobrien break; 47079968Sobrien } 47179968Sobrien case TYPE_A: 47279968Sobrien { 47379968Sobrien FILE *fin; 47479968Sobrien int c; 47579968Sobrien off_t count; 47679968Sobrien struct stat stbuf; 47779968Sobrien fin = fopen(filename, "r"); 47879968Sobrien if (fin == NULL) { 47979968Sobrien perror_reply(550, filename); 48079968Sobrien return; 48179968Sobrien } 48279968Sobrien if (fstat(fileno(fin), &stbuf) < 0 || !S_ISREG(stbuf.st_mode)) { 48379968Sobrien reply(550, "%s: not a plain file.", filename); 48479968Sobrien (void) fclose(fin); 48579968Sobrien return; 48679968Sobrien } 48792282Sobrien if (stbuf.st_size > 10240) { 48892282Sobrien reply(550, "%s: file too large for SIZE.", filename); 48992282Sobrien (void) fclose(fin); 49092282Sobrien return; 49192282Sobrien } 49279968Sobrien 49379968Sobrien count = 0; 49492282Sobrien while((c = getc(fin)) != EOF) { 49579968Sobrien if (c == '\n') /* will get expanded to \r\n */ 49679968Sobrien count++; 49779968Sobrien count++; 49879968Sobrien } 49979968Sobrien (void) fclose(fin); 50079968Sobrien 50179968Sobrien reply(213, LLF, (LLT)count); 50279968Sobrien break; 50379968Sobrien } 50479968Sobrien default: 50579968Sobrien reply(504, "SIZE not implemented for Type %c.", "?AEIL"[type]); 50679968Sobrien } 50779968Sobrien} 50879968Sobrien 50979968Sobrienvoid 51079968Sobrienstatfilecmd(const char *filename) 51179968Sobrien{ 51279968Sobrien FILE *fin; 51379968Sobrien int c; 514108746Sobrien int atstart; 51579968Sobrien char *argv[] = { INTERNAL_LS, "-lgA", "", NULL }; 51679968Sobrien 51779968Sobrien argv[2] = (char *)filename; 51879968Sobrien fin = ftpd_popen(argv, "r", STDOUT_FILENO); 51979968Sobrien reply(-211, "status of %s:", filename); 52079968Sobrien/* XXX: use fgetln() or fparseln() here? */ 521108746Sobrien atstart = 1; 52279968Sobrien while ((c = getc(fin)) != EOF) { 52379968Sobrien if (c == '\n') { 52479968Sobrien if (ferror(stdout)){ 52579968Sobrien perror_reply(421, "control connection"); 52679968Sobrien (void) ftpd_pclose(fin); 52779968Sobrien dologout(1); 52879968Sobrien /* NOTREACHED */ 52979968Sobrien } 53079968Sobrien if (ferror(fin)) { 53179968Sobrien perror_reply(551, filename); 53279968Sobrien (void) ftpd_pclose(fin); 53379968Sobrien return; 53479968Sobrien } 53579968Sobrien CPUTC('\r', stdout); 53679968Sobrien } 537108746Sobrien if (atstart && isdigit(c)) 538108746Sobrien CPUTC(' ', stdout); 53979968Sobrien CPUTC(c, stdout); 540108746Sobrien atstart = (c == '\n'); 54179968Sobrien } 54279968Sobrien (void) ftpd_pclose(fin); 54379968Sobrien reply(211, "End of Status"); 54479968Sobrien} 54579968Sobrien 54679968Sobrien/* -- */ 54779968Sobrien 54879968Sobrienstatic void 54979968Sobrienack(const char *s) 55079968Sobrien{ 55179968Sobrien 55279968Sobrien reply(250, "%s command successful.", s); 55379968Sobrien} 55479968Sobrien 55579968Sobrien/* 55679968Sobrien * Encode len bytes starting at clear using base64 encoding into encoded, 55779968Sobrien * which should be at least ((len + 2) * 4 / 3 + 1) in size. 55879968Sobrien * If nulterm is non-zero, terminate with \0 otherwise pad to 3 byte boundary 55979968Sobrien * with `='. 56079968Sobrien */ 56179968Sobrienstatic void 56279968Sobrienbase64_encode(const char *clear, size_t len, char *encoded, int nulterm) 56379968Sobrien{ 56479968Sobrien static const char base64[] = 56579968Sobrien "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 56679968Sobrien const char *c; 56779968Sobrien char *e, termchar; 56879968Sobrien int i; 56979968Sobrien 57079968Sobrien /* determine whether to pad with '=' or NUL terminate */ 57179968Sobrien termchar = nulterm ? '\0' : '='; 57279968Sobrien c = clear; 57379968Sobrien e = encoded; 57479968Sobrien /* convert all but last 2 bytes */ 57579968Sobrien for (i = len; i > 2; i -= 3, c += 3) { 57679968Sobrien *e++ = base64[(c[0] >> 2) & 0x3f]; 57779968Sobrien *e++ = base64[((c[0] << 4) & 0x30) | ((c[1] >> 4) & 0x0f)]; 57879968Sobrien *e++ = base64[((c[1] << 2) & 0x3c) | ((c[2] >> 6) & 0x03)]; 57979968Sobrien *e++ = base64[(c[2]) & 0x3f]; 58079968Sobrien } 58179968Sobrien /* handle slop at end */ 58279968Sobrien if (i > 0) { 58379968Sobrien *e++ = base64[(c[0] >> 2) & 0x3f]; 58479968Sobrien *e++ = base64[((c[0] << 4) & 0x30) | 58579968Sobrien (i > 1 ? ((c[1] >> 4) & 0x0f) : 0)]; 58679968Sobrien *e++ = (i > 1) ? base64[(c[1] << 2) & 0x3c] : termchar; 58779968Sobrien *e++ = termchar; 58879968Sobrien } 58979968Sobrien *e = '\0'; 59079968Sobrien} 59179968Sobrien 59279968Sobrienstatic void 59379968Sobrienfact_modify(const char *fact, FILE *fd, factelem *fe) 59479968Sobrien{ 59579968Sobrien struct tm *t; 59679968Sobrien 59779968Sobrien t = gmtime(&(fe->stat->st_mtime)); 59879968Sobrien cprintf(fd, "%s=%04d%02d%02d%02d%02d%02d;", fact, 59979968Sobrien TM_YEAR_BASE + t->tm_year, 60079968Sobrien t->tm_mon+1, t->tm_mday, 60179968Sobrien t->tm_hour, t->tm_min, t->tm_sec); 60279968Sobrien} 60379968Sobrien 60479968Sobrienstatic void 60579968Sobrienfact_perm(const char *fact, FILE *fd, factelem *fe) 60679968Sobrien{ 60779968Sobrien int rok, wok, xok, pdirwok; 60879968Sobrien struct stat *pdir; 60979968Sobrien 61079968Sobrien if (fe->stat->st_uid == geteuid()) { 61179968Sobrien rok = ((fe->stat->st_mode & S_IRUSR) != 0); 61279968Sobrien wok = ((fe->stat->st_mode & S_IWUSR) != 0); 61379968Sobrien xok = ((fe->stat->st_mode & S_IXUSR) != 0); 61479968Sobrien } else if (matchgroup(fe->stat->st_gid)) { 61579968Sobrien rok = ((fe->stat->st_mode & S_IRGRP) != 0); 61679968Sobrien wok = ((fe->stat->st_mode & S_IWGRP) != 0); 61779968Sobrien xok = ((fe->stat->st_mode & S_IXGRP) != 0); 61879968Sobrien } else { 61979968Sobrien rok = ((fe->stat->st_mode & S_IROTH) != 0); 62079968Sobrien wok = ((fe->stat->st_mode & S_IWOTH) != 0); 62179968Sobrien xok = ((fe->stat->st_mode & S_IXOTH) != 0); 62279968Sobrien } 62379968Sobrien 62479968Sobrien cprintf(fd, "%s=", fact); 62579968Sobrien 62679968Sobrien /* 62779968Sobrien * if parent info not provided, look it up, but 62879968Sobrien * only if the current class has modify rights, 62979968Sobrien * since we only need this info in such a case. 63079968Sobrien */ 63179968Sobrien pdir = fe->pdirstat; 63279968Sobrien if (pdir == NULL && CURCLASS_FLAGS_ISSET(modify)) { 63379968Sobrien size_t len; 63479968Sobrien char realdir[MAXPATHLEN], *p; 63579968Sobrien struct stat dir; 63679968Sobrien 63779968Sobrien len = strlcpy(realdir, fe->path, sizeof(realdir)); 63879968Sobrien if (len < sizeof(realdir) - 4) { 63979968Sobrien if (S_ISDIR(fe->stat->st_mode)) 64079968Sobrien strlcat(realdir, "/..", sizeof(realdir)); 64179968Sobrien else { 64279968Sobrien /* if has a /, move back to it */ 64379968Sobrien /* otherwise use '..' */ 64479968Sobrien if ((p = strrchr(realdir, '/')) != NULL) { 64579968Sobrien if (p == realdir) 64679968Sobrien p++; 64779968Sobrien *p = '\0'; 64879968Sobrien } else 64979968Sobrien strlcpy(realdir, "..", sizeof(realdir)); 65079968Sobrien } 65179968Sobrien if (stat(realdir, &dir) == 0) 65279968Sobrien pdir = &dir; 65379968Sobrien } 65479968Sobrien } 65579968Sobrien pdirwok = 0; 65679968Sobrien if (pdir != NULL) { 65779968Sobrien if (pdir->st_uid == geteuid()) 65879968Sobrien pdirwok = ((pdir->st_mode & S_IWUSR) != 0); 65979968Sobrien else if (matchgroup(pdir->st_gid)) 66079968Sobrien pdirwok = ((pdir->st_mode & S_IWGRP) != 0); 66179968Sobrien else 66279968Sobrien pdirwok = ((pdir->st_mode & S_IWOTH) != 0); 66379968Sobrien } 66479968Sobrien 66579968Sobrien /* 'a': can APPE to file */ 66679968Sobrien if (wok && CURCLASS_FLAGS_ISSET(upload) && S_ISREG(fe->stat->st_mode)) 66779968Sobrien CPUTC('a', fd); 66879968Sobrien 66979968Sobrien /* 'c': can create or append to files in directory */ 67079968Sobrien if (wok && CURCLASS_FLAGS_ISSET(modify) && S_ISDIR(fe->stat->st_mode)) 67179968Sobrien CPUTC('c', fd); 67279968Sobrien 67379968Sobrien /* 'd': can delete file or directory */ 67479968Sobrien if (pdirwok && CURCLASS_FLAGS_ISSET(modify)) { 67579968Sobrien int candel; 67679968Sobrien 67779968Sobrien candel = 1; 67879968Sobrien if (S_ISDIR(fe->stat->st_mode)) { 67979968Sobrien DIR *dirp; 68079968Sobrien struct dirent *dp; 68179968Sobrien 68279968Sobrien if ((dirp = opendir(fe->display)) == NULL) 68379968Sobrien candel = 0; 68479968Sobrien else { 68579968Sobrien while ((dp = readdir(dirp)) != NULL) { 68679968Sobrien if (ISDOTDIR(dp->d_name) || 68779968Sobrien ISDOTDOTDIR(dp->d_name)) 68879968Sobrien continue; 68979968Sobrien candel = 0; 69079968Sobrien break; 69179968Sobrien } 69279968Sobrien closedir(dirp); 69379968Sobrien } 69479968Sobrien } 69579968Sobrien if (candel) 69679968Sobrien CPUTC('d', fd); 69779968Sobrien } 69879968Sobrien 69979968Sobrien /* 'e': can enter directory */ 70079968Sobrien if (xok && S_ISDIR(fe->stat->st_mode)) 70179968Sobrien CPUTC('e', fd); 70279968Sobrien 70379968Sobrien /* 'f': can rename file or directory */ 70479968Sobrien if (pdirwok && CURCLASS_FLAGS_ISSET(modify)) 70579968Sobrien CPUTC('f', fd); 70679968Sobrien 70779968Sobrien /* 'l': can list directory */ 70879968Sobrien if (rok && xok && S_ISDIR(fe->stat->st_mode)) 70979968Sobrien CPUTC('l', fd); 71079968Sobrien 71179968Sobrien /* 'm': can create directory */ 71279968Sobrien if (wok && CURCLASS_FLAGS_ISSET(modify) && S_ISDIR(fe->stat->st_mode)) 71379968Sobrien CPUTC('m', fd); 71479968Sobrien 71579968Sobrien /* 'p': can remove files in directory */ 71679968Sobrien if (wok && CURCLASS_FLAGS_ISSET(modify) && S_ISDIR(fe->stat->st_mode)) 71779968Sobrien CPUTC('p', fd); 71879968Sobrien 71979968Sobrien /* 'r': can RETR file */ 72079968Sobrien if (rok && S_ISREG(fe->stat->st_mode)) 72179968Sobrien CPUTC('r', fd); 72279968Sobrien 72379968Sobrien /* 'w': can STOR file */ 72479968Sobrien if (wok && CURCLASS_FLAGS_ISSET(upload) && S_ISREG(fe->stat->st_mode)) 72579968Sobrien CPUTC('w', fd); 72679968Sobrien 72779968Sobrien CPUTC(';', fd); 72879968Sobrien} 72979968Sobrien 73079968Sobrienstatic void 73179968Sobrienfact_size(const char *fact, FILE *fd, factelem *fe) 73279968Sobrien{ 73379968Sobrien 73479968Sobrien if (S_ISREG(fe->stat->st_mode)) 73579968Sobrien cprintf(fd, "%s=" LLF ";", fact, (LLT)fe->stat->st_size); 73679968Sobrien} 73779968Sobrien 73879968Sobrienstatic void 73979968Sobrienfact_type(const char *fact, FILE *fd, factelem *fe) 74079968Sobrien{ 74179968Sobrien 74279968Sobrien cprintf(fd, "%s=", fact); 74379968Sobrien switch (fe->stat->st_mode & S_IFMT) { 74479968Sobrien case S_IFDIR: 74592282Sobrien if (fe->flags & FE_MLSD) { 74692282Sobrien if ((fe->flags & FE_ISCURDIR) || ISDOTDIR(fe->display)) 74792282Sobrien cprintf(fd, "cdir"); 74892282Sobrien else if (ISDOTDOTDIR(fe->display)) 74992282Sobrien cprintf(fd, "pdir"); 75092282Sobrien else 75192282Sobrien cprintf(fd, "dir"); 75292282Sobrien } else { 75379968Sobrien cprintf(fd, "dir"); 75492282Sobrien } 75579968Sobrien break; 75679968Sobrien case S_IFREG: 75779968Sobrien cprintf(fd, "file"); 75879968Sobrien break; 75979968Sobrien case S_IFIFO: 76079968Sobrien cprintf(fd, "OS.unix=fifo"); 76179968Sobrien break; 76279968Sobrien case S_IFLNK: /* XXX: probably a NO-OP with stat() */ 76379968Sobrien cprintf(fd, "OS.unix=slink"); 76479968Sobrien break; 76579968Sobrien case S_IFSOCK: 76679968Sobrien cprintf(fd, "OS.unix=socket"); 76779968Sobrien break; 76879968Sobrien case S_IFBLK: 76979968Sobrien case S_IFCHR: 77079968Sobrien cprintf(fd, "OS.unix=%s-%d/%d", 77179968Sobrien S_ISBLK(fe->stat->st_mode) ? "blk" : "chr", 77279968Sobrien major(fe->stat->st_rdev), minor(fe->stat->st_rdev)); 77379968Sobrien break; 77479968Sobrien default: 77579968Sobrien cprintf(fd, "OS.unix=UNKNOWN(0%o)", fe->stat->st_mode & S_IFMT); 77679968Sobrien break; 77779968Sobrien } 77879968Sobrien CPUTC(';', fd); 77979968Sobrien} 78079968Sobrien 78179968Sobrienstatic void 78279968Sobrienfact_unique(const char *fact, FILE *fd, factelem *fe) 78379968Sobrien{ 78479968Sobrien char obuf[(sizeof(dev_t) + sizeof(ino_t) + 2) * 4 / 3 + 2]; 78579968Sobrien char tbuf[sizeof(dev_t) + sizeof(ino_t)]; 78679968Sobrien 78779968Sobrien memcpy(tbuf, 78879968Sobrien (char *)&(fe->stat->st_dev), sizeof(dev_t)); 78979968Sobrien memcpy(tbuf + sizeof(dev_t), 79079968Sobrien (char *)&(fe->stat->st_ino), sizeof(ino_t)); 79179968Sobrien base64_encode(tbuf, sizeof(dev_t) + sizeof(ino_t), obuf, 1); 79279968Sobrien cprintf(fd, "%s=%s;", fact, obuf); 79379968Sobrien} 79479968Sobrien 79579968Sobrienstatic int 79679968Sobrienmatchgroup(gid_t gid) 79779968Sobrien{ 79879968Sobrien int i; 79979968Sobrien 80079968Sobrien for (i = 0; i < gidcount; i++) 80179968Sobrien if (gid == gidlist[i]) 80279968Sobrien return(1); 80379968Sobrien return (0); 80479968Sobrien} 80579968Sobrien 80679968Sobrienstatic void 80779968Sobrienmlsname(FILE *fp, factelem *fe) 80879968Sobrien{ 80992282Sobrien char realfile[MAXPATHLEN]; 810133936Sobrien int i, userf = 0; 81179968Sobrien 81279968Sobrien for (i = 0; i < FACTTABSIZE; i++) { 81379968Sobrien if (facttab[i].enabled) 81479968Sobrien (facttab[i].display)(facttab[i].name, fp, fe); 81579968Sobrien } 81692282Sobrien if ((fe->flags & FE_MLSD) && 81792282Sobrien !(fe->flags & FE_ISCURDIR) && !ISDOTDIR(fe->display)) { 81892282Sobrien /* if MLSD and not "." entry, display as-is */ 81992282Sobrien userf = 0; 82092282Sobrien } else { 82192282Sobrien /* if MLST, or MLSD and "." entry, realpath(3) it */ 82292282Sobrien if (realpath(fe->display, realfile) != NULL) 82392282Sobrien userf = 1; 82492282Sobrien } 82592282Sobrien cprintf(fp, " %s\r\n", userf ? realfile : fe->display); 82679968Sobrien} 82779968Sobrien 82879968Sobrienstatic void 82979968Sobrienreplydirname(const char *name, const char *message) 83079968Sobrien{ 83179968Sobrien char *p, *ep; 83279968Sobrien char npath[MAXPATHLEN * 2]; 83379968Sobrien 83479968Sobrien p = npath; 83579968Sobrien ep = &npath[sizeof(npath) - 1]; 83679968Sobrien while (*name) { 83779968Sobrien if (*name == '"') { 83879968Sobrien if (ep - p < 2) 83979968Sobrien break; 84079968Sobrien *p++ = *name++; 84179968Sobrien *p++ = '"'; 84279968Sobrien } else { 84379968Sobrien if (ep - p < 1) 84479968Sobrien break; 84579968Sobrien *p++ = *name++; 84679968Sobrien } 84779968Sobrien } 84879968Sobrien *p = '\0'; 84979968Sobrien reply(257, "\"%s\" %s", npath, message); 85079968Sobrien} 851110242Sobrien 852110242Sobrienstatic void 853110242Sobriendiscover_path(last_path, new_path) 854110242Sobrien char *last_path; 855110242Sobrien const char *new_path; 856110242Sobrien{ 857110242Sobrien char tp[MAXPATHLEN + 1] = ""; 858110242Sobrien char tq[MAXPATHLEN + 1] = ""; 859110242Sobrien char *cp; 860110242Sobrien char *cq; 861110242Sobrien int sz1, sz2; 862110242Sobrien int nomorelink; 863110242Sobrien struct stat st1, st2; 864110242Sobrien 865110242Sobrien if (new_path[0] != '/') { 866110242Sobrien (void)strlcpy(tp, last_path, MAXPATHLEN); 867110242Sobrien (void)strlcat(tp, "/", MAXPATHLEN); 868110242Sobrien } 869110242Sobrien (void)strlcat(tp, new_path, MAXPATHLEN); 870110242Sobrien (void)strlcat(tp, "/", MAXPATHLEN); 871110242Sobrien 872110242Sobrien /* 873110242Sobrien * resolve symlinks. A symlink may introduce another symlink, so we 874110242Sobrien * loop trying to resolve symlinks until we don't find any of them. 875110242Sobrien */ 876110242Sobrien do { 877110242Sobrien /* Collapse any // into / */ 878110242Sobrien while ((cp = strstr(tp, "//")) != NULL) 879110242Sobrien (void)memmove(cp, cp + 1, strlen(cp) - 1 + 1); 880110242Sobrien 881110242Sobrien /* Collapse any /./ into / */ 882110242Sobrien while ((cp = strstr(tp, "/./")) != NULL) 883110242Sobrien (void)memmove(cp, cp + 2, strlen(cp) - 2 + 1); 884110242Sobrien 885110242Sobrien cp = tp; 886110242Sobrien nomorelink = 1; 887110242Sobrien 888110242Sobrien while ((cp = strstr(++cp, "/")) != NULL) { 889110242Sobrien sz1 = (u_long)cp - (u_long)tp; 890110242Sobrien if (sz1 > MAXPATHLEN) 891110242Sobrien goto bad; 892110242Sobrien *cp = 0; 893110242Sobrien sz2 = readlink(tp, tq, MAXPATHLEN); 894110242Sobrien *cp = '/'; 895110242Sobrien 896110242Sobrien /* If this is not a symlink, move to next / */ 897110242Sobrien if (sz2 <= 0) 898110242Sobrien continue; 899110242Sobrien 900110242Sobrien /* 901110242Sobrien * We found a symlink, so we will have to 902110242Sobrien * do one more pass to check there is no 903110242Sobrien * more symlink in the path 904110242Sobrien */ 905110242Sobrien nomorelink = 0; 906110242Sobrien 907110242Sobrien /* 908110242Sobrien * Null terminate the string and remove trailing / 909110242Sobrien */ 910110242Sobrien tq[sz2] = 0; 911110242Sobrien sz2 = strlen(tq); 912110242Sobrien if (tq[sz2 - 1] == '/') 913110242Sobrien tq[--sz2] = 0; 914110242Sobrien 915110242Sobrien /* 916110242Sobrien * Is this an absolute link or a relative link? 917110242Sobrien */ 918110242Sobrien if (tq[0] == '/') { 919110242Sobrien /* absolute link */ 920110242Sobrien if (strlen(cp) + sz2 > MAXPATHLEN) 921110242Sobrien goto bad; 922110242Sobrien memmove(tp + sz2, cp, strlen(cp) + 1); 923110242Sobrien memcpy(tp, tq, sz2); 924110242Sobrien } else { 925110242Sobrien /* relative link */ 926110242Sobrien for (cq = cp - 1; *cq != '/'; cq--); 927110242Sobrien if (strlen(tp) - ((u_long)cq - (u_long)cp) 928110242Sobrien + 1 + sz2 > MAXPATHLEN) 929110242Sobrien goto bad; 930110242Sobrien (void)memmove(cq + 1 + sz2, 931110242Sobrien cp, strlen(cp) + 1); 932110242Sobrien (void)memcpy(cq + 1, tq, sz2); 933110242Sobrien } 934110242Sobrien 935110242Sobrien /* 936110242Sobrien * start over, looking for new symlinks 937110242Sobrien */ 938110242Sobrien break; 939110242Sobrien } 940110242Sobrien } while (nomorelink == 0); 941110242Sobrien 942110242Sobrien /* Collapse any /foo/../ into /foo/ */ 943110242Sobrien while ((cp = strstr(tp, "/../")) != NULL) { 944110242Sobrien /* ^/../foo/ becomes ^/foo/ */ 945110242Sobrien if (cp == tp) { 946110242Sobrien (void)memmove(cp, cp + 3, 947110242Sobrien strlen(cp) - 3 + 1); 948110242Sobrien } else { 949110242Sobrien for (cq = cp - 1; *cq != '/'; cq--); 950110242Sobrien (void)memmove(cq, cp + 3, 951110242Sobrien strlen(cp) - 3 + 1); 952110242Sobrien } 953110242Sobrien } 954110242Sobrien 955110242Sobrien /* strip strailing / */ 956110242Sobrien if (strlen(tp) != 1) 957110242Sobrien tp[strlen(tp) - 1] = '\0'; 958110242Sobrien 959110242Sobrien /* check that the path is correct */ 960110242Sobrien stat(tp, &st1); 961110242Sobrien stat(".", &st2); 962110242Sobrien if ((st1.st_dev != st2.st_dev) || (st1.st_ino != st2.st_ino)) 963110242Sobrien goto bad; 964110242Sobrien 965110242Sobrien (void)strlcpy(last_path, tp, MAXPATHLEN); 966110242Sobrien return; 967110242Sobrien 968110242Sobrienbad: 969110242Sobrien (void)strlcat(last_path, "/", MAXPATHLEN); 970110242Sobrien (void)strlcat(last_path, new_path, MAXPATHLEN); 971110242Sobrien return; 972110242Sobrien} 973110242Sobrien 974