1 /*-
2  * Copyright (c) 2012-2017 Dag-Erling Smørgrav
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  * 3. The name of the author may not be used to endorse or promote
14  *    products derived from this software without specific prior written
15  *    permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27  * SUCH DAMAGE.
28  */
29 
30 #ifdef HAVE_CONFIG_H
31 # include "config.h"
32 #endif
33 
34 #include <errno.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 
38 #include <security/pam_appl.h>
39 
40 #include "openpam_impl.h"
41 #include "openpam_ctype.h"
42 
43 #define MIN_WORD_SIZE	32
44 
45 /*
46  * OpenPAM extension
47  *
48  * Read a word from a file, respecting shell quoting rules.
49  */
50 
51 char *
52 openpam_readword(FILE *f, int *lineno, size_t *lenp)
53 {
54 	char *word;
55 	size_t size, len;
56 	int ch, escape, quote;
57 	int serrno;
58 
59 	errno = 0;
60 
61 	/* skip initial whitespace */
62 	escape = quote = 0;
63 	while ((ch = getc(f)) != EOF) {
64 		if (ch == '\n') {
65 			/* either EOL or line continuation */
66 			if (!escape)
67 				break;
68 			if (lineno != NULL)
69 				++*lineno;
70 			escape = 0;
71 		} else if (escape) {
72 			/* escaped something else */
73 			break;
74 		} else if (ch == '#') {
75 			/* comment: until EOL, no continuation */
76 			while ((ch = getc(f)) != EOF)
77 				if (ch == '\n')
78 					break;
79 			break;
80 		} else if (ch == '\\') {
81 			escape = 1;
82 		} else if (!is_ws(ch)) {
83 			break;
84 		}
85 	}
86 	if (ch == EOF)
87 		return (NULL);
88 	ungetc(ch, f);
89 	if (ch == '\n')
90 		return (NULL);
91 
92 	word = NULL;
93 	size = len = 0;
94 	while ((ch = fgetc(f)) != EOF && (!is_ws(ch) || quote || escape)) {
95 		if (ch == '\\' && !escape && quote != '\'') {
96 			/* escape next character */
97 			escape = ch;
98 		} else if ((ch == '\'' || ch == '"') && !quote && !escape) {
99 			/* begin quote */
100 			quote = ch;
101 			/* edge case: empty quoted string */
102 			if (openpam_straddch(&word, &size, &len, 0) != 0)
103 				return (NULL);
104 		} else if (ch == quote && !escape) {
105 			/* end quote */
106 			quote = 0;
107 		} else if (ch == '\n' && escape) {
108 			/* line continuation */
109 			escape = 0;
110 		} else {
111 			if (escape && quote && ch != '\\' && ch != quote &&
112 			    openpam_straddch(&word, &size, &len, '\\') != 0) {
113 				free(word);
114 				errno = ENOMEM;
115 				return (NULL);
116 			}
117 			if (openpam_straddch(&word, &size, &len, ch) != 0) {
118 				free(word);
119 				errno = ENOMEM;
120 				return (NULL);
121 			}
122 			escape = 0;
123 		}
124 		if (lineno != NULL && ch == '\n')
125 			++*lineno;
126 	}
127 	if (ch == EOF && ferror(f)) {
128 		serrno = errno;
129 		free(word);
130 		errno = serrno;
131 		return (NULL);
132 	}
133 	if (ch == EOF && (escape || quote)) {
134 		/* Missing escaped character or closing quote. */
135 		free(word);
136 		errno = EINVAL;
137 		return (NULL);
138 	}
139 	ungetc(ch, f);
140 	if (lenp != NULL)
141 		*lenp = len;
142 	return (word);
143 }
144 
145 /**
146  * The =openpam_readword function reads the next word from a file, and
147  * returns it in a NUL-terminated buffer allocated with =!malloc.
148  *
149  * A word is a sequence of non-whitespace characters.
150  * However, whitespace characters can be included in a word if quoted or
151  * escaped according to the following rules:
152  *
153  *  - An unescaped single or double quote introduces a quoted string,
154  *    which ends when the same quote character is encountered a second
155  *    time.
156  *    The quotes themselves are stripped.
157  *
158  *  - Within a single- or double-quoted string, all whitespace characters,
159  *    including the newline character, are preserved as-is.
160  *
161  *  - Outside a quoted string, a backslash escapes the next character,
162  *    which is preserved as-is, unless that character is a newline, in
163  *    which case it is discarded and reading continues at the beginning of
164  *    the next line as if the backslash and newline had not been there.
165  *    In all cases, the backslash itself is discarded.
166  *
167  *  - Within a single-quoted string, double quotes and backslashes are
168  *    preserved as-is.
169  *
170  *  - Within a double-quoted string, a single quote is preserved as-is,
171  *    and a backslash is preserved as-is unless used to escape a double
172  *    quote.
173  *
174  * In addition, if the first non-whitespace character on the line is a
175  * hash character (#), the rest of the line is discarded.
176  * If a hash character occurs within a word, however, it is preserved
177  * as-is.
178  * A backslash at the end of a comment does cause line continuation.
179  *
180  * If =lineno is not =NULL, the integer variable it points to is
181  * incremented every time a quoted or escaped newline character is read.
182  *
183  * If =lenp is not =NULL, the length of the word (after quotes and
184  * backslashes have been removed) is stored in the variable it points to.
185  *
186  * RETURN VALUES
187  *
188  * If successful, the =openpam_readword function returns a pointer to a
189  * dynamically allocated NUL-terminated string containing the first word
190  * encountered on the line.
191  *
192  * The caller is responsible for releasing the returned buffer by passing
193  * it to =!free.
194  *
195  * If =openpam_readword reaches the end of the line or file before any
196  * characters are copied to the word, it returns =NULL.  In the former
197  * case, the newline is pushed back to the file.
198  *
199  * If =openpam_readword reaches the end of the file while a quote or
200  * backslash escape is in effect, it sets :errno to =EINVAL and returns
201  * =NULL.
202  *
203  * IMPLEMENTATION NOTES
204  *
205  * The parsing rules are intended to be equivalent to the normal POSIX
206  * shell quoting rules.
207  * Any discrepancy is a bug and should be reported to the author along
208  * with sample input that can be used to reproduce the error.
209  *
210  * >openpam_readline
211  * >openpam_readlinev
212  *
213  * AUTHOR DES
214  */
215