blob: 104e9dc2bc535adce9916321fc35c6651fe25b55 [file] [log] [blame]
/* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifdef WIN32
#include <sys/stat.h>
#include <stdarg.h>
#include <time.h>
#include <stdlib.h>
#include "httpd.h"
#include "http_log.h"
/* Returns TRUE if the input string is a string
* of one or more '.' characters.
*/
static BOOL OnlyDots(char *pString)
{
char *c;
if (*pString == '\0')
return FALSE;
for (c = pString;*c;c++)
if (*c != '.')
return FALSE;
return TRUE;
}
/* Accepts as input a pathname, and tries to match it to an
* existing path and return the pathname in the case that
* is present on the existing path. This routine also
* converts alias names to long names.
*
* WARNING: Folding to systemcase fails when /path/to/foo/../bar
* is given and foo does not exist, is not a directory.
*/
API_EXPORT(char *) ap_os_systemcase_filename(pool *pPool,
const char *szFile)
{
char *buf, *t, *r;
const char *q, *p;
BOOL bDone = FALSE;
BOOL bFileExists = TRUE;
HANDLE hFind;
WIN32_FIND_DATA wfd;
size_t buflen;
int slack = 0;
if (!szFile || strlen(szFile) == 0)
return ap_pstrdup(pPool, "");
buflen = strlen(szFile);
t = buf = ap_palloc(pPool, buflen + 1);
q = szFile;
/* If there is drive information, copy it over. */
if (szFile[1] == ':') {
/* Lowercase, so that when systemcase is used for
* comparison, d: designations will match
*/
*(t++) = tolower(*(q++));
*(t++) = *(q++);
}
else if ((*q == '/') || (*q == '\\')) {
/* Get past the root path (/ or //foo/bar/) so we can go
* on to normalize individual path elements.
*/
*(t++) = '\\', ++q;
if ((*q == '/') || (*q == '\\')) /* UNC name */
{
/* Lower-case the machine name, so compares match.
* FindFirstFile won't parse \\machine alone
*/
*(t++) = '\\', ++q;
for (p = q; *p && (*p != '/') && (*p != '\\'); ++p)
/* continue */ ;
if (*p || p > q)
{
/* Lower-case the machine name, so compares match.
* FindFirstFile won't parse \\machine\share alone
*/
memcpy(t, q, p - q);
t[p - q] = '\0';
strlwr(t);
t += p - q;
q = p;
if (*p) {
*(t++) = '\\', ++q;
for (p = q; *p && (*p != '/') && (*p != '\\'); ++p)
/* continue */ ;
if (*p || p > q)
{
/* Copy the lower-cased share name. FindFirstFile
* cannot not find a \\machine\share name only
*/
memcpy(t, q, p - q);
t[p - q] = '\0';
strlwr(t);
t += p - q;
q = p;
if (*p)
*(t++) = '\\', ++q;
else
bFileExists = FALSE;
}
else
bFileExists = FALSE;
}
else
bFileExists = FALSE;
}
else
bFileExists = FALSE;
}
}
while (bFileExists) {
/* parse past any leading slashes */
for (; (*q == '/') || (*q == '\\'); ++q)
*(t++) = '\\';
/* break on end of string */
if (!*q)
break;
/* get to the end of this path segment */
for (p = q; *p && (*p != '/') && (*p != '\\'); ++p)
/* continue */ ;
/* copy the segment */
memcpy(t, q, p - q);
t[p - q] = '\0';
/* Test for nasties that can exhibit undesired effects */
if (strpbrk(t, "?\"<>*|:")) {
t += p - q;
q = p;
break;
}
/* If the path exists so far, call FindFirstFile
* again. However, if this portion of the path contains
* only '.' charaters, skip the call to FindFirstFile
* since it will convert '.' and '..' to actual names.
* On win32, '...' is an alias for '..', so we gain
* a bit of slack.
*/
if (*t == '.' && OnlyDots(t)) {
if (p - q == 3) {
t += 2;
q = p;
++slack;
}
else {
t += p - q;
q = p;
}
/* Paths of 4 dots or more are invalid */
if (p - q > 3)
break;
}
else {
if ((hFind = FindFirstFile(buf, &wfd)) == INVALID_HANDLE_VALUE) {
t += p - q;
q = p;
break;
}
else {
size_t fnlen = strlen(wfd.cFileName);
FindClose(hFind);
/* the string length just changed, could have shrunk
* (trailing spaces or dots) or could have grown
* (longer filename aliases). Realloc as necessary
*/
slack -= fnlen - (p - q);
if (slack < 0) {
char *n;
slack += buflen + fnlen - (p - q);
buflen += buflen + fnlen - (p - q);
n = ap_palloc(pPool, buflen + 1);
memcpy (n, buf, t - buf);
t = n + (t - buf);
buf = n;
}
memcpy(t, wfd.cFileName, fnlen);
t += fnlen;
q = p;
if (!(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
break;
}
}
}
/* Convert all parsed '\'s to '/' for canonical form (doesn't touch
* the non-existant portion of the path whatsoever.)
*/
for (r = buf; r < t; ++r) {
if (*r == '\\')
*r = '/';
}
/* Copy the non-existant portion (minimally nul-terminates the string) */
strcpy(t, q);
return buf;
}
/* Perform canonicalization with the exception that the
* input case is preserved.
*/
API_EXPORT(char *) ap_os_case_canonical_filename(pool *pPool,
const char *szFile)
{
char *pNewStr;
char *s;
char *p;
char *q;
if (szFile == NULL || strlen(szFile) == 0)
return ap_pstrdup(pPool, "");
pNewStr = ap_pstrdup(pPool, szFile);
/* Change all '\' characters to '/' characters.
* While doing this, remove any trailing '.'.
* Also, blow away any directories with 3 or
* more '.'
*/
for (p = pNewStr,s = pNewStr; *s; s++,p++) {
if (*s == '\\' || *s == '/') {
q = p;
while (p > pNewStr && *(p-1) == '.')
p--;
if (p == pNewStr && q-p <= 2 && *p == '.')
p = q;
else if (p > pNewStr && p < q && *(p-1) == '/') {
if (q-p > 2)
p--;
else
p = q;
}
*p = '/';
}
else {
*p = *s;
}
}
*p = '\0';
/* Blow away any final trailing '.' since on Win32
* foo.bat == foo.bat. == foo.bat... etc.
* Also blow away any trailing spaces since
* "filename" == "filename "
*/
q = p;
while (p > pNewStr && (*(p-1) == '.' || *(p-1) == ' '))
p--;
if ((p > pNewStr) ||
(p == pNewStr && q-p > 2))
*p = '\0';
/* One more security issue to deal with. Win32 allows
* you to create long filenames. However, alias filenames
* are always created so that the filename will
* conform to 8.3 rules. According to the Microsoft
* Developer's network CD (1/98)
* "Automatically generated aliases are composed of the
* first six characters of the filename plus ~n
* (where n is a number) and the first three characters
* after the last period."
* Here, we attempt to detect and decode these names.
*
* XXX: Netware network clients may have alternate short names,
* simply truncated, with no embedded '~'. Further, this behavior
* can be modified on WinNT volumes. This was not a safe test,
* therefore exclude the '~' pretest.
*/
#ifdef WIN32_SHORT_FILENAME_INSECURE_BEHAVIOR
p = strchr(pNewStr, '~');
if (p != NULL)
#endif
/* ap_os_systemcase_filename now changes the case of only
* the pathname elements that are found.
*/
pNewStr = ap_os_systemcase_filename(pPool, pNewStr);
return pNewStr;
}
/* Perform complete canonicalization.
*/
API_EXPORT(char *) ap_os_canonical_filename(pool *pPool, const char *szFile)
{
char *pNewName;
pNewName = ap_os_case_canonical_filename(pPool, szFile);
strlwr(pNewName);
return pNewName;
}
/*
* ap_os_is_filename_valid is given a filename, and returns 0 if the filename
* is not valid for use on this system. On Windows, this means it fails any
* of the tests below. Otherwise returns 1.
*
* Test for filename validity on Win32. This is of tests come in part from
* the MSDN article at "Technical Articles, Windows Platform, Base Services,
* Guidelines, Making Room for Long Filenames" although the information
* in MSDN about filename testing is incomplete or conflicting. There is a
* similar set of tests in "Technical Articles, Windows Platform, Base Services,
* Guidelines, Moving Unix Applications to Windows NT".
*
* The tests are:
*
* 1) total path length greater than MAX_PATH
*
* 2) anything using the octets 0-31 or characters " < > | :
* (these are reserved for Windows use in filenames. In addition
* each file system has its own additional characters that are
* invalid. See KB article Q100108 for more details).
*
* 3) anything ending in "." (no matter how many)
* (filename doc, doc. and doc... all refer to the same file)
*
* 4) any segment in which the basename (before first period) matches
* one of the DOS device names
* (the list comes from KB article Q100108 although some people
* reports that additional names such as "COM5" are also special
* devices).
*
* If the path fails ANY of these tests, the result must be to deny access.
*/
API_EXPORT(int) ap_os_is_filename_valid(const char *file)
{
const char *segstart;
unsigned int seglength;
const char *pos;
static const char * const invalid_characters = "?\"<>*|:";
static const char * const invalid_filenames[] = {
"CON", "AUX", "COM1", "COM2", "COM3",
"COM4", "LPT1", "LPT2", "LPT3", "PRN", "NUL", NULL
};
/* Test 1 */
if (strlen(file) >= MAX_PATH) {
/* Path too long for Windows. Note that this test is not valid
* if the path starts with //?/ or \\?\. */
return 0;
}
pos = file;
/* Skip any leading non-path components. This can be either a
* drive letter such as C:, or a UNC path such as \\SERVER\SHARE\.
* We continue and check the rest of the path based on the rules above.
* This means we could eliminate valid filenames from servers which
* are not running NT (such as Samba).
*/
if (pos[0] && pos[1] == ':') {
/* Skip leading drive letter */
pos += 2;
}
else {
if ((pos[0] == '\\' || pos[0] == '/') &&
(pos[1] == '\\' || pos[1] == '/')) {
/* Is a UNC, so skip the server name and share name */
pos += 2;
while (*pos && *pos != '/' && *pos != '\\')
pos++;
if (!*pos) {
/* No share name */
return 0;
}
pos++; /* Move to start of share name */
while (*pos && *pos != '/' && *pos != '\\')
pos++;
if (!*pos) {
/* No path information */
return 0;
}
}
}
while (*pos) {
unsigned int idx;
unsigned int baselength;
while (*pos == '/' || *pos == '\\') {
pos++;
}
if (*pos == '\0') {
break;
}
segstart = pos; /* start of segment */
while (*pos && *pos != '/' && *pos != '\\') {
pos++;
}
seglength = pos - segstart;
/*
* Now we have a segment of the path, starting at position "segstart"
* and length "seglength"
*/
/* Test 2 */
for (idx = 0; idx < seglength; idx++) {
if ((segstart[idx] > 0 && segstart[idx] < 32) ||
strchr(invalid_characters, segstart[idx])) {
return 0;
}
}
/* Test 3 */
if (segstart[seglength-1] == '.') {
return 0;
}
/* Test 4 */
for (baselength = 0; baselength < seglength; baselength++) {
if (segstart[baselength] == '.') {
break;
}
}
/* baselength is the number of characters in the base path of
* the segment (which could be the same as the whole segment length,
* if it does not include any dot characters). */
if (baselength == 3 || baselength == 4) {
for (idx = 0; invalid_filenames[idx]; idx++) {
if (strlen(invalid_filenames[idx]) == baselength &&
!strnicmp(invalid_filenames[idx], segstart, baselength)) {
return 0;
}
}
}
}
return 1;
}
API_EXPORT(ap_os_dso_handle_t) ap_os_dso_load(const char *module_name)
{
UINT em;
ap_os_dso_handle_t dsoh;
char path[MAX_PATH], *p;
/* Load the module...
* per PR2555, the LoadLibraryEx function is very picky about slashes.
* Debugging on NT 4 SP 6a reveals First Chance Exception within NTDLL.
* LoadLibrary in the MS PSDK also reveals that it -explicitly- states
* that backslashes must be used.
*
* Transpose '\' for '/' in the filename.
*/
ap_cpystrn(path, module_name, MAX_PATH);
p = path;
while (p = strchr(p, '/'))
*p = '\\';
/* First assume the dso/dll's required by -this- dso are sitting in the
* same path or can be found in the usual places. Failing that, let's
* let that dso look in the apache root.
*/
em = SetErrorMode(SEM_FAILCRITICALERRORS);
dsoh = LoadLibraryEx(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
if (!dsoh) {
dsoh = LoadLibraryEx(path, NULL, 0);
}
SetErrorMode(em);
return dsoh;
}
API_EXPORT(const char *) ap_os_dso_error(void)
{
int len, nErrorCode;
static char errstr[120];
/* This is -not- threadsafe code, but it's about the best we can do.
* mostly a potential problem for isapi modules, since LoadModule
* errors are handled within a single config thread.
*/
nErrorCode = GetLastError();
len = ap_snprintf(errstr, sizeof(errstr), "(%d) ", nErrorCode);
len += FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
nErrorCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), /* Default language */
(LPTSTR) errstr + len,
sizeof(errstr) - len,
NULL
);
/* FormatMessage may have appended a newline (\r\n). So remove it
* and use ": " instead like the Unix errors. The error may also
* end with a . before the return - if so, trash it.
*/
if (len > 1 && errstr[len-2] == '\r' && errstr[len-1] == '\n') {
if (len > 2 && errstr[len-3] == '.')
len--;
errstr[len-2] = ':';
errstr[len-1] = ' ';
}
return errstr;
}
#endif /* WIN32 */