469 lines
15 KiB
Bash
Executable File
469 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#===============================================================================
|
|
# FILE: samba.sh
|
|
#
|
|
# USAGE: ./samba.sh
|
|
#
|
|
# DESCRIPTION: Entrypoint for samba docker container
|
|
#
|
|
# OPTIONS: ---
|
|
# REQUIREMENTS: ---
|
|
# BUGS: ---
|
|
# NOTES: ---
|
|
# AUTHOR: Struchkov Mark (mark@struchkov.dev),
|
|
# ORGANIZATION:
|
|
# CREATED: 09/28/2014 12:11
|
|
# REVISION: 1.0
|
|
#===============================================================================
|
|
|
|
set -o nounset # Treat unset variables as an error
|
|
|
|
# Constants
|
|
readonly SMB_CONF="/etc/samba/smb.conf"
|
|
|
|
### parse_args: safely parse semicolon-separated arguments
|
|
# Arguments:
|
|
# input) semicolon-separated string
|
|
# Return: array PARSED_ARGS with parsed values
|
|
parse_args() {
|
|
local input="$1"
|
|
PARSED_ARGS=()
|
|
local current=""
|
|
local i=0
|
|
while [ $i -lt ${#input} ]; do
|
|
local char="${input:$i:1}"
|
|
if [ "$char" = ";" ]; then
|
|
PARSED_ARGS+=("$current")
|
|
current=""
|
|
else
|
|
current+="$char"
|
|
fi
|
|
i=$((i + 1))
|
|
done
|
|
PARSED_ARGS+=("$current")
|
|
}
|
|
|
|
### charmap: setup character mapping for file/directory names
|
|
# Arguments:
|
|
# chars) from:to character mappings separated by ','
|
|
# Return: configured character mapings
|
|
charmap() { local chars="$1"
|
|
grep -q catia "$SMB_CONF" || sed -i '/TCP_NODELAY/a \
|
|
\
|
|
vfs objects = catia\
|
|
catia:mappings =\
|
|
|
|
' "$SMB_CONF"
|
|
|
|
sed -i '/catia:mappings/s| =.*| = '"$chars"'|' "$SMB_CONF"
|
|
}
|
|
|
|
### set_config_option: set a config option in a specific section
|
|
# Arguments:
|
|
# section) section name (e.g., "global", "share")
|
|
# option) raw option string (e.g., "log level = 2")
|
|
# Return: line added/updated in smb.conf
|
|
set_config_option() {
|
|
local section="$1"
|
|
local key value
|
|
key="$(sed 's| *=.*||' <<< "$2")"
|
|
value="$(sed 's|[^=]*= *||' <<< "$2")"
|
|
|
|
if sed -n '/^\['"$section"'\]/,/^\[/p' "$SMB_CONF" | grep -qE '^;*[[:space:]]*'"$key"; then
|
|
sed -i '/^\['"$section"'\]/,/^\[/s|^;*[[:space:]]*\('"$key"' = \).*| \1'"$value"'|' \
|
|
"$SMB_CONF"
|
|
else
|
|
sed -i '/^\['"$section"'\]/a \ '"$key = $value" "$SMB_CONF"
|
|
fi
|
|
}
|
|
|
|
### generic: set a generic config option in a section
|
|
# Arguments:
|
|
# section) section of config file
|
|
# option) raw option
|
|
# Return: line added to smb.conf (replaces existing line with same key)
|
|
generic() {
|
|
set_config_option "$1" "$2"
|
|
}
|
|
|
|
### global: set a global config option
|
|
# Arguments:
|
|
# option) raw option
|
|
# Return: line added to smb.conf (replaces existing line with same key)
|
|
global() {
|
|
set_config_option "global" "$1"
|
|
}
|
|
|
|
### include: add a samba config file include
|
|
# Arguments:
|
|
# file) file to import
|
|
include() { local includefile="$1"
|
|
sed -i "\\|include = $includefile|d" "$SMB_CONF"
|
|
echo "include = $includefile" >> "$SMB_CONF"
|
|
}
|
|
|
|
### import: import a smbpasswd file
|
|
# Arguments:
|
|
# file) file to import
|
|
# Return: user(s) added to container
|
|
import() { local file="$1" name id
|
|
while read name id; do
|
|
grep -q "^$name:" /etc/passwd || adduser -D -H -u "$id" "$name"
|
|
done < <(cut -d: -f1,2 "$file" | sed 's/:/ /')
|
|
pdbedit -i "smbpasswd:$file"
|
|
}
|
|
|
|
### perms: fix ownership and permissions of share paths
|
|
# Arguments:
|
|
# none)
|
|
# Return: result
|
|
perms() { local i
|
|
while IFS= read -r i; do
|
|
[[ -z "$i" ]] && continue
|
|
chown -Rh smbuser:smb "$i"
|
|
find "$i" -type d ! -perm 775 -exec chmod 775 {} \;
|
|
find "$i" -type f ! -perm 0664 -exec chmod 0664 {} \;
|
|
done < <(awk -F ' = ' '/ path = / {print $2}' "$SMB_CONF")
|
|
}
|
|
export -f perms
|
|
|
|
### recycle: disable recycle bin
|
|
# Arguments:
|
|
# none)
|
|
# Return: result
|
|
recycle() {
|
|
sed -i '/recycle:/d; /vfs objects/s/ recycle / /' "$SMB_CONF"
|
|
}
|
|
|
|
### share: Add share
|
|
# Arguments:
|
|
# share) share name
|
|
# path) path to share
|
|
# browsable) 'yes' or 'no'
|
|
# readonly) 'yes' or 'no'
|
|
# guest) 'yes' or 'no'
|
|
# users) list of allowed users
|
|
# admins) list of admin users
|
|
# writelist) list of users that can write to a RO share
|
|
# comment) description of share
|
|
# Return: result
|
|
share() {
|
|
local share="$1" path="$2" browsable="${3:-yes}" ro="${4:-yes}" \
|
|
guest="${5:-yes}" users="${6:-""}" admins="${7:-""}" \
|
|
writelist="${8:-""}" comment="${9:-""}"
|
|
|
|
# Validate share name (alphanumeric, space, underscore, hyphen only)
|
|
if [[ ! "$share" =~ ^[a-zA-Z0-9\ _-]+$ ]]; then
|
|
echo "ERROR: Invalid share name format: $share" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Validate path (must be absolute)
|
|
if [[ ! "$path" =~ ^/ ]]; then
|
|
echo "ERROR: Share path must be absolute: $path" >&2
|
|
return 1
|
|
fi
|
|
|
|
sed -i "/\\[$share\\]/,/^\$/d" "$SMB_CONF"
|
|
echo "[$share]" >> "$SMB_CONF"
|
|
echo " path = $path" >> "$SMB_CONF"
|
|
echo " browsable = $browsable" >> "$SMB_CONF"
|
|
echo " read only = $ro" >> "$SMB_CONF"
|
|
echo " guest ok = $guest" >> "$SMB_CONF"
|
|
[[ ${VETO:-yes} == no ]] || {
|
|
echo -n " veto files = /.apdisk/.DS_Store/.TemporaryItems/" >> "$SMB_CONF"
|
|
echo -n ".Trashes/desktop.ini/ehthumbs.db/Network Trash Folder/" >> "$SMB_CONF"
|
|
echo "Temporary Items/Thumbs.db/" >> "$SMB_CONF"
|
|
echo " delete veto files = yes" >> "$SMB_CONF"
|
|
}
|
|
[[ ${users:-""} && ! ${users:-""} == all ]] &&
|
|
echo " valid users = $(tr ',' ' ' <<< "$users")" >> "$SMB_CONF"
|
|
[[ ${admins:-""} && ! ${admins:-""} =~ none ]] &&
|
|
echo " admin users = $(tr ',' ' ' <<< "$admins")" >> "$SMB_CONF"
|
|
[[ ${writelist:-""} && ! ${writelist:-""} =~ none ]] &&
|
|
echo " write list = $(tr ',' ' ' <<< "$writelist")" >> "$SMB_CONF"
|
|
[[ ${comment:-""} && ! ${comment:-""} =~ none ]] &&
|
|
echo " comment = $(tr ',' ' ' <<< "$comment")" >> "$SMB_CONF"
|
|
echo "" >> "$SMB_CONF"
|
|
[[ -d "$path" ]] || mkdir -p "$path"
|
|
}
|
|
|
|
### smb: disable SMB2 minimum
|
|
# Arguments:
|
|
# none)
|
|
# Return: result
|
|
smb() {
|
|
sed -i 's/\([^#]*min protocol *=\).*/\1 LANMAN1/' "$SMB_CONF"
|
|
}
|
|
|
|
### user: add a user
|
|
# Arguments:
|
|
# name) for user
|
|
# password) for user
|
|
# id) for user
|
|
# group) for user
|
|
# gid) for group
|
|
# Return: user added to container
|
|
user() {
|
|
local name="$1" passwd="$2" id="${3:-""}" group="${4:-""}" gid="${5:-""}"
|
|
|
|
# Validate username (alphanumeric, underscore, hyphen only)
|
|
if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
echo "ERROR: Invalid username format: $name" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Create group if specified and doesn't exist
|
|
if [[ -n "$group" ]]; then
|
|
if ! grep -q "^$group:" /etc/group; then
|
|
if [[ -n "$gid" ]]; then
|
|
addgroup --gid "$gid" "$group"
|
|
else
|
|
addgroup "$group"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Create user if doesn't exist
|
|
if ! grep -q "^$name:" /etc/passwd; then
|
|
local adduser_args="-D -H"
|
|
[[ -n "$group" ]] && adduser_args="$adduser_args -G $group"
|
|
[[ -n "$id" ]] && adduser_args="$adduser_args -u $id"
|
|
adduser $adduser_args "$name"
|
|
fi
|
|
|
|
# Set password securely using heredoc (not visible in process list)
|
|
smbpasswd -s -a "$name" <<EOF
|
|
$passwd
|
|
$passwd
|
|
EOF
|
|
}
|
|
|
|
### workgroup: set the workgroup
|
|
# Arguments:
|
|
# workgroup) the name to set
|
|
# Return: configure the correct workgroup
|
|
workgroup() { local workgroup="$1"
|
|
sed -i 's|^\( *workgroup = \).*|\1'"$workgroup"'|' "$SMB_CONF"
|
|
}
|
|
|
|
### timemachine: enable Time Machine support for macOS
|
|
# Arguments:
|
|
# none)
|
|
# Return: result
|
|
timemachine() {
|
|
# Add fruit and streams_xattr to vfs objects
|
|
sed -i 's/\(vfs objects = catia\)/\1 fruit streams_xattr/' "$SMB_CONF"
|
|
|
|
# Add Time Machine configuration
|
|
sed -i '/\[global\]/a \
|
|
fruit:aapl = yes\
|
|
fruit:metadata = stream\
|
|
fruit:model = TimeCapsule6,116\
|
|
fruit:delete_empty_adfiles = yes\
|
|
fruit:time machine = yes\
|
|
fruit:veto_appledouble = no\
|
|
fruit:wipe_intentionally_left_blank_rfork = yes' "$SMB_CONF"
|
|
}
|
|
|
|
### widelinks: allow access wide symbolic links
|
|
# Arguments:
|
|
# none)
|
|
# Return: result
|
|
widelinks() {
|
|
local replace='\1\n wide links = yes\n unix extensions = no'
|
|
sed -i 's/\(follow symlinks = yes\)/'"$replace"'/' "$SMB_CONF"
|
|
}
|
|
|
|
### secure: enable enhanced security (signing and encryption)
|
|
# Arguments:
|
|
# none)
|
|
# Return: result
|
|
secure() {
|
|
sed -i '/\[global\]/a \
|
|
# Enhanced security settings\
|
|
server signing = mandatory\
|
|
client signing = mandatory\
|
|
smb encrypt = desired' "$SMB_CONF"
|
|
}
|
|
|
|
### setup_recycle_cron: configure cron job for recycle bin cleanup
|
|
# Arguments:
|
|
# none)
|
|
# Return: cron job configured if recycle is active
|
|
setup_recycle_cron() {
|
|
# Check if recycle bin is enabled (not disabled by -r flag)
|
|
if ! grep -q 'vfs objects.*recycle' "$SMB_CONF"; then
|
|
echo "INFO: Recycle bin is disabled, skipping cron setup" >&2
|
|
return 0
|
|
fi
|
|
|
|
local age="${RECYCLE_AGE}"
|
|
local hour="${RECYCLE_CRON_HOUR:-3}"
|
|
local minute="${RECYCLE_CRON_MINUTE:-0}"
|
|
|
|
# Validate RECYCLE_AGE is a positive integer
|
|
if ! [[ "$age" =~ ^[0-9]+$ ]] || [ "$age" -lt 1 ]; then
|
|
echo "ERROR: RECYCLE_AGE must be a positive integer (got: $age)" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Validate hour (0-23)
|
|
if ! [[ "$hour" =~ ^[0-9]+$ ]] || [ "$hour" -lt 0 ] || [ "$hour" -gt 23 ]; then
|
|
echo "ERROR: RECYCLE_CRON_HOUR must be 0-23 (got: $hour)" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Validate minute (0-59)
|
|
if ! [[ "$minute" =~ ^[0-9]+$ ]] || [ "$minute" -lt 0 ] || [ "$minute" -gt 59 ]; then
|
|
echo "ERROR: RECYCLE_CRON_MINUTE must be 0-59 (got: $minute)" >&2
|
|
return 1
|
|
fi
|
|
|
|
echo "INFO: Setting up recycle cleanup cron (age: ${age} days, schedule: ${hour}:${minute})" >&2
|
|
|
|
# Create crontab with environment variable
|
|
cat > /etc/crontabs/root <<EOF
|
|
# Recycle bin cleanup - delete files older than ${age} days
|
|
RECYCLE_AGE=${age}
|
|
${minute} ${hour} * * * /usr/local/bin/recycle-cleanup.sh >> /var/log/recycle-cleanup.log 2>&1
|
|
EOF
|
|
|
|
# Start crond in background
|
|
crond -b -l 8
|
|
echo "INFO: Crond started for recycle bin cleanup" >&2
|
|
}
|
|
|
|
### usage: Help
|
|
# Arguments:
|
|
# none)
|
|
# Return: Help text
|
|
usage() { local RC="${1:-0}"
|
|
echo "Usage: ${0##*/} [-opt] [command]
|
|
Options (fields in '[]' are optional, '<>' are required):
|
|
-h This help
|
|
-c \"<from:to>\" setup character mapping for file/directory names
|
|
required arg: \"<from:to>\" character mappings separated by ','
|
|
-G \"<section;parameter>\" Provide generic section option for smb.conf
|
|
required arg: \"<section>\" - IE: \"share\"
|
|
required arg: \"<parameter>\" - IE: \"log level = 2\"
|
|
-g \"<parameter>\" Provide global option for smb.conf
|
|
required arg: \"<parameter>\" - IE: \"log level = 2\"
|
|
-i \"<path>\" Import smbpassword
|
|
required arg: \"<path>\" - full file path in container
|
|
-n Start the 'nmbd' daemon to advertise the shares
|
|
-p Set ownership and permissions on the shares
|
|
-r Disable recycle bin for shares
|
|
-S Disable SMB2 minimum version
|
|
-t Enable Time Machine support for macOS
|
|
-s \"<name;/path>[;browse;readonly;guest;users;admins;writelist;comment]\"
|
|
Configure a share
|
|
required arg: \"<name>;</path>\"
|
|
<name> is how it's called for clients
|
|
<path> path to share
|
|
NOTE: for the default value, just leave blank
|
|
[browsable] default:'yes' or 'no'
|
|
[readonly] default:'yes' or 'no'
|
|
[guest] allowed default:'yes' or 'no'
|
|
NOTE: for user lists below, usernames are separated by ','
|
|
[users] allowed default:'all' or list of allowed users
|
|
[admins] allowed default:'none' or list of admin users
|
|
[writelist] list of users that can write to a RO share
|
|
[comment] description of share
|
|
-u \"<username;password>[;ID;group;GID]\" Add a user
|
|
required arg: \"<username>;<passwd>\"
|
|
<username> for user
|
|
<password> for user
|
|
[ID] for user
|
|
[group] for user
|
|
[GID] for group
|
|
-w \"<workgroup>\" Configure the workgroup (domain) samba should use
|
|
required arg: \"<workgroup>\"
|
|
<workgroup> for samba
|
|
-W Allow access wide symbolic links
|
|
-I Add an include option at the end of the smb.conf
|
|
required arg: \"<include file path>\"
|
|
<include file path> in the container, e.g. a bind mount
|
|
-E Enable enhanced security (signing and encryption)
|
|
Enables: server signing, client signing, SMB encryption
|
|
|
|
Environment only options:
|
|
RECYCLE_AGE=<days> Enable automatic recycle bin cleanup
|
|
Files older than <days> will be deleted daily
|
|
Requires recycle bin to be active (no -r flag)
|
|
RECYCLE_CRON_HOUR=<0-23> Hour for cleanup job (default: 3)
|
|
RECYCLE_CRON_MINUTE=<0-59> Minute for cleanup job (default: 0)
|
|
|
|
The 'command' (if provided and valid) will be run instead of samba
|
|
" >&2
|
|
exit $RC
|
|
}
|
|
|
|
[[ "${USERID:-""}" =~ ^[0-9]+$ ]] && usermod -u $USERID -o smbuser
|
|
[[ "${GROUPID:-""}" =~ ^[0-9]+$ ]] && groupmod -g $GROUPID -o smb
|
|
|
|
while getopts ":hc:EG:g:i:nprs:Stu:Ww:I:" opt; do
|
|
case "$opt" in
|
|
h) usage ;;
|
|
c) charmap "$OPTARG" ;;
|
|
E) secure ;;
|
|
G) parse_args "$OPTARG"; generic "${PARSED_ARGS[@]}" ;;
|
|
g) global "$OPTARG" ;;
|
|
i) import "$OPTARG" ;;
|
|
n) NMBD="true" ;;
|
|
p) PERMISSIONS="true" ;;
|
|
r) recycle ;;
|
|
s) parse_args "$OPTARG"; share "${PARSED_ARGS[@]}" ;;
|
|
S) smb ;;
|
|
t) timemachine ;;
|
|
u) parse_args "$OPTARG"; user "${PARSED_ARGS[@]}" ;;
|
|
w) workgroup "$OPTARG" ;;
|
|
W) widelinks ;;
|
|
I) include "$OPTARG" ;;
|
|
"?") echo "Unknown option: -$OPTARG"; usage 1 ;;
|
|
":") echo "No argument value for option: -$OPTARG"; usage 2 ;;
|
|
esac
|
|
done
|
|
shift $(( OPTIND - 1 ))
|
|
|
|
[[ "${CHARMAP:-""}" ]] && charmap "$CHARMAP"
|
|
while read i; do
|
|
global "$i"
|
|
done < <(env | awk '/^GLOBAL[0-9=_]/ {sub (/^[^=]*=/, "", $0); print}')
|
|
[[ "${IMPORT:-""}" ]] && import "$IMPORT"
|
|
[[ "${RECYCLE:-""}" ]] && recycle
|
|
while read i; do
|
|
parse_args "$i"; share "${PARSED_ARGS[@]}"
|
|
done < <(env | awk '/^SHARE[0-9=_]/ {sub (/^[^=]*=/, "", $0); print}')
|
|
# Process GENERIC after SHARE so share sections exist
|
|
while read i; do
|
|
parse_args "$i"; generic "${PARSED_ARGS[@]}"
|
|
done < <(env | awk '/^GENERIC[0-9=_]/ {sub (/^[^=]*=/, "", $0); print}')
|
|
[[ "${SMB:-""}" ]] && smb
|
|
[[ "${TIMEMACHINE:-""}" ]] && timemachine
|
|
while read i; do
|
|
parse_args "$i"; user "${PARSED_ARGS[@]}"
|
|
done < <(env | awk '/^USER[0-9=_]/ {sub (/^[^=]*=/, "", $0); print}')
|
|
[[ "${WORKGROUP:-""}" ]] && workgroup "$WORKGROUP"
|
|
[[ "${WIDELINKS:-""}" ]] && widelinks
|
|
[[ "${SECURE:-""}" ]] && secure
|
|
[[ "${INCLUDE:-""}" ]] && include "$INCLUDE"
|
|
[[ "${PERMISSIONS:-""}" ]] && perms &
|
|
[[ "${RECYCLE_AGE:-""}" ]] && setup_recycle_cron
|
|
|
|
if [[ $# -ge 1 ]]; then
|
|
# Validate command exists and is executable
|
|
cmd_path=$(command -v "$1" 2>/dev/null)
|
|
if [[ -n "$cmd_path" && -x "$cmd_path" ]]; then
|
|
echo "INFO: Executing custom command: $1" >&2
|
|
exec "$@"
|
|
else
|
|
echo "ERROR: command not found or not executable: $1" >&2
|
|
exit 13
|
|
fi
|
|
elif ps -ef | grep -E -v grep | grep -q smbd; then
|
|
echo "Service already running, please restart container to apply changes"
|
|
else
|
|
[[ ${NMBD:-""} ]] && ionice -c 2 -n 4 nmbd -D
|
|
exec ionice -c 2 -n 4 smbd -F --debug-stdout --no-process-group </dev/null
|
|
fi |