an alternative to 0x0: your irc bouncer

not too long ago, the soju irc bouncer added support for the irc protocol soju.im/FILEHOST, which allows irc clients to upload files to the bouncer and return back a the uploaded location of the file. the file itself can be stored on the server's filesystem, or sent to a HTTP url which can be proxied to 0x0 via nullptrproxy.

often times, a file upload service is quite handy, and if you're already self-hosting soju, there's actually a way to make use of the existing filesystem storage of uploaded files (referred to as fileuploadFS), and it's through how the protocol actually proccesses the file.

the protocol states that the bouncer has a HTTP upload uri (which can be retrieved from the client), and the filedata appended, credentials, among other headers. example request:

POST /upload HTTP/1.1
Host: irc.example.org
Content-Type: image/jpeg
Content-Disposition: attachment; filename="picture.jpeg"
Content-Length: 4242
Authorization: Basic c2V1bmdoeWU6bm8=

the authorization header can be of HTTP Basic (which uses the bouncer credentials) or Bearer (SASL OAUTHBEARER). HTTP Basic simply is the username, followed by a colon, followed by the password.

making a simple script or program that utilizes this endpoint is actually really easy, and makes for a great alternative for self-hosting 0x0. however, knowing that this script is going to be specific to a soju bouncer, it's safe to hard-code the POST url to $bouncer/uploads, as that's what soju uses.

the cURL request can look like this:

curl -v "$address/uploads" \
    -H "Content-type: $mime" \
    -H "Content-Disposition: attachment; filename=\"$file\"" \
    -H "Authorization: Basic $auth" \
    --upload-file "$file"

where $address is the URL of the bouncer, and $auth is simply the base64 representation of $username:$password. it should be noted that the cURL -v flag is added to read the header that has the URL of the file, since the response can look like this:

HTTP/1.1 201 Created
Location: /upload/hoh5eFThae4e.jpeg

soju actually doesn't allow files that contain charset=binary in their mime type

if you're using an existing IRC client, such as senpai, it can be possible to retrieve the address and credentials by parsing the configuration file. in POSIX sh, that is doable by iterating over the file:

while read -r directive params; do
    case "$directive" in
    (address)
        address="${params##*//}" # remove the protocol *://
        address="https://${address%%:*}" # remove the port :*
        ;;
    (nickname) nickname="$params" ;;
    (password) password="$params" ;;
    (password-cmd) password="$($params)" ;;
    esac
done < ~/.config/senpai/senpai.scfg

for reference, the senpai configuation looks like this:

address ircs://b.sewn.moe
nickname sewn
password-cmd rbw get b.sewn.moe

you might notice password-cmd is also handled here, by simply executing the command (configuration param); no quotes are added to use the variable since word splitting is actually necessary to allow the command arguments to work. this is obviously unsafe, but senpai already executes this on startup :p

finally, a script that combines credential reading, parsing headers, handling errors, and also handling stdin, and $XDG_CONFIG_HOME, will look like this!

#!/bin/sh
# small script for uploading files or stdin using soju
# FILEHOST with senpai's configuration

# terminal?
if [ -t 0 ]; then
    file="${1:?}"
    # soju doesn't permit charset=binary to be uploaded, workaround by hiding it
    mime="$(file --brief --mime-type "$file")"
else
    file=/dev/stdin
    # avoid reading stdin for mime
    mime='application/octet-stream'
fi

while read -r directive params; do
    case "$directive" in
    (address)
        address="${params##*//}" # remove protocol *://
        address="https://${address%%:*}" # remove port :* & add https
        ;;
    (nickname) nickname="$params" ;;
    (password) password="$params" ;;
    (password-cmd) password="$($params)" ;; # word splitting required
    esac
done < "${XDG_CONFIG_HOME:-$HOME/.config}"/senpai/senpai.scfg

auth="$(printf '%s:%s' "$nickname" "$password" | base64)"

# soju prints errors to the response body, send to stderr;
# no need for cURL --fail or --fail-with-body.
# and send the headers to stdout to be read here.
while IFS=': ' read -r header value; do
    case "$header" in
    # shell won't exit on failure in a here document, exit manually
    # by obtaining the HTTP return code.
    (HTTP*) [ "${value%%\ *}" -gt 400 ] && echo "$value" && exit 1 ;;
    (Location) echo "$address$value" ;;
    esac
done <<-EOCURL
$(curl -sX POST -o /dev/stderr --dump-header /dev/stdout "$address/uploads" \
    -H "Content-type: $mime" \
    -H "Content-Disposition: attachment; filename=\"$file\"" \
    -H "Authorization: Basic $auth" \
    --upload-file "$file")
EOCURL

makes for a great alternative to uploading files.

it should be noted that files uploaded via fileuploadFS will not be removed after a certain number of days, and that duplicated files are not checked for. maybe those feature can be added sometime soon :3