Skip to content

Commit b574573

Browse files
committed
fix(serve): address copilot review findings
1 parent 9d537e9 commit b574573

2 files changed

Lines changed: 94 additions & 14 deletions

File tree

extras/test/test-serve-config.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ conf_body="$(cat "${conf_path}")"
4545
assert_contains "${conf_body}" "protocols = imap" "serve protocols"
4646
assert_contains "${conf_body}" "listen = 127.0.0.1" "serve listen"
4747
assert_contains "${conf_body}" "ssl = no" "serve ssl"
48+
assert_contains "${conf_body}" "disable_plaintext_auth = no" "serve plaintext auth"
4849
assert_contains "${conf_body}" "auth_mechanisms = plain" "serve auth mechanism"
4950
assert_contains "${conf_body}" "mail_location = maildir:${mail_root}:LAYOUT=fs" "serve mail location"
5051
assert_contains "${conf_body}" "port = 61444" "serve selected port"
@@ -93,3 +94,24 @@ JARO_SERVE_CONFIG_ONLY=1 JARO_SERVE_MAX_CONNECTIONS=100 jaro_source serve >/dev/
9394
bad_max_ec=$?
9495
set -e
9596
[[ "${bad_max_ec}" -ne 0 ]] || { print -- "ASSERT FAIL: invalid max connections accepted"; exit 1; }
97+
98+
space_root="${tmp_root}/mail root with spaces"
99+
space_out="$(
100+
JAROMAILDIR="${space_root}" \
101+
JAROWORKDIR="${work_root}" \
102+
JARO_SERVE_CONFIG_ONLY=1 \
103+
JARO_SERVE_PASSWORD=testpass \
104+
"${repo_root}/src/jaro" serve 2>&1
105+
)"
106+
space_conf="${space_root}/.imap/dovecot.conf"
107+
[[ -f "${space_conf}" ]] || { print -- "ASSERT FAIL: missing ${space_conf}"; exit 1; }
108+
space_conf_body="$(cat "${space_conf}")"
109+
escaped_space_root="${space_root// /\\ }"
110+
assert_contains "${space_conf_body}" "mail_location = maildir:${escaped_space_root}:LAYOUT=fs" "serve escaped mail location"
111+
assert_contains "${space_out}" "Host: 127.0.0.1" "serve response with spaced root"
112+
113+
set +e
114+
JARO_SERVE_CONFIG_ONLY=1 JARO_SERVE_USER='bad:user' jaro_source serve >/dev/null 2>&1
115+
bad_user_ec=$?
116+
set -e
117+
[[ "${bad_user_ec}" -ne 0 ]] || { print -- "ASSERT FAIL: invalid user with colon accepted"; exit 1; }

src/zlibs/serve

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ serve_request() {
5252
fi
5353

5454
serve_mail_root="${MAILDIRS:A}"
55+
case "$serve_mail_root" in
56+
*:*)
57+
error "Invalid MAILDIRS path '$serve_mail_root' (':' not supported by dovecot mail_location)"
58+
return 1
59+
;;
60+
esac
5561
serve_runtime="${MAILDIRS}/.imap"
5662
serve_runtime="${serve_runtime:A}"
5763
serve_conf="${serve_runtime}/dovecot.conf"
@@ -65,6 +71,31 @@ serve_request() {
6571
return 0
6672
}
6773

74+
# Dovecot passwd-file fields must not contain separators/newlines.
75+
serve_validate_passwd_field() {
76+
fn serve_validate_passwd_field
77+
local _field_name="$1"
78+
local _field_value="$2"
79+
80+
[[ "$_field_value" == *:* ]] && {
81+
error "Invalid ${_field_name}: ':' is not allowed"
82+
return 1
83+
}
84+
[[ "$_field_value" == *$'\n'* || "$_field_value" == *$'\r'* ]] && {
85+
error "Invalid ${_field_name}: newlines are not allowed"
86+
return 1
87+
}
88+
return 0
89+
}
90+
91+
# Escape paths for dovecot.conf values.
92+
serve_escape_dovecot_path() {
93+
local _path="$1"
94+
_path="${_path//\\/\\\\}"
95+
_path="${_path// /\\\\ }"
96+
print -- "$_path"
97+
}
98+
6899
# Keep canonical maildirs present so IMAP exposure is predictable.
69100
serve_ensure_maildirs() {
70101
fn serve_ensure_maildirs
@@ -121,23 +152,31 @@ serve_generate_password() {
121152

122153
# Render dovecot.conf from request/domain state.
123154
serve_render_dovecot_conf() {
155+
local _mail_root _runtime _passwd_file _log_file _info_log_file
156+
_mail_root="$(serve_escape_dovecot_path "$serve_mail_root")"
157+
_runtime="$(serve_escape_dovecot_path "$serve_runtime")"
158+
_passwd_file="$(serve_escape_dovecot_path "$serve_passwd_file")"
159+
_log_file="$(serve_escape_dovecot_path "$serve_log_file")"
160+
_info_log_file="$(serve_escape_dovecot_path "$serve_info_log_file")"
161+
124162
cat <<EOF
125163
protocols = imap
126164
listen = ${serve_host}
127165
ssl = no
166+
disable_plaintext_auth = no
128167
auth_mechanisms = plain
129168
mail_debug = no
130169
131-
mail_location = maildir:${serve_mail_root}:LAYOUT=fs:INDEX=${serve_runtime}/index:CONTROL=${serve_runtime}/control
170+
mail_location = maildir:${_mail_root}:LAYOUT=fs:INDEX=${_runtime}/index:CONTROL=${_runtime}/control
132171
133-
base_dir = ${serve_runtime}/run
134-
state_dir = ${serve_runtime}/state
135-
log_path = ${serve_log_file}
136-
info_log_path = ${serve_info_log_file}
172+
base_dir = ${_runtime}/run
173+
state_dir = ${_runtime}/state
174+
log_path = ${_log_file}
175+
info_log_path = ${_info_log_file}
137176
138177
passdb {
139178
driver = passwd-file
140-
args = ${serve_passwd_file}
179+
args = ${_passwd_file}
141180
}
142181
143182
userdb {
@@ -164,6 +203,9 @@ EOF
164203
serve_write_runtime_files() {
165204
fn serve_write_runtime_files
166205

206+
serve_validate_passwd_field "JARO_SERVE_USER" "$serve_user" || return 1
207+
serve_validate_passwd_field "JARO_SERVE_PASSWORD" "$serve_password" || return 1
208+
167209
cat <<EOF > "$serve_passwd_file"
168210
${serve_user}:{PLAIN}${serve_password}:${serve_uid}:${serve_gid}::${serve_mail_root}::
169211
EOF
@@ -187,26 +229,42 @@ serve_validate_config() {
187229
return 0
188230
}
189231

190-
# Return 0 when selected port is already listening, 1 when free, 2 when unknown.
232+
# Return 0 when a listener conflicts with requested bind.
233+
serve_port_listener_conflicts() {
234+
fn serve_port_listener_conflicts
235+
local _listener="$1"
236+
237+
case "$_listener" in
238+
"127.0.0.1:${serve_port}"|"localhost:${serve_port}"|"0.0.0.0:${serve_port}"|"*:${serve_port}"|"[::]:${serve_port}"|":::${serve_port}")
239+
return 0
240+
;;
241+
esac
242+
return 1
243+
}
244+
245+
# Return 0 when selected port is already listening on conflicting address, 1 when free, 2 when unknown.
191246
serve_port_in_use() {
192247
fn serve_port_in_use
248+
local _listener
193249

194250
if command -v lsof >/dev/null 2>&1; then
195-
lsof -nP -iTCP:${serve_port} -sTCP:LISTEN 2>/dev/null \
196-
| awk '{print $9}' \
197-
| grep -Eq "(^|:)${serve_port}$" && return 0
251+
while IFS= read -r _listener; do
252+
serve_port_listener_conflicts "$_listener" && return 0
253+
done < <(lsof -nP -iTCP:${serve_port} -sTCP:LISTEN 2>/dev/null | awk '{print $9}')
198254
return 1
199255
fi
200256

201257
if command -v ss >/dev/null 2>&1; then
202-
ss -ltn 2>/dev/null | awk '{print $4}' \
203-
| grep -Eq "(^|:)${serve_port}$" && return 0
258+
while IFS= read -r _listener; do
259+
serve_port_listener_conflicts "$_listener" && return 0
260+
done < <(ss -ltn 2>/dev/null | awk '{print $4}')
204261
return 1
205262
fi
206263

207264
if command -v netstat >/dev/null 2>&1; then
208-
netstat -ltn 2>/dev/null | awk '{print $4}' \
209-
| grep -Eq "(^|:)${serve_port}$" && return 0
265+
while IFS= read -r _listener; do
266+
serve_port_listener_conflicts "$_listener" && return 0
267+
done < <(netstat -ltn 2>/dev/null | awk '{print $4}')
210268
return 1
211269
fi
212270

0 commit comments

Comments
 (0)