Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

Drag-and-drop target to open video weblinks in VLC or youtube-dl

Drop-videos is a small project for personal use, that displays an icon that acts as a drag-and-drop target that invokes either a downloader or a viewer, depending on how you invoked the program. The main utility is drop-videos.sh which displays a small icon that lets you drop video links on it. Depending on the environment variables at invocation, the program will download/view the links.

Using

Several example scripts are provided, which is every script in this directory excluding drop-videos.sh. My main use cases include the following provided examples. Use case | file
---|---
Download all videos from this playlist | save-playlist.sh
Download the specific link | save-audio.sh
View the specific link | view-video.sh
The application accepts the standard environment variables DEBUG and DRYRUN.

Upstreams

Dragon

This project uses a customized dragon binary. The diff is provided here, as well as the customized source code.

Alternatives

I read article Open YouTube (And More) Videos From Your Web Browser With mpv [Firefox, Chrome] - Linux Uprising Blog which describes how to set up a browser extension that lets you add a userscript that shows a small button in a youtube browser page that opens that video in a local video player. This worked on my systems (aside from the fact that I don't have mpv installed so the protocol handler fails), but I wanted a solution that would work without having to visit each and every link first. This article that inspired me refers to Tampermonkey which appears to be a fork of greasemonkey which has a fork for Palemoon. This Greasemonkey can use the userscript that will introduce a custom protocol which you can configure the OS to send to a specific program.

Dependencies

Compiling

To compile the C binary, you need gtk3 libraries.

Running

Bourne shell youtube-dl vlc or other media player

Modify html file to use correctly-cased href and src filenames

I was trying to read the included index.html file for the Civilization 2 scenario Star Trek: Battle for the Alpha Quadrant by Kobayashi. When I exploded the 7z file on my GNU/Linux system, the html file points to invalid files for some images and links because of the case- sensitive filesystem. So rather than reading through the file manually to investigate how I should rename things, I decided to modify the html file to point to the extant files. Here is my mostly general solution for that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/usr/bin/env python3
# Startdate: 2021-07-17
# Purpose: fix incorrectly-cased links in html file
# Usage:
#    ~/dev/fix-links/fix_links.py ~/Downloads/BAQ/Index.HTM ~/Downloads/BAQ/asdf.html
from bs4 import BeautifulSoup
import sys, os, glob

def fix_links(contents,outfile=None):
   soup = BeautifulSoup(contents,'html.parser')
   changed=0
   for item in soup.find_all(src=True): # mostly for img
      if not os.path.exists(item['src']):
         print(f"need to fix {item['src']}")
         item['src'] = find_case_insensitive(item['src'])
         changed += 1
   for item in soup.find_all(href=True): # finds anything with an href, so A and other tags.
      if not os.path.exists(item['href']):
         print(f"need to fix {item['href']}")
         newvalue=find_case_insensitive(item['href'])
         #print(f"Is it {newvalue}?")
         item['href'] = newvalue
         changed += 1
   if changed > 0:
      print("Made a change!")
      if outfile:
         with open(outfile,"w") as o:
            o.write(str(soup.prettify()))
      else:
         print(soup)

def find_case_insensitive(filename, dirname = os.path.curdir):
   # screw doing it the pythonic way. Just do it the real way.
   # major flaw: this only works for current-path files, i.e., no directory name is in the searched filename.
   output = os.popen(f"find {dirname} -iname {filename} -printf '%f'").read()
   return output

if __name__ == "__main__":
   contents="<html>dummy text</html>"
   if len(sys.argv) >= 2:
      infile= sys.argv[1]
      with open(infile,"r") as i:
         contents=i.read()
   outfile = None
   if len(sys.argv) >= 3:
      outfile = sys.argv[2]
   fix_links(contents, outfile)

Yes, I don't do a lot of protection of the parameters, and I don't even handle when using files in subdirectories. It wasn't in my use case so I didn't have to solve for that. So stay tuned for if I ever need to add that! I use the beautiful soup python library, to find all elements that have a src or href tag, and then ensure it points to a file. And probably unfortunately, but I only know an easy way to find a case-insensitive file with low-level tools, not some fancy- pants pythonic way.

Listen to your own microphone in pulseaudio

Recently I wanted to pipe my mic input to my headphones (in addition to sending it on to Discord). I learned how to listen to my line-in input with pulseaudio. This is a direct ripoff of https://thelinuxexperiment.com/pulseaudio-monitoring-your-line-in- interface/. You need to know the names of your sources (inputs) and sinks (outputs) that pulseaudio uses.

pacmd list-source-outputs | grep 'source:'
pacmd list-sink-inputs | grep 'sink:'

But my tab autocompletion worked, so the above steps may not be entirely necessary if you have a simple setup like me. So now you can run pacat and pipe the input to the output.

pacat -r --latency-msec=1 -d alsa_input.pci-0000_00_1b.0.analog-stereo | pacat -p --latency-msec=1 -d alsa_output.pci-0000_00_1b.0.analog-stereo

Another way to do it, but it incurs a terrible delay:

pactl load-module module-loopback
pactl unload-module module-loopback

And actually, the pacat with the 1-millisecond delay because untenable after enough time because its 1ms delay adds up over time. It might be worth scripting killing that every 10 minutes and starting a new pacat process pair.

Thoughts on my documentation process

When I write a solution for myself, such as my gallery or my transparent web proxy, I write a master document that describes how I built it, why I built it, and how to use it. Why I built it usually starts the document, and explains what I needed it for: What problem it solves. Sometimes my solutions are proof-of-concept or demonstrations only. But still, I like to remind my future self of what it's for. How I built it ("Architecture") is the section that explains the initial steps, such as packages installed, users added, selinux contexts or policies built (and not necessarily the methodology for building an selinux policy, at this point, due to the repetition), pip packages versus system packages, and so on. Anything that I needed to do only once for the solution (per host, if a multi-node solution). The how to use it section is titled "Operations." I describe how to do tasks, such as set up a new entry or new instance, or modify a commonly-updated setting. This section tends to grow over time as I update the solution. On rare occasions I need to update the architecture section of the document because I had to make a one-time change for the solution. The how-to section can duplicate some information from the Architecture section, if I expect some of those first-time steps are what are repeated for the proposed future steps. When I publish a solution, I try to move all my configs to a separate conf file, so that I can include a app.conf.example file or similar, and exclude app.conf from source control. I even have a small sed script that helps scrub my private data:

$ sed -r -f /mnt/public/work/gitlab/scrub.sed < app.conf > app.conf.example

The contents of the script are mostly just substitutions.

s/privateservername([0-9]*)/serve\1/g;
s/username73/exampleuser5/g;

I always include a References section, so that I can review my sources in the future. I do return to them, because I am seeking to expand my solution or fix a part that breaks (i.e., used to work but doesn't now).

Trick for youtube-dl with UnicodeEncodeError

I got this message with youtube-dl, which is a python problem.

UnicodeEncodeError: 'latin-1' codec can't encode characters in position 5-9: ordinal not in range(256)

After a small amount of Internet searches I found clues to a solution. I really just wanted the file on the local filesystem, and I can worry about renaming it later.

LANG=latin-1 PYTHONIOENCODING=latin-1 youtube-dl -x --audio-format mp3 https://www.youtube.com/watch?v=1234567890

Oh, sure the rendered file has a untypeable name, but bash autocompletion somehow still works.

mv A\ New\ Way\ Of\ Thinking \([tab]newfile.mp3

Sync files and use hardlinks

I had a small project for myself: synchronize a subset of my locally-stored music to my mobile phone. I already have syncthing which is great. The biggest problem is my music collection is way larger than the current storage on my mobile phone. So I wrote some functions to randomly select so many megabytes of my collection, and then sync those in to a separate directory. I thought rsync would do what I want, but I couldn't get it to actually create hardlinks even with the --link-dest=/src/dir/. But I eventually found cp -l which does what I want!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/bin/sh
# File: /mnt/public/Support/Programs/syncthing/device18_music-sync.sh
# License: CC-BY-SA 4.0
# Author: bgstack15
# Startdate: 2021-06-26 09:19
# Title: Music Subset Selection for Syncing to device18
# Purpose: Place hardlinks to randomly-selected music directories in a spot for syncthing
# History:
# Usage:
#    This should be run from server1 itself, because of the hardlinks involved.
#    Can use this in cron.
# Reference:
# Improve:
#    make excludes work!
# Documentation: 
#    Rsync
#    Workflow:
#       generate list of X number of inputdirs from INDIR
#       for any outdirs in OUTDIR that are not in the list, unlink all files and then rmdir these expired outdirs.
#       create directories in OUTDIR that match these inputdirs
#       hardlink contents in inputdirs to each outputdir.

INDIR=/var/server1/shares/bgstack15/Music
OUTDIR=/var/server1/shares/public/Music/syncthing/device18_music
EXCLUDE_PATTERNS="*.png;*.jpg;*.txt;*.m3u"
MAXDISKSPACE="2048" # in MB
LOGFILE="/var/server1/shares/public/Support/Systems/server1/var/log/sync-device18_music/$( date "+%F" ).log"
# accept variables DEBUG APPLY

generate_list() {
   # return to stdout a list of directory names underneath INDIR
   # call:
   #    NOT FULLY IMPLEMENTED: generate_list "${INDIR}" "${COUNT}" "${MAXDISKSPACE}"
   full_list="$( cd "${INDIR}" ; find . -maxdepth 1 -mindepth 1 ! -type f -exec du -sxBM {} \; )"
   partial_list="$( echo "${full_list}" | sort --random-sort | awk -v "maxsize=${MAXDISKSPACE}" '{a=a+$1; final=final""$0"\n";if(a>=maxsize){print final;exit;};}' | head -n -1 )"
   echo "${partial_list}" | awk '{$1="";gsub("./","",$2);print;}' | sed -r -e 's/^ //;' | sort
}

sync_selected_folders() {
   # read from stdin the directory names underneath $INDIR
   ssf_input="$( cat )"
   mkdir -p "${OUTDIR}"
   cd "${OUTDIR}"
   rsync_excludes="$( echo "${EXCLUDE_PATTERNS}" | sed -r -e 's/^|;/  --exclude=/g;' )"

   # step 1: remove anything here not in the list
   # which we accomplish by moving every old thing out of the way to a new dir
   mkdir -p "old" ; mv * old/ 2>/dev/null ;

   # step 2: make new dirs
   echo "${ssf_input}" | while read td ;
   do
      if ls -l "old/${td}" 1>/dev/null 2>&1 ;
      then
         test -n "${DEBUG}" && printf '%s\n' "Already exists: ${td}"
         test -n "${APPLY}" && mv "old/${td}" .
      else
         test -n "${DEBUG}" && printf '%s\n' "Copy in: ${td}"
         test -n "${APPLY}" && {
            cp -prl "${INDIR}/${td}" "./${td}"
         }
         # 2021-07-01 12:09 rsync just isn't making hardlinks like I want
         #test -n "${APPLY}" && rsync -rpltgoD -t -H ${DEBUG:+-v} --link-dest="${INDIR}/${td}/" ${EXCLUDE_PATTERNS:+${rsync_excludes}} "${INDIR}/${td}" ./
      fi
   done

   # yes, inverse. We needed everything in ./old/ for the checks above, so now we need to move them back, if we were not applying anything.
   test -z "${APPLY}" && { ls -1 old/* 1>/dev/null 2>&1 && mv "old/"* . ; } ;
   # so now, anything still in the old/ is not on today's list. So time to delete it.
   test -n "${DEBUG}" && { find "./old/" -maxdepth 1 -mindepth 1 -type d -printf '%f\n' 2>/dev/null | sed -r -e "s/^/Removing /;" ; }
   test -n "${APPLY}" && rm -rf "./old/"

   cd "${OLDPWD}"
}

{
   lecho "START device18_music-sync"
   echo "INDIR=${INDIR}"
   echo "OUTDIR=${OUTDIR}"
   echo "DEBUG=${DEBUG}"
   echo "APPLY=\"${APPLY}\""
   echo "LOGFILE=${LOGFILE}"
   list="$( generate_list )"
   echo "${list}" | sync_selected_folders
   lecho "END device18_music-sync"
} 2>&1 | tee -a "${LOGFILE}"

Yes, the lecho is my recently-revamped lecho script, which merely wraps around plecho, which really just uses moreutils' ts utility.

TZ=UTC ts "[%FT%TZ]${USER}@${HOSTNAME}:${@+ $@}"

This script sounds very similar to one I wrote last year for symlinks. Who knows how many times I've redone my work?

Combine pdfs

I remember needing to combine PDFs in the past, even on GNU/Linux, but I forgot how I did it back then. The first search result for query combine pdfs linux I found said to use ImageMagick! I keep that around for all sorts of tasks already, so:

convert file1.pdf file2.pdf mergedfile.pdf

But the quality was terrible. I didn't want to bother with dealing with quality settings in ImageMagick, when I remembered there was something out there that would work. It was farther down in that first result: just use pdftk.

pdftk file1.pdf file2.pdf cat output mergedfile.pdf

I'm not sure why this is so hard for the Internet.

Bonus

Also, LibreOffice draw struggles with pdfs that have fonts that are not on your system. You can achieve limited success by using mutool extract from package mupdf-tools. Unfortunately for me, the font name used didn't match up with the ttf font name properties because of the whole "FontName-Bold" stuff. So I still had to manually munge stuff, and apparently some of the embedded glyphs were not included so it took some playing around with manually changing a random letter "V" into the non-bold version to get it to appear. That was frustrating. And I had gotten so far as to use ttx filename.ttf from package fonttols but didn't get any farther in that experiment.

Flask session demo, with kerberos and ldap auth

I have written a little demo application in python and flask. I might have use of it in the future, particularly the group-based access to an endpoint. If you need to write a small webapp that uses kerberos or ldap auth (from a form, or basic auth), go clone my application from gitlab! The way the application works is it accepts basic authentication at /login/basic/, form authentication at /login/form/ (which defaults to logintype=ldap) and kerberos www-negotiate at /login/kerberos. Any of these methods, upon successful authentication, establish a session on the server side and a cookie for the client to use. Use the cookie for each future request. The admin can set how many minutes the session/cookie is valid for. The full examples with curl are documented in INTERACT.md.

Start server in a separate shell session.

    $ FLASK_APP=session_app.py FLASK_DEBUG=1 flask run --host 0.0.0.0

Reset any cookies and kerberos tickets.

    $ kdestroy -A
    $ rm ~/cookiejar.txt

Try visiting protected page without authorization.

    $ curl -L http://d2-03a.ipa.example.com:5000/protected -b ~/cookiejar.txt -c ~/cookiejar.txt
    requires session

Get kerberos ticket and then visit kerberos login url.

    $ kinit ${USER}
    $ klist
    Ticket cache: FILE:/tmp/krb5cc_960600001_Hjgmv7lby2
    Default principal: bgstack15@IPA.EXAMPLE.COM

    Valid starting     Expires            Service principal
    06/20/21 16:04:10  06/21/21 16:04:07  krbtgt/IPA.EXAMPLE.COM@IPA.EXAMPLE.COM
    06/20/21 16:04:15  06/21/21 16:04:07  HTTP/d2-03a.ipa.example.com@IPA.EXAMPLE.COM

    $ curl -L http://d2-03a.ipa.example.com:5000/login/kerberos --negotiate -u ':' -b ~/cookiejar.txt -c ~/cookiejar.txt
    <meta http-equiv="Refresh" content="1; url=/protected/">success with kerberos

Visit protected page now that we have a session.

    $ cat ~/cookiejar.txt 
    # Netscape HTTP Cookie File
    # https://curl.se/docs/http-cookies.html
    # This file was generated by libcurl! Edit at your own risk.

    d2-03a.ipa.example.com  FALSE   /   FALSE   0   user    "bgstack15@IPA.EXAMPLE.COM"
    d2-03a.ipa.example.com  FALSE   /   FALSE   0   type    kerberos
    d2-03a.ipa.example.com  FALSE   /   FALSE   0   timestamp   2021-06-20T20:06:15Z
    #HttpOnly_d2-03a.ipa.example.com    FALSE   /   FALSE   1624219691  session eyJfcGVybWFuZW50Ijp0cnVlLCJlbmRfdGltZSI6IjIwMjEtMDYtMjBUMjA6MDY6MTVaIiwidXNlciI6ImJnaXJ0b25ASVBBLlNNSVRIMTIyLkNPTSJ9.YM-fsw.ZeI4ec-d7D64IEJ9Ab4RfpXfLt4

    $ curl -L http://d2-03a.ipa.example.com:5000/protected -b ~/cookiejar.txt -c ~/cookiejar.txt
    <html>
    <title>View Session Cookie</title>
    Username: bgstack15@IPA.EXAMPLE.COM<br/>
    Session expires: 2021-06-20T20:06:15Z<br/>
    Logged in through: kerberos
    </html>

For submitting to the form, pass in form data using fields `username`, `password`, and optionally `logintype` which can be defined within the application. An included option is `ldap`. Kerberos auth through the form is not supported.

    curl -L -X POST http://d2-03a:5000/login/ --data 'username=bgstack15&password=qwerty' -b ~/cookiejar.txt -c ~/cookiejar.txt

Basic auth can be provided as a POST to /login/basic/.

    $ curl -X POST -L http://d2-03a:5000/login/basic/ -b ~/cookiejar.txt -c ~/cookiejar.txt --user 'bgstack15'
    Enter host password for user 'bgstack15':
    <meta http-equiv="Refresh" content="1; url=/protected/">success with ldap
    $ curl -X POST -L http://d2-03a:5000/login/basic/ -b ~/cookiejar.txt -c ~/cookiejar.txt --header "Authorization: Basic $( printf '%s' "${username}:${pw}" | base64 )"
    <meta http-equiv="Refresh" content="1; url=/protected/">success with ldap

To set any settings that are currently supported by the /protected/settings/ page, you need to be a member of the ldap group "admins."

    $ curl -L http://d2-03a:5000/protected/settings/ -b ~/cookiejar.txt -c ~/cookiejar.txt -X POST --data 'ldap_uri=ldaps://dns1.ipa.example.com'
    Settings updated:<ul><li>LDAP_URI</li></ul><form action='/protected/settings/' method='get'><input type='submit' value='Return to settings'/></form>

Python list groups of an ldap user

This is a snippet from my session_app demo flask app. I wanted to return a list of the usergroups a user is a member of. I also wanted to provide the option to the admin to choose which attribute to show for the group, such as cn or description or whatever the admin wants.

def get_ldap_user_groups(server_uri, bind_dn, bind_pw,user_dn,user_attrib_memberof,group_name_attrib,group_base):
   server = ldap3.Server(server_uri)
   conn = ldap3.Connection(server, auto_bind=True,user=bind_dn, password=bind_pw)
   conn.search(
      search_base=user_dn,
      search_filter="(cn=*)", # this has the potential to not work in a directory where CN is not a part of any dn?
      search_scope="BASE",
      attributes=[user_attrib_memberof]
   )
   these_groups = conn.entries[0].entry_attributes_as_dict[user_attrib_memberof]
   #print(f"DEBUG: these_groups={these_groups}")
   result = []
   for group in these_groups:
      #print(f"DEBUG: will check for value {group_base} in {group}")
      if group_base in group:
         if group_name_attrib == "dn":
            #print(f"DEBUG: just add group via dn {group}")
            result.append(group)
         else:
            # we need to lookup this group and pick the attribute of it the admin wants.
            #print(f"DEBUG: need to lookup group {group} and extract attrib {group_name_attrib}")
            conn.search(
               search_base=group,
               search_filter="(objectClass=*)",
               search_scope="BASE",
               attributes=[group_name_attrib]
            )
            this_group=conn.entries[0].entry_attributes_as_dict[group_name_attrib][0]
            #print(f"DEBUG: Group {group} identified as attrib {group_name_attrib}={this_group}")
            result.append(this_group)
   return result

Some example calls:

>>> get_ldap_user_groups("ldaps://dns1.ipa.example.com:636","uid=serviceaccount,cn=users,cn=accounts,dc=ipa,dc=example,dc=com","nicetry","uid=bgstack15,cn=users,cn=accounts,dc=ipa,dc=example,dc=com","memberof","dn","cn=groups,cn=accounts,dc=ipa,dc=example,dc=com")
['cn=public,cn=groups,cn=accounts,dc=ipa,dc=example,dc=com', 'cn=netdev,cn=groups,cn=accounts,dc=ipa,dc=example,dc=com', 'cn=ipausers,cn=groups,cn=accounts,dc=ipa,dc=example,dc=com', 'cn=audio,cn=groups,cn=accounts,dc=ipa,dc=example,dc=com', 'cn=video,cn=groups,cn=accounts,dc=ipa,dc=example,dc=com']
>>> get_ldap_user_groups("ldaps://dns1.ipa.example.com:636","uid=serviceaccount,cn=users,cn=accounts,dc=ipa,dc=example,dc=com","nicetry","uid=bgstack15,cn=users,cn=accounts,dc=ipa,dc=example,dc=com","memberof","dn","cn=groups,cn=accounts,dc=ipa,dc=example,dc=com")
['public', 'netdev', 'ipausers', 'audio', 'video']

Flask sessions, with kerberos auth

I was fiddling around with Flask again, and came across the Flask-Kerberos library which includes an example of how to protect an endpoint with kerberos! I have started messing around with this tutorial, as well as with tutorials on how to use sessions and cookies, so that the session protection is required for certain endpoints and the kerberos auth is only required at login time. My work-in-progress repository is session_app on Gitlab. This Flask library has great tricks inside it, like setting maximum session time! I still have to add a login form and a POST endpoint for basic auth with ldap. And I hope to add usergroup logic to be able to enforce arbitrary group membership. So far, it's just been an experiment with no real purpose; just playing and learning. But if I ever come up with a need to protect endpoints with sessions, kerberos, and eventually ldap, I'll be ready! Here's my notes on how to interact with the app so far:

Start server in a separate shell session.

    $ FLASK_APP=session_app.py FLASK_DEBUG=1 flask run --host 0.0.0.0

Reset any cookies and kerberos tickets.

    $ kdestroy -A
    $ rm ~/cookiejar.txt

Try visiting protected page without authorization.

    $ curl -L http://d2-03a.ipa.example.com:5000/protected -b ~/cookiejar.txt -c ~/cookiejar.txt
    requires session

Get kerberos ticket and then visit login url. This /login redirects to /login/kerberos by default.

    $ kinit ${USER}
    $ klist
    Ticket cache: FILE:/tmp/krb5cc_960600001_Hjgmv7lby2
    Default principal: bgstack15@IPA.EXAMPLE.COM

    Valid starting     Expires            Service principal
    06/20/21 16:04:10  06/21/21 16:04:07  krbtgt/IPA.EXAMPLE.COM@IPA.EXAMPLE.COM
    06/20/21 16:04:15  06/21/21 16:04:07  HTTP/d2-03a.ipa.example.com@IPA.EXAMPLE.COM

    $ curl -L http://d2-03a.ipa.example.com:5000/login --negotiate -u ':' -b ~/cookiejar.txt -c ~/cookiejar.txt
    <meta http-equiv="Refresh" content="1; url=/protected/">success with kerberos

Visit protected page now that we have a session.

    $ cat ~/cookiejar.txt 
    # Netscape HTTP Cookie File
    # https://curl.se/docs/http-cookies.html
    # This file was generated by libcurl! Edit at your own risk.

    d2-03a.ipa.example.com  FALSE   /   FALSE   0   user    "bgstack15@IPA.EXAMPLE.COM"
    d2-03a.ipa.example.com  FALSE   /   FALSE   0   type    kerberos
    d2-03a.ipa.example.com  FALSE   /   FALSE   0   timestamp   2021-06-20T20:06:15Z
    #HttpOnly_d2-03a.ipa.example.com    FALSE   /   FALSE   1624219691  session eyJfcGVybWFuZW50Ijp0cnVlLCJlbmRfdGltZSI6IjIwMjEtMDYtMjBUMjA6MDY6MTVaIiwidXNlciI6ImJnaXJ0b25ASVBBLlNNSVRIMTIyLkNPTSJ9.YM-fsw.ZeI4ec-d7D64IEJ9Ab4RfpXfLt4

    $ curl -L http://d2-03a.ipa.example.com:5000/protected -b ~/cookiejar.txt -c ~/cookiejar.txt
    <html>
    <title>View Session Cookie</title>
    Username: bgstack15@IPA.EXAMPLE.COM<br/>
    Session expires: 2021-06-20T20:06:15Z<br/>
    Logged in through: kerberos
    </html>

    2021-06-20 ldap basic auth, and a login form are still pending.

As a bonus, I also learned how to display UTC time for right now, in a standard format, in python:

now_str = datetime.datetime.strftime(datetime.datetime.now(datetime.timezone.utc),"%FT%TZ")

You can configure a Mozilla-based browser to accept certain domains for kerberos authentication: Share your browser prefs.js! (search for "kerberos" on that page).