#!/bin/bash
# SPDX-License-Identifier: MIT

VERSION=1.0.0
SELF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
VERBOSE=false
DEBUG=false
: ${EXIT_ON_ERROR:=true}
: ${TOKEN_NAME:=forgejo-curl}
: ${DOT:=$HOME/.forgejo-curl}

function debug() {
    DEBUG=true
    set -x
    PS4='${BASH_SOURCE[0]}:$LINENO: ${FUNCNAME[0]}:  '
}

function verbose() {
    VERBOSE=true
}

function log() {
    echo "$@" >&2
}

function log_error() {
    log "$@"
}

function log_verbose() {
    if $VERBOSE ; then
	log "$@"
    fi
}

function log_info() {
    log "$@"
}

function fatal_error() {
    log_error "$@"
    if $EXIT_ON_ERROR ; then
	exit 1
    else
	return 1
    fi
}

function dot_ensure() {
    mkdir -p $DOT
}

HEADER_JSON='-H Content-Type:application/json'
HEADER_TOKEN="-H @$DOT/header-token"
HEADER_CSRF="-H @$DOT/header-csrf"

function api() {
    client $HEADER_TOKEN "$@"
}

function api_json() {
    api $HEADER_JSON "$@"
}

function login_api() {
    local user="$1" password="$2" token="$3" scopes="${4:-[\"all\"]}" url="$5"

    dot_ensure
    if test -s $DOT/token ; then
	log_info "already logged in, ignored"
	return
    fi

    if test -z "$token" ; then
	log_verbose curl -sS -X DELETE --user "${user}:${password}" "${url}/api/v1/users/$user/tokens/${TOKEN_NAME}" -o /dev/null -w "%{http_code}"
	local basic="${user:-unknown}:${password:-unknown}"
	local status=$(curl -sS -X DELETE --user "${basic}" "${url}/api/v1/users/$user/tokens/${TOKEN_NAME}" -o /dev/null -w "%{http_code}")
	if test "${status}" != 404 -a "${status}" != 204 ; then
	    fatal_error permission denied, the user or password are probably incorrect, try again with --verbose
	    return 1
	fi
	token=$(client $HEADER_JSON --user "${basic}" --data-raw '{"name":"'${TOKEN_NAME}'","scopes":'${scopes}'}' "${url}/api/v1/users/${user}/tokens"  | jq --raw-output .sha1)
    fi
    if [[ "$token" =~ ^@ ]] ; then
	cp "${token##@}" $DOT/token
    else
	echo "$token" > $DOT/token
    fi
    ( echo -n "Authorization: token " ; cat $DOT/token ) > $DOT/header-token
    #
    # Verify the token works
    #
    local status=$(api -w "%{http_code}" -o /dev/null "${url}/api/v1/user")
    if test "${status}" != 200 ; then
	fatal_error "${url}/api/v1/user returns status code '${status}', the token is invalid, $0 logout and login again"
	return 1
    fi
}

function client() {
    log_verbose curl --cookie $DOT/cookies -f -sS "$@"
    if ! curl --cookie $DOT/cookies -f -sS "$@" ; then
	fatal_error
    fi
}

function web() {
    client $HEADER_CSRF "$@"
}

function client_update_cookies() {
    log_verbose curl --cookie-jar $DOT/cookies --cookie $DOT/cookies -w "%{http_code}" -f -sS "$@"
    local status=$(curl --cookie-jar $DOT/cookies --cookie $DOT/cookies -w "%{http_code}" -f -sS "$@")
    if ! test "${status}" = 200 -o "${status}" = 303 ; then
	fatal_error
    fi
}

function login_client() {
    local user="$1" password="$2" url="$3"

    if test -z "$password" ; then
	log_verbose "no password, web will not be authenticated"
	return
    fi

    dot_ensure
    #
    # Get the CSRF required for login
    #
    client_update_cookies -o /dev/null "${url}/user/login"
    #
    # The login stores a cookie
    #
    client_update_cookies -X POST --data "user_name=${user}" --data "password=${password}" "${url}/user/login" -o $DOT/login.html
    #
    # Get the CSRF for reuse by other requests
    #
    client_update_cookies -o /dev/null "${url}/user/login"
    local csrf=$(sed -n -e '/csrf/s/.*csrf\t//p' $DOT/cookies)
    echo "X-Csrf-Token: $csrf" > $DOT/header-csrf
    #
    # Verify it works
    #
    local status=$(web -o /dev/null -w "%{http_code}" "${url}/user/settings")
    if test "${status}" != 200 ; then
	grep -C 1 flash-error $DOT/login.html
	if ${DEBUG} ; then
	    cat $DOT/login.html
	fi
	fatal_error login failed, the user or password are probably incorrect, try again with --verbose
    fi
}

function login() {
    local user="$1" password="$2" token="$3" scope="$4" url="$5"
    login_client "${user}" "${password}" "${url}"
    login_api "${user}" "${password}" "${token}" "${scope}" "${url}"
}

function logout() {
    rm -f $DOT/*
    if test -d $DOT ; then
	rmdir $DOT
    fi
}


function usage() {
    cat >&2 <<EOF
forgejo-curl.sh - thin curl wrapper that helps with Forgejo authentication

COMMON OPTIONS

  --verbose  display curl commands
  --debug    equivalent to set -x

LOGIN AND TOKEN

  The API endpoints that require authentication will be given the
  token provided with the --token argument. If not provided, it will
  be generated (and named $TOKEN_NAME), using the --user and
  --password credentials.

  The web endpoints that require authentication will be given a cookie
  and CSRF token created using the the --user and --password credentials

  On a successful login the credentials are stored in the $DOT
  directory to be used by the web, api, api_json commands. The logout
  command removes the $DOT directory.

  If the argument of --token starts with @, it is used as a filename
  from which the token will be read.

  forgejo-curl.sh [--verbose] [--debug]
		  [--user <user>] [--password <password>]
                  [--token {<token>|<@tokenfilename>}]
		  [--scopes <scopes>] login URL
  forgejo-curl.sh logout

  OPTIONS

    --user <user>                      username
    --password <password>              password of <user>
    --scopes <scopes>                  scopes of the token to be created (default ["all"])
    --token {<token>|<@tokenfilename>} personal access token

  EXAMPLES

    forgejo-curl.sh --token ABCD \\
		    login https://forgejo.example.com

      web           is not authenticated
      api, api_json use ABCD to authenticate

    forgejo-curl.sh --token @/tmp/token \\
		    login https://forgejo.example.com

      web           is not authenticated
      api, api_json use the content of /tmp/token to authenticate

    forgejo-curl.sh --user joe --password passw0rd \\
		    login https://forgejo.example.com

      web           is authenticated
      api, api_json use a newly generated token that belongs to user joe
		    with scope ["all"] to authenticate

    forgejo-curl.sh --user joe --password passw0rd --scopes '["write:package","write:issue"]' \\
		    login https://forgejo.example.com

      web           is authenticated
      api, api_json use a newly generated token with write permission to packages and issues
		    to authenticate

forgejo-curl.sh [--verbose] [--debug] web [curl options]"

  call curl using the CSRF token generated by the login command

  EXAMPLES

    forgejo-curl.sh web --form avatar=@avatar.png https://forgejo.example.com/settings/avatar

      upload the file avatar.png and update the avatar of the logged in user

forgejo-curl.sh [--verbose] [--debug] api|api_json [curl options]"

  call curl using the token given to (or generated by) the login command. If called using
  api_json, the Content-Type header is set to application/json.

  EXAMPLES

    forgejo-curl.sh api_json --data-raw '{"title":"TITLE"}' \\
		    https://forgejo.example.com/api/v1/repos/joe/test/issues

      create a new issue in the repository test

    forgejo-curl.sh api --form name=image.png --form attachment=@image.png \\
		    https://forgejo.example.com/api/v1/repos/joe/test/issues/1234/assets

      add the image.png file as an attachment to the issue 1234 in the test repository

forgejo-curl.sh --help - display help
forgejo-curl.sh --version - show the version
EOF
}

function main() {
    local command=login user password token scopes

    while true; do
	case "$1" in
	    --verbose)
		shift
		verbose
		;;
	    --debug)
		shift
		debug
		;;
	    --user)
		shift
		user="$1"
		shift
		;;
	    --password)
		shift
		password="$1"
		shift
		;;
	    --token)
		shift
		token="$1"
		shift
		;;
	    --scopes)
		shift
		scopes="$1"
		shift
		;;
	    login)
		shift
		login "$user" "$password" "$token" "$scopes" "$1"
		return 0
		;;
	    logout)
		shift
		logout
		return 0
		;;
	    web)
		shift
		web "$@"
		return 0
		;;
	    api)
		shift
		api "$@"
		return 0
		;;
	    api_json)
		shift
		api_json "$@"
		return 0
		;;
	    --version)
		echo "forgejo-curl.sh version $VERSION"
		return 0
		;;
	    --help|*)
		usage
		return 1
		;;
	esac
    done
}

${MAIN:-main} "${@}"