gmn.clttr.info

my personal gemini capsule
git clone https://git.clttr.info/gmn.clttr.info.git
Log (Feed) | Files | Refs (Tags)

commit 25092bbd3b288f40af3bb76ad5c1092b998a8b35
parent 22d763f48fdb27f11dc0d5346e5c851ea103d19e
Author: René Wagner <rwa@clttr.info>
Date:   Sat, 24 Jul 2021 08:05:02 +0200

remove git repos from repo

Diffstat:
M.gitignore | 1+
Dsources/cgmnlm.git/commits/01567e578c9632960903e1f56dd2086547806da3.patch | 285-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/021d8f8fdfcb9be636a73d7c59d540d8255cc0df.patch | 22----------------------
Dsources/cgmnlm.git/commits/02c2be62daceb04e4891d415c997dd64db84b9d9.patch | 12------------
Dsources/cgmnlm.git/commits/02f6af661513683f0c6c1465c5ff1dd8f03a30c9.patch | 751-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/0513b91be1173b1ed43a0f1d28cf502a81267185.patch | 14--------------
Dsources/cgmnlm.git/commits/05cc8b85cdf731ea3a664b6099aad04f22bbca6c.patch | 49-------------------------------------------------
Dsources/cgmnlm.git/commits/05d112b7d347b737bdac503ea05292db3347f2a8.patch | 66------------------------------------------------------------------
Dsources/cgmnlm.git/commits/0976b0e44655163a34d1b53e62a348cbf4335940.patch | 13-------------
Dsources/cgmnlm.git/commits/0a03e6dadf7c30cea1fb388a9e5386a00c853dbb.patch | 12------------
Dsources/cgmnlm.git/commits/0b5c37d2e65a46fe8e4a49c2f00cb6228fad59e3.patch | 13-------------
Dsources/cgmnlm.git/commits/0eaf9cc109a99d6efb0d9c763291f6a5d9e74391.patch | 15---------------
Dsources/cgmnlm.git/commits/0ed7a4527c967ce3f14909923277cf62624f0900.patch | 28----------------------------
Dsources/cgmnlm.git/commits/100759a7d796f4e486a89b65b3ca491c1141056f.patch | 34----------------------------------
Dsources/cgmnlm.git/commits/122fb0a9fd5456e3b1fd9f084130df85c859394b.patch | 562-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/144693a3d001a436abaa37f11b1c1c2bdf88c813.patch | 47-----------------------------------------------
Dsources/cgmnlm.git/commits/174fbd5d09bc13212fc1edc0cd1d3fa2400a8b7e.patch | 365-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/1808e6cd1880d3c08abb0ddfa19044afada925dd.patch | 24------------------------
Dsources/cgmnlm.git/commits/18ead2644a8c525d1d3bbc729d9ccd9aa7e0d63c.patch | 93-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/1a747cb6c2765ee818506c886bad4ed36b2b9d51.patch | 47-----------------------------------------------
Dsources/cgmnlm.git/commits/1c9a6e6a35448b76063f16b0f6aaaf8d43ebee9a.patch | 32--------------------------------
Dsources/cgmnlm.git/commits/1cfe0e794936cc51b9306327634a35f1c443643f.patch | 13-------------
Dsources/cgmnlm.git/commits/1da4ff928a44f590e2c72cda1dcb4b097845cbc3.patch | 12------------
Dsources/cgmnlm.git/commits/207a72012ef69de654a78e18d28182ecde1326e2.patch | 92-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/211b8c3dd36a950132efa5ba4e2f0172a42bb6bc.patch | 66------------------------------------------------------------------
Dsources/cgmnlm.git/commits/22a28fa92755b49254cac144894be1fdb917a6a3.patch | 114-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/262ccc2005617eb633f5b3ba434ee26f8000e0a8.patch | 23-----------------------
Dsources/cgmnlm.git/commits/26666e7838fd40ca7d6f20af7e0cb554ff8bb0f0.patch | 12------------
Dsources/cgmnlm.git/commits/28283bda98accf122b6424ac611fd4ff25dedbc9.patch | 12------------
Dsources/cgmnlm.git/commits/2e593cd48bd5e1f90fcbb54b83955752cd392466.patch | 22----------------------
Dsources/cgmnlm.git/commits/2e9d3c0bab8e7df635a8f0968f04fe9b1e2d979c.patch | 26--------------------------
Dsources/cgmnlm.git/commits/30660fc160a15504274d40d4a5ec1b31539f8c2f.patch | 18------------------
Dsources/cgmnlm.git/commits/320676ca5bc1980c96f5e4bc14240a741be8f3be.patch | 30------------------------------
Dsources/cgmnlm.git/commits/3270a74590d4bfbbc9ae1fdc4c1d36eed943844f.patch | 30------------------------------
Dsources/cgmnlm.git/commits/33495e8dd86139cafade2888227e37b1572d18ea.patch | 52----------------------------------------------------
Dsources/cgmnlm.git/commits/3547fd11d57da5c7aa610366d54eef3b47d0b1a4.patch | 141-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/37396a375a68868490342e16140e67287445be17.patch | 16----------------
Dsources/cgmnlm.git/commits/39339c348f593bda9ca0a556affa38cd5e15138c.patch | 21---------------------
Dsources/cgmnlm.git/commits/3c63a64288f665a272974698d547bbca79769d5a.patch | 13-------------
Dsources/cgmnlm.git/commits/3ce02e5183da68e017b572265d68f19fef59043c.patch | 183-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/3dd06ab4813b6c8f4992e19fce9d4b094fd3a1a9.patch | 43-------------------------------------------
Dsources/cgmnlm.git/commits/40308b8b0bd7e15d0f6e2971b901a9c09a4bc681.patch | 18------------------
Dsources/cgmnlm.git/commits/4134dc1b4a37b65f8176d799f03342583b49d932.patch | 16----------------
Dsources/cgmnlm.git/commits/4274b06fe4b2702af297cd0cee3d7871741899ec.patch | 30------------------------------
Dsources/cgmnlm.git/commits/46b5d74576ffce397c83ac53ebfacb25e1cdc851.patch | 131-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/479ea9e74f4b66645c0d7b51d99adf420d831f23.patch | 36------------------------------------
Dsources/cgmnlm.git/commits/48d0feed6d097c54662a7f231c7bc4704837f023.patch | 248-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/49c0c523c69842f8ebc33135947591cf6f7a7cab.patch | 13-------------
Dsources/cgmnlm.git/commits/49eea555e605e6e0155756ad9739a5729340db81.patch | 12------------
Dsources/cgmnlm.git/commits/4a6172f1bf9cb41eb1ce3a5f720f9ebe4febc62b.patch | 43-------------------------------------------
Dsources/cgmnlm.git/commits/4b2437b17e00d61da9356ac96bb38a8043c66fca.patch | 12------------
Dsources/cgmnlm.git/commits/4b7fba261a70bd37e160a7304d454c72c1f75b69.patch | 13-------------
Dsources/cgmnlm.git/commits/4bc55aaa138171db365377db0624c3ce0d878257.patch | 59-----------------------------------------------------------
Dsources/cgmnlm.git/commits/4c0f931d6688d06df2e22d001182f6fa1b776fab.patch | 77-----------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/4c12342bcad95cdc44f9107161429524226a2d37.patch | 13-------------
Dsources/cgmnlm.git/commits/4dd50ac07e82dfc1785f98a3535109e2d738029d.patch | 31-------------------------------
Dsources/cgmnlm.git/commits/4e61e266076fbda20cbf268300e7f645669c7062.patch | 14--------------
Dsources/cgmnlm.git/commits/514cb373019e94f743424b2813602722ca09b917.patch | 13-------------
Dsources/cgmnlm.git/commits/563922a7e2da77b3973dcf707854121932ca244e.patch | 331-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/5799323f4c92181a3446a729366b230456e93c81.patch | 135-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/59d19b9894083cecafc4439f7df1031bd6cefb01.patch | 33---------------------------------
Dsources/cgmnlm.git/commits/59d43726bb18a1e240a7188b3dd33af5876a126e.patch | 21---------------------
Dsources/cgmnlm.git/commits/5a955c5f241b87018dfb0cda6872dc7ae2784222.patch | 456-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/5ad3f0aaccbcf328756d0eaad0e98068587395d1.patch | 61-------------------------------------------------------------
Dsources/cgmnlm.git/commits/5d3ae7b7f52ba83428ba8d728712e8c1710b2ea0.patch | 20--------------------
Dsources/cgmnlm.git/commits/5fd43e8d02ffea38b5e4a3531e366f2b9b510201.patch | 15---------------
Dsources/cgmnlm.git/commits/601f9008863af571980c1cd39920483d59cfbfb4.patch | 243-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/60496bae0cbda1162ae00bc6f6f4047ba9c7d86f.patch | 22----------------------
Dsources/cgmnlm.git/commits/60cf41e7dd66897e6987704921b6cd57da1f084f.patch | 62--------------------------------------------------------------
Dsources/cgmnlm.git/commits/61af57e302efd90458e17fa9f0bfaf5b3828954f.patch | 13-------------
Dsources/cgmnlm.git/commits/678bff58ed32e77c9af90a5d8fc7b1f3c38af86c.patch | 31-------------------------------
Dsources/cgmnlm.git/commits/689fb8b470f19fb83ee1e32efe64b42d6961630c.patch | 28----------------------------
Dsources/cgmnlm.git/commits/6d2f78eeded101ccd755b1b2be16105fe5af881d.patch | 16----------------
Dsources/cgmnlm.git/commits/6f36d2a0fc5de0a9d25229c47c75481f47f32c87.patch | 62--------------------------------------------------------------
Dsources/cgmnlm.git/commits/71ececc4f264eed36f022b4b52c9100b9e7b1b12.patch | 21---------------------
Dsources/cgmnlm.git/commits/73f5a5bc2b97fb36ded8feb36828d598d6e9fed3.patch | 39---------------------------------------
Dsources/cgmnlm.git/commits/75087ce65f54c86d44ce13bb63e1041226a53f7b.patch | 20--------------------
Dsources/cgmnlm.git/commits/7619edcd116385414b55764a3401a0c66c04da79.patch | 13-------------
Dsources/cgmnlm.git/commits/77de1bb2a84e0980d23b7fc2dda1480a1093ca21.patch | 22----------------------
Dsources/cgmnlm.git/commits/78cfe1b669fad6b7a3638d371ed9825e2ee53243.patch | 53-----------------------------------------------------
Dsources/cgmnlm.git/commits/78eb57cad45aa27e83d3a5e78be0bb5ce4d631f7.patch | 281-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/7a099135cd9dae483679cf51a4b630a5dd64c74e.patch | 115-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/7c453fb45f831ce9178799af9855ecb0bda518ea.patch | 116-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/7e4e43b05c298aa812027bf1921ce3f224e86bda.patch | 20--------------------
Dsources/cgmnlm.git/commits/801d9b8f13f6adef25fb14ec2e9acbc6dd4e92a9.patch | 87-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/84da4b3f2b95bead2c1609eb572a2369576eae77.patch | 46----------------------------------------------
Dsources/cgmnlm.git/commits/84df94447cdb081ec305a5ba9d2b0ef89dd34fc3.patch | 13-------------
Dsources/cgmnlm.git/commits/852bc7198f9d1d838d76d74a006cc2a2e63e4f1c.patch | 120-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/86b299819c86758f2b537c1de0475a2906f0a4d2.patch | 28----------------------------
Dsources/cgmnlm.git/commits/8970adc23e0a1bcf29d211f353dbd5ebd68cfe66.patch | 13-------------
Dsources/cgmnlm.git/commits/8a83030e5a390c2151c485b3c091ba28ddebcab7.patch | 35-----------------------------------
Dsources/cgmnlm.git/commits/8bb1d81f539f1e223e8fcd79e2d18f58e3c9d28f.patch | 14--------------
Dsources/cgmnlm.git/commits/8c473eda5e4c6537058d0ff1815f2943e7b41498.patch | 197-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/8cac260a4b7c0b4df4d1229a5e41e64c3a687173.patch | 30------------------------------
Dsources/cgmnlm.git/commits/8d897e4a00be9986209f1ca394ed46befadf6088.patch | 13-------------
Dsources/cgmnlm.git/commits/8ddc99fdc336957d7565cd50e329da9cbe9e4de8.patch | 12------------
Dsources/cgmnlm.git/commits/90995e834f2e87427f2f4bddf26a93258b45aa31.patch | 50--------------------------------------------------
Dsources/cgmnlm.git/commits/95518992983e6531106b48c82edeb0ce825bf351.patch | 164-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/9551d0a3822312a0a4917ccbe80fdaeb49954d70.patch | 42------------------------------------------
Dsources/cgmnlm.git/commits/955f7524b955e19bc89c6e9f76f3f3ecfb7bfb58.patch | 445-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/963700d8d6e31aecfc14e12184637f4c3360f6ed.patch | 22----------------------
Dsources/cgmnlm.git/commits/996bd24225e7a63fd160d1feb9af193225a065b3.patch | 14--------------
Dsources/cgmnlm.git/commits/9a195d92566b5790b2b7d3ca848987a095bf3d9c.patch | 13-------------
Dsources/cgmnlm.git/commits/9b0006509931c9a3defb64c64f4b0071657f8e61.patch | 31-------------------------------
Dsources/cgmnlm.git/commits/9b1a618b4211a029c352c72f6d273e3085c8457d.patch | 221-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/9bd1a7457ea58ddd568fdbe46a1155c28424e8be.patch | 12------------
Dsources/cgmnlm.git/commits/9ddd5c16dae4b556c7aeac88c219568c479d87f2.patch | 363-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/9ef33fb102426d0bf56e93ceebcd81eb24171a9e.patch | 108-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/9f98e013a6cd966cf4dc2d98187d6f0ba6f7fb5c.patch | 27---------------------------
Dsources/cgmnlm.git/commits/a3d5169d71f181efaa59a619e7362911a6c048b7.patch | 28----------------------------
Dsources/cgmnlm.git/commits/a5eae7ea6b35f7b2540fefdf4613a86916f0a0b0.patch | 20--------------------
Dsources/cgmnlm.git/commits/a61a75f837239bed3aa74331699d301fb93d9da8.patch | 84-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/a6e0326291eee1e1f8ee723ac1e8467ed0561e86.patch | 37-------------------------------------
Dsources/cgmnlm.git/commits/ab66dd2be92931bef04cbccdb3aa008615bd8eba.patch | 19-------------------
Dsources/cgmnlm.git/commits/abcb9caf86020a7cdd9f502fe01eb5db3c70c685.patch | 2412-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/ac86b2f9fece0e39be57b81e02cb9946a10df570.patch | 35-----------------------------------
Dsources/cgmnlm.git/commits/ae43b9190e1a18796222b94ec1e78b35f5826964.patch | 111-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/afab58cb64f205ce9f469a328a7477b808b0c76c.patch | 30------------------------------
Dsources/cgmnlm.git/commits/b050b9e467589561b1203f99e9f58c990c824b1a.patch | 24------------------------
Dsources/cgmnlm.git/commits/b136eea98a33ffcaf2d965e907dd6799078d2110.patch | 144-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/b25b4576e3fd889b3edadb51ff9a387ef0bae653.patch | 15---------------
Dsources/cgmnlm.git/commits/b298fadb216bfbac8c84e05363e508b3b3a314a5.patch | 30------------------------------
Dsources/cgmnlm.git/commits/b39e196040623a80bf9f1a0a05c3da8523e26ee3.patch | 65-----------------------------------------------------------------
Dsources/cgmnlm.git/commits/b4fc0c0993229b8fc8242e314e701f33a8102688.patch | 15---------------
Dsources/cgmnlm.git/commits/b54a100d7156ea279641a9e779b7658c42300fe9.patch | 33---------------------------------
Dsources/cgmnlm.git/commits/b64d3d5ac9121bd3c5df1c48defe5fdc209e467d.patch | 18------------------
Dsources/cgmnlm.git/commits/b90888e71878eea7727cd507503198d134d91ab7.patch | 21---------------------
Dsources/cgmnlm.git/commits/bb696e6e2823d38bf6ad2f5106f3808555c48b18.patch | 13-------------
Dsources/cgmnlm.git/commits/be0cf0dfd1e83a1ba4f6b27fb9464c4e95d10752.patch | 13-------------
Dsources/cgmnlm.git/commits/c036a43801d60b620262687e4bb6d98f97e23dbd.patch | 12------------
Dsources/cgmnlm.git/commits/c08e934c449c7a030fb1ebacd3166820b23faeb3.patch | 42------------------------------------------
Dsources/cgmnlm.git/commits/c0c891f87b101db98c589da9a60e4a39bf048f0d.patch | 12------------
Dsources/cgmnlm.git/commits/c3aa884144bb173073c6b973835a266bae27bf1e.patch | 50--------------------------------------------------
Dsources/cgmnlm.git/commits/c414388c0f2c020c0e22d53f6df577e1fbeb32fc.patch | 13-------------
Dsources/cgmnlm.git/commits/c7592c6a5c2ff7b390af89f297d5c9e1c34c9414.patch | 28----------------------------
Dsources/cgmnlm.git/commits/c8041a15ac7d36ecc2e1c34dcaa14c51e62de788.patch | 92-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/cb63b8ddf093711607ff98e933d4bd04154a854b.patch | 67-------------------------------------------------------------------
Dsources/cgmnlm.git/commits/cc3f9a5eea25ad350039d7e552bec944b3a121b0.patch | 16----------------
Dsources/cgmnlm.git/commits/ccec255833fa27789ffb75551b29528b10f8c62a.patch | 900-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/ce1a524642e25da8a66a18797e9afc8f54f20903.patch | 13-------------
Dsources/cgmnlm.git/commits/ce1ef1abde0d5519e0464f9326edea01b73a845f.patch | 34----------------------------------
Dsources/cgmnlm.git/commits/d06c4cda5c5538ea71401bbfb9ff0bb6657d8413.patch | 34----------------------------------
Dsources/cgmnlm.git/commits/d0acd0f4d08a0d5e8f6c729f53de2f381b270202.patch | 59-----------------------------------------------------------
Dsources/cgmnlm.git/commits/d2fa1b4567aa841020f4d4bbdb1298b310534c98.patch | 96-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/d371589381e57835d37796f1d349638b806e43b4.patch | 14--------------
Dsources/cgmnlm.git/commits/d3afac098e2bf0a4ed9fad89f6a748f6288ae3bc.patch | 16----------------
Dsources/cgmnlm.git/commits/d5936353392a17ae6bac3303d506a7e79855da2d.patch | 18------------------
Dsources/cgmnlm.git/commits/d6777ec2788f9ece56a6201fb091dba4a15f739a.patch | 15---------------
Dsources/cgmnlm.git/commits/d754f34e7eb5f700b6de13c6c4692837d0a123f4.patch | 22----------------------
Dsources/cgmnlm.git/commits/d84ee77e249eae11c4b240294c6d37851f1ba11f.patch | 61-------------------------------------------------------------
Dsources/cgmnlm.git/commits/d8f0870446c471a42612d6a8e853ad9b723a6d39.patch | 93-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/dbc726616e6675ae82d9bb55be5693371255ed2f.patch | 1502-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/dcc0484a8c238acdbe988a898cf2deeac4f34ae5.patch | 20--------------------
Dsources/cgmnlm.git/commits/dcc2b34d434d8e0f695e43a1f775759846e60417.patch | 102-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/dde8799e758b1e6c3985492a8205c189d4d47b9c.patch | 121-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/df2e4f8ab2f151eac61702b27f8c5cfe4145912e.patch | 14--------------
Dsources/cgmnlm.git/commits/e1d4e9a07ec31664e61aa72ac9a6f7ab2efea6b9.patch | 13-------------
Dsources/cgmnlm.git/commits/e1d9773742b9832a87e912b44d4ad9e2e34364ef.patch | 14--------------
Dsources/cgmnlm.git/commits/e299aec4a10ba3aaf01c50263cbcabe8d39a5214.patch | 91-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/e7b2013160db902df5b833ff2e49eaff90807514.patch | 13-------------
Dsources/cgmnlm.git/commits/e80d852a1ba62c8877dab40324ac90a14a0b7eea.patch | 13-------------
Dsources/cgmnlm.git/commits/ea595cdb2ba9d1e443c7ccdccf3842e49e7162a8.patch | 77-----------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/eb01fde6007ea86b61227f690f8ee7ff0047bfeb.patch | 64----------------------------------------------------------------
Dsources/cgmnlm.git/commits/eb2873b2ebe1f436d8fb4cd7c336889ad0ddddfd.patch | 12------------
Dsources/cgmnlm.git/commits/eb6a4e9740237bd1bd71c115476e187520a2fc8c.patch | 14--------------
Dsources/cgmnlm.git/commits/ec88f4558c48b3eece906143867dfba6d81e5e49.patch | 22----------------------
Dsources/cgmnlm.git/commits/ee6fe47e44bfb40cd92d6668e85fd893df3a12b8.patch | 40----------------------------------------
Dsources/cgmnlm.git/commits/f4a4be2513580809c01212a08a5284f9cf16ad5f.patch | 264-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/f5d540bc5d0112895376aebe6bf54adb32545d6e.patch | 40----------------------------------------
Dsources/cgmnlm.git/commits/f6643cf1b5ecbdd030420fb504ae2edcb9102410.patch | 346-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/f80e4037c21c87b11b7b4bfefba5d33a0fcc1ea6.patch | 23-----------------------
Dsources/cgmnlm.git/commits/f8f6c5a12beeaa4ad614cc349a04e2984c06c83b.patch | 13-------------
Dsources/cgmnlm.git/commits/fa78663748958eebb442f03d0406f42f6190522a.patch | 200-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/fc0bf889a3e5ce270600811875ef9a50729c4135.patch | 1043-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/fc6d4a6f69e305627d06955da27b8e8c4c5af6e0.patch | 96-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/fd914b3aa40d63eb7fb2aafb1f6a30c1f78ee92f.patch | 40----------------------------------------
Dsources/cgmnlm.git/commits/fed9c0561947126442fbb32e3437922dcc467ef1.patch | 39---------------------------------------
Dsources/cgmnlm.git/commits/ffc89b6cf01e8b68232b3de3f001d2e635936dfa.patch | 155-------------------------------------------------------------------------------
Dsources/cgmnlm.git/commits/index.gmi | 1307-------------------------------------------------------------------------------
Dsources/cgmnlm.git/index.gmi | 63---------------------------------------------------------------
Dsources/cgmnlm.git/tree/.gitignore.txt | 8--------
Dsources/cgmnlm.git/tree/COPYING.txt | 674-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/Makefile.txt | 79-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/README.md.txt | 66------------------------------------------------------------------
Dsources/cgmnlm.git/tree/config.sh.txt | 195-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/configure.txt | 46----------------------------------------------
Dsources/cgmnlm.git/tree/doc/cgmnlm.scd.txt | 33---------------------------------
Dsources/cgmnlm.git/tree/doc/gmni.scd.txt | 72------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/doc/index.gmi | 6------
Dsources/cgmnlm.git/tree/include/escape.h.txt | 175-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/include/gmni/certs.h.txt | 27---------------------------
Dsources/cgmnlm.git/tree/include/gmni/gmni.h.txt | 174-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/include/gmni/index.gmi | 8--------
Dsources/cgmnlm.git/tree/include/gmni/tofu.h.txt | 61-------------------------------------------------------------
Dsources/cgmnlm.git/tree/include/gmni/url.h.txt | 103-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/include/index.gmi | 7-------
Dsources/cgmnlm.git/tree/include/util.h.txt | 17-----------------
Dsources/cgmnlm.git/tree/index.gmi | 13-------------
Dsources/cgmnlm.git/tree/src/certs.c.txt | 156-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/src/client.c.txt | 330-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/src/escape.c.txt | 213-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/src/gmni.c.txt | 407-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/src/gmnlm.c.txt | 1402-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/src/index.gmi | 13-------------
Dsources/cgmnlm.git/tree/src/parser.c.txt | 160-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/src/tofu.c.txt | 253-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/src/url.c.txt | 1435-------------------------------------------------------------------------------
Dsources/cgmnlm.git/tree/src/util.c.txt | 105-------------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/0cfdd64ea47cf0d24b3280ba8b821dff01a849d9.patch | 124-------------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/2d3b160b0cf8a1e87693e53645deb2690efaa3fc.patch | 12------------
Dsources/gmnifaq.git/commits/4102d2a37da9349f1842a15a1c161e69a4cb2d16.patch | 189-------------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/4516bfc5a39f8ea8632cd1ed240d350454878c33.patch | 127-------------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/46ca4c42e2fa4202c1574d014a18460316505f42.patch | 36------------------------------------
Dsources/gmnifaq.git/commits/5b8503c2d37e7d32484bd7128f903a4f97bee9d8.patch | 35-----------------------------------
Dsources/gmnifaq.git/commits/6cc21c44f0e1ca40fcd3408ed4a3edae0c6c7b47.patch | 203-------------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/7e2f5958e9055dfcf7ceedf8a583584bb8ca52c8.patch | 154-------------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/98ab6ca1ab3770d925bed026084215e36cf65913.patch | 44--------------------------------------------
Dsources/gmnifaq.git/commits/9ae9dd44d5e06347d29f28250d32a92fb1ad7725.patch | 80-------------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/ac99b4092b057be8aeb3237c92a32329d931aa01.patch | 75---------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/adbf69557321fece135394ffd331da789fc7379a.patch | 301-------------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/e3616bfde048241b5ea2c2e0266283ba725a0894.patch | 292-------------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/ed9009e79d88ebe5eeb1ce78cedd0d1bb4fe5dd8.patch | 330-------------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/fae1610eb1c7a29f29244578926a97477471e5b7.patch | 72------------------------------------------------------------------------
Dsources/gmnifaq.git/commits/fc9c20994275edb9dbc6d1b5e97d53785643c021.patch | 35-----------------------------------
Dsources/gmnifaq.git/commits/index.gmi | 100-------------------------------------------------------------------------------
Dsources/gmnifaq.git/index.gmi | 46----------------------------------------------
Dsources/gmnifaq.git/tree/.gitignore.txt | 2--
Dsources/gmnifaq.git/tree/LICENSE.txt | 29-----------------------------
Dsources/gmnifaq.git/tree/README.md.txt | 36------------------------------------
Dsources/gmnifaq.git/tree/data/data.sqlite.example | 0
Dsources/gmnifaq.git/tree/data/index.gmi | 5-----
Dsources/gmnifaq.git/tree/faqs.pl.txt | 82-------------------------------------------------------------------------------
Dsources/gmnifaq.git/tree/gmnifaq.conf.example.txt | 2--
Dsources/gmnifaq.git/tree/index.gmi | 15---------------
Dsources/gmnifaq.git/tree/index.pl.txt | 49-------------------------------------------------
Dsources/gmnifaq.git/tree/lib/gmnifaq.pm.txt | 71-----------------------------------------------------------------------
Dsources/gmnifaq.git/tree/lib/index.gmi | 5-----
Dsources/gmnifaq.git/tree/scripts.sql.txt | 17-----------------
Dsources/gmnifaq.git/tree/search.pl.txt | 75---------------------------------------------------------------------------
Dsources/gmnifaq.git/tree/tags.pl.txt | 55-------------------------------------------------------
Dsources/orrg.git/commits/01370e679836f2a4a9a78b9e08ff08b5d9469329.patch | 28----------------------------
Dsources/orrg.git/commits/01d0eb250880b32d366fbfa861c1e452094f6a0a.patch | 30------------------------------
Dsources/orrg.git/commits/02a05701a382e226b83d63bdad35b0a01209beec.patch | 185-------------------------------------------------------------------------------
Dsources/orrg.git/commits/08a67a7b51c33a348137b4e6b5dc2366e9f90e47.patch | 15---------------
Dsources/orrg.git/commits/0a7ca079581bd01b63d6769cc34d2d6d40429b4c.patch | 22----------------------
Dsources/orrg.git/commits/17fb028c7177296e19bfc4c5ae18b04b5e82a27d.patch | 20--------------------
Dsources/orrg.git/commits/226986013afd88a8b39ef37d5b6f85b63107d1ee.patch | 28----------------------------
Dsources/orrg.git/commits/2531331a6c1f20a3585c146ea73bce18bdba596c.patch | 42------------------------------------------
Dsources/orrg.git/commits/33875dd1e6936d33e97dfaca4c2f82c92558034a.patch | 36------------------------------------
Dsources/orrg.git/commits/373ae43bf2d6f2e6ca2a31beccda552122cd987f.patch | 149-------------------------------------------------------------------------------
Dsources/orrg.git/commits/3a8b1c9b9529a20915c21c16c2004e5dbde83893.patch | 52----------------------------------------------------
Dsources/orrg.git/commits/4454cc3f1b97506f166c883145dc8773de99b4be.patch | 83-------------------------------------------------------------------------------
Dsources/orrg.git/commits/44e27d26947e61b9f2731ec9620ce4852a0b683c.patch | 11-----------
Dsources/orrg.git/commits/589d4a8d337c759535a8d7eee270f25667aaec18.patch | 123-------------------------------------------------------------------------------
Dsources/orrg.git/commits/5a95a2973524305024fb2bd775b447737073d1d5.patch | 85-------------------------------------------------------------------------------
Dsources/orrg.git/commits/637126e743aa72c05564095f71020e57bb44b22f.patch | 13-------------
Dsources/orrg.git/commits/644fe70ba62746fa237c0de9e241951a9c9e6f6f.patch | 13-------------
Dsources/orrg.git/commits/70dfde6e6d1bc071379528ff85e97452ba341a7e.patch | 47-----------------------------------------------
Dsources/orrg.git/commits/7b18a7528ee0938b19f35bd40ea2a110c80eadc5.patch | 142-------------------------------------------------------------------------------
Dsources/orrg.git/commits/7eaf663655cd395a0c3b925142b7f07901a3cca4.patch | 72------------------------------------------------------------------------
Dsources/orrg.git/commits/89d2888f0c1880256b7e86745b75f7e282a7b54a.patch | 40----------------------------------------
Dsources/orrg.git/commits/8c21cb40de18a1fc4f8cc0ee540a1c021d08d50a.patch | 209-------------------------------------------------------------------------------
Dsources/orrg.git/commits/8d1d84dc5de30083cf64f405572ba62fe8d1da0a.patch | 29-----------------------------
Dsources/orrg.git/commits/8ebe00a117ed9d1528a0a8e268ad30b67884ba09.patch | 16----------------
Dsources/orrg.git/commits/9bdd8978ec1177627a2182317b202bce243f17ca.patch | 36------------------------------------
Dsources/orrg.git/commits/a09b98b4d50d664bce1dd9f206d4802754dd7abe.patch | 56--------------------------------------------------------
Dsources/orrg.git/commits/a354cf94d629843e4a3f17fe9d48ad3ea4d89ba4.patch | 29-----------------------------
Dsources/orrg.git/commits/a7927c64c3c34b17a034148ded8d9aacda7a56fa.patch | 7-------
Dsources/orrg.git/commits/af7333093a747a6b9bf6e969664352bbe65348ae.patch | 22----------------------
Dsources/orrg.git/commits/b687e40056a5ab549f4cd0d2be68db4e1fc05e74.patch | 168-------------------------------------------------------------------------------
Dsources/orrg.git/commits/c200079891e3277da48e952a1c7decd219c8aa6d.patch | 20--------------------
Dsources/orrg.git/commits/ecf0d4d8ec1179291f21d20237d19026eb60c7d3.patch | 26--------------------------
Dsources/orrg.git/commits/f1fa213eb470dbebf16dd42ce165726006a8e352.patch | 73-------------------------------------------------------------------------
Dsources/orrg.git/commits/f847c41903390f842e3b4a1b1d7b4c6a99b30327.patch | 24------------------------
Dsources/orrg.git/commits/index.gmi | 254-------------------------------------------------------------------------------
Dsources/orrg.git/index.gmi | 53-----------------------------------------------------
Dsources/orrg.git/tree/.gitignore.txt | 1-
Dsources/orrg.git/tree/LICENSE.txt | 29-----------------------------
Dsources/orrg.git/tree/README.md.txt | 43-------------------------------------------
Dsources/orrg.git/tree/gcat.txt | 72------------------------------------------------------------------------
Dsources/orrg.git/tree/index.gmi | 14--------------
Dsources/orrg.git/tree/index.pl.txt | 51---------------------------------------------------
Dsources/orrg.git/tree/lib/index.gmi | 5-----
Dsources/orrg.git/tree/lib/orrg.pm.txt | 134-------------------------------------------------------------------------------
Dsources/orrg.git/tree/orrg.pl.txt | 130-------------------------------------------------------------------------------
Dsources/orrg.git/tree/popular.pl.txt | 49-------------------------------------------------
Dsources/orrg.git/tree/recent.pl.txt | 44--------------------------------------------
Dsources/orrg.git/tree/robots.txt.txt | 2--
288 files changed, 1 insertion(+), 30974 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1 +1,2 @@ /transit/index.gmi +/sources/*.git diff --git a/sources/cgmnlm.git/commits/01567e578c9632960903e1f56dd2086547806da3.patch b/sources/cgmnlm.git/commits/01567e578c9632960903e1f56dd2086547806da3.patch @@ -1,285 +0,0 @@ -diff --git a/Makefile b/Makefile -index 69a241a8825a6a7aa979eb2ae95a26faaf3a0532..3d4df602cd7f7ab4f5a45b47dee0d47729f0739c 100644 ---- a/Makefile -+++ b/Makefile -@@ -8,6 +8,10 @@ gmni: $(gmni_objects) - @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmni_objects) - -+gmnlm: $(gmnlm_objects) -+ @printf 'CCLD\t$@\n' -+ @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnlm_objects) -+ - doc/gmni.1: doc/gmni.scd - - .SUFFIXES: .c .o .scd .1 -diff --git a/config.sh b/config.sh -index b93815ada4a25ec508e7a86cc79b9e9be3eba428..52931ab241c3177285b56258bf9b67ac4b63a7ea 100644 ---- a/config.sh -+++ b/config.sh -@@ -134,6 +134,7 @@ - all: ${all} - EOF - gmni >>"$outdir"/config.mk -+ gmnlm >>"$outdir"/config.mk - echo done - - touch $outdir/cppcache -diff --git a/configure b/configure -index aca6e8a1eaa9a6271f03eb8863640d0d93cdf435..151bdae8c12d9ad07d3a5240d7c554f4c62664c8 100755 ---- a/configure -+++ b/configure -@@ -7,10 +7,18 @@ genrules gmni \ - src/client.c \ - src/escape.c \ - src/gmni.c \ -+ src/url.c -+} -+ -+gmnlm() { -+ genrules gmnlm \ -+ src/client.c \ -+ src/escape.c \ -+ src/gmnlm.c \ - src/parser.c \ - src/url.c - } - --all="gmni" -+all="gmni gmnlm" - - run_configure -diff --git a/src/gmni.c b/src/gmni.c -index 75c6c5afb6e7f1f286d314d93f2b44ef8414afb3..dc0c5c7f61679369fe4fadafd0508147868cc3c3 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -15,7 +15,7 @@ #include <unistd.h> - #include "gmni.h" - - static void --usage(char *argv_0) -+usage(const char *argv_0) - { - fprintf(stderr, - "usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n", -diff --git a/src/gmnlm.c b/src/gmnlm.c -new file mode 100644 -index 0000000000000000000000000000000000000000..bc3f647d42372678b4180f539df8637d0ba69a12 ---- /dev/null -+++ b/src/gmnlm.c -@@ -0,0 +1,215 @@ -+#include <assert.h> -+#include <ctype.h> -+#include <getopt.h> -+#include <openssl/bio.h> -+#include <openssl/err.h> -+#include <stdbool.h> -+#include <stdio.h> -+#include <string.h> -+#include <sys/ioctl.h> -+#include <termios.h> -+#include <unistd.h> -+#include "gmni.h" -+#include "url.h" -+ -+struct link { -+ char *url; -+ struct link *next; -+}; -+ -+static void -+usage(const char *argv_0) -+{ -+ fprintf(stderr, "usage: %s [gemini://...]\n", argv_0); -+} -+ -+static bool -+set_url(struct Curl_URL *url, char *new_url) -+{ -+ if (curl_url_set(url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { -+ fprintf(stderr, "Error: invalid URL\n"); -+ return false; -+ } -+ return true; -+} -+ -+static char * -+trim_ws(char *in) -+{ -+ for (int i = strlen(in) - 1; in[i] && isspace(in[i]); --i) { -+ in[i] = 0; -+ } -+ for (; *in && isspace(*in); ++in); -+ return in; -+} -+ -+static void -+display_gemini(FILE *tty, struct gemini_response *resp, -+ struct link **next, bool pagination) -+{ -+ int nlinks = 0; -+ struct gemini_parser p; -+ gemini_parser_init(&p, resp->bio); -+ -+ struct winsize ws; -+ ioctl(fileno(tty), TIOCGWINSZ, &ws); -+ -+ int row = 0, col = 0; -+ struct gemini_token tok; -+ while (gemini_parser_next(&p, &tok) == 0) { -+ switch (tok.token) { -+ case GEMINI_TEXT: -+ // TODO: word wrap -+ col += fprintf(tty, " %s\n", trim_ws(tok.text)); -+ break; -+ case GEMINI_LINK: -+ (void)next; // TODO: Record links -+ col += fprintf(tty, "[%d] %s\n", nlinks++, trim_ws( -+ tok.link.text ? tok.link.text : tok.link.url)); -+ break; -+ case GEMINI_PREFORMATTED: -+ continue; // TODO -+ case GEMINI_HEADING: -+ for (int n = tok.heading.level; n; --n) { -+ col += fprintf(tty, "#"); -+ } -+ col += fprintf(tty, " %s\n", trim_ws(tok.heading.title)); -+ break; -+ case GEMINI_LIST_ITEM: -+ // TODO: Option to disable Unicode -+ col += fprintf(tty, " • %s\n", trim_ws(tok.list_item)); -+ break; -+ case GEMINI_QUOTE: -+ // TODO: Option to disable Unicode -+ col += fprintf(tty, " | %s\n", trim_ws(tok.quote_text)); -+ break; -+ } -+ -+ while (col >= ws.ws_col) { -+ col -= ws.ws_col; -+ ++row; -+ } -+ ++row; -+ col = 0; -+ -+ if (pagination && row >= ws.ws_row - 1) { -+ fprintf(tty, "[Enter for more, or q to stop] "); -+ -+ size_t n = 0; -+ char *l = NULL; -+ if (getline(&l, &n, tty) == -1) { -+ return; -+ } -+ if (strcmp(l, "q\n") == 0) { -+ return; -+ } -+ -+ free(l); -+ row = col = 0; -+ } -+ } -+ -+ gemini_parser_finish(&p); -+} -+ -+int -+main(int argc, char *argv[]) -+{ -+ bool pagination = true; -+ -+ bool have_url = false; -+ struct Curl_URL *url = curl_url(); -+ -+ FILE *tty = fopen("/dev/tty", "w+"); -+ -+ int c; -+ while ((c = getopt(argc, argv, "hP")) != -1) { -+ switch (c) { -+ case 'P': -+ pagination = false; -+ break; -+ case 'h': -+ usage(argv[0]); -+ return 0; -+ default: -+ fprintf(stderr, "fatal: unknown flag %c\n", c); -+ return 1; -+ } -+ } -+ -+ if (optind == argc - 1) { -+ set_url(url, argv[optind]); -+ have_url = true; -+ } else if (optind < argc - 1) { -+ usage(argv[0]); -+ return 1; -+ } -+ -+ SSL_load_error_strings(); -+ ERR_load_crypto_strings(); -+ struct gemini_options opts = { -+ .ssl_ctx = SSL_CTX_new(TLS_method()), -+ }; -+ -+ bool run = true; -+ struct gemini_response resp; -+ while (run) { -+ assert(have_url); // TODO -+ -+ struct link *links; -+ static char prompt[4096]; -+ -+ char *plain_url; -+ CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); -+ assert(uc == CURLUE_OK); // Invariant -+ -+ snprintf(prompt, sizeof(prompt), "\n\t%s\n" -+ "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit\n" -+ "=> ", plain_url); -+ -+ enum gemini_result res = gemini_request(plain_url, &opts, &resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", gemini_strerr(res, &resp)); -+ assert(0); // TODO: Prompt -+ } -+ -+ switch (gemini_response_class(resp.status)) { -+ case GEMINI_STATUS_CLASS_INPUT: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_REDIRECT: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -+ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -+ fprintf(stderr, "Server returned %s %d %s\n", -+ resp.status / 10 == 4 ? -+ "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ resp.status, resp.meta); -+ break; -+ case GEMINI_STATUS_CLASS_SUCCESS: -+ display_gemini(tty, &resp, &links, pagination); -+ break; -+ } -+ -+ gemini_response_finish(&resp); -+ -+ fprintf(tty, "%s", prompt); -+ size_t l = 0; -+ char *in = NULL; -+ ssize_t n = getline(&in, &l, tty); -+ if (n == -1 && feof(tty)) { -+ break; -+ } -+ -+ if (strcmp(in, "q\n") == 0) { -+ run = false; -+ } -+ -+ free(in); -+ } -+ -+ SSL_CTX_free(opts.ssl_ctx); -+ curl_url_cleanup(url); -+ return 0; -+} diff --git a/sources/cgmnlm.git/commits/021d8f8fdfcb9be636a73d7c59d540d8255cc0df.patch b/sources/cgmnlm.git/commits/021d8f8fdfcb9be636a73d7c59d540d8255cc0df.patch @@ -1,22 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index e8b25f9060c021bda099aa97fc2ec5657aeb2672..4f563ed19e0edc52086f55e4057a91f0802a1f93 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -20,7 +20,7 @@ static void - usage(const char *argv_0) - { - fprintf(stderr, -- "usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n", -+ "usage: %s [-46lLiIN] [-j mode] [-E cert] [-d input] [-D path] gemini://...\n", - argv_0); - } - -@@ -86,7 +86,7 @@ fprintf(stderr, - "The certificate offered by this server is of unknown trust. " - "Its fingerprint is: \n" - "%s\n\n" -- "Use -j once to trust temporarily, or -j always to add to the trust store.\n", fingerprint); -+ "Use '-j once' to trust temporarily, or '-j always' to add to the trust store.\n", fingerprint); - break; - case TOFU_FINGERPRINT_MISMATCH: - fprintf(stderr, diff --git a/sources/cgmnlm.git/commits/02c2be62daceb04e4891d415c997dd64db84b9d9.patch b/sources/cgmnlm.git/commits/02c2be62daceb04e4891d415c997dd64db84b9d9.patch @@ -1,12 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index ab0908de5ec9682865504e940a5489a43c41b12e..bdaa9baaca75e5f167add9a0ebed73cc60eaa647 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -147,6 +147,7 @@ - new_url = gemini_input_url(url, input); - free(url); - url = new_url; -+ assert(url); - goto next; - case 3: // REDIRECT - free(url); diff --git a/sources/cgmnlm.git/commits/02f6af661513683f0c6c1465c5ff1dd8f03a30c9.patch b/sources/cgmnlm.git/commits/02f6af661513683f0c6c1465c5ff1dd8f03a30c9.patch @@ -1,751 +0,0 @@ -diff --git a/configure b/configure -index 7412cf545fa96e6be169c0055778b83a06b557f5..44db11c4765077677a1f506f034c846cc736ab8c 100755 ---- a/configure -+++ b/configure -@@ -7,7 +7,9 @@ genrules gmni \ - src/client.c \ - src/escape.c \ - src/gmni.c \ -- src/url.c -+ src/tofu.c \ -+ src/url.c \ -+ src/util.c - } - - gmnlm() { -@@ -16,6 +18,7 @@ src/client.c \ - src/escape.c \ - src/gmnlm.c \ - src/parser.c \ -+ src/tofu.c \ - src/url.c \ - src/util.c - } -diff --git a/doc/gmni.scd b/doc/gmni.scd -index 0d84d4d974f2c38b426f6ae85d228cdd4847cbda..2c1dc54272aaf3a4cbd6b1c13e29323f418f199b 100644 ---- a/doc/gmni.scd -+++ b/doc/gmni.scd -@@ -6,7 +6,7 @@ gmni - Gemini client - - # SYNPOSIS - --*gmni* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ -+*gmni* [-46lLiIN] [-j _mode_] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ - - # DESCRIPTION - -@@ -51,6 +51,11 @@ this behavior. - - *-L* - Follow redirects. -+ -+*-j* _mode_ -+ Sets the TOFU (trust on first use) configuration, which controls if the -+ client shall trust new certificates. _mode_ can be one of *always*, -+ *once*, or *fail*. - - *-i* - Print the response status and meta text to stdout. -diff --git a/doc/gmnlm.scd b/doc/gmnlm.scd -index c5e7bf7f6b189f984e01ea2a942f47acb993f7e7..b11f3612e044ad0667d8c4d306445ae86e9f73d4 100644 ---- a/doc/gmnlm.scd -+++ b/doc/gmnlm.scd -@@ -6,13 +6,18 @@ gmnlm - Gemini line-mode browser - - # SYNPOSIS - --*gmnlm* [-PU] _gemini://..._ -+*gmnlm* [-PU] [-j _mode_] _gemini://..._ - - # DESCRIPTION - - *gmnlm* is an interactive line-mode Gemini browser. - - # OPTIONS -+ -+*-j* _mode_ -+ Sets the TOFU (trust on first use) configuration, which controls if the -+ client shall trust new certificates. _mode_ can be one of *always*, -+ *once*, or *fail*. - - *-P* - Disable pagination. -diff --git a/include/gmni.h b/include/gmni.h -index 4240c6231010ebb86aead2d49700fe2e3d00b65c..7e27b489d71fd3a43ca60292b17d56cab3caa5f8 100644 ---- a/include/gmni.h -+++ b/include/gmni.h -@@ -13,6 +13,7 @@ GEMINI_ERR_NOT_GEMINI, - GEMINI_ERR_RESOLVE, - GEMINI_ERR_CONNECT, - GEMINI_ERR_SSL, -+ GEMINI_ERR_SSL_VERIFY, - GEMINI_ERR_IO, - GEMINI_ERR_PROTOCOL, - }; -@@ -64,10 +65,6 @@ struct gemini_options { - // If NULL, an SSL context will be created. If unset, the ssl field - // must also be NULL. - SSL_CTX *ssl_ctx; -- -- // If NULL, an SSL connection will be established. If set, it is -- // presumed that the caller pre-established the SSL connection. -- SSL *ssl; - - // If ai_family != AF_UNSPEC (the default value on most systems), the - // client will connect to this address and skip name resolution. -diff --git a/include/tofu.h b/include/tofu.h -new file mode 100644 -index 0000000000000000000000000000000000000000..29aa9bc21567868cafb25a09dbc25ea0685ab01c ---- /dev/null -+++ b/include/tofu.h -@@ -0,0 +1,48 @@ -+#ifndef GEMINI_TOFU_H -+#define GEMINI_TOFU_H -+#include <limits.h> -+#include <openssl/ssl.h> -+#include <openssl/x509.h> -+#include <time.h> -+ -+enum tofu_error { -+ TOFU_VALID, -+ // Expired, wrong CN, etc. -+ TOFU_INVALID_CERT, -+ // Cert is valid but we haven't seen it before -+ TOFU_UNTRUSTED_CERT, -+ // Cert is valid but we already trust another cert for this host -+ TOFU_FINGERPRINT_MISMATCH, -+}; -+ -+enum tofu_action { -+ TOFU_ASK, -+ TOFU_FAIL, -+ TOFU_TRUST_ONCE, -+ TOFU_TRUST_ALWAYS, -+}; -+ -+struct known_host { -+ char *host, *fingerprint; -+ time_t expires; -+ int lineno; -+ struct known_host *next; -+}; -+ -+// Called when the user needs to be prompted to agree to trust an unknown -+// certificate. Return true to trust this certificate. -+typedef enum tofu_action (tofu_callback_t)(enum tofu_error error, -+ const char *fingerprint, struct known_host *host, void *data); -+ -+struct gemini_tofu { -+ char known_hosts_path[PATH_MAX+1]; -+ struct known_host *known_hosts; -+ int lineno; -+ tofu_callback_t *callback; -+ void *cb_data; -+}; -+ -+void gemini_tofu_init(struct gemini_tofu *tofu, -+ SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); -+ -+#endif -diff --git a/src/client.c b/src/client.c -index d8b67d7b9f47b4473ba6eb278853ea0565739f09..07460f917b153b0d368eb2a98f368aa6a5df56a4 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -95,6 +95,7 @@ { - assert(url); - assert(resp); - resp->meta = NULL; -+ resp->bio = NULL; - if (strlen(url) > 1024) { - return GEMINI_ERR_INVALID_URL; - } -@@ -110,7 +111,7 @@ res = GEMINI_ERR_INVALID_URL; - goto cleanup; - } - -- char *scheme; -+ char *scheme, *host; - if (curl_url_get(uri, CURLUPART_SCHEME, &scheme, 0) != CURLUE_OK) { - res = GEMINI_ERR_INVALID_URL; - goto cleanup; -@@ -120,6 +121,10 @@ res = GEMINI_ERR_NOT_GEMINI; - goto cleanup; - } - } -+ if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) { -+ res = GEMINI_ERR_INVALID_URL; -+ goto cleanup; -+ } - - if (options && options->ssl_ctx) { - resp->ssl_ctx = options->ssl_ctx; -@@ -127,42 +132,54 @@ SSL_CTX_up_ref(options->ssl_ctx); - } else { - resp->ssl_ctx = SSL_CTX_new(TLS_method()); - assert(resp->ssl_ctx); -+ SSL_CTX_set_verify(resp->ssl_ctx, SSL_VERIFY_PEER, NULL); - } - -+ int r; - BIO *sbio = BIO_new(BIO_f_ssl()); -- if (options && options->ssl) { -- resp->ssl = options->ssl; -- SSL_up_ref(resp->ssl); -- BIO_set_ssl(sbio, resp->ssl, 0); -- resp->fd = -1; -- } else { -- res = gemini_connect(uri, options, resp, &resp->fd); -- if (res != GEMINI_OK) { -- goto cleanup; -- } -+ res = gemini_connect(uri, options, resp, &resp->fd); -+ if (res != GEMINI_OK) { -+ goto cleanup; -+ } -+ -+ resp->ssl = SSL_new(resp->ssl_ctx); -+ assert(resp->ssl); -+ SSL_set_connect_state(resp->ssl); -+ if ((r = SSL_set1_host(resp->ssl, host)) != 1) { -+ goto ssl_error; -+ } -+ if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) { -+ goto ssl_error; -+ } -+ if ((r = SSL_set_fd(resp->ssl, resp->fd)) != 1) { -+ goto ssl_error; -+ } -+ if ((r = SSL_connect(resp->ssl)) != 1) { -+ goto ssl_error; -+ } -+ -+ X509 *cert = SSL_get_peer_certificate(resp->ssl); -+ if (!cert) { -+ resp->status = X509_V_ERR_UNSPECIFIED; -+ res = GEMINI_ERR_SSL_VERIFY; -+ goto cleanup; -+ } -+ X509_free(cert); - -- resp->ssl = SSL_new(resp->ssl_ctx); -- assert(resp->ssl); -- int r = SSL_set_fd(resp->ssl, resp->fd); -- if (r != 1) { -- resp->status = r; -- res = GEMINI_ERR_SSL; -- goto cleanup; -- } -- r = SSL_connect(resp->ssl); -- if (r != 1) { -- resp->status = r; -- res = GEMINI_ERR_SSL; -- goto cleanup; -- } -- BIO_set_ssl(sbio, resp->ssl, 0); -+ long vr = SSL_get_verify_result(resp->ssl); -+ if (vr != X509_V_OK) { -+ resp->status = vr; -+ res = GEMINI_ERR_SSL_VERIFY; -+ goto cleanup; - } - -+ BIO_set_ssl(sbio, resp->ssl, 0); -+ - resp->bio = BIO_new(BIO_f_buffer()); - BIO_push(resp->bio, sbio); - - char req[1024 + 3]; -- int r = snprintf(req, sizeof(req), "%s\r\n", url); -+ r = snprintf(req, sizeof(req), "%s\r\n", url); - assert(r > 0); - - r = BIO_puts(sbio, req); -@@ -199,6 +216,10 @@ - cleanup: - curl_url_cleanup(uri); - return res; -+ssl_error: -+ res = GEMINI_ERR_SSL; -+ resp->status = r; -+ goto cleanup; - } - - void -@@ -248,6 +269,8 @@ case GEMINI_ERR_SSL: - return ERR_error_string( - SSL_get_error(resp->ssl, resp->status), - NULL); -+ case GEMINI_ERR_SSL_VERIFY: -+ return X509_verify_cert_error_string(resp->status); - case GEMINI_ERR_IO: - return "I/O error"; - case GEMINI_ERR_PROTOCOL: -diff --git a/src/gmni.c b/src/gmni.c -index dc0c5c7f61679369fe4fadafd0508147868cc3c3..c13e0cd55623f557a8dc676d69aea54661b11faf 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -13,6 +13,7 @@ #include <sys/types.h> - #include <termios.h> - #include <unistd.h> - #include "gmni.h" -+#include "tofu.h" - - static void - usage(const char *argv_0) -@@ -57,6 +58,55 @@ } - return input; - } - -+struct tofu_config { -+ struct gemini_tofu tofu; -+ enum tofu_action action; -+}; -+ -+static enum tofu_action -+tofu_callback(enum tofu_error error, const char *fingerprint, -+ struct known_host *host, void *data) -+{ -+ struct tofu_config *cfg = (struct tofu_config *)data; -+ enum tofu_action action = cfg->action; -+ switch (error) { -+ case TOFU_VALID: -+ assert(0); // Invariant -+ case TOFU_INVALID_CERT: -+ fprintf(stderr, -+ "The server presented an invalid certificate with fingerprint %s.\n", -+ fingerprint); -+ if (action == TOFU_TRUST_ALWAYS) { -+ action = TOFU_TRUST_ONCE; -+ } -+ break; -+ case TOFU_UNTRUSTED_CERT: -+ fprintf(stderr, -+ "The certificate offered by this server is of unknown trust. " -+ "Its fingerprint is: \n" -+ "%s\n\n", fingerprint); -+ break; -+ case TOFU_FINGERPRINT_MISMATCH: -+ fprintf(stderr, -+ "The certificate offered by this server DOES NOT MATCH the one we have on file.\n" -+ "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" -+ "The unknown certificate's fingerprint is:\n" -+ "%s\n\n" -+ "The expected fingerprint is:\n" -+ "%s\n\n" -+ "If you're certain that this is correct, edit %s:%d\n", -+ fingerprint, host->fingerprint, -+ cfg->tofu.known_hosts_path, host->lineno); -+ return TOFU_FAIL; -+ } -+ -+ if (action == TOFU_ASK) { -+ return TOFU_FAIL; -+ } -+ -+ return action; -+} -+ - int - main(int argc, char *argv[]) - { -@@ -71,7 +121,6 @@ enum input_mode { - INPUT_READ, - INPUT_SUPPRESS, - }; -- - enum input_mode input_mode = INPUT_READ; - FILE *input_source = stdin; - -@@ -82,9 +131,11 @@ struct addrinfo hints = {0}; - struct gemini_options opts = { - .hints = &hints, - }; -+ struct tofu_config cfg; -+ cfg.action = TOFU_ASK; - - int c; -- while ((c = getopt(argc, argv, "46d:D:E:hlLiINR:")) != -1) { -+ while ((c = getopt(argc, argv, "46d:D:E:hj:lLiINR:")) != -1) { - switch (c) { - case '4': - hints.ai_family = AF_INET; -@@ -115,6 +166,18 @@ break; - case 'h': - usage(argv[0]); - return 0; -+ case 'j': -+ if (strcmp(optarg, "fail") == 0) { -+ cfg.action = TOFU_FAIL; -+ } else if (strcmp(optarg, "once") == 0) { -+ cfg.action = TOFU_TRUST_ONCE; -+ } else if (strcmp(optarg, "always") == 0) { -+ cfg.action = TOFU_TRUST_ALWAYS; -+ } else { -+ usage(argv[0]); -+ return 1; -+ } -+ break; - case 'l': - linefeed = false; - break; -@@ -153,6 +216,8 @@ } - - SSL_load_error_strings(); - ERR_load_crypto_strings(); -+ opts.ssl_ctx = SSL_CTX_new(TLS_method()); -+ gemini_tofu_init(&cfg.tofu, opts.ssl_ctx, &tofu_callback, &cfg); - - bool exit = false; - char *url = strdup(argv[optind]); -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 69b9a75ca1caab6f7e5f74685e550539be149ab6..41284df2235e869f49e542f5e1f2dcc240deb684 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -13,6 +13,7 @@ #include <sys/ioctl.h> - #include <termios.h> - #include <unistd.h> - #include "gmni.h" -+#include "tofu.h" - #include "url.h" - #include "util.h" - -@@ -29,6 +30,8 @@ - struct browser { - bool pagination, unicode; - struct gemini_options opts; -+ struct gemini_tofu tofu; -+ enum tofu_action tofu_mode; - - FILE *tty; - char *plain_url; -@@ -657,22 +660,113 @@ - return false; - } - -+static enum tofu_action -+tofu_callback(enum tofu_error error, const char *fingerprint, -+ struct known_host *host, void *data) -+{ -+ struct browser *browser = data; -+ if (browser->tofu_mode != TOFU_ASK) { -+ return browser->tofu_mode; -+ } -+ -+ static char prompt[8192]; -+ switch (error) { -+ case TOFU_VALID: -+ assert(0); // Invariant -+ case TOFU_INVALID_CERT: -+ snprintf(prompt, sizeof(prompt), -+ "The server presented an invalid certificate. If you choose to proceed, " -+ "you should not disclose personal information or trust the contents of the page.\n" -+ "trust [o]nce; [a]bort\n" -+ "=> "); -+ break; -+ case TOFU_UNTRUSTED_CERT: -+ snprintf(prompt, sizeof(prompt), -+ "The certificate offered by this server is of unknown trust. " -+ "Its fingerprint is: \n" -+ "%s\n\n" -+ "If you knew the fingerprint to expect in advance, verify that this matches.\n" -+ "Otherwise, it should be safe to trust this certificate.\n\n" -+ "[t]rust always; trust [o]nce; [a]bort\n" -+ "=> ", fingerprint); -+ break; -+ case TOFU_FINGERPRINT_MISMATCH: -+ snprintf(prompt, sizeof(prompt), -+ "The certificate offered by this server DOES NOT MATCH the one we have on file.\n" -+ "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" -+ "The unknown certificate's fingerprint is:\n" -+ "%s\n\n" -+ "The expected fingerprint is:\n" -+ "%s\n\n" -+ "If you're certain that this is correct, edit %s:%d\n", -+ fingerprint, host->fingerprint, -+ browser->tofu.known_hosts_path, host->lineno); -+ return TOFU_FAIL; -+ } -+ -+ bool prompting = true; -+ while (prompting) { -+ fprintf(browser->tty, "%s", prompt); -+ -+ size_t sz = 0; -+ char *line = NULL; -+ if (getline(&line, &sz, browser->tty) == -1) { -+ free(line); -+ return TOFU_FAIL; -+ } -+ if (line[1] != '\n') { -+ free(line); -+ continue; -+ } -+ -+ char c = line[0]; -+ free(line); -+ -+ switch (c) { -+ case 't': -+ if (error == TOFU_INVALID_CERT) { -+ break; -+ } -+ return TOFU_TRUST_ALWAYS; -+ case 'o': -+ return TOFU_TRUST_ONCE; -+ case 'a': -+ return TOFU_FAIL; -+ } -+ } -+ -+ return TOFU_FAIL; -+} -+ - int - main(int argc, char *argv[]) - { - struct browser browser = { - .pagination = true, -+ .tofu_mode = TOFU_ASK, - .unicode = true, - .url = curl_url(), - .tty = fopen("/dev/tty", "w+"), - }; - - int c; -- while ((c = getopt(argc, argv, "hPU")) != -1) { -+ while ((c = getopt(argc, argv, "hj:PU")) != -1) { - switch (c) { - case 'h': - usage(argv[0]); - return 0; -+ case 'j': -+ if (strcmp(optarg, "fail") == 0) { -+ browser.tofu_mode = TOFU_FAIL; -+ } else if (strcmp(optarg, "once") == 0) { -+ browser.tofu_mode = TOFU_TRUST_ONCE; -+ } else if (strcmp(optarg, "always") == 0) { -+ browser.tofu_mode = TOFU_TRUST_ALWAYS; -+ } else { -+ usage(argv[0]); -+ return 1; -+ } -+ break; - case 'P': - browser.pagination = false; - break; -@@ -695,6 +789,8 @@ - SSL_load_error_strings(); - ERR_load_crypto_strings(); - browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); -+ gemini_tofu_init(&browser.tofu, browser.opts.ssl_ctx, -+ &tofu_callback, &browser); - - struct gemini_response resp; - browser.running = true; -diff --git a/src/tofu.c b/src/tofu.c -new file mode 100644 -index 0000000000000000000000000000000000000000..e8efeaf69fcb9bd890711f76959e86efa75cfec6 ---- /dev/null -+++ b/src/tofu.c -@@ -0,0 +1,201 @@ -+#include <assert.h> -+#include <errno.h> -+#include <libgen.h> -+#include <limits.h> -+#include <openssl/asn1.h> -+#include <openssl/evp.h> -+#include <openssl/ssl.h> -+#include <openssl/x509.h> -+#include <stdio.h> -+#include <string.h> -+#include <time.h> -+#include "tofu.h" -+#include "util.h" -+ -+static int -+verify_callback(X509_STORE_CTX *ctx, void *data) -+{ -+ // Gemini clients handle TLS verification differently from the rest of -+ // the internet. We use a TOFU system, so trust is based on two factors: -+ // -+ // - Is the certificate valid at the time of the request? -+ // - Has the user trusted this certificate yet? -+ // -+ // If the answer to the latter is "no", then we give the user an -+ // opportunity to explicitly agree to trust the certificate before -+ // rejecting it. -+ // -+ // If you're reading this code with the intent to re-use it, think -+ // twice. -+ // -+ // TODO: Check that the subject name is valid for the requested URL. -+ struct gemini_tofu *tofu = (struct gemini_tofu *)data; -+ X509 *cert = X509_STORE_CTX_get0_cert(ctx); -+ -+ int rc; -+ int day, sec; -+ const ASN1_TIME *notBefore = X509_get0_notBefore(cert); -+ const ASN1_TIME *notAfter = X509_get0_notAfter(cert); -+ if (!ASN1_TIME_diff(&day, &sec, NULL, notBefore)) { -+ rc = X509_V_ERR_UNSPECIFIED; -+ goto invalid_cert; -+ } -+ if (day > 0 || sec > 0) { -+ rc = X509_V_ERR_CERT_NOT_YET_VALID; -+ goto invalid_cert; -+ } -+ if (!ASN1_TIME_diff(&day, &sec, NULL, notAfter)) { -+ rc = X509_V_ERR_UNSPECIFIED; -+ goto invalid_cert; -+ } -+ if (day < 0 || sec < 0) { -+ rc = X509_V_ERR_CERT_HAS_EXPIRED; -+ goto invalid_cert; -+ } -+ -+ unsigned char md[256 / 8]; -+ const EVP_MD *sha512 = EVP_sha512(); -+ unsigned int len = sizeof(md); -+ rc = X509_digest(cert, sha512, md, &len); -+ assert(rc == 1); -+ -+ char fingerprint[256 / 8 * 3]; -+ for (size_t i = 0; i < sizeof(md); ++i) { -+ snprintf(&fingerprint[i * 3], 4, "%02X%s", -+ md[i], i + 1 == sizeof(md) ? "" : ":"); -+ } -+ -+ SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, -+ SSL_get_ex_data_X509_STORE_CTX_idx()); -+ const char *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); -+ if (!servername) { -+ rc = X509_V_ERR_HOSTNAME_MISMATCH; -+ goto invalid_cert; -+ } -+ -+ time_t now; -+ time(&now); -+ -+ enum tofu_error error = TOFU_UNTRUSTED_CERT; -+ struct known_host *host = tofu->known_hosts; -+ while (host) { -+ if (host->expires < now) { -+ goto next; -+ } -+ if (strcmp(host->host, servername) != 0) { -+ goto next; -+ } -+ if (strcmp(host->fingerprint, fingerprint) == 0) { -+ // Valid match in known hosts -+ return 0; -+ } -+ error = TOFU_FINGERPRINT_MISMATCH; -+ break; -+next: -+ host = host->next; -+ } -+ -+ rc = X509_V_ERR_CERT_UNTRUSTED; -+ -+callback: -+ switch (tofu->callback(error, fingerprint, host, tofu->cb_data)) { -+ case TOFU_ASK: -+ assert(0); // Invariant -+ case TOFU_FAIL: -+ X509_STORE_CTX_set_error(ctx, rc); -+ break; -+ case TOFU_TRUST_ONCE: -+ // No further action necessary -+ return 0; -+ case TOFU_TRUST_ALWAYS:; -+ FILE *f = fopen(tofu->known_hosts_path, "a"); -+ if (!f) { -+ fprintf(stderr, "Error opening %s for writing: %s\n", -+ tofu->known_hosts_path, strerror(errno)); -+ break; -+ }; -+ struct tm expires_tm; -+ ASN1_TIME_to_tm(notAfter, &expires_tm); -+ time_t expires = mktime(&expires_tm); -+ fprintf(f, "%s %s %s %ld\n", servername, -+ "SHA-512", fingerprint, expires); -+ fclose(f); -+ -+ host = calloc(1, sizeof(struct known_host)); -+ host->host = strdup(servername); -+ host->fingerprint = strdup(fingerprint); -+ host->expires = expires; -+ host->lineno = ++tofu->lineno; -+ host->next = tofu->known_hosts; -+ tofu->known_hosts = host; -+ return 0; -+ } -+ -+ X509_STORE_CTX_set_error(ctx, rc); -+ return 0; -+ -+invalid_cert: -+ error = TOFU_INVALID_CERT; -+ goto callback; -+} -+ -+ -+void -+gemini_tofu_init(struct gemini_tofu *tofu, -+ SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data) -+{ -+ const struct pathspec paths[] = { -+ {.var = "GMNIDATA", .path = "/%s"}, -+ {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, -+ {.var = "HOME", .path = "/.local/share/gmni/%s"} -+ }; -+ const char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); -+ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -+ path_fmt, "known_hosts"); -+ -+ if (mkdirs(dirname(tofu->known_hosts_path), 0755) != 0) { -+ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -+ path_fmt, "known_hosts"); -+ fprintf(stderr, "Error creating directory %s: %s\n", -+ dirname(tofu->known_hosts_path), strerror(errno)); -+ return; -+ } -+ -+ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -+ path_fmt, "known_hosts"); -+ -+ tofu->callback = cb; -+ tofu->cb_data = cb_data; -+ SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu); -+ -+ FILE *f = fopen(tofu->known_hosts_path, "r"); -+ if (!f) { -+ return; -+ } -+ size_t n = 0; -+ char *line = NULL; -+ while (getline(&line, &n, f) != -1) { -+ struct known_host *host = calloc(1, sizeof(struct known_host)); -+ char *tok = strtok(line, " "); -+ assert(tok); -+ host->host = strdup(tok); -+ -+ tok = strtok(NULL, " "); -+ assert(tok); -+ if (strcmp(tok, "SHA-512") != 0) { -+ free(host); -+ continue; -+ } -+ -+ tok = strtok(NULL, " "); -+ assert(tok); -+ host->fingerprint = strdup(tok); -+ -+ tok = strtok(NULL, " "); -+ assert(tok); -+ host->expires = strtoul(tok, NULL, 10); -+ -+ host->next = tofu->known_hosts; -+ tofu->known_hosts = host; -+ } -+} diff --git a/sources/cgmnlm.git/commits/0513b91be1173b1ed43a0f1d28cf502a81267185.patch b/sources/cgmnlm.git/commits/0513b91be1173b1ed43a0f1d28cf502a81267185.patch @@ -1,14 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index c70a4126acc5337a768c3a70513aecc1b0e16668..95b85fedd9fb84b50bf1c8a89be94daaeb99a4a6 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -730,6 +730,9 @@ case '\r': - if (!s[i+1]) break; - /* fallthrough */ - default: -+ // skip unicode continuation bytes -+ if ((s[i] & 0xc0) == 0x80) break; -+ - if (iscntrl(s[i])) { - s[i] = '.'; - } diff --git a/sources/cgmnlm.git/commits/05cc8b85cdf731ea3a664b6099aad04f22bbca6c.patch b/sources/cgmnlm.git/commits/05cc8b85cdf731ea3a664b6099aad04f22bbca6c.patch @@ -1,49 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 129324718d13f8311f38cc3c80b7f0f3ccb3bd09..b7cc12dbbb391dd3e72e5e30d21a7e7957872f60 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -41,6 +41,14 @@ PROMPT_QUIT, - PROMPT_ANSWERED, - }; - -+const char *help_msg = -+ "The following commands are available:\n\n" -+ "q: Quit\n" -+ "N: Follow Nth link (where N is a number)\n" -+ "b: Back (in the page history)\n" -+ "f: Forward (in the page history)\n" -+ ; -+ - static void - usage(const char *argv_0) - { -@@ -100,6 +108,11 @@ if (strcmp(in, "q\n") == 0) { - result = PROMPT_QUIT; - goto exit; - } -+ if (strcmp(in, "?\n") == 0) { -+ fprintf(browser->tty, "%s", help_msg); -+ result = PROMPT_AGAIN; -+ goto exit; -+ } - if (strcmp(in, "b\n") == 0) { - if (!browser->history->prev) { - fprintf(stderr, "At beginning of history\n"); -@@ -291,7 +304,7 @@ - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit; or type a URL\n" -+ "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "(more) => ", resp->meta, browser->plain_url, - browser->history->prev ? "[b]ack; " : "", - browser->history->next ? "[f]orward; " : ""); -@@ -500,7 +513,7 @@ goto next; - } - - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[N]: follow Nth link; %s%s[q]uit; or type a URL\n" -+ "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "=> ", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", - browser.plain_url, diff --git a/sources/cgmnlm.git/commits/05d112b7d347b737bdac503ea05292db3347f2a8.patch b/sources/cgmnlm.git/commits/05d112b7d347b737bdac503ea05292db3347f2a8.patch @@ -1,66 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 5b1f37522e93da360e74ba92ccb00c530bd41463..6e27b2f859c692f37fb483184cda245a7e979cbe 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -71,16 +71,20 @@ enum input_mode { - INPUT_READ, - INPUT_SUPPRESS, - }; -+ - enum input_mode input_mode = INPUT_READ; - FILE *input_source = stdin; -+ - bool follow_redirects = false, linefeed = true; -+ int max_redirect = 5; -+ - struct addrinfo hints = {0}; - struct gemini_options opts = { - .hints = &hints, - }; - - int c; -- while ((c = getopt(argc, argv, "46d:D:E:hlLiIN")) != -1) { -+ while ((c = getopt(argc, argv, "46d:D:E:hlLiINR:")) != -1) { - switch (c) { - case '4': - hints.ai_family = AF_INET; -@@ -127,6 +131,15 @@ break; - case 'N': - input_mode = INPUT_SUPPRESS; - break; -+ case 'R':; -+ char *endptr; -+ errno = 0; -+ max_redirect = strtoul(optarg, &endptr, 10); -+ if (*endptr || errno != 0) { -+ fprintf(stderr, "Error: -R expects numeric argument\n"); -+ return 1; -+ } -+ break; - default: - fprintf(stderr, "fatal: unknown flag %c\n", c); - return 1; -@@ -144,7 +157,7 @@ - bool exit = false; - char *url = strdup(argv[optind]); - -- int ret = 0; -+ int ret = 0, nredir = 0; - while (!exit) { - struct gemini_response resp; - enum gemini_result r = gemini_request(url, &opts, &resp); -@@ -177,6 +190,14 @@ free(url); - url = new_url; - goto next; - case GEMINI_STATUS_CLASS_REDIRECT: -+ if (++nredir >= max_redirect) { -+ fprintf(stderr, -+ "Error: maximum redirects (%d) exceeded", -+ max_redirect); -+ exit = true; -+ goto next; -+ } -+ - free(url); - url = strdup(resp.meta); - if (!follow_redirects) { diff --git a/sources/cgmnlm.git/commits/0976b0e44655163a34d1b53e62a348cbf4335940.patch b/sources/cgmnlm.git/commits/0976b0e44655163a34d1b53e62a348cbf4335940.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 199572c363f9e50acdd052111350f3926d6a350d..5d45ffe7372f71ab32461bf71a590cc494269e96 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -246,7 +246,7 @@ open_bookmarks(browser); - result = PROMPT_ANSWERED; - goto exit; - case '/': -- if (in[1]) break; -+ if (!in[1]) break; - if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) { - static char buf[1024]; - r = regerror(r, &browser->regex, buf, sizeof(buf)); diff --git a/sources/cgmnlm.git/commits/0a03e6dadf7c30cea1fb388a9e5386a00c853dbb.patch b/sources/cgmnlm.git/commits/0a03e6dadf7c30cea1fb388a9e5386a00c853dbb.patch @@ -1,12 +0,0 @@ -diff --git a/src/parser.c b/src/parser.c -index 2f78e4641c08b86f5dc50cc9776ea80ec7f9aead..04501b6b644af28abe7843076ae593b7165912b2 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -113,6 +113,7 @@ if (end && end + 1 < p->buf + p->bufln) { - size_t len = end - p->buf + 1; - memmove(p->buf, end + 1, p->bufln - len); - p->bufln -= len; -+ p->buf[p->bufln] = 0; - } else { - p->buf[0] = 0; - p->bufln = 0; diff --git a/sources/cgmnlm.git/commits/0b5c37d2e65a46fe8e4a49c2f00cb6228fad59e3.patch b/sources/cgmnlm.git/commits/0b5c37d2e65a46fe8e4a49c2f00cb6228fad59e3.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index da99a84580894312ba86b92e6abd02cfdfc80fd4..26654324b4fb845633f7db2b2ac01e5e3b4635a1 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -493,7 +493,7 @@ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: -- col += fprintf(out, "%s ", -+ col += fprintf(out, " %s ", - browser->unicode ? "┃" : ">"); - if (text == NULL) { - text = trim_ws(tok.quote_text); diff --git a/sources/cgmnlm.git/commits/0eaf9cc109a99d6efb0d9c763291f6a5d9e74391.patch b/sources/cgmnlm.git/commits/0eaf9cc109a99d6efb0d9c763291f6a5d9e74391.patch @@ -1,15 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index 45e1275d568cfcb4aacfe7f86788b76fff97916b..4eeb7fd0eb8df7423fe8fd8f106e2fe433fa8716 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -147,8 +147,8 @@ SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data) - { - const struct pathspec paths[] = { - {.var = "GMNIDATA", .path = "/%s"}, -- {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, -- {.var = "HOME", .path = "/.local/share/gmni/%s"} -+ {.var = "XDG_DATA_HOME", .path = "/gemini/%s"}, -+ {.var = "HOME", .path = "/.local/share/gemini/%s"} - }; - char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); - snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), diff --git a/sources/cgmnlm.git/commits/0ed7a4527c967ce3f14909923277cf62624f0900.patch b/sources/cgmnlm.git/commits/0ed7a4527c967ce3f14909923277cf62624f0900.patch @@ -1,28 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index a021a97298bb41bff8cdbbceca172f561b64c7a5..8e85d091e54e3c3c11dbd6980ae660be6838c7c8 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -349,7 +349,7 @@ case 0: - close(pfd[1]); - dup2(pfd[0], STDIN_FILENO); - close(pfd[0]); -- execlp("sh", "sh", "-c", cmd); -+ execlp("sh", "sh", "-c", cmd, NULL); - perror("exec"); - _exit(1); - } -diff --git a/src/tofu.c b/src/tofu.c -index b9100c77fd61c71ce561f2194b991eee7130e689..ba5493352968b563ad76e0ecd25bab6370d2f6e5 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -124,8 +124,8 @@ }; - struct tm expires_tm; - ASN1_TIME_to_tm(notAfter, &expires_tm); - time_t expires = mktime(&expires_tm); -- fprintf(f, "%s %s %s %ld\n", servername, -- "SHA-512", fingerprint, expires); -+ fprintf(f, "%s %s %s %jd\n", servername, -+ "SHA-512", fingerprint, (intmax_t)expires); - fclose(f); - - host = calloc(1, sizeof(struct known_host)); diff --git a/sources/cgmnlm.git/commits/100759a7d796f4e486a89b65b3ca491c1141056f.patch b/sources/cgmnlm.git/commits/100759a7d796f4e486a89b65b3ca491c1141056f.patch @@ -1,34 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 930ab19abe717b186070151a184b2a8e98542f77..1292bb6b8f526f923e9783ba8c0a014c2743af05 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1106,7 +1106,7 @@ "=> ", host, fingerprint); - free(host); - break; - case TOFU_FINGERPRINT_MISMATCH: -- snprintf(prompt, sizeof(prompt), -+ fprintf(browser->tty, - "The certificate offered by this server DOES NOT MATCH the one we have on file.\n" - "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" - "The unknown certificate's fingerprint is:\n" -diff --git a/src/tofu.c b/src/tofu.c -index ba5493352968b563ad76e0ecd25bab6370d2f6e5..48395c08cdbf68b31c5defd15b360f1eb2897a3f 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -188,6 +188,7 @@ if (!f) { - return; - } - n = 0; -+ int lineno = 1; - char *line = NULL; - while (getline(&line, &n, f) != -1) { - struct known_host *host = calloc(1, sizeof(struct known_host)); -@@ -210,6 +211,8 @@ - tok = strtok(NULL, " "); - assert(tok); - host->expires = strtoul(tok, NULL, 10); -+ -+ host->lineno = lineno++; - - host->next = tofu->known_hosts; - tofu->known_hosts = host; diff --git a/sources/cgmnlm.git/commits/122fb0a9fd5456e3b1fd9f084130df85c859394b.patch b/sources/cgmnlm.git/commits/122fb0a9fd5456e3b1fd9f084130df85c859394b.patch @@ -1,562 +0,0 @@ -diff --git a/.gitignore b/.gitignore -index 6ec8d5188d32b997f3a91f8a11f64f4e79b43770..4a40b62507e740b789fdb6d2d4a63c41e3b102c8 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -4,3 +4,5 @@ gmni - gmnlm - *.1 - *.o -+*.a -+*.pc -diff --git a/Makefile b/Makefile -index 5ac44dbda9d629823f99124f67ace8125e9bc697..22fed5d19ec4f989ead26d22d3d1887614e4927c 100644 ---- a/Makefile -+++ b/Makefile -@@ -1,6 +1,7 @@ - .POSIX: - .SUFFIXES: - OUTDIR=.build -+VERSION=0.0.0 - include $(OUTDIR)/config.mk - include $(OUTDIR)/cppcache - -@@ -12,9 +13,26 @@ gmnlm: $(gmnlm_objects) - @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) -o $@ $(gmnlm_objects) $(LIBS) - -+libgmni.a: $(libgmni.a_objects) -+ @printf 'AR\t$@\n' -+ @$(AR) -rcs $@ $(libgmni.a_objects) -+ - doc/gmni.1: doc/gmni.scd - doc/gmnlm.1: doc/gmnlm.scd - -+libgmni.pc: -+ @printf 'GEN\t$@\n' -+ @printf 'prefix=%s\n' "$(PREFIX)" > $@ -+ @printf 'exec_prefix=$${prefix}\n' >> $@ -+ @printf 'includedir=$${prefix}/include\n' >> $@ -+ @printf 'libdir=$${prefix}/lib\n' >> $@ -+ @printf 'Name: libgmni\n' >> $@ -+ @printf 'Version: %s\n' "$(VERSION)" >> $@ -+ @printf 'Description: The gmni client library\n' >> $@ -+ @printf 'Requires: libssl libcrypto\n' >> $@ -+ @printf 'Cflags: -I$${includedir}/gmni\n' >> $@ -+ @printf 'Libs: -L$${libdir} -lgmni\n' >> $@ -+ - .SUFFIXES: .c .o .scd .1 - - .c.o: -@@ -22,7 +40,7 @@ @printf 'CC\t$@\n' - @touch $(OUTDIR)/cppcache - @grep $< $(OUTDIR)/cppcache >/dev/null || \ - $(CPP) $(CFLAGS) -MM -MT $@ $< >> $(OUTDIR)/cppcache -- @$(CC) -c $(CFLAGS) -o $@ $< -+ @$(CC) -c -fPIC $(CFLAGS) -o $@ $< - - .scd.1: - @printf 'SCDOC\t$@\n' -@@ -31,7 +49,7 @@ - docs: doc/gmni.1 doc/gmnlm.1 - - clean: -- @rm -f gmni gmnlm doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) -+ @rm -f gmni gmnlm libgmni.a libgmni.pc doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) - - distclean: clean - @rm -rf "$(OUTDIR)" -@@ -41,6 +59,11 @@ mkdir -p $(BINDIR) - mkdir -p $(MANDIR)/man1 - install -Dm755 gmni $(BINDIR)/gmni - install -Dm755 gmnlm $(BINDIR)/gmnlm -+ install -Dm755 libgmni.a $(LIBDIR)/libgmni.a -+ install -Dm644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h -+ install -Dm644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h -+ install -Dm644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h -+ install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig - install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 - install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 - -diff --git a/config.sh b/config.sh -index b03dfffbc5976e69d2d878dbf3b6c545ebcad377..a6963622d4b41d6e25bb4fe230c2513849c077d3 100644 ---- a/config.sh -+++ b/config.sh -@@ -134,6 +134,7 @@ OUTDIR=${outdir} - _INSTDIR=\$(DESTDIR)\$(PREFIX) - BINDIR?=${BINDIR:-\$(_INSTDIR)/bin} - LIBDIR?=${LIBDIR:-\$(_INSTDIR)/lib} -+ INCLUDEDIR?=${INCLUDEDIR:-\$(_INSTDIR)/include} - MANDIR?=${MANDIR:-\$(_INSTDIR)/share/man} - CACHE=\$(OUTDIR)/cache - CFLAGS=${CFLAGS} -@@ -146,7 +147,7 @@ EOF - - for target in $all - do -- $target >>"$outdir"/config.mk -+ ${target//./_} >>"$outdir"/config.mk - done - echo done - -diff --git a/configure b/configure -index 44db11c4765077677a1f506f034c846cc736ab8c..e82a0e27e27c9873c8dac92fd3385e65bf511648 100755 ---- a/configure -+++ b/configure -@@ -23,6 +23,21 @@ src/url.c \ - src/util.c - } - --all="gmni gmnlm" -+libgmni_a() { -+ genrules libgmni.a \ -+ src/client.c \ -+ src/escape.c \ -+ src/tofu.c \ -+ src/url.c \ -+ src/util.c \ -+ src/parser.c -+} -+ -+libgmni_pc() { -+ : -+} -+ -+all="gmni gmnlm libgmni.a libgmni.pc" -+ - - run_configure -diff --git a/include/gmni.h b/include/gmni.h -deleted file mode 100644 -index 7e27b489d71fd3a43ca60292b17d56cab3caa5f8..0000000000000000000000000000000000000000 ---- a/include/gmni.h -+++ /dev/null -@@ -1,164 +0,0 @@ --#ifndef GEMINI_CLIENT_H --#define GEMINI_CLIENT_H --#include <netdb.h> --#include <openssl/ssl.h> --#include <stdbool.h> --#include <sys/socket.h> -- --enum gemini_result { -- GEMINI_OK, -- GEMINI_ERR_OOM, -- GEMINI_ERR_INVALID_URL, -- GEMINI_ERR_NOT_GEMINI, -- GEMINI_ERR_RESOLVE, -- GEMINI_ERR_CONNECT, -- GEMINI_ERR_SSL, -- GEMINI_ERR_SSL_VERIFY, -- GEMINI_ERR_IO, -- GEMINI_ERR_PROTOCOL, --}; -- --enum gemini_status { -- GEMINI_STATUS_INPUT = 10, -- GEMINI_STATUS_SENSITIVE_INPUT = 11, -- GEMINI_STATUS_SUCCESS = 20, -- GEMINI_STATUS_REDIRECT_TEMPORARY = 30, -- GEMINI_STATUS_REDIRECT_PERMANENT = 31, -- GEMINI_STATUS_TEMPORARY_FAILURE = 40, -- GEMINI_STATUS_SERVER_UNAVAILABLE = 41, -- GEMINI_STATUS_CGI_ERROR = 42, -- GEMINI_STATUS_PROXY_ERROR = 43, -- GEMINI_STATUS_SLOW_DOWN = 44, -- GEMINI_STATUS_PERMANENT_FAILURE = 50, -- GEMINI_STATUS_NOT_FOUND = 51, -- GEMINI_STATUS_GONE = 52, -- GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, -- GEMINI_STATUS_BAD_REQUEST = 59, -- GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, -- GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, -- GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, --}; -- --enum gemini_status_class { -- GEMINI_STATUS_CLASS_INPUT = 10, -- GEMINI_STATUS_CLASS_SUCCESS = 20, -- GEMINI_STATUS_CLASS_REDIRECT = 30, -- GEMINI_STATUS_CLASS_TEMPORARY_FAILURE = 40, -- GEMINI_STATUS_CLASS_PERMANENT_FAILURE = 50, -- GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED = 60, --}; -- --struct gemini_response { -- enum gemini_status status; -- char *meta; -- -- // Response body may be read from here if appropriate: -- BIO *bio; -- -- // Connection state -- SSL_CTX *ssl_ctx; -- SSL *ssl; -- int fd; --}; -- --struct gemini_options { -- // If NULL, an SSL context will be created. If unset, the ssl field -- // must also be NULL. -- SSL_CTX *ssl_ctx; -- -- // If ai_family != AF_UNSPEC (the default value on most systems), the -- // client will connect to this address and skip name resolution. -- struct addrinfo *addr; -- -- // If non-NULL, these hints are provided to getaddrinfo. Useful, for -- // example, to force IPv4/IPv6. -- struct addrinfo *hints; --}; -- --// Requests the specified URL via the gemini protocol. If options is non-NULL, --// it may specify some additional configuration to adjust client behavior. --// --// Returns a value indicating the success of the request. --// --// Caller must call gemini_response_finish afterwards to clean up resources --// before exiting or re-using it for another request. --enum gemini_result gemini_request(const char *url, -- struct gemini_options *options, -- struct gemini_response *resp); -- --// Must be called after gemini_request in order to free up the resources --// allocated during the request. --void gemini_response_finish(struct gemini_response *resp); -- --// Returns a user-friendly string describing an error. --const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); -- --// Returns the given URL with the input response set to the specified value. --// The caller must free the string. --char *gemini_input_url(const char *url, const char *input); -- --// Returns the general response class (i.e. with the second digit set to zero) --// of the given Gemini status code. --enum gemini_status_class gemini_response_class(enum gemini_status status); -- --enum gemini_tok { -- GEMINI_TEXT, -- GEMINI_LINK, -- GEMINI_PREFORMATTED_BEGIN, -- GEMINI_PREFORMATTED_END, -- GEMINI_PREFORMATTED_TEXT, -- GEMINI_HEADING, -- GEMINI_LIST_ITEM, -- GEMINI_QUOTE, --}; -- --struct gemini_token { -- enum gemini_tok token; -- -- // The token field determines which of the union members is valid. -- union { -- char *text; -- -- struct { -- char *text; -- char *url; // May be NULL -- } link; -- -- char *preformatted; -- -- struct { -- char *title; -- int level; // 1, 2, or 3 -- } heading; -- -- char *list_item; -- char *quote_text; -- }; --}; -- --struct gemini_parser { -- BIO *f; -- char *buf; -- size_t bufsz; -- size_t bufln; -- bool preformatted; --}; -- --// Initializes a text/gemini parser which reads from the specified BIO. --void gemini_parser_init(struct gemini_parser *p, BIO *f); -- --// Finishes this text/gemini parser and frees up its resources. --void gemini_parser_finish(struct gemini_parser *p); -- --// Reads the next token from a text/gemini file. --// --// Returns 0 on success, 1 on EOF, and -1 on failure. --// --// Caller must call gemini_token_finish before exiting or re-using the token --// parameter. --int gemini_parser_next(struct gemini_parser *p, struct gemini_token *token); -- --// Must be called after gemini_next to free up resources for the next token. --void gemini_token_finish(struct gemini_token *token); -- --#endif -diff --git a/include/tofu.h b/include/tofu.h -deleted file mode 100644 -index a88167ba0fb6606b2b170e5005c55131f1861972..0000000000000000000000000000000000000000 ---- a/include/tofu.h -+++ /dev/null -@@ -1,49 +0,0 @@ --#ifndef GEMINI_TOFU_H --#define GEMINI_TOFU_H --#include <limits.h> --#include <openssl/ssl.h> --#include <openssl/x509.h> --#include <time.h> -- --enum tofu_error { -- TOFU_VALID, -- // Expired, wrong CN, etc. -- TOFU_INVALID_CERT, -- // Cert is valid but we haven't seen it before -- TOFU_UNTRUSTED_CERT, -- // Cert is valid but we already trust another cert for this host -- TOFU_FINGERPRINT_MISMATCH, --}; -- --enum tofu_action { -- TOFU_ASK, -- TOFU_FAIL, -- TOFU_TRUST_ONCE, -- TOFU_TRUST_ALWAYS, --}; -- --struct known_host { -- char *host, *fingerprint; -- time_t expires; -- int lineno; -- struct known_host *next; --}; -- --// Called when the user needs to be prompted to agree to trust an unknown --// certificate. Return true to trust this certificate. --typedef enum tofu_action (tofu_callback_t)(enum tofu_error error, -- const char *fingerprint, struct known_host *host, void *data); -- --struct gemini_tofu { -- char known_hosts_path[PATH_MAX+1]; -- struct known_host *known_hosts; -- int lineno; -- tofu_callback_t *callback; -- void *cb_data; --}; -- --void gemini_tofu_init(struct gemini_tofu *tofu, -- SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); --void gemini_tofu_finish(struct gemini_tofu *tofu); -- --#endif -diff --git a/include/url.h b/include/url.h -deleted file mode 100644 -index 155fd55740dbe47a498062713aca217ab259734f..0000000000000000000000000000000000000000 ---- a/include/url.h -+++ /dev/null -@@ -1,103 +0,0 @@ --#ifndef URLAPI_H --#define URLAPI_H --/*************************************************************************** -- * _ _ ____ _ -- * Project ___| | | | _ \| | -- * / __| | | | |_) | | -- * | (__| |_| | _ <| |___ -- * \___|\___/|_| \_\_____| -- * -- * Copyright (C) 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -- * -- * This software is licensed as described in the file COPYING, which -- * you should have received as part of this distribution. The terms -- * are also available at https://curl.haxx.se/docs/copyright.html. -- * -- * You may opt to use, copy, modify, merge, publish, distribute and/or sell -- * copies of the Software, and permit persons to whom the Software is -- * furnished to do so, under the terms of the COPYING file. -- * -- * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -- * KIND, either express or implied. -- * -- ***************************************************************************/ -- --/* the error codes for the URL API */ --typedef enum { -- CURLUE_OK, -- CURLUE_BAD_HANDLE, /* 1 */ -- CURLUE_BAD_PARTPOINTER, /* 2 */ -- CURLUE_MALFORMED_INPUT, /* 3 */ -- CURLUE_BAD_PORT_NUMBER, /* 4 */ -- CURLUE_UNSUPPORTED_SCHEME, /* 5 */ -- CURLUE_URLDECODE, /* 6 */ -- CURLUE_OUT_OF_MEMORY, /* 7 */ -- CURLUE_USER_NOT_ALLOWED, /* 8 */ -- CURLUE_UNKNOWN_PART, /* 9 */ -- CURLUE_NO_SCHEME, /* 10 */ -- CURLUE_NO_USER, /* 11 */ -- CURLUE_NO_PASSWORD, /* 12 */ -- CURLUE_NO_OPTIONS, /* 13 */ -- CURLUE_NO_HOST, /* 14 */ -- CURLUE_NO_PORT, /* 15 */ -- CURLUE_NO_QUERY, /* 16 */ -- CURLUE_NO_FRAGMENT /* 17 */ --} CURLUcode; -- --typedef enum { -- CURLUPART_URL, -- CURLUPART_SCHEME, -- CURLUPART_USER, -- CURLUPART_PASSWORD, -- CURLUPART_OPTIONS, -- CURLUPART_HOST, -- CURLUPART_PORT, -- CURLUPART_PATH, -- CURLUPART_QUERY, -- CURLUPART_FRAGMENT --} CURLUPart; -- --#define CURLU_PATH_AS_IS (1<<4) /* leave dot sequences */ --#define CURLU_DISALLOW_USER (1<<5) /* no user+password allowed */ --#define CURLU_URLDECODE (1<<6) /* URL decode on get */ --#define CURLU_URLENCODE (1<<7) /* URL encode on set */ --#define CURLU_APPENDQUERY (1<<8) /* append a form style part */ -- --typedef struct Curl_URL CURLU; -- --/* -- * curl_url() creates a new CURLU handle and returns a pointer to it. -- * Must be freed with curl_url_cleanup(). -- */ --struct Curl_URL *curl_url(void); -- --/* -- * curl_url_cleanup() frees the CURLU handle and related resources used for -- * the URL parsing. It will not free strings previously returned with the URL -- * API. -- */ --void curl_url_cleanup(struct Curl_URL *handle); -- --/* -- * curl_url_dup() duplicates a CURLU handle and returns a new copy. The new -- * handle must also be freed with curl_url_cleanup(). -- */ --struct Curl_URL *curl_url_dup(struct Curl_URL *in); -- --/* -- * curl_url_get() extracts a specific part of the URL from a CURLU -- * handle. Returns error code. The returned pointer MUST be freed with -- * free() afterwards. -- */ --CURLUcode curl_url_get(struct Curl_URL *handle, CURLUPart what, -- char **part, unsigned int flags); -- --/* -- * curl_url_set() sets a specific part of the URL in a CURLU handle. Returns -- * error code. The passed in string will be copied. Passing a NULL instead of -- * a part string, clears that part. -- */ --CURLUcode curl_url_set(struct Curl_URL *handle, CURLUPart what, -- const char *part, unsigned int flags); -- --#endif -diff --git a/src/client.c b/src/client.c -index 398e133b182af7f2fbe907be83cd5ecdc1b3a671..8b6b9e7a3c09656a5d59ed86977cb8b5c39541d4 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -9,8 +9,8 @@ #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> - #include <unistd.h> --#include "gmni.h" --#include "url.h" -+#include <gmni/gmni.h> -+#include <gmni/url.h> - - static enum gemini_result - gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options, -diff --git a/src/gmni.c b/src/gmni.c -index 61f41e29c324d980bd3a1f270e78649cdcec88e0..e8b25f9060c021bda099aa97fc2ec5657aeb2672 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -12,8 +12,8 @@ #include <sys/socket.h> - #include <sys/types.h> - #include <termios.h> - #include <unistd.h> --#include "gmni.h" --#include "tofu.h" -+#include <gmni/gmni.h> -+#include <gmni/tofu.h> - #include "util.h" - - static void -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 245ec85fa59fbcabde7e49c7d4d6e978aefa5f9a..2fba84e1f3c9e312ac53417d1b873158ceaf8c73 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -14,9 +14,9 @@ #include <sys/stat.h> - #include <sys/wait.h> - #include <termios.h> - #include <unistd.h> --#include "gmni.h" --#include "tofu.h" --#include "url.h" -+#include <gmni/gmni.h> -+#include <gmni/tofu.h> -+#include <gmni/url.h> - #include "util.h" - - struct link { -diff --git a/src/parser.c b/src/parser.c -index 579415150f842557b6faf4097a63aac9324928fb..ad2c0e6306872f8dde450063ba8815e2ee7e314a 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -5,7 +5,7 @@ #include <stdbool.h> - #include <stddef.h> - #include <stdlib.h> - #include <string.h> --#include "gmni.h" -+#include <gmni/gmni.h> - - void - gemini_parser_init(struct gemini_parser *p, BIO *f) -diff --git a/src/tofu.c b/src/tofu.c -index 48a627fe131996070d991bcc54bb7bcb40fddce4..863efc644a691370ee58ef0fb92291e9471adeb9 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -10,8 +10,8 @@ #include <openssl/x509v3.h> - #include <stdio.h> - #include <string.h> - #include <time.h> --#include "gmni.h" --#include "tofu.h" -+#include <gmni/gmni.h> -+#include <gmni/tofu.h> - #include "util.h" - - static int -diff --git a/src/url.c b/src/url.c -index dabf45f234e1e0957007b94ef12ef1125250dfea..47741e4ab1f91d336e5aba8117bde01094e370c3 100644 ---- a/src/url.c -+++ b/src/url.c -@@ -31,7 +31,7 @@ #include <stdlib.h> - #include <string.h> - #include <strings.h> - #include "escape.h" --#include "url.h" -+#include <gmni/url.h> - - /* Provided by gmni */ - static char * -diff --git a/src/util.c b/src/util.c -index 360c99ac95de9dcfce860610aeef85e8986e99c2..0a479af3ea734ce25d0806aa086e61bef6b8953b 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -7,7 +7,7 @@ #include <stdio.h> - #include <stdlib.h> - #include <string.h> - #include <sys/stat.h> --#include "gmni.h" -+#include <gmni/gmni.h> - #include "util.h" - - static void diff --git a/sources/cgmnlm.git/commits/144693a3d001a436abaa37f11b1c1c2bdf88c813.patch b/sources/cgmnlm.git/commits/144693a3d001a436abaa37f11b1c1c2bdf88c813.patch @@ -1,47 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 8841c4613e208e379c6cd768a65a9a3b631d3146..8fc92928ba0d8532a5f94f8e637d0b0aa025bc09 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -78,9 +78,13 @@ - static bool - set_url(struct browser *browser, char *new_url, struct history **history) - { -+ if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { -+ fprintf(stderr, "Error: invalid URL\n"); -+ return false; -+ } - if (history) { - struct history *next = calloc(1, sizeof(struct history)); -- next->url = strdup(new_url); -+ curl_url_get(browser->url, CURLUPART_URL, &next->url, 0); - next->prev = *history; - if (*history) { - if ((*history)->next) { -@@ -89,10 +93,6 @@ } - (*history)->next = next; - } - *history = next; -- } -- if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { -- fprintf(stderr, "Error: invalid URL\n"); -- return false; - } - return true; - } -@@ -153,8 +153,14 @@ result = PROMPT_ANSWERED; - } - goto exit_re; - case 'n': -- result = PROMPT_NEXT; -- goto exit_re; -+ if (browser->searching) { -+ result = PROMPT_NEXT; -+ goto exit_re; -+ } else { -+ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); -+ result = PROMPT_AGAIN; -+ goto exit; -+ } - case '?': - fprintf(browser->tty, "%s", help_msg); - result = PROMPT_AGAIN; diff --git a/sources/cgmnlm.git/commits/174fbd5d09bc13212fc1edc0cd1d3fa2400a8b7e.patch b/sources/cgmnlm.git/commits/174fbd5d09bc13212fc1edc0cd1d3fa2400a8b7e.patch @@ -1,365 +0,0 @@ -diff --git a/include/tofu.h b/include/tofu.h -index 29aa9bc21567868cafb25a09dbc25ea0685ab01c..a88167ba0fb6606b2b170e5005c55131f1861972 100644 ---- a/include/tofu.h -+++ b/include/tofu.h -@@ -44,5 +44,6 @@ }; - - void gemini_tofu_init(struct gemini_tofu *tofu, - SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); -+void gemini_tofu_finish(struct gemini_tofu *tofu); - - #endif -diff --git a/src/client.c b/src/client.c -index 2d39f56cb8e45f7b708e5a5dea1ec6fa84e188c9..bb508e04c8d41fe12aa99ae42b8fa30d4f3a9a2e 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -118,11 +118,14 @@ goto cleanup; - } else { - if (strcmp(scheme, "gemini") != 0) { - res = GEMINI_ERR_NOT_GEMINI; -+ free(scheme); - goto cleanup; - } -+ free(scheme); - } - if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) { - res = GEMINI_ERR_INVALID_URL; -+ free(host); - goto cleanup; - } - -@@ -139,6 +142,7 @@ int r; - BIO *sbio = BIO_new(BIO_f_ssl()); - res = gemini_connect(uri, options, resp, &resp->fd); - if (res != GEMINI_OK) { -+ free(host); - goto cleanup; - } - -@@ -146,11 +150,14 @@ resp->ssl = SSL_new(resp->ssl_ctx); - assert(resp->ssl); - SSL_set_connect_state(resp->ssl); - if ((r = SSL_set1_host(resp->ssl, host)) != 1) { -+ free(host); - goto ssl_error; - } - if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) { -+ free(host); - goto ssl_error; - } -+ free(host); - if ((r = SSL_set_fd(resp->ssl, resp->fd)) != 1) { - goto ssl_error; - } -@@ -235,15 +242,16 @@ resp->fd = -1; - } - - if (resp->bio) { -- BIO_free(BIO_pop(resp->bio)); // ssl bio -- BIO_free(resp->bio); // buffered bio -+ BIO_free_all(resp->bio); - resp->bio = NULL; - } - - if (resp->ssl) { - SSL_free(resp->ssl); - } -- SSL_CTX_free(resp->ssl_ctx); -+ if (resp->ssl_ctx) { -+ SSL_CTX_free(resp->ssl_ctx); -+ } - free(resp->meta); - - resp->ssl = NULL; -diff --git a/src/gmni.c b/src/gmni.c -index c13e0cd55623f557a8dc676d69aea54661b11faf..4af98fbbe70b09ce5a7fce46863438b0487d8738 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -336,6 +336,8 @@ next: - gemini_response_finish(&resp); - } - -+ SSL_CTX_free(opts.ssl_ctx); - free(url); -+ gemini_tofu_finish(&cfg.tofu); - return ret; - } -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 5d45ffe7372f71ab32461bf71a590cc494269e96..a2717fb84c76fd50a000ceda540af54c60396252 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -83,6 +83,7 @@ if (!history) { - return; - } - history_free(history->next); -+ free(history->url); - free(history); - } - -@@ -92,6 +93,9 @@ { - if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { - fprintf(stderr, "Error: invalid URL\n"); - return false; -+ } -+ if (browser->plain_url != NULL) { -+ free(browser->plain_url); - } - curl_url_get(browser->url, CURLUPART_URL, &browser->plain_url, 0); - if (history) { -@@ -130,17 +134,19 @@ - static void - save_bookmark(struct browser *browser) - { -- const char *path_fmt = get_data_pathfmt(); -+ char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - if (mkdirs(dirname(path), 0755) != 0) { - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); - fprintf(stderr, "Error creating directory %s: %s\n", - dirname(path), strerror(errno)); - return; - } - - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); - FILE *f = fopen(path, "a"); - if (!f) { - fprintf(stderr, "Error opening %s for writing: %s\n", -@@ -150,7 +156,7 @@ } - - char *title = browser->page_title; - if (title) { -- title = trim_ws(browser->page_title); -+ title = trim_ws(strdup(browser->page_title)); - } - - fprintf(f, "=> %s%s%s\n", browser->plain_url, -@@ -159,6 +165,9 @@ fclose(f); - - fprintf(browser->tty, "Bookmark saved: %s\n", - title ? title : browser->plain_url); -+ if (title != NULL) { -+ free(title); -+ } - } - - static void -@@ -411,12 +420,14 @@ col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED_BEGIN: -+ gemini_token_finish(&tok); -+ /* fallthrough */ - case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: - col += fprintf(out, "` "); - if (text == NULL) { -- text = tok.text; -+ text = tok.preformatted; - } - break; - case GEMINI_HEADING: -@@ -484,6 +495,9 @@ if (!text[0]) { - text = NULL; - } - } -+ if (text == NULL) { -+ gemini_token_finish(&tok); -+ } - - while (col >= ws.ws_col) { - col -= ws.ws_col; -@@ -510,8 +524,16 @@ case PROMPT_MORE: - break; - case PROMPT_QUIT: - browser->running = false; -+ if (text != NULL) { -+ gemini_token_finish(&tok); -+ } -+ gemini_parser_finish(&p); - return true; - case PROMPT_ANSWERED: -+ if (text != NULL) { -+ gemini_token_finish(&tok); -+ } -+ gemini_parser_finish(&p); - return true; - case PROMPT_NEXT: - searching = true; -@@ -523,6 +545,7 @@ row = col = 0; - } - } - -+ gemini_token_finish(&tok); - gemini_parser_finish(&p); - return false; - } -@@ -617,6 +640,7 @@ CURLUcode uc = curl_url_get(browser->url, - CURLUPART_SCHEME, &scheme, 0); - assert(uc == CURLUE_OK); // Invariant - if (strcmp(scheme, "file") == 0) { -+ free(scheme); - requesting = false; - - char *path; -@@ -630,6 +654,7 @@ - FILE *fp = fopen(path, "r"); - if (!fp) { - resp->status = GEMINI_STATUS_NOT_FOUND; -+ free(path); - break; - } - -@@ -643,9 +668,14 @@ resp->meta = strdup("text/plain"); - } else { - resp->meta = strdup("application/x-octet-stream"); - } -+ free(path); - resp->status = GEMINI_STATUS_SUCCESS; -+ resp->fd = -1; -+ resp->ssl = NULL; -+ resp->ssl_ctx = NULL; - return display_response(browser, resp); - } -+ free(scheme); - - enum gemini_result res = gemini_request(browser->plain_url, - &browser->opts, resp); -@@ -672,7 +702,7 @@ break; - case GEMINI_STATUS_CLASS_REDIRECT: - if (++nredir >= 5) { - requesting = false; -- fprintf(stderr, "Error: maximum redirects (5) exceeded"); -+ fprintf(stderr, "Error: maximum redirects (5) exceeded\n"); - break; - } - fprintf(stderr, "Following redirect to %s\n", resp->meta); -@@ -816,6 +846,7 @@ browser.unicode = false; - break; - default: - fprintf(stderr, "fatal: unknown flag %c\n", c); -+ curl_url_cleanup(browser.url); - return 1; - } - } -@@ -841,6 +872,7 @@ while (browser.running) { - static char prompt[4096]; - if (do_requests(&browser, &resp)) { - // Skip prompts -+ gemini_response_finish(&resp); - goto next; - } - -@@ -880,8 +912,16 @@ } - browser.links = NULL; - } - -- history_free(browser.history); -+ gemini_tofu_finish(&browser.tofu); -+ struct history *hist = browser.history; -+ while (hist && hist->prev) { -+ hist = hist->prev; -+ } -+ history_free(hist); - SSL_CTX_free(browser.opts.ssl_ctx); - curl_url_cleanup(browser.url); -+ free(browser.page_title); -+ free(browser.plain_url); -+ fclose(browser.tty); - return 0; - } -diff --git a/src/parser.c b/src/parser.c -index 5b0f01399d934f593cdf987e096dd2cfb134d114..2f78e4641c08b86f5dc50cc9776ea80ec7f9aead 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -35,15 +35,14 @@ { - memset(tok, 0, sizeof(*tok)); - - int eof = 0; -- while (!strstr(p->buf, "\n")) { -- if (p->bufln == p->bufsz) { -+ while (!strchr(p->buf, '\n')) { -+ while (p->bufln >= p->bufsz - 1) { - p->bufsz *= 2; -- char *buf = realloc(p->buf, p->bufsz); -- assert(buf); -- p->buf = buf; -+ p->buf = realloc(p->buf, p->bufsz); -+ assert(p->buf); - } - -- int n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln); -+ ssize_t n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln - 1); - if (n == -1) { - return -1; - } else if (n == 0) { -@@ -55,7 +54,7 @@ p->buf[p->bufln] = 0; - } - - char *end; -- if ((end = strstr(p->buf, "\n")) != NULL) { -+ if ((end = strchr(p->buf, '\n')) != NULL) { - *end = 0; - } - -diff --git a/src/tofu.c b/src/tofu.c -index 354e211c856451bdea120bbb1aed5917099eb285..45e1275d568cfcb4aacfe7f86788b76fff97916b 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -150,7 +150,7 @@ {.var = "GMNIDATA", .path = "/%s"}, - {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, - {.var = "HOME", .path = "/.local/share/gmni/%s"} - }; -- const char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); -+ char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); - snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); - -@@ -164,6 +164,7 @@ } - - snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); -+ free(path_fmt); - - tofu->callback = cb; - tofu->cb_data = cb_data; -@@ -175,6 +176,7 @@ return; - } - size_t n = 0; - char *line = NULL; -+ tofu->known_hosts = NULL; - while (getline(&line, &n, f) != -1) { - struct known_host *host = calloc(1, sizeof(struct known_host)); - char *tok = strtok(line, " "); -@@ -184,6 +186,7 @@ - tok = strtok(NULL, " "); - assert(tok); - if (strcmp(tok, "SHA-512") != 0) { -+ free(host->host); - free(host); - continue; - } -@@ -198,5 +201,20 @@ host->expires = strtoul(tok, NULL, 10); - - host->next = tofu->known_hosts; - tofu->known_hosts = host; -+ } -+ free(line); -+ fclose(f); -+} -+ -+void -+gemini_tofu_finish(struct gemini_tofu *tofu) -+{ -+ struct known_host *host = tofu->known_hosts; -+ while (host) { -+ struct known_host *tmp = host; -+ host = host->next; -+ free(tmp->host); -+ free(tmp->fingerprint); -+ free(tmp); - } - } diff --git a/sources/cgmnlm.git/commits/1808e6cd1880d3c08abb0ddfa19044afada925dd.patch b/sources/cgmnlm.git/commits/1808e6cd1880d3c08abb0ddfa19044afada925dd.patch @@ -1,24 +0,0 @@ -diff --git a/src/url.c b/src/url.c -index 47e31b5fcbeeecb5bc3962585311b20e3518bb73..dabf45f234e1e0957007b94ef12ef1125250dfea 100644 ---- a/src/url.c -+++ b/src/url.c -@@ -1361,19 +1361,6 @@ bool free_part = false; - char *enc = malloc(nalloc * 3 + 1); /* for worst case! */ - if(!enc) - return CURLUE_OUT_OF_MEMORY; -- if(plusencode) { -- /* space to plus */ -- i = part; -- for(o = enc; *i; ++o, ++i) -- *o = (*i == ' ') ? '+' : *i; -- *o = 0; /* zero terminate */ -- part = strdup(enc); -- if(!part) { -- free(enc); -- return CURLUE_OUT_OF_MEMORY; -- } -- free_part = true; -- } - for(i = part, o = enc; *i; i++) { - if(Curl_isunreserved(*i) || - ((*i == '/') && urlskipslash) || diff --git a/sources/cgmnlm.git/commits/18ead2644a8c525d1d3bbc729d9ccd9aa7e0d63c.patch b/sources/cgmnlm.git/commits/18ead2644a8c525d1d3bbc729d9ccd9aa7e0d63c.patch @@ -1,93 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 9efe3e231d445d55ea43d444a72eaa79f595ac76..37b4db277d8fcd961eb1746294724783e638109c 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -87,11 +87,11 @@ "<Enter>\t\tread more lines (if available)\n" - "<url>\t\tgo to url\n" - "[N]\t\tFollow Nth link (where N is a number)\n" - "p[N]\t\tPrint URL of Nth link (where N is a number)\n" -- "e\t\tSend current URL of browser to external default program\n" -- "e[N]\t\tSend URL of Nth link in external default program\n" -+ "e[N]\t\tSend URL of Nth link or current URL when N is ommited to external default program\n" - "t[N]\t\tDownload content of Nth link to a temporary file\n" - "b[N]\t\tJump back N entries in history, N is optional, default 1\n" - "f[N]\t\tJump forward N entries in history, N is optional, default 1\n" -+ "u\t\tone path element up\n" - "H\t\tView all page history\n" - "m\t\tSave bookmark\n" - "M\t\tBrowse bookmarks\n" -@@ -623,6 +623,7 @@ goto exit; - } - in[n - 1] = 0; // Remove LF - -+ char url[1024] = {0}; - int r; - switch (in[0]) { - case '\0': -@@ -664,6 +665,22 @@ } - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; -+ case 'u':; -+ int keep = 0; -+ int len = strlen(browser->plain_url); -+ for (int i=0; i<len; i++) -+ { -+ // ignore trailing / on uri path -+ if (browser->plain_url[i] == '/' && i != len-1) { -+ keep = i; -+ } -+ } -+ if (keep > 9) { -+ strncpy(url , browser->plain_url, keep+1); -+ set_url(browser, url, &browser->history); -+ } -+ result = PROMPT_ANSWERED; -+ goto exit; - case 'H': - if (in[1]) break; - struct history *cur = browser->history; -@@ -723,9 +740,8 @@ case 'p': - case 't': - if (!in[1]) { - if (in[0] == 'e') { -- char xdgopen[4096]; -- snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", browser->plain_url); -- if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); -+ snprintf(url, sizeof(url), "xdg-open %s", browser->plain_url); -+ if ( !system(url) ) fprintf(browser->tty, "Link send to xdg-open\n"); - goto exit; - } else { - break; -@@ -743,13 +759,11 @@ fprintf(stderr, "Error: no such link.\n"); - } else { - fprintf(browser->tty, "=> %s\n", link->url); - if (in[0] == 'e') { -- char xdgopen[4096]; -- snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", link->url); -- if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); -+ snprintf(url, sizeof(url), "xdg-open %s", link->url); -+ if ( !system(url) ) fprintf(browser->tty, "Link send to xdg-open\n"); - } - if (in[0] == 't') { - struct gemini_response resp; -- char url[1024] = {0}; - strncpy(&url[0], browser->plain_url, sizeof(link->url)-1); - set_url(browser, link->url, &browser->history); - // XXX: may affect history, do we care? -@@ -785,7 +799,6 @@ goto exit; - case 'd': - if (in[1] != '\0' && !isspace(in[1])) break; - struct gemini_response resp; -- char url[1024] = {0}; - strncpy(&url[0], browser->plain_url, sizeof(url)-1); - // XXX: may affect history, do we care? - enum gemini_result res = do_requests(browser, &resp); -@@ -831,7 +844,6 @@ - if (!link) { - fprintf(stderr, "Error: no such link.\n"); - } else if (endptr[0] == '|') { -- char url[1024] = {0}; - struct gemini_response resp; - strncpy(url, browser->plain_url, sizeof(url) - 1); - set_url(browser, link->url, &browser->history); diff --git a/sources/cgmnlm.git/commits/1a747cb6c2765ee818506c886bad4ed36b2b9d51.patch b/sources/cgmnlm.git/commits/1a747cb6c2765ee818506c886bad4ed36b2b9d51.patch @@ -1,47 +0,0 @@ -diff --git a/Makefile b/Makefile -index c5bc1ed76a0a61d70c2bfe3506a868f0d7eace5e..9d17d35eb67ee4b86a2d9219c3f1fce98695a521 100644 ---- a/Makefile -+++ b/Makefile -@@ -54,9 +54,8 @@ - distclean: clean - @rm -rf "$(OUTDIR)" - --install: all -+install: all install_docs - mkdir -p $(BINDIR) -- mkdir -p $(MANDIR)/man1 - install -Dm755 gmni $(BINDIR)/gmni - install -Dm755 gmnlm $(BINDIR)/gmnlm - install -Dm755 libgmni.a $(LIBDIR)/libgmni.a -@@ -64,8 +63,6 @@ install -Dm644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h - install -Dm644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h - install -Dm644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h - install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc -- install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 -- install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 - - uninstall: - rm -f $(BINDIR)/gmni -diff --git a/config.sh b/config.sh -index 5bd34bb30dec02422855edbf37817f8276840211..ca5444c73b47daa204d9dd17a3949147385dedec 100644 ---- a/config.sh -+++ b/config.sh -@@ -125,6 +125,10 @@ if scdoc -v >/dev/null 2>&1 - then - echo yes - all="$all docs" -+ install_docs=" -+ mkdir -p \$(MANDIR)/man1 -+ install -Dm644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 -+ install -Dm644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" - else - echo no - fi -@@ -148,6 +152,7 @@ CFLAGS+=-DPREFIX='"\$(PREFIX)"' - CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' - - all: ${all} -+ install_docs: ${install_docs} - EOF - - for target in $(printf '%s\n' $all | tr '.' '_') diff --git a/sources/cgmnlm.git/commits/1c9a6e6a35448b76063f16b0f6aaaf8d43ebee9a.patch b/sources/cgmnlm.git/commits/1c9a6e6a35448b76063f16b0f6aaaf8d43ebee9a.patch @@ -1,32 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index cbc04be66573d4980e24f00761c54d4ac8a1a88f..e80b38b6521ef0eccd5629a1f0b16d2406d8cac1 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -278,8 +278,10 @@ - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[Enter]: read more; [N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" -- "(more) => ", resp->meta, browser->plain_url); -+ "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit\n" -+ "(more) => ", resp->meta, browser->plain_url, -+ browser->history->prev ? "[b]ack; " : "", -+ browser->history->next ? "[f]orward; " : ""); - enum prompt_result result = PROMPT_AGAIN; - while (result == PROMPT_AGAIN) { - result = do_prompts(prompt, browser); -@@ -485,10 +487,12 @@ goto next; - } - - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" -+ "[N]: follow Nth link; %s%s[q]uit\n" - "=> ", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", -- browser.plain_url); -+ browser.plain_url, -+ browser.history->prev ? "[b]ack; " : "", -+ browser.history->next ? "[f]orward; " : ""); - gemini_response_finish(&resp); - - enum prompt_result result = PROMPT_AGAIN; diff --git a/sources/cgmnlm.git/commits/1cfe0e794936cc51b9306327634a35f1c443643f.patch b/sources/cgmnlm.git/commits/1cfe0e794936cc51b9306327634a35f1c443643f.patch @@ -1,13 +0,0 @@ -diff --git a/config.sh b/config.sh -index 8d586cfde54fa3c3bc369ff0156ad5d55a79f383..424cbda346869c7476859d55a600c414c86bbb46 100644 ---- a/config.sh -+++ b/config.sh -@@ -73,7 +73,7 @@ *-Werror*) - werror="-Werror" - ;; - esac -- if $CC $werror "$@" -o /dev/null "$outdir"/check.c >/dev/null 2>&1 -+ if $CC $werror "$@" -o "$outdir"/check "$outdir"/check.c >/dev/null 2>&1 - then - append_cflags "$@" - else diff --git a/sources/cgmnlm.git/commits/1da4ff928a44f590e2c72cda1dcb4b097845cbc3.patch b/sources/cgmnlm.git/commits/1da4ff928a44f590e2c72cda1dcb4b097845cbc3.patch @@ -1,12 +0,0 @@ -diff --git a/README.md b/README.md -index e1b424367e49daad233793f663f6340849eb4286..9564df898ce60d9895b818f95ae020c39edb83af 100644 ---- a/README.md -+++ b/README.md -@@ -18,6 +18,7 @@ - It includes the following modifications: - - default 4 char indenting - - e[N] command to open a link in default external program (requires `xdg-open`) -+- t[N] command to download the content behind a link to a temporary file - - colored headings & links - - The actual colors used depend on your terminal palette: diff --git a/sources/cgmnlm.git/commits/207a72012ef69de654a78e18d28182ecde1326e2.patch b/sources/cgmnlm.git/commits/207a72012ef69de654a78e18d28182ecde1326e2.patch @@ -1,92 +0,0 @@ -diff --git a/Makefile b/Makefile -index 0070b703ea0af36743878d2264259bc04edb6b70..d6b576c49053adcc21557a8d88137101deeb5846 100644 ---- a/Makefile -+++ b/Makefile -@@ -4,11 +4,11 @@ OUTDIR=.build - include $(OUTDIR)/config.mk - include $(OUTDIR)/cppcache - --gmnic: $(gmnic_objects) -+gmni: $(gmni_objects) - @printf 'CCLD\t$@\n' -- @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnic_objects) -+ @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmni_objects) - --doc/gmnic.1: doc/gmnic.scd -+doc/gmni.1: doc/gmni.scd - - .SUFFIXES: .c .o .scd .1 - -@@ -23,10 +23,10 @@ .scd.1: - @printf 'SCDOC\t$@\n' - @$(SCDOC) < $< > $@ - --docs: doc/gmnic.1 -+docs: doc/gmni.1 - - clean: -- @rm -f gmnic -+ @rm -f gmni doc/gmni.1 - - distclean: clean - @rm -rf "$(OUTDIR)" -diff --git a/configure b/configure -index 680b57fc9e319c2707e0cc2ded0bc22597544d78..407886859d8ca0b9cbcb085ecef89d004c68cde4 100755 ---- a/configure -+++ b/configure -@@ -3,13 +3,13 @@ srcdir=${SRCDIR:-$(dirname "$0")} - eval ". $srcdir/config.sh" - - gmni() { -- genrules gmnic \ -+ genrules gmni \ - src/client.c \ - src/escape.c \ -- src/gmnic.c \ -+ src/gmni.c \ - src/url.c - } - --all="gmnic" -+all="gmni" - - run_configure -diff --git a/doc/gmnic.scd b/doc/gmni.scd -rename from doc/gmnic.scd -rename to doc/gmni.scd -index 9eec29cfddf50c2678ef7ae080275fbfbe3fcaa1..0d84d4d974f2c38b426f6ae85d228cdd4847cbda 100644 ---- a/doc/gmnic.scd -+++ b/doc/gmni.scd -@@ -1,16 +1,16 @@ --gmnic(1) -+gmni(1) - - # NAME - --gmnic - Gemini client -+gmni - Gemini client - - # SYNPOSIS - --*gmnic* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ -+*gmni* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ - - # DESCRIPTION - --*gmnic* executes a gemini request and, if successful, prints the response body -+*gmni* executes a gemini request and, if successful, prints the response body - to stdout. - - If an error is returned, information is printed to stderr and the process exits -@@ -45,7 +45,7 @@ accept a password, append ":" to the path and it will be intepreted as - an empty password. - - *-l* -- For *text/\** responses, gmnic normally adds a line feed if stdout is a -+ For *text/\** responses, *gmni* normally adds a line feed if stdout is a - TTY and the response body does not include one. This flag suppresses - this behavior. - -diff --git a/src/gmnic.c b/src/gmni.c -rename from src/gmnic.c -rename to src/gmni.c diff --git a/sources/cgmnlm.git/commits/211b8c3dd36a950132efa5ba4e2f0172a42bb6bc.patch b/sources/cgmnlm.git/commits/211b8c3dd36a950132efa5ba4e2f0172a42bb6bc.patch @@ -1,66 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 15256bbd0dcd1c6899e8e91ba5228baf93ce289f..2c2b67e0e56d2e6be04bba6cee96cb4a24ff3b51 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -192,6 +192,40 @@ } - } - - static char * -+get_input(const struct gemini_response *resp, FILE *source) -+{ -+ int r = 0; -+ struct termios attrs; -+ bool tty = fileno(source) != -1 && isatty(fileno(source)); -+ char *input = NULL; -+ if (tty) { -+ fprintf(stderr, "%s: ", resp->meta); -+ if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { -+ r = tcgetattr(fileno(source), &attrs); -+ struct termios new_attrs; -+ r = tcgetattr(fileno(source), &new_attrs); -+ if (r != -1) { -+ new_attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &new_attrs); -+ } -+ } -+ } -+ size_t s = 0; -+ ssize_t n = getline(&input, &s, source); -+ if (n == -1) { -+ fprintf(stderr, "Error reading input: %s\n", -+ feof(source) ? "EOF" : strerror(ferror(source))); -+ return NULL; -+ } -+ input[n - 1] = '\0'; // Drop LF -+ if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { -+ attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &attrs); -+ } -+ return input; -+} -+ -+static char * - do_requests(struct browser *browser, struct gemini_response *resp) - { - char *plain_url; -@@ -210,9 +244,19 @@ requesting = false; - break; - } - -+ char *input; - switch (gemini_response_class(resp->status)) { - case GEMINI_STATUS_CLASS_INPUT: -- assert(0); // TODO -+ input = get_input(resp, browser->tty); -+ if (!input) { -+ requesting = false; -+ break; -+ } -+ -+ char *new_url = gemini_input_url(plain_url, input); -+ assert(new_url); -+ set_url(browser, new_url, NULL); -+ break; - case GEMINI_STATUS_CLASS_REDIRECT: - if (++nredir >= 5) { - requesting = false; diff --git a/sources/cgmnlm.git/commits/22a28fa92755b49254cac144894be1fdb917a6a3.patch b/sources/cgmnlm.git/commits/22a28fa92755b49254cac144894be1fdb917a6a3.patch @@ -1,114 +0,0 @@ -diff --git a/README.md b/README.md -index d68381569be5736bac9887345b5c053e69393b5a..3cb5f020cab021c3b4ffd7eb98908c76c352e90d 100644 ---- a/README.md -+++ b/README.md -@@ -27,6 +27,8 @@ - default 4 char indenting - - k command to remove the bookmark for the current page - - e[N] command to open a link in default external program (requires `xdg-open`) - - t[N] command to download the content behind a link to a temporary file -+- b & f commands to navigate history can jump multiple entries at once -+- colored headings & links - - The actual colors used depend on your terminal palette: - - heading 1: light red -@@ -39,11 +41,13 @@ - preformatted text: light gray - - Besides this rendering adjustments i'll try to keep track of upstream changes or send patches to upstream. - --## Dependencies: -+## Usage - --- A POSIX-like system and a C11 compiler --- OpenSSL --- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional, build only) -+See `gmni(1)`, `cgmnlm(1)`. -+ -+# Installation -+ -+* ArchLinux and derivates: https://aur.archlinux.org/packages/cgmnlm-git/ - - ## Compiling - -@@ -54,6 +58,9 @@ $ make - # make install - ``` - --## Usage -+### Dependencies: -+ -+- A POSIX-like system and a C11 compiler -+- OpenSSL -+- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) - --See `gmni(1)`, `cgmnlm(1)`. -diff --git a/src/gmnlm.c b/src/gmnlm.c -index cc429cdfe6d482e549afee2bd9b05520985e92f9..eaa34c055cbb92564ebd279fcf06189dc577360d 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -84,8 +84,8 @@ "[N]\t\tFollow Nth link (where N is a number)\n" - "p[N]\t\tPrint URL of Nth link (where N is a number)\n" - "e[N]\t\tSend URL of Nth link in external default program\n" - "t[N]\t\tDownload content of Nth link to a temporary file\n" -- "b\t\tBack (in the page history)\n" -- "f\t\tForward (in the page history)\n" -+ "b[N]\t\tJump back N entries in history, N is optional, default 1\n" -+ "f[N]\t\tJump forward N entries in history, N is optional, default 1\n" - "H\t\tView all page history\n" - "m\t\tSave bookmark\n" - "M\t\tBrowse bookmarks\n" -@@ -546,6 +546,8 @@ - struct link *link = browser->links; - char *endptr = NULL; - int linksel = 0; -+ int historyhops = 1; -+ - char *in = NULL; - size_t l = 0; - ssize_t n = getline(&in, &l, browser->tty); -@@ -554,7 +556,7 @@ result = PROMPT_QUIT; - goto exit; - } - in[n - 1] = 0; // Remove LF -- -+ - int r; - switch (in[0]) { - case '\0': -@@ -565,25 +567,24 @@ if (in[1]) break; - result = PROMPT_QUIT; - goto exit; - case 'b': -- if (in[1]) break; -- if (!browser->history->prev) { -- fprintf(stderr, "At beginning of history\n"); -- result = PROMPT_AGAIN; -- goto exit; -+ if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); -+ while (historyhops > 0) { -+ if (browser->history->prev) { -+ browser->history = browser->history->prev; -+ } -+ historyhops--; - } -- if (in[1]) break; -- browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; - case 'f': -- if (in[1]) break; -- if (!browser->history->next) { -- fprintf(stderr, "At end of history\n"); -- result = PROMPT_AGAIN; -- goto exit; -+ if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); -+ while (historyhops > 0) { -+ if (browser->history->next) { -+ browser->history = browser->history->next; -+ } -+ historyhops--; - } -- browser->history = browser->history->next; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; diff --git a/sources/cgmnlm.git/commits/262ccc2005617eb633f5b3ba434ee26f8000e0a8.patch b/sources/cgmnlm.git/commits/262ccc2005617eb633f5b3ba434ee26f8000e0a8.patch @@ -1,23 +0,0 @@ -diff --git a/src/gmnic.c b/src/gmnic.c -index fd6add8a61546a662a3fe7bb4f3beb9848a19452..2a06a180ebd5478e25b97cacea8468c86d11df00 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -17,7 +17,7 @@ static void - usage(char *argv_0) - { - fprintf(stderr, -- "usage: %s [-LI] [-C cert] [-d input] gemini://...\n", -+ "usage: %s [-46lLiIN] [-C cert] [-d input] [-D path] gemini://...\n", - argv_0); - } - -@@ -209,7 +209,8 @@ w += x; - } - } - if (strncmp(resp.meta, "text/", 5) == 0 -- && linefeed && last != '\n') { -+ && linefeed && last != '\n' -+ && isatty(STDOUT_FILENO)) { - printf("\n"); - } - break; diff --git a/sources/cgmnlm.git/commits/26666e7838fd40ca7d6f20af7e0cb554ff8bb0f0.patch b/sources/cgmnlm.git/commits/26666e7838fd40ca7d6f20af7e0cb554ff8bb0f0.patch @@ -1,12 +0,0 @@ -diff --git a/README.md b/README.md -index 3cb5f020cab021c3b4ffd7eb98908c76c352e90d..3c999cee8bbcb5b0bfbc6732f9ecea0ba231d043 100644 ---- a/README.md -+++ b/README.md -@@ -28,7 +28,6 @@ - k command to remove the bookmark for the current page - - e[N] command to open a link in default external program (requires `xdg-open`) - - t[N] command to download the content behind a link to a temporary file - - b & f commands to navigate history can jump multiple entries at once --- colored headings & links - - The actual colors used depend on your terminal palette: - - heading 1: light red diff --git a/sources/cgmnlm.git/commits/28283bda98accf122b6424ac611fd4ff25dedbc9.patch b/sources/cgmnlm.git/commits/28283bda98accf122b6424ac611fd4ff25dedbc9.patch @@ -1,12 +0,0 @@ -diff --git a/src/parser.c b/src/parser.c -index eb9aa5ec3d1da4a222e9ec0c76ad19ba004b9a2c..b9db3d2000f1176df4a21300f7b806ed6a5ded75 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -52,7 +52,6 @@ p->bufln += n; - p->buf[p->bufln] = 0; - } - -- // TODO: Collapse multi-line text for the user-agent to wrap - char *end; - if ((end = strstr(p->buf, "\n")) != NULL) { - *end = 0; diff --git a/sources/cgmnlm.git/commits/2e593cd48bd5e1f90fcbb54b83955752cd392466.patch b/sources/cgmnlm.git/commits/2e593cd48bd5e1f90fcbb54b83955752cd392466.patch @@ -1,22 +0,0 @@ -diff --git a/config.sh b/config.sh -index 70c4489bf5fd48fc0eb636bf739a5d82dcff8663..ef533ec79238744458fb2e69b95e3d34333c0e5a 100644 ---- a/config.sh -+++ b/config.sh -@@ -5,7 +5,6 @@ AS=${AS:-as} - CC=${CC:-cc} - CFLAGS=${CFLAGS:-} - LD=${LD:-ld} --LIBSSL= - - for arg - do -@@ -13,9 +12,6 @@ # TODO: Add args for install directories - case "$arg" in - --prefix=*) - PREFIX=${arg#*=} -- ;; -- --with-libssl=*) -- LIBSSL=${arg#*=} - ;; - esac - done diff --git a/sources/cgmnlm.git/commits/2e9d3c0bab8e7df635a8f0968f04fe9b1e2d979c.patch b/sources/cgmnlm.git/commits/2e9d3c0bab8e7df635a8f0968f04fe9b1e2d979c.patch @@ -1,26 +0,0 @@ -diff --git a/README.md b/README.md -index ce71012d402a0c975c73f6d4acd2922f32c16cbc..c269ec775d39d0c26445cfd39358829aae06b575 100644 ---- a/README.md -+++ b/README.md -@@ -5,13 +5,19 @@ - - A CLI utility (like curl): gmni - - A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm - --[![Screenshot of the line-mode browser](https://l.sr.ht/7kaA.png)](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) -- - Dependencies: - - - A POSIX-like system and a C11 compiler - - OpenSSL - - [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) -+ -+Features: -+ -+- Page history -+- Regex searches -+- Bookmarks -+ -+[![Screenshot of the line-mode browser](https://l.sr.ht/7kaA.png)](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) - - ## Compiling - diff --git a/sources/cgmnlm.git/commits/30660fc160a15504274d40d4a5ec1b31539f8c2f.patch b/sources/cgmnlm.git/commits/30660fc160a15504274d40d4a5ec1b31539f8c2f.patch @@ -1,18 +0,0 @@ -diff --git a/Makefile b/Makefile -index 12ff4b45e4b043ae5c93629b502e42e8396b2b24..4dc668c893cfeb03020f992bf92c7ae46953d299 100644 ---- a/Makefile -+++ b/Makefile -@@ -6,11 +6,11 @@ include $(OUTDIR)/cppcache - - gmni: $(gmni_objects) - @printf 'CCLD\t$@\n' -- @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmni_objects) -+ $(CC) $(LDFLAGS) -o $@ $(gmni_objects) $(LIBS) - - gmnlm: $(gmnlm_objects) - @printf 'CCLD\t$@\n' -- @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnlm_objects) -+ @$(CC) $(LDFLAGS) -o $@ $(gmnlm_objects) $(LIBS) - - doc/gmni.1: doc/gmni.scd - doc/gmnlm.1: doc/gmnlm.scd diff --git a/sources/cgmnlm.git/commits/320676ca5bc1980c96f5e4bc14240a741be8f3be.patch b/sources/cgmnlm.git/commits/320676ca5bc1980c96f5e4bc14240a741be8f3be.patch @@ -1,30 +0,0 @@ -diff --git a/.gitignore b/.gitignore -index d622c7ceba259e04d4a2e8c2ee4c91de45e13187..66d40426a4b715ec6bceb934219dff612ea2f011 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -1,7 +1,7 @@ - .build - build - /gmni --gmnlm -+cgmnlm - *.1 - *.o - *.a -diff --git a/cgmnlm b/cgmnlm -deleted file mode 100755 -index a7237df4a6c75847b8bfaee4dc4d1df687975936..0000000000000000000000000000000000000000 -Binary files a/cgmnlm and /dev/null differ -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index cb0bc10b924eb6afa057d41158815b133fd9372f..60b275d1f8b8425206dd7000a221d60c7473ddff 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -897,7 +897,7 @@ char *end = NULL; - if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) { - *end = 0; - } -- snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -+ snprintf(prompt, sizeof(prompt), "%s at %s\n" - "[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "(more) => ", resp->meta, browser->plain_url, - browser->searching ? "[n]ext result; " : "", diff --git a/sources/cgmnlm.git/commits/3270a74590d4bfbbc9ae1fdc4c1d36eed943844f.patch b/sources/cgmnlm.git/commits/3270a74590d4bfbbc9ae1fdc4c1d36eed943844f.patch @@ -1,30 +0,0 @@ -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index 60b275d1f8b8425206dd7000a221d60c7473ddff..38ac821cff06602aba88c5e1c414aeccf3eb1e1f 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -733,9 +733,7 @@ default: - // skip unicode continuation bytes - if ((s[i] & 0xc0) == 0x80) break; - -- if (iscntrl(s[i])) { -- s[i] = '.'; -- } -+ if (iscntrl(s[i])) s[i] = '.'; - *col += 1; - break; - } -@@ -743,12 +741,10 @@ - if (*col >= ws->ws_col) { - int j = i--; - while (&s[i] != s && !isspace(s[i])) --i; -- if (&s[i] == s) { -- i = j; -- } -+ if (&s[i] == s) i = j; - char c = s[i]; - s[i] = 0; -- int n = fprintf(f, "%s\n", s); -+ int n = fprintf(f, "%s\n", s) - (isspace(c) ? 0 : 1); - s[i] = c; - *row += 1; - *col = 0; diff --git a/sources/cgmnlm.git/commits/33495e8dd86139cafade2888227e37b1572d18ea.patch b/sources/cgmnlm.git/commits/33495e8dd86139cafade2888227e37b1572d18ea.patch @@ -1,52 +0,0 @@ -diff --git a/include/gmni.h b/include/gmni.h -index 42cfdac95530c9d6a5f4e6e6c3b85635908cc1e6..4d46380f2ef13db72ebf2e1a3434865142b6bcbc 100644 ---- a/include/gmni.h -+++ b/include/gmni.h -@@ -8,6 +8,7 @@ enum gemini_result { - GEMINI_OK, - GEMINI_ERR_OOM, - GEMINI_ERR_INVALID_URL, -+ GEMINI_ERR_NOT_GEMINI, - GEMINI_ERR_RESOLVE, - GEMINI_ERR_CONNECT, - GEMINI_ERR_SSL, -diff --git a/src/client.c b/src/client.c -index f1674d534a9a5f2edcb6c7be678e17ad5724a9a7..34d25f3794f8069b15a688ebd89b7bf82fdd01e2 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -104,11 +104,24 @@ struct Curl_URL *uri = curl_url(); - if (!uri) { - return GEMINI_ERR_OOM; - } -+ -+ enum gemini_result res = GEMINI_OK; - if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { -- return GEMINI_ERR_INVALID_URL; -+ res = GEMINI_ERR_INVALID_URL; -+ goto cleanup; -+ } -+ -+ char *scheme; -+ if (curl_url_get(uri, CURLUPART_SCHEME, &scheme, 0) != CURLUE_OK) { -+ res = GEMINI_ERR_INVALID_URL; -+ goto cleanup; -+ } else { -+ if (strcmp(scheme, "gemini") != 0) { -+ res = GEMINI_ERR_NOT_GEMINI; -+ goto cleanup; -+ } - } - -- enum gemini_result res = GEMINI_OK; - if (options && options->ssl_ctx) { - resp->ssl_ctx = options->ssl_ctx; - SSL_CTX_up_ref(options->ssl_ctx); -@@ -226,6 +239,8 @@ case GEMINI_ERR_OOM: - return "Out of memory"; - case GEMINI_ERR_INVALID_URL: - return "Invalid URL"; -+ case GEMINI_ERR_NOT_GEMINI: -+ return "Not a gemini URL"; - case GEMINI_ERR_RESOLVE: - return gai_strerror(resp->status); - case GEMINI_ERR_CONNECT: diff --git a/sources/cgmnlm.git/commits/3547fd11d57da5c7aa610366d54eef3b47d0b1a4.patch b/sources/cgmnlm.git/commits/3547fd11d57da5c7aa610366d54eef3b47d0b1a4.patch @@ -1,141 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index dd8c8d200a949a24d030ce3429a4d20f1430fe97..94e9933e514438c633663ae8871d4fba3e64c349 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -17,15 +17,42 @@ char *url; - struct link *next; - }; - -+struct history { -+ char *url; -+ struct history *prev, *next; -+}; -+ - static void - usage(const char *argv_0) - { - fprintf(stderr, "usage: %s [gemini://...]\n", argv_0); -+} -+ -+static void -+history_free(struct history *history) -+{ -+ if (!history) { -+ return; -+ } -+ history_free(history->next); -+ free(history); - } - - static bool --set_url(struct Curl_URL *url, char *new_url) -+set_url(struct Curl_URL *url, char *new_url, struct history **history) - { -+ if (history) { -+ struct history *next = calloc(1, sizeof(struct history)); -+ next->url = strdup(new_url); -+ next->prev = *history; -+ if (*history) { -+ if ((*history)->next) { -+ history_free((*history)->next); -+ } -+ (*history)->next = next; -+ } -+ *history = next; -+ } - if (curl_url_set(url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { - fprintf(stderr, "Error: invalid URL\n"); - return false; -@@ -138,8 +165,9 @@ return 1; - } - } - -+ struct history *history; - if (optind == argc - 1) { -- set_url(url, argv[optind]); -+ set_url(url, argv[optind], &history); - } else { - usage(argv[0]); - return 1; -@@ -161,9 +189,10 @@ char *plain_url; - CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); - assert(uc == CURLUE_OK); // Invariant - -- snprintf(prompt, sizeof(prompt), "\n\t%s\n" -- "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit " -- "[b]ack; [f]orward\n" -+ snprintf(prompt, sizeof(prompt), "\nat %s\n" -+ "[n]: follow Nth link; [o <url>]: open URL; " -+ "[b]ack; [f]orward; " -+ "[q]uit\n" - "=> ", plain_url); - - enum gemini_result res = gemini_request(plain_url, &opts, &resp); -@@ -201,11 +230,29 @@ size_t l = 0; - char *in = NULL; - ssize_t n = getline(&in, &l, tty); - if (n == -1 && feof(tty)) { -- prompting = run = false; -+ run = false; - break; - } - if (strcmp(in, "q\n") == 0) { -- prompting = run = false; -+ run = false; -+ break; -+ } -+ if (strcmp(in, "b\n") == 0) { -+ if (!history->prev) { -+ fprintf(stderr, "At beginning of history\n"); -+ continue; -+ } -+ history = history->prev; -+ set_url(url, history->url, NULL); -+ break; -+ } -+ if (strcmp(in, "f\n") == 0) { -+ if (!history->next) { -+ fprintf(stderr, "At end of history\n"); -+ continue; -+ } -+ history = history->next; -+ set_url(url, history->url, NULL); - break; - } - -@@ -221,23 +268,24 @@ - if (!link) { - fprintf(stderr, "Error: no such link.\n"); - } else { -- prompting = false; -- set_url(url, link->url); -+ set_url(url, link->url, &history); -+ break; - } - } -+ free(in); -+ } - -- link = links; -- while (link) { -- struct link *next = link->next; -- free(link->url); -- free(link); -- link = next; -- } -- -- free(in); -+ struct link *link = links; -+ while (link) { -+ struct link *next = link->next; -+ free(link->url); -+ free(link); -+ link = next; - } -+ links = NULL; - } - -+ history_free(history); - SSL_CTX_free(opts.ssl_ctx); - curl_url_cleanup(url); - return 0; diff --git a/sources/cgmnlm.git/commits/37396a375a68868490342e16140e67287445be17.patch b/sources/cgmnlm.git/commits/37396a375a68868490342e16140e67287445be17.patch @@ -1,16 +0,0 @@ -diff --git a/Makefile b/Makefile -index d6b576c49053adcc21557a8d88137101deeb5846..69a241a8825a6a7aa979eb2ae95a26faaf3a0532 100644 ---- a/Makefile -+++ b/Makefile -@@ -31,4 +31,10 @@ - distclean: clean - @rm -rf "$(OUTDIR)" - --.PHONY: clean distclean docs -+install: all -+ mkdir -p $(BINDIR) -+ mkdir -p $(MANDIR)/man1 -+ install -Dm755 gmni $(BINDIR)/gmni -+ install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 -+ -+.PHONY: clean distclean docs install diff --git a/sources/cgmnlm.git/commits/39339c348f593bda9ca0a556affa38cd5e15138c.patch b/sources/cgmnlm.git/commits/39339c348f593bda9ca0a556affa38cd5e15138c.patch @@ -1,21 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index e8efeaf69fcb9bd890711f76959e86efa75cfec6..354e211c856451bdea120bbb1aed5917099eb285 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -31,6 +31,7 @@ // - // TODO: Check that the subject name is valid for the requested URL. - struct gemini_tofu *tofu = (struct gemini_tofu *)data; - X509 *cert = X509_STORE_CTX_get0_cert(ctx); -+ struct known_host *host = NULL; - - int rc; - int day, sec; -@@ -77,7 +78,7 @@ time_t now; - time(&now); - - enum tofu_error error = TOFU_UNTRUSTED_CERT; -- struct known_host *host = tofu->known_hosts; -+ host = tofu->known_hosts; - while (host) { - if (host->expires < now) { - goto next; diff --git a/sources/cgmnlm.git/commits/3c63a64288f665a272974698d547bbca79769d5a.patch b/sources/cgmnlm.git/commits/3c63a64288f665a272974698d547bbca79769d5a.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 3bd2ea9a9b70cc33f6fc5b330234c17a974f80f5..c5b778095021a7592fce5c7e4a83c70be822790e 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -246,7 +246,7 @@ fclose(fi); - fclose(fo); - free(line); - if ( rename(tempfile, path) != 0) { -- fprintf(browser->tty, "Failed to udpate bookmarks: %s\n", strerror(errno)); -+ fprintf(browser->tty, "Failed to update bookmarks: %s\n", strerror(errno)); - } - } - diff --git a/sources/cgmnlm.git/commits/3ce02e5183da68e017b572265d68f19fef59043c.patch b/sources/cgmnlm.git/commits/3ce02e5183da68e017b572265d68f19fef59043c.patch @@ -1,183 +0,0 @@ -diff --git a/Makefile b/Makefile -index 5e468e26f12fef2c066826e4e5662e51ca4dde42..00e31d3fcee294d181c736b75e8e2bc36bdc7053 100644 ---- a/Makefile -+++ b/Makefile -@@ -9,16 +9,16 @@ gmni: $(gmni_objects) - @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) -o $@ $(gmni_objects) $(LIBS) - --gmnlm: $(gmnlm_objects) -+cgmnlm: $(cgmnlm_objects) - @printf 'CCLD\t$@\n' -- @$(CC) $(LDFLAGS) -o $@ $(gmnlm_objects) $(LIBS) -+ @$(CC) $(LDFLAGS) -o $@ $(cgmnlm_objects) $(LIBS) - - libgmni.a: $(libgmni.a_objects) - @printf 'AR\t$@\n' - @$(AR) -rcs $@ $(libgmni.a_objects) - - doc/gmni.1: doc/gmni.scd --doc/gmnlm.1: doc/gmnlm.scd -+doc/cgmnlm.1: doc/cgmnlm.scd - - libgmni.pc: - @printf 'GEN\t$@\n' -@@ -46,10 +46,10 @@ .scd.1: - @printf 'SCDOC\t$@\n' - @$(SCDOC) < $< > $@ - --docs: doc/gmni.1 doc/gmnlm.1 -+docs: doc/gmni.1 doc/cgmnlm.1 - - clean: -- @rm -f gmni gmnlm libgmni.a libgmni.pc doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) -+ @rm -f gmni cgmnlm libgmni.a libgmni.pc doc/gmni.1 doc/cgmnlm.1 $(cgmnlm_objects) $(gmni_objects) - - distclean: clean - @rm -rf "$(OUTDIR)" -@@ -60,7 +60,7 @@ mkdir -p $(LIBDIR) - mkdir -p $(INCLUDEDIR)/gmni - mkdir -p $(LIBDIR)/pkgconfig - install -m755 gmni $(BINDIR)/gmni -- install -m755 gmnlm $(BINDIR)/gmnlm -+ install -m755 cgmnlm $(BINDIR)/cgmnlm - install -m755 libgmni.a $(LIBDIR)/libgmni.a - install -m644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h - install -m644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h -@@ -69,11 +69,11 @@ install -m644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc - - uninstall: - rm -f $(BINDIR)/gmni -- rm -f $(BINDIR)/gmnlm -+ rm -f $(BINDIR)/cgmnlm - rm -f $(LIBDIR)/libgmni.a - rm -rf $(INCLUDEDIR)/gmni - rm -f $(LIBDIR)/pkgconfig/libgmni.pc - rm -f $(MANDIR)/man1/gmni.1 -- rm -f $(MANDIR)/man1/gmnlm.1 -+ rm -f $(MANDIR)/man1/cgmnlm.1 - - .PHONY: clean distclean docs install -diff --git a/README.md b/README.md -index c269ec775d39d0c26445cfd39358829aae06b575..9e1c9831127d8395d3fa8173f4ecb775c27e31b1 100644 ---- a/README.md -+++ b/README.md -@@ -1,23 +1,29 @@ --# gmni - A Gemini client -+# cgmnlm - A colorful Gemini line mode client - - This is a [Gemini](https://gemini.circumlunar.space/) client. Included are: - - - A CLI utility (like curl): gmni --- A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm -- --Dependencies: -- --- A POSIX-like system and a C11 compiler --- OpenSSL --- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) -+- A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): cgmnlm - --Features: -+## Features: - - - Page history - - Regex searches - - Bookmarks - --[![Screenshot of the line-mode browser](https://l.sr.ht/7kaA.png)](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) -+### Modifications compared to upstream -+ -+This project is of fork of https://git.sr.ht/~sircmpwn/gmni -+ -+It includes the following modifications: -+- default 4 char indenting -+- colored headings & links -+ -+## Dependencies: -+ -+- A POSIX-like system and a C11 compiler -+- OpenSSL -+- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) - - ## Compiling - -@@ -30,4 +36,4 @@ ``` - - ## Usage - --See `gmni(1)`, `gmnlm(1)`. -+See `gmni(1)`, `cgmnlm(1)`. -diff --git a/cgmnlm b/cgmnlm -new file mode 100755 -index 0000000000000000000000000000000000000000..5c05cd3ecca44b577a75544d7e2119421a5da102 -Binary files /dev/null and b/cgmnlm differ -diff --git a/config.sh b/config.sh -index fd6a325bb21807ed6b6dc871b64479ebfe89a68d..8d586cfde54fa3c3bc369ff0156ad5d55a79f383 100644 ---- a/config.sh -+++ b/config.sh -@@ -128,7 +128,7 @@ all="$all docs" - install_docs=" - mkdir -p \$(MANDIR)/man1 - install -m644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 -- install -m644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" -+ install -m644 doc/cgmnlm.1 \$(MANDIR)/man1/cgmnlm.1" - else - echo no - fi -diff --git a/configure b/configure -index e82a0e27e27c9873c8dac92fd3385e65bf511648..aa761971fe95d70ed242977bed8dfd6d55a9f794 100755 ---- a/configure -+++ b/configure -@@ -12,11 +12,11 @@ src/url.c \ - src/util.c - } - --gmnlm() { -- genrules gmnlm \ -+cgmnlm() { -+ genrules cgmnlm \ - src/client.c \ - src/escape.c \ -- src/gmnlm.c \ -+ src/cgmnlm.c \ - src/parser.c \ - src/tofu.c \ - src/url.c \ -@@ -37,7 +37,7 @@ libgmni_pc() { - : - } - --all="gmni gmnlm libgmni.a libgmni.pc" -+all="gmni cgmnlm libgmni.a libgmni.pc" - - - run_configure -diff --git a/doc/gmnlm.scd b/doc/cgmnlm.scd -rename from doc/gmnlm.scd -rename to doc/cgmnlm.scd -index e0a368f8921253c76219790d35380848e486720d..1ed6e620cb39e04ade8b98fa9bab7e7c0bacc5e9 100644 ---- a/doc/gmnlm.scd -+++ b/doc/cgmnlm.scd -@@ -2,15 +2,15 @@ gmnlm(1) - - # NAME - --gmnlm - Gemini line-mode browser -+cgmnlm - colored Gemini line-mode browser - - # SYNPOSIS - --*gmnlm* [-PU] [-j _mode_] [-W _width_] _gemini://..._ -+*cgmnlm* [-PU] [-j _mode_] [-W _width_] _gemini://..._ - - # DESCRIPTION - --*gmnlm* is an interactive line-mode Gemini browser. -+*cgmnlm* is an interactive line-mode Gemini browser. - - # OPTIONS - -diff --git a/src/gmnlm.c b/src/cgmnlm.c -rename from src/gmnlm.c -rename to src/cgmnlm.c diff --git a/sources/cgmnlm.git/commits/3dd06ab4813b6c8f4992e19fce9d4b094fd3a1a9.patch b/sources/cgmnlm.git/commits/3dd06ab4813b6c8f4992e19fce9d4b094fd3a1a9.patch @@ -1,43 +0,0 @@ -diff --git a/include/util.h b/include/util.h -index 8ec9ac5d94092a75813a3e083140fbb88079c29d..6c241f41a175c3d27390ecb6ca5b041bd637cb2b 100644 ---- a/include/util.h -+++ b/include/util.h -@@ -9,6 +9,7 @@ const char *path; - }; - - char *getpath(const struct pathspec *paths, size_t npaths); -+void posix_dirname(char *path, char *dname); - int mkdirs(char *path, mode_t mode); - int download_resp(FILE *out, struct gemini_response resp, const char *path, - char *url); -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 12a6c971b7ae7d18bcdafc3b2b81f55539340095..0ab21849e186a99c84d0f94c59ef6c821e91b97d 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -562,6 +562,13 @@ assert(host); - n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); - assert(n < sizeof(certpath)); - n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); -+ char dname[PATH_MAX + 1]; -+ posix_dirname(certpath, dname); -+ if (mkdirs(dname, 0755) != 0) { -+ fprintf(stderr, "Error creating directory %s: %s\n", -+ dname, strerror(errno)); -+ break; -+ } - assert(n < sizeof(keypath)); - fprintf(stderr, "The server requested a client certificate.\n" - "Presently, this process is not automated.\n" -diff --git a/src/util.c b/src/util.c -index 1cb0bf42b6319e6bd1b0cf0cc94e28627e4f9c68..8441b584ff199ffd81472539a59acce5d0a18f42 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -11,7 +11,7 @@ #include <string.h> - #include <sys/stat.h> - #include "util.h" - --static void -+void - posix_dirname(char *path, char *dname) - { - char p[PATH_MAX+1]; diff --git a/sources/cgmnlm.git/commits/40308b8b0bd7e15d0f6e2971b901a9c09a4bc681.patch b/sources/cgmnlm.git/commits/40308b8b0bd7e15d0f6e2971b901a9c09a4bc681.patch @@ -1,18 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 0546486340b7299be3c97266dd534a1354bf31a3..d05180c7a74c88bd7ed8ba551a4a522bb8b74009 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -759,6 +759,13 @@ - FILE *fp = fopen(path, "r"); - if (!fp) { - resp->status = GEMINI_STATUS_NOT_FOUND; -+ /* Make sure members of resp evaluate to false, so that -+ gemini_response_finish does not try to free them. */ -+ resp->bio = NULL; -+ resp->ssl = NULL; -+ resp->ssl_ctx = NULL; -+ resp->meta = NULL; -+ resp->fd = -1; - free(path); - break; - } diff --git a/sources/cgmnlm.git/commits/4134dc1b4a37b65f8176d799f03342583b49d932.patch b/sources/cgmnlm.git/commits/4134dc1b4a37b65f8176d799f03342583b49d932.patch @@ -1,16 +0,0 @@ -diff --git a/config.sh b/config.sh -index 55bc9a2ab6b15f91c655a4ab033fcbd6107b83fe..5bd34bb30dec02422855edbf37817f8276840211 100644 ---- a/config.sh -+++ b/config.sh -@@ -85,6 +85,11 @@ find_library() { - name="$1" - pc="$2" - printf "Checking for %s... " "$name" -+ if ! command -v pkg-config >/dev/null -+ then -+ printf "ERROR: pkg-config not found\n" -+ return 1 -+ fi - if ! pkg-config "$pc" 2>/dev/null - then - printf "NOT FOUND\n" diff --git a/sources/cgmnlm.git/commits/4274b06fe4b2702af297cd0cee3d7871741899ec.patch b/sources/cgmnlm.git/commits/4274b06fe4b2702af297cd0cee3d7871741899ec.patch @@ -1,30 +0,0 @@ -diff --git a/README.md b/README.md -index 9aa989fe358ecccd9b984b1aff0df97c6c2ceec0..4b03a4782f0b97d8687b3360f20d9c8748f88ca3 100644 ---- a/README.md -+++ b/README.md -@@ -14,7 +14,7 @@ - basic Client Certificate support (no autocreation of client certs currently) - - ## Non-Features: - --- no inlinig of any link type -+- no inlining of any link type - - no caching of page content - - no persistent history across sessions - -@@ -25,11 +25,11 @@ - It includes the following modifications: - - colored headings & links - - default 4 char indenting --- s command to directly search in geminispace (via geminispace.info) --- k command to remove the bookmark for the current page --- e[N] command to open a link in default external program (requires `xdg-open`) --- t[N] command to download the content behind a link to a temporary file --- a command to switch between display of preformatted blocks and alt text (if available) -+- `s` command to directly search in geminispace (via geminispace.info) -+- `k` command to remove the bookmark for the current page -+- `e[N]` command to open a link in default external program (requires `xdg-open`) -+- `t[N]` command to download the content behind a link to a temporary file -+- `a` command to switch between display of preformatted blocks and alt text (if available) - - The actual colors used depend on your terminal palette: - - heading 1: light red diff --git a/sources/cgmnlm.git/commits/46b5d74576ffce397c83ac53ebfacb25e1cdc851.patch b/sources/cgmnlm.git/commits/46b5d74576ffce397c83ac53ebfacb25e1cdc851.patch @@ -1,131 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 391d2b4a2a2dddd7e688a950bb852dfa0687cc32..cbc04be66573d4980e24f00761c54d4ac8a1a88f 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -139,10 +139,7 @@ - static char * - trim_ws(char *in) - { -- for (int i = strlen(in) - 1; in[i] && isspace(in[i]); --i) { -- in[i] = 0; -- } -- for (; *in && isspace(*in); ++in); -+ while (*in && isspace(*in)) ++in; - return in; - } - -@@ -203,61 +200,80 @@ struct link **next = &browser->links; - while (text != NULL || gemini_parser_next(&p, &tok) == 0) { - switch (tok.token) { - case GEMINI_TEXT: -- // TODO: Run other stuff through wrap() -+ col += fprintf(browser->tty, " "); - if (text == NULL) { - text = tok.text; - } -- -- do { -+ break; -+ case GEMINI_LINK: -+ if (text == NULL) { -+ col += fprintf(browser->tty, "%d) ", nlinks++); -+ text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); -+ *next = calloc(1, sizeof(struct link)); -+ (*next)->url = strdup(trim_ws(tok.link.url)); -+ next = &(*next)->next; -+ } else { - col += fprintf(browser->tty, " "); -- int w = wrap(browser->tty, text, &ws, &row, &col); -- text += w; -- if (row >= ws.ws_row - 4) { -- break; -- } -- } while (text[0]); -- -- if (!text[0]) { -- text = NULL; - } - break; -- case GEMINI_LINK: -- col += fprintf(browser->tty, "%d) %s\n", nlinks++, -- trim_ws(tok.link.text ? tok.link.text : tok.link.url)); -- *next = calloc(1, sizeof(struct link)); -- (*next)->url = strdup(trim_ws(tok.link.url)); -- next = &(*next)->next; -- break; - case GEMINI_PREFORMATTED: - continue; // TODO - case GEMINI_HEADING: -- for (int n = tok.heading.level; n; --n) { -- col += fprintf(browser->tty, "#"); -- } -- for (int n = 3 - tok.heading.level; n > 1; --n) { -- col += fprintf(browser->tty, " "); -+ if (text == NULL) { -+ for (int n = tok.heading.level; n; --n) { -+ col += fprintf(browser->tty, "#"); -+ } -+ switch (tok.heading.level) { -+ case 1: -+ col += fprintf(browser->tty, " "); -+ break; -+ case 2: -+ case 3: -+ col += fprintf(browser->tty, " "); -+ break; -+ } -+ text = trim_ws(tok.heading.title); -+ } else { -+ col += fprintf(browser->tty, " "); - } -- col += fprintf(browser->tty, " %s\n", -- trim_ws(tok.heading.title)); - break; - case GEMINI_LIST_ITEM: -- col += fprintf(browser->tty, " %s %s\n", -- browser->unicode ? "•" : "*", -- trim_ws(tok.list_item)); -+ if (text == NULL) { -+ col += fprintf(browser->tty, " %s ", -+ browser->unicode ? "•" : "*"); -+ text = trim_ws(tok.list_item); -+ } else { -+ col += fprintf(browser->tty, " "); -+ } - break; - case GEMINI_QUOTE: -- col += fprintf(browser->tty, " %s %s\n", -- browser->unicode ? "|" : "|", -- trim_ws(tok.quote_text)); -+ if (text == NULL) { -+ col += fprintf(browser->tty, " %s ", -+ browser->unicode ? "|" : "|"); -+ text = trim_ws(tok.quote_text); -+ } else { -+ col += fprintf(browser->tty, " "); -+ } - break; - } - -+ if (text) { -+ int w = wrap(browser->tty, text, &ws, &row, &col); -+ text += w; -+ if (text[0] && row < ws.ws_row - 4) { -+ continue; -+ } -+ -+ if (!text[0]) { -+ text = NULL; -+ } -+ } -+ - while (col >= ws.ws_col) { - col -= ws.ws_col; - ++row; - } -- ++row; -- col = 0; -+ ++row; col = 0; - - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; diff --git a/sources/cgmnlm.git/commits/479ea9e74f4b66645c0d7b51d99adf420d831f23.patch b/sources/cgmnlm.git/commits/479ea9e74f4b66645c0d7b51d99adf420d831f23.patch @@ -1,36 +0,0 @@ -diff --git a/README.md b/README.md -index 3c999cee8bbcb5b0bfbc6732f9ecea0ba231d043..aa9f811a9bfd9cfb7b6654954e33fc62e868f8c4 100644 ---- a/README.md -+++ b/README.md -@@ -24,6 +24,7 @@ - It includes the following modifications: - - colored headings & links - - default 4 char indenting -+- s command to directly search in geminispace (via geminispace.info) - - k command to remove the bookmark for the current page - - e[N] command to open a link in default external program (requires `xdg-open`) - - t[N] command to download the content behind a link to a temporary file -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 2c35f76f8ee4b6dfa6af5bbe8af790a3e98afbbc..e28bf70afc5d6fd4c5c6eff85d85198d62a127e8 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -93,6 +93,7 @@ "m\t\tSave bookmark\n" - "M\t\tBrowse bookmarks\n" - "k\t\tRemove bookmark for current page\n" - "r\t\tReload the page\n" -+ "s\t\tSearch via geminispace.info\n" - "/<text>\t\tsearch for text (POSIX regular expression)\n" - "n\t\tjump to next search match\n" - "d <path>\tDownload page to <path>\n" -@@ -567,6 +568,11 @@ goto exit; - case 'q': - if (in[1]) break; - result = PROMPT_QUIT; -+ goto exit; -+ case 's': -+ if (in[1]) break; -+ set_url(browser, "gemini://geminispace.info/search", &browser->history); -+ result = PROMPT_ANSWERED; - goto exit; - case 'b': - if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); diff --git a/sources/cgmnlm.git/commits/48d0feed6d097c54662a7f231c7bc4704837f023.patch b/sources/cgmnlm.git/commits/48d0feed6d097c54662a7f231c7bc4704837f023.patch @@ -1,248 +0,0 @@ -diff --git a/configure b/configure -index 407886859d8ca0b9cbcb085ecef89d004c68cde4..aca6e8a1eaa9a6271f03eb8863640d0d93cdf435 100755 ---- a/configure -+++ b/configure -@@ -7,6 +7,7 @@ genrules gmni \ - src/client.c \ - src/escape.c \ - src/gmni.c \ -+ src/parser.c \ - src/url.c - } - -diff --git a/include/gmni.h b/include/gmni.h -index 4d46380f2ef13db72ebf2e1a3434865142b6bcbc..d78d1c8eb4e94507c363219573691e60d40e8ea8 100644 ---- a/include/gmni.h -+++ b/include/gmni.h -@@ -103,4 +103,64 @@ // Returns the general response class (i.e. with the second digit set to zero) - // of the given Gemini status code. - enum gemini_status_class gemini_response_class(enum gemini_status status); - -+enum gemini_tok { -+ GEMINI_TEXT, -+ GEMINI_LINK, -+ GEMINI_PREFORMATTED, -+ GEMINI_HEADING, -+ GEMINI_LIST_ITEM, -+ GEMINI_QUOTE, -+}; -+ -+struct gemini_token { -+ enum gemini_tok token; -+ -+ // The token field determines which of the union members is valid. -+ union { -+ char *text; -+ -+ struct { -+ char *text; -+ char *url; // May be NULL -+ } link; -+ -+ struct { -+ char *text; -+ char *alt_text; // May be NULL -+ } preformatted; -+ -+ struct { -+ char *title; -+ int level; // 1, 2, or 3 -+ } heading; -+ -+ char *list_item; -+ char *quote_text; -+ }; -+}; -+ -+struct gemini_parser { -+ BIO *f; -+ char *buf; -+ size_t bufsz; -+ size_t bufln; -+}; -+ -+// Initializes a text/gemini parser which reads from the specified BIO. -+void gemini_parser_init(struct gemini_parser *p, BIO *f); -+ -+// Finishes this text/gemini parser and frees up its resources. -+void gemini_parser_finish(struct gemini_parser *p); -+ -+// Reads the next token from a text/gemini file. -+// -+// Returns 0 on success, 1 on EOF, and -1 on failure. -+// -+// Caller must call gemini_token_finish before exiting or re-using the token -+// parameter. -+int gemini_parser_next(struct gemini_parser *p, struct gemini_token *token); -+ -+// Must be called after gemini_next to free up resources for the next token. -+void gemini_token_finish(struct gemini_token *token); -+ - #endif -diff --git a/src/gmni.c b/src/gmni.c -index 6e27b2f859c692f37fb483184cda245a7e979cbe..75c6c5afb6e7f1f286d314d93f2b44ef8414afb3 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -233,9 +233,11 @@ case SHOW_HEADERS: - printf("%d %s\n", resp.status, resp.meta); - /* fallthrough */ - case OMIT_HEADERS: -- if (resp.status / 10 != 2) { -+ if (gemini_response_class(resp.status) != -+ GEMINI_STATUS_CLASS_SUCCESS) { - break; - } -+ - char last; - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { -diff --git a/src/parser.c b/src/parser.c -new file mode 100644 -index 0000000000000000000000000000000000000000..ffcc28767be7d638d18609764999d004790aa9a2 ---- /dev/null -+++ b/src/parser.c -@@ -0,0 +1,144 @@ -+#include <assert.h> -+#include <ctype.h> -+#include <openssl/bio.h> -+#include <stddef.h> -+#include <stdlib.h> -+#include <string.h> -+#include "gmni.h" -+ -+void -+gemini_parser_init(struct gemini_parser *p, BIO *f) -+{ -+ p->f = f; -+ p->bufln = 0; -+ p->bufsz = BUFSIZ; -+ p->buf = malloc(p->bufsz + 1); -+ p->buf[0] = 0; -+ BIO_up_ref(p->f); -+} -+ -+void -+gemini_parser_finish(struct gemini_parser *p) -+{ -+ if (!p) { -+ return; -+ } -+ BIO_free(p->f); -+ free(p->buf); -+} -+ -+int -+gemini_parser_next(struct gemini_parser *p, struct gemini_token *tok) -+{ -+ memset(tok, 0, sizeof(*tok)); -+ -+ int eof = 0; -+ while (!strstr(p->buf, "\n")) { -+ if (p->bufln == p->bufsz) { -+ p->bufsz *= 2; -+ char *buf = realloc(p->buf, p->bufsz); -+ assert(buf); -+ p->buf = buf; -+ } -+ -+ int n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln); -+ if (n == -1) { -+ return -1; -+ } else if (n == 0) { -+ eof = 1; -+ break; -+ } -+ p->bufln += n; -+ p->buf[p->bufln] = 0; -+ } -+ -+ // TODO: Collapse multi-line text for the user-agent to wrap -+ char *end; -+ if ((end = strstr(p->buf, "\n")) != NULL) { -+ *end = 0; -+ } -+ -+ // TODO: Provide whitespace trimming helper function -+ if (strncmp(p->buf, "=>", 2) == 0) { -+ tok->token = GEMINI_LINK; -+ int i = 2; -+ while (p->buf[i] && isspace(p->buf[i])) ++i; -+ tok->link.url = &p->buf[i]; -+ -+ for (; p->buf[i]; ++i) { -+ if (isspace(p->buf[i])) { -+ p->buf[i++] = 0; -+ while (isspace(p->buf[i])) ++i; -+ if (p->buf[i]) { -+ tok->link.text = strdup(&p->buf[i]); -+ } -+ break; -+ } -+ } -+ -+ tok->link.url = strdup(tok->link.url); -+ } else if (strncmp(p->buf, "```", 3) == 0) { -+ tok->token = GEMINI_PREFORMATTED; // TODO -+ tok->preformatted.text = strdup("<text>"); -+ tok->preformatted.alt_text = strdup("<alt-text>"); -+ } else if (p->buf[0] == '#') { -+ tok->token = GEMINI_HEADING; -+ int level = 1; -+ while (p->buf[level] == '#' && level < 3) { -+ ++level; -+ } -+ tok->heading.level = level; -+ tok->heading.title = strdup(&p->buf[level]); -+ } else if (p->buf[0] == '*') { -+ tok->token = GEMINI_LIST_ITEM; -+ tok->list_item = strdup(&p->buf[1]); -+ } else if (p->buf[0] == '>') { -+ tok->token = GEMINI_QUOTE; -+ tok->quote_text = strdup(&p->buf[1]); -+ } else { -+ tok->token = GEMINI_TEXT; -+ tok->text = strdup(p->buf); -+ } -+ -+ if (end && end + 1 < p->buf + p->bufln) { -+ size_t len = end - p->buf + 1; -+ memmove(p->buf, end + 1, p->bufln - len); -+ p->bufln -= len; -+ } else { -+ p->buf[0] = 0; -+ p->bufln = 0; -+ } -+ -+ return eof; -+} -+ -+void -+gemini_token_finish(struct gemini_token *tok) -+{ -+ if (!tok) { -+ return; -+ } -+ -+ switch (tok->token) { -+ case GEMINI_TEXT: -+ free(tok->text); -+ break; -+ case GEMINI_LINK: -+ free(tok->link.text); -+ free(tok->link.url); -+ break; -+ case GEMINI_PREFORMATTED: -+ free(tok->preformatted.text); -+ free(tok->preformatted.alt_text); -+ break; -+ case GEMINI_HEADING: -+ free(tok->heading.title); -+ break; -+ case GEMINI_LIST_ITEM: -+ free(tok->list_item); -+ break; -+ case GEMINI_QUOTE: -+ free(tok->quote_text); -+ break; -+ } -+} diff --git a/sources/cgmnlm.git/commits/49c0c523c69842f8ebc33135947591cf6f7a7cab.patch b/sources/cgmnlm.git/commits/49c0c523c69842f8ebc33135947591cf6f7a7cab.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 000a56550a686928f84e2c301c9017a8c48b3d68..61f41e29c324d980bd3a1f270e78649cdcec88e0 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -315,7 +315,7 @@ ret = download_resp(stderr, resp, output_file, url); - break; - } - -- char last; -+ char last = 0; - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { - n = BIO_read(resp.bio, buf, BUFSIZ); diff --git a/sources/cgmnlm.git/commits/49eea555e605e6e0155756ad9739a5729340db81.patch b/sources/cgmnlm.git/commits/49eea555e605e6e0155756ad9739a5729340db81.patch @@ -1,12 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index 50a295870876265849eae7fdf0de926a3ad309fd..5b34850de9e17dcd59fc64a3227cd92bfb0a6cef 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -88,7 +88,6 @@ cc->hash[i], i + 1 == sizeof(cc->hash) ? "" : ":"); - } - - enum tofu_error error = TOFU_UNTRUSTED_CERT; -- (void)error; - struct known_host *host = cc->store->known_hosts; - while (host) { - if (strcmp(host->host, cc->server_name) != 0) { diff --git a/sources/cgmnlm.git/commits/4a6172f1bf9cb41eb1ce3a5f720f9ebe4febc62b.patch b/sources/cgmnlm.git/commits/4a6172f1bf9cb41eb1ce3a5f720f9ebe4febc62b.patch @@ -1,43 +0,0 @@ -diff --git a/Makefile b/Makefile -index 9d17d35eb67ee4b86a2d9219c3f1fce98695a521..5e468e26f12fef2c066826e4e5662e51ca4dde42 100644 ---- a/Makefile -+++ b/Makefile -@@ -56,13 +56,16 @@ @rm -rf "$(OUTDIR)" - - install: all install_docs - mkdir -p $(BINDIR) -- install -Dm755 gmni $(BINDIR)/gmni -- install -Dm755 gmnlm $(BINDIR)/gmnlm -- install -Dm755 libgmni.a $(LIBDIR)/libgmni.a -- install -Dm644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h -- install -Dm644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h -- install -Dm644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h -- install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc -+ mkdir -p $(LIBDIR) -+ mkdir -p $(INCLUDEDIR)/gmni -+ mkdir -p $(LIBDIR)/pkgconfig -+ install -m755 gmni $(BINDIR)/gmni -+ install -m755 gmnlm $(BINDIR)/gmnlm -+ install -m755 libgmni.a $(LIBDIR)/libgmni.a -+ install -m644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h -+ install -m644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h -+ install -m644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h -+ install -m644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc - - uninstall: - rm -f $(BINDIR)/gmni -diff --git a/config.sh b/config.sh -index ca5444c73b47daa204d9dd17a3949147385dedec..fd6a325bb21807ed6b6dc871b64479ebfe89a68d 100644 ---- a/config.sh -+++ b/config.sh -@@ -127,8 +127,8 @@ echo yes - all="$all docs" - install_docs=" - mkdir -p \$(MANDIR)/man1 -- install -Dm644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 -- install -Dm644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" -+ install -m644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 -+ install -m644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" - else - echo no - fi diff --git a/sources/cgmnlm.git/commits/4b2437b17e00d61da9356ac96bb38a8043c66fca.patch b/sources/cgmnlm.git/commits/4b2437b17e00d61da9356ac96bb38a8043c66fca.patch @@ -1,12 +0,0 @@ -diff --git a/.gitignore b/.gitignore -index 7b35cd8ef5366f89f8db24b1f62f0a1f2c3e336d..6ec8d5188d32b997f3a91f8a11f64f4e79b43770 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -1,5 +1,6 @@ - .build - build --gmnic -+gmni -+gmnlm - *.1 - *.o diff --git a/sources/cgmnlm.git/commits/4b7fba261a70bd37e160a7304d454c72c1f75b69.patch b/sources/cgmnlm.git/commits/4b7fba261a70bd37e160a7304d454c72c1f75b69.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index a61ee7bcb3ce77da24b1db97d379c2003c61d055..171e1407ba2dfa3341f0693ae8d60bb9cedb6564 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -85,7 +85,7 @@ fprintf(stderr, - "The certificate offered by this server is of unknown trust. " - "Its fingerprint is: \n" - "%s\n\n" -- "Use -j trust to trust temporarily, or -j always to add to the trust store.\n", fingerprint); -+ "Use -j once to trust temporarily, or -j always to add to the trust store.\n", fingerprint); - break; - case TOFU_FINGERPRINT_MISMATCH: - fprintf(stderr, diff --git a/sources/cgmnlm.git/commits/4bc55aaa138171db365377db0624c3ce0d878257.patch b/sources/cgmnlm.git/commits/4bc55aaa138171db365377db0624c3ce0d878257.patch @@ -1,59 +0,0 @@ -diff --git a/src/gmnic.c b/src/gmnic.c -index 794b94ba2235757864acb81194b8de88220cc6da..60d5c68530fffacbeea62c1a8b78497857e02a02 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -34,9 +34,10 @@ INPUT_SUPPRESS, - }; - enum input_mode input_mode = INPUT_READ; - FILE *input_source = stdin; -+ bool linefeed = true; - - int c; -- while ((c = getopt(argc, argv, "46C:d:D:hLiIN")) != -1) { -+ while ((c = getopt(argc, argv, "46C:d:D:hlLiIN")) != -1) { - switch (c) { - case '4': - assert(0); // TODO -@@ -67,6 +68,9 @@ break; - case 'h': - usage(argv[0]); - return 0; -+ case 'l': -+ linefeed = false; -+ break; - case 'L': - assert(0); // TODO: Follow redirects - break; -@@ -81,7 +85,7 @@ case 'N': - input_mode = INPUT_SUPPRESS; - break; - default: -- fprintf(stderr, "fatal: unknown flag %c", c); -+ fprintf(stderr, "fatal: unknown flag %c\n", c); - return 1; - } - } -@@ -167,8 +171,9 @@ case OMIT_HEADERS: - if (resp.status / 10 != 2) { - break; - } -- for (int n = 1; n > 0;) { -- char buf[BUFSIZ]; -+ char buf[BUFSIZ]; -+ int n; -+ for (n = 1; n > 0;) { - n = BIO_read(resp.bio, buf, BUFSIZ); - if (n == -1) { - fprintf(stderr, "Error: read\n"); -@@ -184,6 +189,11 @@ return 1; - } - w += x; - } -+ } -+ if (strncmp(resp.meta, "text/", 5) == 0 -+ && linefeed -+ && buf[n - 1] != '\n') { -+ printf("\n"); - } - break; - } diff --git a/sources/cgmnlm.git/commits/4c0f931d6688d06df2e22d001182f6fa1b776fab.patch b/sources/cgmnlm.git/commits/4c0f931d6688d06df2e22d001182f6fa1b776fab.patch @@ -1,77 +0,0 @@ -diff --git a/README.md b/README.md -index ea1f81f04aac1b93f73fca7d984b6e3f02415a2b..e1b424367e49daad233793f663f6340849eb4286 100644 ---- a/README.md -+++ b/README.md -@@ -17,6 +17,7 @@ This project is of fork of https://git.sr.ht/~sircmpwn/gmni - - It includes the following modifications: - - default 4 char indenting -+- e[N] command to open a link in default external program (requires `xdg-open`) - - colored headings & links - - The actual colors used depend on your terminal palette: -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index 6f0b8773c16001ab95edaec345879874483114a9..5bf7dfdbea9a60cd610d76b82bb8e9bc3d5ffa1d 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -82,6 +82,7 @@ "The following commands are available:\n\n" - "q\tQuit\n" - "[N]\tFollow Nth link (where N is a number)\n" - "u[N]\tShow URL of Nth link (where N is a number)\n" -+ "e[N]\tSend URL of Nth link in external default program\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" - "H\tView all page history\n" -@@ -502,8 +503,11 @@ { - enum prompt_result result; - fprintf(browser->tty, "%s", prompt); - -- size_t l = 0; -+ struct link *link = browser->links; -+ char *endptr = NULL; -+ int linksel = 0; - char *in = NULL; -+ size_t l = 0; - ssize_t n = getline(&in, &l, browser->tty); - if (n == -1 && feof(browser->tty)) { - result = PROMPT_QUIT; -@@ -592,11 +596,10 @@ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); - result = PROMPT_AGAIN; - goto exit; - } -+ case 'e': - case 'u': - if (!in[1]) break; -- struct link *link = browser->links; -- char *endptr; -- int linksel = (int)strtol(in+1, &endptr, 10); -+ linksel = (int)strtol(in+1, &endptr, 10); - if (!endptr[0] && linksel >= 0) { - while (linksel > 0 && link) { - link = link->next; -@@ -607,8 +610,12 @@ if (!link) { - fprintf(stderr, "Error: no such link.\n"); - } else { - fprintf(browser->tty, "=> %s\n", link->url); -- result = PROMPT_AGAIN; -- goto exit; -+ if (in[0] == 'e') { -+ char xdgopen[4096]; -+ snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", link->url); -+ if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); -+ } -+ fprintf(browser->tty, "\n"); - } - } else { - fprintf(stderr, "Error: invalid argument.\n"); -@@ -664,9 +671,7 @@ result = PROMPT_AGAIN; - goto exit; - } - -- struct link *link = browser->links; -- char *endptr; -- int linksel = (int)strtol(in, &endptr, 10); -+ linksel = (int)strtol(in, &endptr, 10); - if ((endptr[0] == '\0' || endptr[0] == '|') && linksel >= 0) { - while (linksel > 0 && link) { - link = link->next; diff --git a/sources/cgmnlm.git/commits/4c12342bcad95cdc44f9107161429524226a2d37.patch b/sources/cgmnlm.git/commits/4c12342bcad95cdc44f9107161429524226a2d37.patch @@ -1,13 +0,0 @@ -diff --git a/README.md b/README.md -index dfd2e1744003996f1e629c78e459dac70985a430..ce71012d402a0c975c73f6d4acd2922f32c16cbc 100644 ---- a/README.md -+++ b/README.md -@@ -5,7 +5,7 @@ - - A CLI utility (like curl): gmni - - A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm - --[![Screenshot of the line-mode browser](https://l.sr.ht/7kaA.png)](https://asciinema.org/a/ldo2gV7qiDoBXvGwuD6x1jbn3) -+[![Screenshot of the line-mode browser](https://l.sr.ht/7kaA.png)](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) - - Dependencies: - diff --git a/sources/cgmnlm.git/commits/4dd50ac07e82dfc1785f98a3535109e2d738029d.patch b/sources/cgmnlm.git/commits/4dd50ac07e82dfc1785f98a3535109e2d738029d.patch @@ -1,31 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index f3015ac679eba77397fb4aac5068f3a63e1cdbab..ff6c619f7c6cde57faa7398d4201d2cf867bf05a 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -290,6 +290,10 @@ exit = true; - goto next; - } - -+ if (header_mode != OMIT_HEADERS) { -+ printf("%d %s\n", resp.status, resp.meta); -+ } -+ - switch (gemini_response_class(resp.status)) { - case GEMINI_STATUS_CLASS_INPUT: - if (input_mode == INPUT_SUPPRESS) { -@@ -351,14 +355,7 @@ exit = true; - break; - } - -- switch (header_mode) { -- case ONLY_HEADERS: -- printf("%d %s\n", resp.status, resp.meta); -- break; -- case SHOW_HEADERS: -- printf("%d %s\n", resp.status, resp.meta); -- /* fallthrough */ -- case OMIT_HEADERS: -+ if (header_mode != ONLY_HEADERS) { - if (gemini_response_class(resp.status) != - GEMINI_STATUS_CLASS_SUCCESS) { - break; diff --git a/sources/cgmnlm.git/commits/4e61e266076fbda20cbf268300e7f645669c7062.patch b/sources/cgmnlm.git/commits/4e61e266076fbda20cbf268300e7f645669c7062.patch @@ -1,14 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 245ba76363b58e9c9ddbf87464d48cfb38463892..a61ee7bcb3ce77da24b1db97d379c2003c61d055 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -84,7 +84,8 @@ case TOFU_UNTRUSTED_CERT: - fprintf(stderr, - "The certificate offered by this server is of unknown trust. " - "Its fingerprint is: \n" -- "%s\n\n", fingerprint); -+ "%s\n\n" -+ "Use -j trust to trust temporarily, or -j always to add to the trust store.\n", fingerprint); - break; - case TOFU_FINGERPRINT_MISMATCH: - fprintf(stderr, diff --git a/sources/cgmnlm.git/commits/514cb373019e94f743424b2813602722ca09b917.patch b/sources/cgmnlm.git/commits/514cb373019e94f743424b2813602722ca09b917.patch @@ -1,13 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index 8b6b9e7a3c09656a5d59ed86977cb8b5c39541d4..a8a12f3cfbe1e98fd2045eec257cfaf95c319910 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -212,7 +212,7 @@ } - - char *endptr; - resp->status = (enum gemini_status)strtol(buf, &endptr, 10); -- if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) { -+ if (*endptr != ' ' || resp->status < 10 || (int)resp->status >= 70) { - fprintf(stderr, "invalid status\n"); - res = GEMINI_ERR_PROTOCOL; - goto cleanup; diff --git a/sources/cgmnlm.git/commits/563922a7e2da77b3973dcf707854121932ca244e.patch b/sources/cgmnlm.git/commits/563922a7e2da77b3973dcf707854121932ca244e.patch @@ -1,331 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 2c2b67e0e56d2e6be04bba6cee96cb4a24ff3b51..3598e3fb1770aca2bd646d7a3f2bf642b54c2ab9 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -27,9 +27,18 @@ bool pagination; - struct gemini_options opts; - - FILE *tty; -+ char *plain_url; - struct Curl_URL *url; - struct link *links; - struct history *history; -+ bool running; -+}; -+ -+enum prompt_result { -+ PROMPT_AGAIN, -+ PROMPT_MORE, -+ PROMPT_QUIT, -+ PROMPT_ANSWERED, - }; - - static void -@@ -70,6 +79,63 @@ } - return true; - } - -+static enum prompt_result -+do_prompts(const char *prompt, struct browser *browser) -+{ -+ fprintf(browser->tty, "%s", prompt); -+ -+ size_t l = 0; -+ char *in = NULL; -+ ssize_t n = getline(&in, &l, browser->tty); -+ if (n == -1 && feof(browser->tty)) { -+ return PROMPT_QUIT; -+ } -+ if (strcmp(in, "\n") == 0) { -+ return PROMPT_MORE; -+ } -+ if (strcmp(in, "q\n") == 0) { -+ return PROMPT_QUIT; -+ } -+ if (strcmp(in, "b\n") == 0) { -+ if (!browser->history->prev) { -+ fprintf(stderr, "At beginning of history\n"); -+ return PROMPT_AGAIN; -+ } -+ browser->history = browser->history->prev; -+ set_url(browser, browser->history->url, NULL); -+ return PROMPT_ANSWERED; -+ } -+ if (strcmp(in, "f\n") == 0) { -+ if (!browser->history->next) { -+ fprintf(stderr, "At end of history\n"); -+ return PROMPT_AGAIN; -+ } -+ browser->history = browser->history->next; -+ set_url(browser, browser->history->url, NULL); -+ return PROMPT_ANSWERED; -+ } -+ -+ struct link *link = browser->links; -+ char *endptr; -+ int linksel = (int)strtol(in, &endptr, 10); -+ if (endptr[0] == '\n' && linksel >= 0) { -+ while (linksel > 0 && link) { -+ link = link->next; -+ --linksel; -+ } -+ -+ if (!link) { -+ fprintf(stderr, "Error: no such link.\n"); -+ } else { -+ set_url(browser, link->url, &browser->history); -+ return PROMPT_ANSWERED; -+ } -+ } -+ free(in); -+ -+ return PROMPT_AGAIN; -+} -+ - static char * - trim_ws(char *in) - { -@@ -80,7 +146,7 @@ for (; *in && isspace(*in); ++in); - return in; - } - --static void -+static bool - display_gemini(struct browser *browser, struct gemini_response *resp) - { - // TODO: Strip ANSI escape sequences -@@ -136,29 +202,36 @@ } - ++row; - col = 0; - -- // TODO: It would be nice if we could follow links from this -- // prompt -- if (browser->pagination && row >= ws.ws_row - 1) { -- fprintf(browser->tty, "[Enter for more, or q to stop] "); -+ if (browser->pagination && row >= ws.ws_row - 4) { -+ char prompt[4096]; -+ snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -+ "[Enter]: read more; [n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" -+ "(more) => ", resp->meta, browser->plain_url); -+ enum prompt_result result = PROMPT_AGAIN; -+ while (result == PROMPT_AGAIN) { -+ result = do_prompts(prompt, browser); -+ } - -- size_t n = 0; -- char *l = NULL; -- if (getline(&l, &n, browser->tty) == -1) { -- return; -- } -- if (strcmp(l, "q\n") == 0) { -- return; -+ switch (result) { -+ case PROMPT_AGAIN: -+ case PROMPT_MORE: -+ break; -+ case PROMPT_QUIT: -+ browser->running = false; -+ return true; -+ case PROMPT_ANSWERED: -+ return true; - } - -- free(l); - row = col = 0; - } - } - - gemini_parser_finish(&p); -+ return false; - } - --static void -+static bool - display_plaintext(struct browser *browser, struct gemini_response *resp) - { - // TODO: Strip ANSI escape sequences -@@ -175,20 +248,20 @@ } - } - - (void)row; (void)col; // TODO: generalize pagination -+ return false; - } - --static void -+static bool - display_response(struct browser *browser, struct gemini_response *resp) - { - if (strcmp(resp->meta, "text/gemini") == 0 - || strncmp(resp->meta, "text/gemini;", 12) == 0) { -- display_gemini(browser, resp); -- return; -+ return display_gemini(browser, resp); - } - if (strncmp(resp->meta, "text/", 5) == 0) { -- display_plaintext(browser, resp); -- return; -+ return display_plaintext(browser, resp); - } -+ assert(0); // TODO: Deal with other mimetypes - } - - static char * -@@ -225,19 +298,19 @@ } - return input; - } - --static char * -+// Returns true to skip prompting -+static bool - do_requests(struct browser *browser, struct gemini_response *resp) - { -- char *plain_url; - int nredir = 0; - bool requesting = true; - while (requesting) { - CURLUcode uc = curl_url_get(browser->url, -- CURLUPART_URL, &plain_url, 0); -+ CURLUPART_URL, &browser->plain_url, 0); - assert(uc == CURLUE_OK); // Invariant - -- enum gemini_result res = gemini_request( -- plain_url, &browser->opts, resp); -+ enum gemini_result res = gemini_request(browser->plain_url, -+ &browser->opts, resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); - requesting = false; -@@ -253,7 +326,8 @@ requesting = false; - break; - } - -- char *new_url = gemini_input_url(plain_url, input); -+ char *new_url = gemini_input_url( -+ browser->plain_url, input); - assert(new_url); - set_url(browser, new_url, NULL); - break; -@@ -278,8 +352,7 @@ resp->status, resp->meta); - break; - case GEMINI_STATUS_CLASS_SUCCESS: - requesting = false; -- display_response(browser, resp); -- break; -+ return display_response(browser, resp); - } - - if (requesting) { -@@ -287,63 +360,7 @@ gemini_response_finish(resp); - } - } - -- return plain_url; --} -- --static bool --do_prompts(const char *prompt, struct browser *browser) --{ -- bool prompting = true; -- while (prompting) { -- fprintf(browser->tty, "%s", prompt); -- -- size_t l = 0; -- char *in = NULL; -- ssize_t n = getline(&in, &l, browser->tty); -- if (n == -1 && feof(browser->tty)) { -- return false; -- } -- if (strcmp(in, "q\n") == 0) { -- return false; -- } -- if (strcmp(in, "b\n") == 0) { -- if (!browser->history->prev) { -- fprintf(stderr, "At beginning of history\n"); -- continue; -- } -- browser->history = browser->history->prev; -- set_url(browser, browser->history->url, NULL); -- break; -- } -- if (strcmp(in, "f\n") == 0) { -- if (!browser->history->next) { -- fprintf(stderr, "At end of history\n"); -- continue; -- } -- browser->history = browser->history->next; -- set_url(browser, browser->history->url, NULL); -- break; -- } -- -- struct link *link = browser->links; -- char *endptr; -- int linksel = (int)strtol(in, &endptr, 10); -- if (endptr[0] == '\n' && linksel >= 0) { -- while (linksel > 0 && link) { -- link = link->next; -- --linksel; -- } -- -- if (!link) { -- fprintf(stderr, "Error: no such link.\n"); -- } else { -- set_url(browser, link->url, &browser->history); -- break; -- } -- } -- free(in); -- } -- return true; -+ return false; - } - - int -@@ -381,24 +398,38 @@ SSL_load_error_strings(); - ERR_load_crypto_strings(); - browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); - -- bool run = true; - struct gemini_response resp; -- while (run) { -+ browser.running = true; -+ while (browser.running) { - static char prompt[4096]; -- char *plain_url = do_requests(&browser, &resp); -+ if (do_requests(&browser, &resp)) { -+ // Skip prompts -+ goto next; -+ } - -- snprintf(prompt, sizeof(prompt), "\n%s%s at %s\n" -- "[n]: follow Nth link; [o <url>]: open URL; " -- "[b]ack; [f]orward; " -- "[q]uit\n" -+ snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -+ "[n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" - "=> ", -- resp.status == GEMINI_STATUS_SUCCESS ? " " : "", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", -- plain_url); -+ browser.plain_url); - gemini_response_finish(&resp); - -- run = do_prompts(prompt, &browser); -+ enum prompt_result result = PROMPT_AGAIN; -+ while (result == PROMPT_AGAIN || result == PROMPT_MORE) { -+ result = do_prompts(prompt, &browser); -+ } -+ switch (result) { -+ case PROMPT_AGAIN: -+ case PROMPT_MORE: -+ assert(0); -+ case PROMPT_QUIT: -+ browser.running = false; -+ break; -+ case PROMPT_ANSWERED: -+ break; -+ } - -+next:; - struct link *link = browser.links; - while (link) { - struct link *next = link->next; diff --git a/sources/cgmnlm.git/commits/5799323f4c92181a3446a729366b230456e93c81.patch b/sources/cgmnlm.git/commits/5799323f4c92181a3446a729366b230456e93c81.patch @@ -1,135 +0,0 @@ -diff --git a/include/gmni.h b/include/gmni.h -index d78d1c8eb4e94507c363219573691e60d40e8ea8..4240c6231010ebb86aead2d49700fe2e3d00b65c 100644 ---- a/include/gmni.h -+++ b/include/gmni.h -@@ -2,6 +2,7 @@ #ifndef GEMINI_CLIENT_H - #define GEMINI_CLIENT_H - #include <netdb.h> - #include <openssl/ssl.h> -+#include <stdbool.h> - #include <sys/socket.h> - - enum gemini_result { -@@ -106,7 +107,9 @@ - enum gemini_tok { - GEMINI_TEXT, - GEMINI_LINK, -- GEMINI_PREFORMATTED, -+ GEMINI_PREFORMATTED_BEGIN, -+ GEMINI_PREFORMATTED_END, -+ GEMINI_PREFORMATTED_TEXT, - GEMINI_HEADING, - GEMINI_LIST_ITEM, - GEMINI_QUOTE, -@@ -124,10 +127,7 @@ char *text; - char *url; // May be NULL - } link; - -- struct { -- char *text; -- char *alt_text; // May be NULL -- } preformatted; -+ char *preformatted; - - struct { - char *title; -@@ -144,6 +144,7 @@ BIO *f; - char *buf; - size_t bufsz; - size_t bufln; -+ bool preformatted; - }; - - // Initializes a text/gemini parser which reads from the specified BIO. -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 5e5d828f6f246e0baa3917177acd8cb6482387f8..8841c4613e208e379c6cd768a65a9a3b631d3146 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -278,8 +278,15 @@ } else { - col += fprintf(out, " "); - } - break; -- case GEMINI_PREFORMATTED: -- continue; // TODO -+ case GEMINI_PREFORMATTED_BEGIN: -+ case GEMINI_PREFORMATTED_END: -+ continue; // Not used -+ case GEMINI_PREFORMATTED_TEXT: -+ col += fprintf(out, "` "); -+ if (text == NULL) { -+ text = tok.text; -+ } -+ break; - case GEMINI_HEADING: - if (text == NULL) { - for (int n = tok.heading.level; n; --n) { -diff --git a/src/parser.c b/src/parser.c -index b9db3d2000f1176df4a21300f7b806ed6a5ded75..5b0f01399d934f593cdf987e096dd2cfb134d114 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -1,6 +1,7 @@ - #include <assert.h> - #include <ctype.h> - #include <openssl/bio.h> -+#include <stdbool.h> - #include <stddef.h> - #include <stdlib.h> - #include <string.h> -@@ -15,6 +16,7 @@ p->bufsz = BUFSIZ; - p->buf = malloc(p->bufsz + 1); - p->buf[0] = 0; - BIO_up_ref(p->f); -+ p->preformatted = false; - } - - void -@@ -57,7 +59,15 @@ if ((end = strstr(p->buf, "\n")) != NULL) { - *end = 0; - } - -- if (strncmp(p->buf, "=>", 2) == 0) { -+ if (p->preformatted) { -+ if (strncmp(p->buf, "```", 3) == 0) { -+ tok->token = GEMINI_PREFORMATTED_END; -+ p->preformatted = false; -+ } else { -+ tok->token = GEMINI_PREFORMATTED_TEXT; -+ tok->preformatted = strdup(p->buf); -+ } -+ } else if (strncmp(p->buf, "=>", 2) == 0) { - tok->token = GEMINI_LINK; - int i = 2; - while (p->buf[i] && isspace(p->buf[i])) ++i; -@@ -76,9 +86,11 @@ } - - tok->link.url = strdup(tok->link.url); - } else if (strncmp(p->buf, "```", 3) == 0) { -- tok->token = GEMINI_PREFORMATTED; // TODO -- tok->preformatted.text = strdup("<text>"); -- tok->preformatted.alt_text = strdup("<alt-text>"); -+ tok->token = GEMINI_PREFORMATTED_BEGIN; -+ if (p->buf[3]) { -+ tok->preformatted = strdup(&p->buf[3]); -+ } -+ p->preformatted = true; - } else if (p->buf[0] == '#') { - tok->token = GEMINI_HEADING; - int level = 1; -@@ -125,9 +137,14 @@ case GEMINI_LINK: - free(tok->link.text); - free(tok->link.url); - break; -- case GEMINI_PREFORMATTED: -- free(tok->preformatted.text); -- free(tok->preformatted.alt_text); -+ case GEMINI_PREFORMATTED_BEGIN: -+ free(tok->preformatted); -+ break; -+ case GEMINI_PREFORMATTED_TEXT: -+ free(tok->preformatted); -+ break; -+ case GEMINI_PREFORMATTED_END: -+ // Nothing to free - break; - case GEMINI_HEADING: - free(tok->heading.title); diff --git a/sources/cgmnlm.git/commits/59d19b9894083cecafc4439f7df1031bd6cefb01.patch b/sources/cgmnlm.git/commits/59d19b9894083cecafc4439f7df1031bd6cefb01.patch @@ -1,33 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index f218f0a43c1d32223e6bb73ecc5069c5cd0cf21a..92e866a2a58a222f56993e57849c348ead658ab4 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -157,7 +157,7 @@ } - - char *title = browser->page_title; - if (title) { -- title = trim_ws(strdup(browser->page_title)); -+ title = trim_ws(browser->page_title); - } - - fprintf(f, "=> %s%s%s\n", browser->plain_url, -@@ -166,17 +166,15 @@ fclose(f); - - fprintf(browser->tty, "Bookmark saved: %s\n", - title ? title : browser->plain_url); -- if (title != NULL) { -- free(title); -- } - } - - static void - open_bookmarks(struct browser *browser) - { -- const char *path_fmt = get_data_pathfmt(); -+ char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); - static char url[PATH_MAX+1+7]; - snprintf(url, sizeof(url), "file://%s", path); - set_url(browser, url, &browser->history); diff --git a/sources/cgmnlm.git/commits/59d43726bb18a1e240a7188b3dd33af5876a126e.patch b/sources/cgmnlm.git/commits/59d43726bb18a1e240a7188b3dd33af5876a126e.patch @@ -1,21 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index 34d25f3794f8069b15a688ebd89b7bf82fdd01e2..d8b67d7b9f47b4473ba6eb278853ea0565739f09 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -59,7 +59,7 @@ { - struct addrinfo *addr; - enum gemini_result res = gemini_get_addrinfo(uri, options, resp, &addr); - if (res != GEMINI_OK) { -- goto cleanup; -+ return res; - } - - struct addrinfo *rp; -@@ -79,7 +79,6 @@ res = GEMINI_ERR_CONNECT; - return res; - } - --cleanup: - if (!options || !options->addr) { - freeaddrinfo(addr); - } diff --git a/sources/cgmnlm.git/commits/5a955c5f241b87018dfb0cda6872dc7ae2784222.patch b/sources/cgmnlm.git/commits/5a955c5f241b87018dfb0cda6872dc7ae2784222.patch @@ -1,456 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 273423b8261a9608c89505b4e154899806f994f4..15256bbd0dcd1c6899e8e91ba5228baf93ce289f 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -22,6 +22,16 @@ char *url; - struct history *prev, *next; - }; - -+struct browser { -+ bool pagination; -+ struct gemini_options opts; -+ -+ FILE *tty; -+ struct Curl_URL *url; -+ struct link *links; -+ struct history *history; -+}; -+ - static void - usage(const char *argv_0) - { -@@ -39,7 +49,7 @@ free(history); - } - - static bool --set_url(struct Curl_URL *url, char *new_url, struct history **history) -+set_url(struct browser *browser, char *new_url, struct history **history) - { - if (history) { - struct history *next = calloc(1, sizeof(struct history)); -@@ -53,7 +63,7 @@ (*history)->next = next; - } - *history = next; - } -- if (curl_url_set(url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { -+ if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { - fprintf(stderr, "Error: invalid URL\n"); - return false; - } -@@ -71,27 +81,29 @@ return in; - } - - static void --display_gemini(FILE *tty, struct gemini_response *resp, -- struct link **next, bool pagination) -+display_gemini(struct browser *browser, struct gemini_response *resp) - { -+ // TODO: Strip ANSI escape sequences - int nlinks = 0; - struct gemini_parser p; - gemini_parser_init(&p, resp->bio); - - struct winsize ws; -- ioctl(fileno(tty), TIOCGWINSZ, &ws); -+ ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - - int row = 0, col = 0; - struct gemini_token tok; -+ struct link **next = &browser->links; - while (gemini_parser_next(&p, &tok) == 0) { - switch (tok.token) { - case GEMINI_TEXT: - // TODO: word wrap -- col += fprintf(tty, " %s\n", trim_ws(tok.text)); -+ col += fprintf(browser->tty, " %s\n", -+ trim_ws(tok.text)); - break; - case GEMINI_LINK: -- col += fprintf(tty, "[%d] %s\n", nlinks++, trim_ws( -- tok.link.text ? tok.link.text : tok.link.url)); -+ col += fprintf(browser->tty, "[%d] %s\n", nlinks++, -+ trim_ws(tok.link.text ? tok.link.text : tok.link.url)); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); - next = &(*next)->next; -@@ -100,17 +112,20 @@ case GEMINI_PREFORMATTED: - continue; // TODO - case GEMINI_HEADING: - for (int n = tok.heading.level; n; --n) { -- col += fprintf(tty, "#"); -+ col += fprintf(browser->tty, "#"); - } -- col += fprintf(tty, " %s\n", trim_ws(tok.heading.title)); -+ col += fprintf(browser->tty, " %s\n", -+ trim_ws(tok.heading.title)); - break; - case GEMINI_LIST_ITEM: - // TODO: Option to disable Unicode -- col += fprintf(tty, " • %s\n", trim_ws(tok.list_item)); -+ col += fprintf(browser->tty, " • %s\n", -+ trim_ws(tok.list_item)); - break; - case GEMINI_QUOTE: - // TODO: Option to disable Unicode -- col += fprintf(tty, " | %s\n", trim_ws(tok.quote_text)); -+ col += fprintf(browser->tty, " | %s\n", -+ trim_ws(tok.quote_text)); - break; - } - -@@ -121,12 +136,14 @@ } - ++row; - col = 0; - -- if (pagination && row >= ws.ws_row - 1) { -- fprintf(tty, "[Enter for more, or q to stop] "); -+ // TODO: It would be nice if we could follow links from this -+ // prompt -+ if (browser->pagination && row >= ws.ws_row - 1) { -+ fprintf(browser->tty, "[Enter for more, or q to stop] "); - - size_t n = 0; - char *l = NULL; -- if (getline(&l, &n, tty) == -1) { -+ if (getline(&l, &n, browser->tty) == -1) { - return; - } - if (strcmp(l, "q\n") == 0) { -@@ -142,52 +159,163 @@ gemini_parser_finish(&p); - } - - static void --display_plaintext(FILE *tty, struct gemini_response *resp, bool pagination) -+display_plaintext(struct browser *browser, struct gemini_response *resp) - { -+ // TODO: Strip ANSI escape sequences - struct winsize ws; - int row = 0, col = 0; -- ioctl(fileno(tty), TIOCGWINSZ, &ws); -+ ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - - char buf[BUFSIZ]; - int n; - while ((n = BIO_read(resp->bio, buf, sizeof(buf)) != 0)) { - while (n) { -- n -= fwrite(buf, 1, n, tty); -+ n -= fwrite(buf, 1, n, browser->tty); - } - } - -- (void)pagination; (void)row; (void)col; // TODO: generalize pagination -+ (void)row; (void)col; // TODO: generalize pagination - } - - static void --display_response(FILE *tty, struct gemini_response *resp, -- struct link **next, bool pagination) -+display_response(struct browser *browser, struct gemini_response *resp) - { - if (strcmp(resp->meta, "text/gemini") == 0 - || strncmp(resp->meta, "text/gemini;", 12) == 0) { -- display_gemini(tty, resp, next, pagination); -+ display_gemini(browser, resp); - return; - } - if (strncmp(resp->meta, "text/", 5) == 0) { -- display_plaintext(tty, resp, pagination); -+ display_plaintext(browser, resp); - return; - } - } - -+static char * -+do_requests(struct browser *browser, struct gemini_response *resp) -+{ -+ char *plain_url; -+ int nredir = 0; -+ bool requesting = true; -+ while (requesting) { -+ CURLUcode uc = curl_url_get(browser->url, -+ CURLUPART_URL, &plain_url, 0); -+ assert(uc == CURLUE_OK); // Invariant -+ -+ enum gemini_result res = gemini_request( -+ plain_url, &browser->opts, resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); -+ requesting = false; -+ break; -+ } -+ -+ switch (gemini_response_class(resp->status)) { -+ case GEMINI_STATUS_CLASS_INPUT: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_REDIRECT: -+ if (++nredir >= 5) { -+ requesting = false; -+ fprintf(stderr, "Error: maximum redirects (5) exceeded"); -+ break; -+ } -+ fprintf(stderr, "Following redirect to %s\n", resp->meta); -+ set_url(browser, resp->meta, NULL); -+ break; -+ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -+ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -+ requesting = false; -+ fprintf(stderr, "Server returned %s %d %s\n", -+ resp->status / 10 == 4 ? -+ "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ resp->status, resp->meta); -+ break; -+ case GEMINI_STATUS_CLASS_SUCCESS: -+ requesting = false; -+ display_response(browser, resp); -+ break; -+ } -+ -+ if (requesting) { -+ gemini_response_finish(resp); -+ } -+ } -+ -+ return plain_url; -+} -+ -+static bool -+do_prompts(const char *prompt, struct browser *browser) -+{ -+ bool prompting = true; -+ while (prompting) { -+ fprintf(browser->tty, "%s", prompt); -+ -+ size_t l = 0; -+ char *in = NULL; -+ ssize_t n = getline(&in, &l, browser->tty); -+ if (n == -1 && feof(browser->tty)) { -+ return false; -+ } -+ if (strcmp(in, "q\n") == 0) { -+ return false; -+ } -+ if (strcmp(in, "b\n") == 0) { -+ if (!browser->history->prev) { -+ fprintf(stderr, "At beginning of history\n"); -+ continue; -+ } -+ browser->history = browser->history->prev; -+ set_url(browser, browser->history->url, NULL); -+ break; -+ } -+ if (strcmp(in, "f\n") == 0) { -+ if (!browser->history->next) { -+ fprintf(stderr, "At end of history\n"); -+ continue; -+ } -+ browser->history = browser->history->next; -+ set_url(browser, browser->history->url, NULL); -+ break; -+ } -+ -+ struct link *link = browser->links; -+ char *endptr; -+ int linksel = (int)strtol(in, &endptr, 10); -+ if (endptr[0] == '\n' && linksel >= 0) { -+ while (linksel > 0 && link) { -+ link = link->next; -+ --linksel; -+ } -+ -+ if (!link) { -+ fprintf(stderr, "Error: no such link.\n"); -+ } else { -+ set_url(browser, link->url, &browser->history); -+ break; -+ } -+ } -+ free(in); -+ } -+ return true; -+} -+ - int - main(int argc, char *argv[]) - { -- bool pagination = true; -- -- struct Curl_URL *url = curl_url(); -- -- FILE *tty = fopen("/dev/tty", "w+"); -+ struct browser browser = { -+ .pagination = true, -+ .url = curl_url(), -+ .tty = fopen("/dev/tty", "w+"), -+ }; - - int c; - while ((c = getopt(argc, argv, "hP")) != -1) { - switch (c) { - case 'P': -- pagination = false; -+ browser.pagination = false; - break; - case 'h': - usage(argv[0]); -@@ -198,9 +326,8 @@ return 1; - } - } - -- struct history *history; - if (optind == argc - 1) { -- set_url(url, argv[optind], &history); -+ set_url(&browser, argv[optind], &browser.history); - } else { - usage(argv[0]); - return 1; -@@ -208,65 +335,13 @@ } - - SSL_load_error_strings(); - ERR_load_crypto_strings(); -- struct gemini_options opts = { -- .ssl_ctx = SSL_CTX_new(TLS_method()), -- }; -+ browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); - - bool run = true; - struct gemini_response resp; - while (run) { -- struct link *links; - static char prompt[4096]; -- char *plain_url; -- -- int nredir = 0; -- bool requesting = true; -- while (requesting) { -- CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); -- assert(uc == CURLUE_OK); // Invariant -- -- enum gemini_result res = gemini_request( -- plain_url, &opts, &resp); -- if (res != GEMINI_OK) { -- fprintf(stderr, "Error: %s\n", -- gemini_strerr(res, &resp)); -- requesting = false; -- break; -- } -- -- switch (gemini_response_class(resp.status)) { -- case GEMINI_STATUS_CLASS_INPUT: -- assert(0); // TODO -- case GEMINI_STATUS_CLASS_REDIRECT: -- if (++nredir >= 5) { -- requesting = false; -- fprintf(stderr, "Error: maximum redirects (5) exceeded"); -- break; -- } -- fprintf(stderr, -- "Following redirect to %s\n", resp.meta); -- set_url(url, resp.meta, NULL); -- break; -- case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -- assert(0); // TODO -- case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -- case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -- requesting = false; -- fprintf(stderr, "Server returned %s %d %s\n", -- resp.status / 10 == 4 ? -- "TEMPORARY FAILURE" : "PERMANENT FALIURE", -- resp.status, resp.meta); -- break; -- case GEMINI_STATUS_CLASS_SUCCESS: -- requesting = false; -- display_response(tty, &resp, &links, pagination); -- break; -- } -- -- if (requesting) { -- gemini_response_finish(&resp); -- } -- } -+ char *plain_url = do_requests(&browser, &resp); - - snprintf(prompt, sizeof(prompt), "\n%s%s at %s\n" - "[n]: follow Nth link; [o <url>]: open URL; " -@@ -276,74 +351,22 @@ "=> ", - resp.status == GEMINI_STATUS_SUCCESS ? " " : "", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", - plain_url); -- - gemini_response_finish(&resp); - -- bool prompting = true; -- while (prompting) { -- fprintf(tty, "%s", prompt); -+ run = do_prompts(prompt, &browser); - -- size_t l = 0; -- char *in = NULL; -- ssize_t n = getline(&in, &l, tty); -- if (n == -1 && feof(tty)) { -- run = false; -- break; -- } -- if (strcmp(in, "q\n") == 0) { -- run = false; -- break; -- } -- if (strcmp(in, "b\n") == 0) { -- if (!history->prev) { -- fprintf(stderr, "At beginning of history\n"); -- continue; -- } -- history = history->prev; -- set_url(url, history->url, NULL); -- break; -- } -- if (strcmp(in, "f\n") == 0) { -- if (!history->next) { -- fprintf(stderr, "At end of history\n"); -- continue; -- } -- history = history->next; -- set_url(url, history->url, NULL); -- break; -- } -- -- struct link *link = links; -- char *endptr; -- int linksel = (int)strtol(in, &endptr, 10); -- if (endptr[0] == '\n' && linksel >= 0) { -- while (linksel > 0 && link) { -- link = link->next; -- --linksel; -- } -- -- if (!link) { -- fprintf(stderr, "Error: no such link.\n"); -- } else { -- set_url(url, link->url, &history); -- break; -- } -- } -- free(in); -- } -- -- struct link *link = links; -+ struct link *link = browser.links; - while (link) { - struct link *next = link->next; - free(link->url); - free(link); - link = next; - } -- links = NULL; -+ browser.links = NULL; - } - -- history_free(history); -- SSL_CTX_free(opts.ssl_ctx); -- curl_url_cleanup(url); -+ history_free(browser.history); -+ SSL_CTX_free(browser.opts.ssl_ctx); -+ curl_url_cleanup(browser.url); - return 0; - } diff --git a/sources/cgmnlm.git/commits/5ad3f0aaccbcf328756d0eaad0e98068587395d1.patch b/sources/cgmnlm.git/commits/5ad3f0aaccbcf328756d0eaad0e98068587395d1.patch @@ -1,61 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 25afbaf0ce2621c4559fd34a035a9f944485aa3d..7fba942066c9d9870f78311f050ef19dc42f7ce1 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -10,6 +10,7 @@ #include <stdbool.h> - #include <stdio.h> - #include <string.h> - #include <sys/ioctl.h> -+#include <sys/stat.h> - #include <termios.h> - #include <unistd.h> - #include "gmni.h" -@@ -52,6 +53,17 @@ PROMPT_QUIT, - PROMPT_ANSWERED, - PROMPT_NEXT, - }; -+ -+const char *default_bookmarks = -+ "# Welcome to gmni\n\n" -+ "Links:\n\n" -+ // TODO: sub out the URL for the appropriate geminispace version once -+ // sr.ht supports gemini -+ "=> https://sr.ht/~sircmpwn/gmni The gmni browser\n" -+ "=> gemini://gemini.circumlunar.space The gemini protocol\n\n" -+ "This file can be found at %s and may be edited at your pleasure.\n\n" -+ "Bookmarks:\n" -+ ; - - const char *help_msg = - "The following commands are available:\n\n" -@@ -175,6 +187,20 @@ char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); -+ -+ struct stat buf; -+ if (stat(path, &buf) == -1 && errno == ENOENT) { -+ // TOCTOU, but we almost certainly don't care -+ FILE *f = fopen(path, "a"); -+ if (f == NULL) { -+ fprintf(stderr, "Error opening %s for writing: %s\n", -+ path, strerror(errno)); -+ return; -+ } -+ fprintf(f, default_bookmarks, path); -+ fclose(f); -+ } -+ - static char url[PATH_MAX+1+7]; - snprintf(url, sizeof(url), "file://%s", path); - set_url(browser, url, &browser->history); -@@ -867,8 +893,7 @@ if (!set_url(&browser, argv[optind], &browser.history)) { - return 1; - } - } else { -- usage(argv[0]); -- return 1; -+ open_bookmarks(&browser); - } - - SSL_load_error_strings(); diff --git a/sources/cgmnlm.git/commits/5d3ae7b7f52ba83428ba8d728712e8c1710b2ea0.patch b/sources/cgmnlm.git/commits/5d3ae7b7f52ba83428ba8d728712e8c1710b2ea0.patch @@ -1,20 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index de3746563ed3e0c515a5c52a053f3a535fda5e09..af5d9f20d78558b3ff7dc9253bef53f2deef1d25 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -175,13 +175,14 @@ tofu->callback = cb; - tofu->cb_data = cb_data; - SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu); - -+ tofu->known_hosts = NULL; -+ - FILE *f = fopen(tofu->known_hosts_path, "r"); - if (!f) { - return; - } - size_t n = 0; - char *line = NULL; -- tofu->known_hosts = NULL; - while (getline(&line, &n, f) != -1) { - struct known_host *host = calloc(1, sizeof(struct known_host)); - char *tok = strtok(line, " "); diff --git a/sources/cgmnlm.git/commits/5fd43e8d02ffea38b5e4a3531e366f2b9b510201.patch b/sources/cgmnlm.git/commits/5fd43e8d02ffea38b5e4a3531e366f2b9b510201.patch @@ -1,15 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 4af98fbbe70b09ce5a7fce46863438b0487d8738..245ba76363b58e9c9ddbf87464d48cfb38463892 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -315,8 +315,8 @@ last = buf[n - 1]; - } - ssize_t w = 0; - while (w < (ssize_t)n) { -- ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); -- if (x == -1) { -+ ssize_t x = fwrite(&buf[w], 1, n - w, stdout); -+ if (ferror(stdout)) { - fprintf(stderr, "Error: write: %s\n", - strerror(errno)); - return 1; diff --git a/sources/cgmnlm.git/commits/601f9008863af571980c1cd39920483d59cfbfb4.patch b/sources/cgmnlm.git/commits/601f9008863af571980c1cd39920483d59cfbfb4.patch @@ -1,243 +0,0 @@ -diff --git a/configure b/configure -index 151bdae8c12d9ad07d3a5240d7c554f4c62664c8..7412cf545fa96e6be169c0055778b83a06b557f5 100755 ---- a/configure -+++ b/configure -@@ -16,7 +16,8 @@ src/client.c \ - src/escape.c \ - src/gmnlm.c \ - src/parser.c \ -- src/url.c -+ src/url.c \ -+ src/util.c - } - - all="gmni gmnlm" -diff --git a/include/util.h b/include/util.h -new file mode 100644 -index 0000000000000000000000000000000000000000..cf731bfdc75a750df6f18873f48dd859786587c7 ---- /dev/null -+++ b/include/util.h -@@ -0,0 +1,12 @@ -+#ifndef GEMINI_UTIL_H -+#define GEMINI_UTIL_H -+ -+struct pathspec { -+ const char *var; -+ const char *path; -+}; -+ -+char *getpath(const struct pathspec *paths, size_t npaths); -+int mkdirs(char *path, mode_t mode); -+ -+#endif -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 4b844200eeb4318d694566d7a84eb3e64c7c7668..c5d5da36ffad8772315928e8a69b785d994511e4 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -12,6 +12,7 @@ #include <termios.h> - #include <unistd.h> - #include "gmni.h" - #include "url.h" -+#include "util.h" - - struct link { - char *url; -@@ -29,6 +30,7 @@ struct gemini_options opts; - - FILE *tty; - char *plain_url; -+ char *page_title; - struct Curl_URL *url; - struct link *links; - struct history *history; -@@ -52,6 +54,8 @@ "q\tQuit\n" - "N\tFollow Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" -+ "m\tSave bookmark\n" -+ "M\tBrowse bookmarks\n" - "\n" - "Other commands include:\n\n" - "<Enter>\tread more lines\n" -@@ -98,6 +102,63 @@ } - return true; - } - -+static char * -+get_data_pathfmt() -+{ -+ const struct pathspec paths[] = { -+ {.var = "GMNIDATA", .path = "/%s"}, -+ {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, -+ {.var = "HOME", .path = "/.local/share/gmni/%s"} -+ }; -+ return getpath(paths, sizeof(paths) / sizeof(paths[0])); -+} -+ -+static char * -+trim_ws(char *in) -+{ -+ while (*in && isspace(*in)) ++in; -+ return in; -+} -+ -+static void -+save_bookmark(struct browser *browser) -+{ -+ const char *path_fmt = get_data_pathfmt(); -+ static char path[PATH_MAX+1]; -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ mkdirs(path, 0755); -+ -+ FILE *f = fopen(path, "a"); -+ if (!f) { -+ fprintf(stderr, "Error opening %s for writing: %s\n", -+ path, strerror(errno)); -+ return; -+ } -+ -+ char *title = browser->page_title; -+ if (title) { -+ title = trim_ws(browser->page_title); -+ } -+ -+ fprintf(f, "=> %s%s%s\n", browser->plain_url, -+ title ? " " : "", title ? title : ""); -+ fclose(f); -+ -+ fprintf(browser->tty, "Bookmark saved: %s\n", -+ title ? title : browser->plain_url); -+} -+ -+static void -+open_bookmarks(struct browser *browser) -+{ -+ const char *path_fmt = get_data_pathfmt(); -+ static char path[PATH_MAX+1]; -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ static char url[PATH_MAX+1+7]; -+ snprintf(url, sizeof(url), "file://%s", path); -+ set_url(browser, url, &browser->history); -+} -+ - static enum prompt_result - do_prompts(const char *prompt, struct browser *browser) - { -@@ -134,6 +195,16 @@ browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; -+ case 'm': -+ if (in[1]) break; -+ save_bookmark(browser); -+ result = PROMPT_AGAIN; -+ goto exit; -+ case 'M': -+ if (in[1]) break; -+ open_bookmarks(browser); -+ result = PROMPT_ANSWERED; -+ goto exit; - case 'f': - if (in[1]) break; - if (!browser->history->next) { -@@ -205,13 +276,6 @@ free(in); - return result; - } - --static char * --trim_ws(char *in) --{ -- while (*in && isspace(*in)) ++in; -- return in; --} -- - static int - wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col) - { -@@ -258,6 +322,8 @@ { - int nlinks = 0; - struct gemini_parser p; - gemini_parser_init(&p, resp->bio); -+ free(browser->page_title); -+ browser->page_title = NULL; - - struct winsize ws; - ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); -@@ -302,6 +368,9 @@ text = tok.text; - } - break; - case GEMINI_HEADING: -+ if (!browser->page_title) { -+ browser->page_title = strdup(tok.heading.title); -+ } - if (text == NULL) { - for (int n = tok.heading.level; n; --n) { - col += fprintf(out, "#"); -diff --git a/src/util.c b/src/util.c -new file mode 100644 -index 0000000000000000000000000000000000000000..26e7283a26cc8e593bb3e3fd53d4b7a9bc646e3f ---- /dev/null -+++ b/src/util.c -@@ -0,0 +1,62 @@ -+#include <assert.h> -+#include <errno.h> -+#include <libgen.h> -+#include <limits.h> -+#include <stdlib.h> -+#include <string.h> -+#include <sys/stat.h> -+#include "util.h" -+ -+static void -+posix_dirname(char *path, char *dname) -+{ -+ char p[PATH_MAX+1]; -+ char *t; -+ -+ assert(strlen(path) <= PATH_MAX); -+ -+ strcpy(p, path); -+ t = dirname(path); -+ memmove(dname, t, strlen(t) + 1); -+ -+ /* restore the path if dirname worked in-place */ -+ if (t == path && path != dname) { -+ strcpy(path, p); -+ } -+} -+ -+/** Make directory and all of its parents */ -+int -+mkdirs(char *path, mode_t mode) -+{ -+ char dname[PATH_MAX + 1]; -+ posix_dirname(path, dname); -+ if (strcmp(dname, "/") == 0) { -+ return 0; -+ } -+ if (mkdirs(dname, mode) != 0) { -+ return -1; -+ } -+ if (mkdir(path, mode) != 0 && errno != EEXIST) { -+ return -1; -+ } -+ errno = 0; -+ return 0; -+} -+ -+char * -+getpath(const struct pathspec *paths, size_t npaths) { -+ for (size_t i = 0; i < npaths; i++) { -+ const char *var = ""; -+ if (paths[i].var) { -+ var = getenv(paths[i].var); -+ } -+ if (var) { -+ char *out = calloc(1, -+ strlen(var) + strlen(paths[i].path) + 1); -+ strcat(strcat(out, var), paths[i].path); -+ return out; -+ } -+ } -+ return NULL; -+} diff --git a/sources/cgmnlm.git/commits/60496bae0cbda1162ae00bc6f6f4047ba9c7d86f.patch b/sources/cgmnlm.git/commits/60496bae0cbda1162ae00bc6f6f4047ba9c7d86f.patch @@ -1,22 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 07b699134c0c71a20d4780148ad74cb246634549..da99a84580894312ba86b92e6abd02cfdfc80fd4 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -457,7 +457,6 @@ /* fallthrough */ - case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: -- col += fprintf(out, "` "); - if (text == NULL) { - text = tok.preformatted; - } -@@ -494,7 +493,8 @@ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: -- col += fprintf(out, "> "); -+ col += fprintf(out, "%s ", -+ browser->unicode ? "┃" : ">"); - if (text == NULL) { - text = trim_ws(tok.quote_text); - } diff --git a/sources/cgmnlm.git/commits/60cf41e7dd66897e6987704921b6cd57da1f084f.patch b/sources/cgmnlm.git/commits/60cf41e7dd66897e6987704921b6cd57da1f084f.patch @@ -1,62 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 41284df2235e869f49e542f5e1f2dcc240deb684..756520c728baad52206866a3af64b0ffebbac3cd 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -59,6 +59,7 @@ "q\tQuit\n" - "N\tFollow Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" -+ "H\tView all page history\n" - "m\tSave bookmark\n" - "M\tBrowse bookmarks\n" - "\n" -@@ -206,16 +207,6 @@ browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; -- case 'm': -- if (in[1]) break; -- save_bookmark(browser); -- result = PROMPT_AGAIN; -- goto exit; -- case 'M': -- if (in[1]) break; -- open_bookmarks(browser); -- result = PROMPT_ANSWERED; -- goto exit; - case 'f': - if (in[1]) break; - if (!browser->history->next) { -@@ -225,6 +216,32 @@ goto exit; - } - browser->history = browser->history->next; - set_url(browser, browser->history->url, NULL); -+ result = PROMPT_ANSWERED; -+ goto exit; -+ case 'H': -+ if (in[1]) break; -+ struct history *cur = browser->history; -+ while (cur->prev) cur = cur->prev; -+ while (cur != browser->history) { -+ fprintf(browser->tty, " %s\n", cur->url); -+ cur = cur->next; -+ } -+ fprintf(browser->tty, "* %s\n", cur->url); -+ cur = cur->next; -+ while (cur) { -+ fprintf(browser->tty, " %s\n", cur->url); -+ cur = cur->next; -+ } -+ result = PROMPT_AGAIN; -+ goto exit; -+ case 'm': -+ if (in[1]) break; -+ save_bookmark(browser); -+ result = PROMPT_AGAIN; -+ goto exit; -+ case 'M': -+ if (in[1]) break; -+ open_bookmarks(browser); - result = PROMPT_ANSWERED; - goto exit; - case '/': diff --git a/sources/cgmnlm.git/commits/61af57e302efd90458e17fa9f0bfaf5b3828954f.patch b/sources/cgmnlm.git/commits/61af57e302efd90458e17fa9f0bfaf5b3828954f.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 4f563ed19e0edc52086f55e4057a91f0802a1f93..1c3e5f2bebddf5b606d11acd8924562cabde9a6f 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -287,7 +287,7 @@ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: - if (header_mode == OMIT_HEADERS) { - fprintf(stderr, "%s: %d %s\n", - resp.status / 10 == 4 ? -- "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ "TEMPORARY FAILURE" : "PERMANENT FAILURE", - resp.status, resp.meta); - } - exit = true; diff --git a/sources/cgmnlm.git/commits/678bff58ed32e77c9af90a5d8fc7b1f3c38af86c.patch b/sources/cgmnlm.git/commits/678bff58ed32e77c9af90a5d8fc7b1f3c38af86c.patch @@ -1,31 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 81923101e320b8517405ef2ed7efdbc626241201..6d77264cabb0db317ddf0807953d327f2379d9be 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -168,7 +168,7 @@ col += fprintf(browser->tty, " %s\n", - trim_ws(tok.text)); - break; - case GEMINI_LINK: -- col += fprintf(browser->tty, "[%d] %s\n", nlinks++, -+ col += fprintf(browser->tty, "%d) %s\n", nlinks++, - trim_ws(tok.link.text ? tok.link.text : tok.link.url)); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); -@@ -205,7 +205,7 @@ - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[Enter]: read more; [n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" -+ "[Enter]: read more; [N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" - "(more) => ", resp->meta, browser->plain_url); - enum prompt_result result = PROMPT_AGAIN; - while (result == PROMPT_AGAIN) { -@@ -412,7 +412,7 @@ goto next; - } - - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" -+ "[N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" - "=> ", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", - browser.plain_url); diff --git a/sources/cgmnlm.git/commits/689fb8b470f19fb83ee1e32efe64b42d6961630c.patch b/sources/cgmnlm.git/commits/689fb8b470f19fb83ee1e32efe64b42d6961630c.patch @@ -1,28 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index bb508e04c8d41fe12aa99ae42b8fa30d4f3a9a2e..18b5115371bc6b13b9b796a85f7ad4f826eef973 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -236,11 +236,6 @@ if (!resp) { - return; - } - -- if (resp->fd != -1) { -- close(resp->fd); -- resp->fd = -1; -- } -- - if (resp->bio) { - BIO_free_all(resp->bio); - resp->bio = NULL; -@@ -253,6 +248,11 @@ if (resp->ssl_ctx) { - SSL_CTX_free(resp->ssl_ctx); - } - free(resp->meta); -+ -+ if (resp->fd != -1) { -+ close(resp->fd); -+ resp->fd = -1; -+ } - - resp->ssl = NULL; - resp->ssl_ctx = NULL; diff --git a/sources/cgmnlm.git/commits/6d2f78eeded101ccd755b1b2be16105fe5af881d.patch b/sources/cgmnlm.git/commits/6d2f78eeded101ccd755b1b2be16105fe5af881d.patch @@ -1,16 +0,0 @@ -diff --git a/configure b/configure -index aa761971fe95d70ed242977bed8dfd6d55a9f794..70a19b193d6e33b41b5cc7ef13511bb0f5b4e076 100755 ---- a/configure -+++ b/configure -@@ -16,7 +16,7 @@ cgmnlm() { - genrules cgmnlm \ - src/client.c \ - src/escape.c \ -- src/cgmnlm.c \ -+ src/gmnlm.c \ - src/parser.c \ - src/tofu.c \ - src/url.c \ -diff --git a/src/cgmnlm.c b/src/gmnlm.c -rename from src/cgmnlm.c -rename to src/gmnlm.c diff --git a/sources/cgmnlm.git/commits/6f36d2a0fc5de0a9d25229c47c75481f47f32c87.patch b/sources/cgmnlm.git/commits/6f36d2a0fc5de0a9d25229c47c75481f47f32c87.patch @@ -1,62 +0,0 @@ -diff --git a/Makefile b/Makefile -index 3d4df602cd7f7ab4f5a45b47dee0d47729f0739c..12ff4b45e4b043ae5c93629b502e42e8396b2b24 100644 ---- a/Makefile -+++ b/Makefile -@@ -13,6 +13,7 @@ @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnlm_objects) - - doc/gmni.1: doc/gmni.scd -+doc/gmnlm.1: doc/gmnlm.scd - - .SUFFIXES: .c .o .scd .1 - -@@ -27,10 +28,10 @@ .scd.1: - @printf 'SCDOC\t$@\n' - @$(SCDOC) < $< > $@ - --docs: doc/gmni.1 -+docs: doc/gmni.1 doc/gmnlm.1 - - clean: -- @rm -f gmni doc/gmni.1 -+ @rm -f gmni doc/gmni.1 doc/gmnlm.1 - - distclean: clean - @rm -rf "$(OUTDIR)" -@@ -39,6 +40,8 @@ install: all - mkdir -p $(BINDIR) - mkdir -p $(MANDIR)/man1 - install -Dm755 gmni $(BINDIR)/gmni -+ install -Dm755 gmnlm $(BINDIR)/gmnlm - install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 -+ install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 - - .PHONY: clean distclean docs install -diff --git a/doc/gmnlm.scd b/doc/gmnlm.scd -new file mode 100644 -index 0000000000000000000000000000000000000000..c5e7bf7f6b189f984e01ea2a942f47acb993f7e7 ---- /dev/null -+++ b/doc/gmnlm.scd -@@ -0,0 +1,22 @@ -+gmnlm(1) -+ -+# NAME -+ -+gmnlm - Gemini line-mode browser -+ -+# SYNPOSIS -+ -+*gmnlm* [-PU] _gemini://..._ -+ -+# DESCRIPTION -+ -+*gmnlm* is an interactive line-mode Gemini browser. -+ -+# OPTIONS -+ -+*-P* -+ Disable pagination. -+ -+*-U* -+ Disable conservative use of Unicode symbols to render Gemini layout -+ features. diff --git a/sources/cgmnlm.git/commits/71ececc4f264eed36f022b4b52c9100b9e7b1b12.patch b/sources/cgmnlm.git/commits/71ececc4f264eed36f022b4b52c9100b9e7b1b12.patch @@ -1,21 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index cb7e2843798f66f596e071823d9e41153a555fcc..c70a4126acc5337a768c3a70513aecc1b0e16668 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -89,6 +89,7 @@ "B\tBrowse bookmarks\n" - "r\tReload the page\n" - "d <path>\tDownload page to <path>\n" - "|<prog>\tPipe page into program\n" -+ "[N]|<prog>\tPipe content of Nth link into program\n" - "\n" - "Other commands include:\n\n" - "<Enter>\tread more lines\n" -@@ -791,7 +792,7 @@ } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(out, "%2d> %s", nlinks++, ANSI_COLOR_CYAN); -+ col += fprintf(out, "%2d) %s", nlinks++, ANSI_COLOR_CYAN); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); diff --git a/sources/cgmnlm.git/commits/73f5a5bc2b97fb36ded8feb36828d598d6e9fed3.patch b/sources/cgmnlm.git/commits/73f5a5bc2b97fb36ded8feb36828d598d6e9fed3.patch @@ -1,39 +0,0 @@ -diff --git a/include/client.h b/include/client.h -index 40729073bcaa2c0a172d3375662ac91cef0ea550..671de12e7ee53fbbfc03407760723d2a66458c2d 100644 ---- a/include/client.h -+++ b/include/client.h -@@ -39,11 +39,8 @@ enum gemini_result { - GEMINI_OK, - GEMINI_ERR_OOM, - GEMINI_ERR_INVALID_URL, -- // status is set to the return value from getaddrinfo - GEMINI_ERR_RESOLVE, -- // status is set to errno - GEMINI_ERR_CONNECT, -- // use SSL_get_error(resp->ssl, resp->status) to get details - GEMINI_ERR_SSL, - GEMINI_ERR_IO, - GEMINI_ERR_PROTOCOL, -@@ -52,17 +49,16 @@ - // Requests the specified URL via the gemini protocol. If options is non-NULL, - // it may specify some additional configuration to adjust client behavior. - // --// Returns a value indicating the success of the request. If GEMINI_OK is --// returned, the response details shall be written to the gemini_response --// argument. -+// Returns a value indicating the success of the request. -+// -+// Caller must call gemini_response_finish afterwards to clean up resources -+// before exiting or re-using it for another request. - enum gemini_result gemini_request(const char *url, - struct gemini_options *options, - struct gemini_response *resp); - - // Must be called after gemini_request in order to free up the resources --// allocated during the request. If you intend to re-use the SSL_CTX provided by --// gemini_options, set the ctx pointer to NULL before calling --// gemini_response_finish. -+// allocated during the request. - void gemini_response_finish(struct gemini_response *resp); - - // Returns a user-friendly string describing an error. diff --git a/sources/cgmnlm.git/commits/75087ce65f54c86d44ce13bb63e1041226a53f7b.patch b/sources/cgmnlm.git/commits/75087ce65f54c86d44ce13bb63e1041226a53f7b.patch @@ -1,20 +0,0 @@ -diff --git a/config.sh b/config.sh -index a6963622d4b41d6e25bb4fe230c2513849c077d3..55bc9a2ab6b15f91c655a4ab033fcbd6107b83fe 100644 ---- a/config.sh -+++ b/config.sh -@@ -144,11 +144,11 @@ CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' - - all: ${all} - EOF -- -- for target in $all -+ -+ for target in $(printf '%s\n' $all | tr '.' '_') - do -- ${target//./_} >>"$outdir"/config.mk -- done -+ $target -+ done >>"$outdir"/config.mk - echo done - - touch $outdir/cppcache diff --git a/sources/cgmnlm.git/commits/7619edcd116385414b55764a3401a0c66c04da79.patch b/sources/cgmnlm.git/commits/7619edcd116385414b55764a3401a0c66c04da79.patch @@ -1,13 +0,0 @@ -diff --git a/src/parser.c b/src/parser.c -index 04501b6b644af28abe7843076ae593b7165912b2..579415150f842557b6faf4097a63aac9324928fb 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -46,7 +46,7 @@ ssize_t n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln - 1); - if (n == -1) { - return -1; - } else if (n == 0) { -- eof = 1; -+ eof = p->bufln == 0; - break; - } - p->bufln += n; diff --git a/sources/cgmnlm.git/commits/77de1bb2a84e0980d23b7fc2dda1480a1093ca21.patch b/sources/cgmnlm.git/commits/77de1bb2a84e0980d23b7fc2dda1480a1093ca21.patch @@ -1,22 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 07d0000babe7cd51876ead407e83c3062adfda56..07b699134c0c71a20d4780148ad74cb246634549 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -69,7 +69,7 @@ const char *help_msg = - "The following commands are available:\n\n" - "q\tQuit\n" - "N\tFollow Nth link (where N is a number)\n" -- "p N\tShow URL of Nth link (where N is a number)\n" -+ "p[N]\tShow URL of Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" - "H\tView all page history\n" -@@ -303,7 +303,7 @@ result = PROMPT_AGAIN; - goto exit; - } - case 'p': -- if (!isspace(in[1])) break; -+ if (!in[1]) break; - struct link *link = browser->links; - char *endptr; - int linksel = (int)strtol(in+1, &endptr, 10); diff --git a/sources/cgmnlm.git/commits/78cfe1b669fad6b7a3638d371ed9825e2ee53243.patch b/sources/cgmnlm.git/commits/78cfe1b669fad6b7a3638d371ed9825e2ee53243.patch @@ -1,53 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index ef4c97950495e369e4fd3b1256c185fdb01f8cf1..8cafaea17de972c240031b0fe6ab88e3621bd871 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -179,8 +179,7 @@ - n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - assert(n < sizeof(path)); -- strncpy(dname, path, sizeof(dname)-1); -- dirname(dname); -+ posix_dirname(path, dname); - if (mkdirs(dname, 0755) != 0) { - fprintf(stderr, "Error creating directory %s: %s\n", - dname, strerror(errno)); -@@ -262,8 +261,7 @@ n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - - assert(n < sizeof(path)); -- strncpy(dname, path, sizeof(dname)-1); -- dirname(dname); -+ posix_dirname(path, dname); - if (mkdirs(dname, 0755) != 0) { - fprintf(stderr, "Error creating directory %s: %s\n", - dname, strerror(errno)); -diff --git a/src/tofu.c b/src/tofu.c -index 54183a79278de45bfbe5a1f8ddbcbf10be1c01c4..0923aed3f3d9ac02a7bc6b81db965d6924e4af74 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -177,20 +177,15 @@ - n = snprintf(tofu->known_hosts_path, - sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); -+ free(path_fmt); - assert(n < sizeof(tofu->known_hosts_path)); - -- strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1); -+ posix_dirname(tofu->known_hosts_path, dname); - if (mkdirs(dname, 0755) != 0) { -- snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -- path_fmt, "known_hosts"); -- fprintf(stderr, "Error creating directory %s: %s\n", -- dirname(tofu->known_hosts_path), strerror(errno)); -+ fprintf(stderr, "Error creating directory %s: %s\n", dname, -+ strerror(errno)); - return; - } -- -- snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -- path_fmt, "known_hosts"); -- free(path_fmt); - - tofu->callback = cb; - tofu->cb_data = cb_data; diff --git a/sources/cgmnlm.git/commits/78eb57cad45aa27e83d3a5e78be0bb5ce4d631f7.patch b/sources/cgmnlm.git/commits/78eb57cad45aa27e83d3a5e78be0bb5ce4d631f7.patch @@ -1,281 +0,0 @@ -diff --git a/include/client.h b/include/client.h -index f711eea91c3842ac29a884b38e0d7a0ab99cb7fb..40729073bcaa2c0a172d3375662ac91cef0ea550 100644 ---- a/include/client.h -+++ b/include/client.h -@@ -68,4 +68,8 @@ - // Returns a user-friendly string describing an error. - const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); - -+// Returns the given URL with the input response set to the specified value. -+// The caller must free the string. -+char *gemini_input_url(const char *url, const char *input); -+ - #endif -diff --git a/src/client.c b/src/client.c -index 67671ccca2a25a460681bdffacb981c7f26c1407..c252c9d9ab1783dd462343ce346cc74611e9ae85 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -176,7 +176,7 @@ } - - char *endptr; - resp->status = (int)strtol(buf, &endptr, 10); -- if (*endptr != ' ' || resp->status <= 10 || resp->status >= 70) { -+ if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) { - res = GEMINI_ERR_PROTOCOL; - goto cleanup; - } -@@ -195,14 +195,25 @@ { - if (!resp) { - return; - } -+ - if (resp->fd != -1) { - close(resp->fd); -+ resp->fd = -1; - } -- BIO_free(BIO_pop(resp->bio)); // ssl bio -- BIO_free(resp->bio); // buffered bio -+ -+ if (resp->bio) { -+ BIO_free(BIO_pop(resp->bio)); // ssl bio -+ BIO_free(resp->bio); // buffered bio -+ resp->bio = NULL; -+ } -+ - SSL_free(resp->ssl); - SSL_CTX_free(resp->ssl_ctx); - free(resp->meta); -+ -+ resp->ssl = NULL; -+ resp->ssl_ctx = NULL; -+ resp->meta = NULL; - } - - const char * -@@ -230,3 +241,26 @@ return "Protocol error"; - } - assert(0); - } -+ -+char * -+gemini_input_url(const char *url, const char *input) -+{ -+ char *new_url = NULL; -+ struct Curl_URL *uri = curl_url(); -+ if (!uri) { -+ return NULL; -+ } -+ if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { -+ goto cleanup; -+ } -+ if (curl_url_set(uri, CURLUPART_QUERY, input, CURLU_URLENCODE) != CURLUE_OK) { -+ goto cleanup; -+ } -+ if (curl_url_get(uri, CURLUPART_URL, &new_url, 0) != CURLUE_OK) { -+ new_url = NULL; -+ goto cleanup; -+ } -+cleanup: -+ curl_url_cleanup(uri); -+ return new_url; -+} -diff --git a/src/gmnic.c b/src/gmnic.c -index 014211dc3784ba3eadcf1885ef7bcf6fe84d8462..794b94ba2235757864acb81194b8de88220cc6da 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -26,10 +26,17 @@ OMIT_HEADERS, - SHOW_HEADERS, - ONLY_HEADERS, - }; -- enum header_mode headers = OMIT_HEADERS; -+ enum header_mode header_mode = OMIT_HEADERS; -+ -+ enum input_mode { -+ INPUT_READ, -+ INPUT_SUPPRESS, -+ }; -+ enum input_mode input_mode = INPUT_READ; -+ FILE *input_source = stdin; - - int c; -- while ((c = getopt(argc, argv, "46C:d:hLiI")) != -1) { -+ while ((c = getopt(argc, argv, "46C:d:D:hLiIN")) != -1) { - switch (c) { - case '4': - assert(0); // TODO -@@ -41,7 +48,21 @@ case 'C': - assert(0); // TODO: Client certificates - break; - case 'd': -- assert(0); // TODO: Input -+ input_mode = INPUT_READ; -+ input_source = fmemopen(optarg, strlen(optarg), "r"); -+ break; -+ case 'D': -+ input_mode = INPUT_READ; -+ if (strcmp(optarg, "-") == 0) { -+ input_source = stdin; -+ } else { -+ input_source = fopen(optarg, "r"); -+ if (!input_source) { -+ fprintf(stderr, "Error: open %s: %s", -+ optarg, strerror(errno)); -+ return 1; -+ } -+ } - break; - case 'h': - usage(argv[0]); -@@ -50,10 +71,14 @@ case 'L': - assert(0); // TODO: Follow redirects - break; - case 'i': -- headers = SHOW_HEADERS; -+ header_mode = SHOW_HEADERS; - break; - case 'I': -- headers = ONLY_HEADERS; -+ header_mode = ONLY_HEADERS; -+ input_mode = INPUT_SUPPRESS; -+ break; -+ case 'N': -+ input_mode = INPUT_SUPPRESS; - break; - default: - fprintf(stderr, "fatal: unknown flag %c", c); -@@ -69,43 +94,105 @@ - SSL_load_error_strings(); - ERR_load_crypto_strings(); - -- struct gemini_response resp; -- enum gemini_result r = gemini_request(argv[optind], NULL, &resp); -- if (r != GEMINI_OK) { -- fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); -- gemini_response_finish(&resp); -- return (int)r; -- } -+ bool exit = false; -+ char *url = strdup(argv[optind]); -+ -+ int ret = 0; -+ while (!exit) { -+ struct gemini_response resp; -+ enum gemini_result r = gemini_request(url, NULL, &resp); -+ if (r != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); -+ ret = (int)r; -+ exit = true; -+ goto next; -+ } -+ -+ char *new_url, *input = NULL; -+ switch (resp.status / 10) { -+ case 1: // INPUT -+ if (input_mode == INPUT_SUPPRESS) { -+ exit = true; -+ break; -+ } -+ -+ if (fileno(input_source) != -1 && -+ isatty(fileno(input_source))) { -+ fprintf(stderr, "%s: ", resp.meta); -+ } - -- switch (headers) { -- case ONLY_HEADERS: -- printf("%d %s\n", resp.status, resp.meta); -- break; -- case SHOW_HEADERS: -- printf("%d %s\n", resp.status, resp.meta); -- /* fallthrough */ -- case OMIT_HEADERS: -- for (int n = 1; n > 0;) { -- char buf[BUFSIZ]; -- n = BIO_read(resp.bio, buf, BUFSIZ); -+ size_t s = 0; -+ ssize_t n = getline(&input, &s, input_source); - if (n == -1) { -- fprintf(stderr, "Error: read\n"); -- return 1; -+ fprintf(stderr, "Error reading input: %s\n", -+ feof(input_source) ? "EOF" : -+ strerror(ferror(input_source))); -+ r = 1; -+ exit = true; -+ break; -+ } -+ input[n - 1] = '\0'; // Drop LF -+ -+ new_url = gemini_input_url(url, input); -+ free(url); -+ url = new_url; -+ goto next; -+ case 3: // REDIRECT -+ assert(0); // TODO -+ case 6: // CLIENT CERTIFICATE REQUIRED -+ assert(0); // TODO -+ case 4: // TEMPORARY FAILURE -+ case 5: // PERMANENT FAILURE -+ if (header_mode == OMIT_HEADERS) { -+ fprintf(stderr, "%s: %d %s\n", -+ resp.status / 10 == 4 ? -+ "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ resp.status, resp.meta); - } -- ssize_t w = 0; -- while (w < (ssize_t)n) { -- ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); -- if (x == -1) { -- fprintf(stderr, "Error: write: %s\n", -- strerror(errno)); -+ exit = true; -+ break; -+ case 2: // SUCCESS -+ exit = true; -+ break; -+ } -+ -+ switch (header_mode) { -+ case ONLY_HEADERS: -+ printf("%d %s\n", resp.status, resp.meta); -+ break; -+ case SHOW_HEADERS: -+ printf("%d %s\n", resp.status, resp.meta); -+ /* fallthrough */ -+ case OMIT_HEADERS: -+ if (resp.status / 10 != 2) { -+ break; -+ } -+ for (int n = 1; n > 0;) { -+ char buf[BUFSIZ]; -+ n = BIO_read(resp.bio, buf, BUFSIZ); -+ if (n == -1) { -+ fprintf(stderr, "Error: read\n"); - return 1; - } -- w += x; -+ ssize_t w = 0; -+ while (w < (ssize_t)n) { -+ ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); -+ if (x == -1) { -+ fprintf(stderr, "Error: write: %s\n", -+ strerror(errno)); -+ return 1; -+ } -+ w += x; -+ } - } -+ break; - } -- break; -+ -+next: -+ gemini_response_finish(&resp); - } - -- gemini_response_finish(&resp); -- return 0; -+ (void)input_mode; -+ free(url); -+ return ret; - } diff --git a/sources/cgmnlm.git/commits/7a099135cd9dae483679cf51a4b630a5dd64c74e.patch b/sources/cgmnlm.git/commits/7a099135cd9dae483679cf51a4b630a5dd64c74e.patch @@ -1,115 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index bc3f647d42372678b4180f539df8637d0ba69a12..dd8c8d200a949a24d030ce3429a4d20f1430fe97 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -63,9 +63,11 @@ // TODO: word wrap - col += fprintf(tty, " %s\n", trim_ws(tok.text)); - break; - case GEMINI_LINK: -- (void)next; // TODO: Record links - col += fprintf(tty, "[%d] %s\n", nlinks++, trim_ws( - tok.link.text ? tok.link.text : tok.link.url)); -+ *next = calloc(1, sizeof(struct link)); -+ (*next)->url = strdup(trim_ws(tok.link.url)); -+ next = &(*next)->next; - break; - case GEMINI_PREFORMATTED: - continue; // TODO -@@ -117,7 +119,6 @@ main(int argc, char *argv[]) - { - bool pagination = true; - -- bool have_url = false; - struct Curl_URL *url = curl_url(); - - FILE *tty = fopen("/dev/tty", "w+"); -@@ -139,8 +140,7 @@ } - - if (optind == argc - 1) { - set_url(url, argv[optind]); -- have_url = true; -- } else if (optind < argc - 1) { -+ } else { - usage(argv[0]); - return 1; - } -@@ -154,8 +154,6 @@ - bool run = true; - struct gemini_response resp; - while (run) { -- assert(have_url); // TODO -- - struct link *links; - static char prompt[4096]; - -@@ -164,7 +162,8 @@ CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); - assert(uc == CURLUE_OK); // Invariant - - snprintf(prompt, sizeof(prompt), "\n\t%s\n" -- "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit\n" -+ "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit " -+ "[b]ack; [f]orward\n" - "=> ", plain_url); - - enum gemini_result res = gemini_request(plain_url, &opts, &resp); -@@ -194,19 +193,49 @@ } - - gemini_response_finish(&resp); - -- fprintf(tty, "%s", prompt); -- size_t l = 0; -- char *in = NULL; -- ssize_t n = getline(&in, &l, tty); -- if (n == -1 && feof(tty)) { -- break; -- } -+ bool prompting = true; -+ while (prompting) { -+ fprintf(tty, "%s", prompt); -+ -+ size_t l = 0; -+ char *in = NULL; -+ ssize_t n = getline(&in, &l, tty); -+ if (n == -1 && feof(tty)) { -+ prompting = run = false; -+ break; -+ } -+ if (strcmp(in, "q\n") == 0) { -+ prompting = run = false; -+ break; -+ } -+ -+ struct link *link = links; -+ char *endptr; -+ int linksel = (int)strtol(in, &endptr, 10); -+ if (endptr[0] == '\n' && linksel >= 0) { -+ while (linksel > 0 && link) { -+ link = link->next; -+ --linksel; -+ } - -- if (strcmp(in, "q\n") == 0) { -- run = false; -- } -+ if (!link) { -+ fprintf(stderr, "Error: no such link.\n"); -+ } else { -+ prompting = false; -+ set_url(url, link->url); -+ } -+ } - -- free(in); -+ link = links; -+ while (link) { -+ struct link *next = link->next; -+ free(link->url); -+ free(link); -+ link = next; -+ } -+ -+ free(in); -+ } - } - - SSL_CTX_free(opts.ssl_ctx); diff --git a/sources/cgmnlm.git/commits/7c453fb45f831ce9178799af9855ecb0bda518ea.patch b/sources/cgmnlm.git/commits/7c453fb45f831ce9178799af9855ecb0bda518ea.patch @@ -1,116 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index 86152d6183462636cca4eecbd47824569a688cb1..f1674d534a9a5f2edcb6c7be678e17ad5724a9a7 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -175,7 +175,7 @@ goto cleanup; - } - - char *endptr; -- resp->status = (int)strtol(buf, &endptr, 10); -+ resp->status = (enum gemini_status)strtol(buf, &endptr, 10); - if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) { - res = GEMINI_ERR_PROTOCOL; - goto cleanup; -@@ -268,5 +268,5 @@ - enum gemini_status_class - gemini_response_class(enum gemini_status status) - { -- return status / 10; -+ return status / 10 * 10; - } -diff --git a/src/gmni.c b/src/gmni.c -index b4efdc0235dac0279ce284f2c6b19f680a191647..5b1f37522e93da360e74ba92ccb00c530bd41463 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -10,6 +10,7 @@ #include <stdlib.h> - #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> -+#include <termios.h> - #include <unistd.h> - #include "gmni.h" - -@@ -19,6 +20,41 @@ { - fprintf(stderr, - "usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n", - argv_0); -+} -+ -+static char * -+get_input(const struct gemini_response *resp, FILE *source) -+{ -+ int r = 0; -+ struct termios attrs; -+ bool tty = fileno(source) != -1 && isatty(fileno(source)); -+ char *input = NULL; -+ if (tty) { -+ fprintf(stderr, "%s: ", resp->meta); -+ if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { -+ r = tcgetattr(fileno(source), &attrs); -+ struct termios new_attrs; -+ r = tcgetattr(fileno(source), &new_attrs); -+ if (r != -1) { -+ new_attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &new_attrs); -+ } -+ } -+ } -+ size_t s = 0; -+ ssize_t n = getline(&input, &s, source); -+ if (n == -1) { -+ fprintf(stderr, "Error reading input: %s\n", -+ feof(source) ? "EOF" : -+ strerror(ferror(source))); -+ return NULL; -+ } -+ input[n - 1] = '\0'; // Drop LF -+ if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { -+ attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &attrs); -+ } -+ return input; - } - - int -@@ -119,7 +155,6 @@ exit = true; - goto next; - } - -- char *new_url, *input = NULL; - switch (gemini_response_class(resp.status)) { - case GEMINI_STATUS_CLASS_INPUT: - if (input_mode == INPUT_SUPPRESS) { -@@ -127,27 +162,19 @@ exit = true; - break; - } - -- if (fileno(input_source) != -1 && -- isatty(fileno(input_source))) { -- fprintf(stderr, "%s: ", resp.meta); -- } -- -- size_t s = 0; -- ssize_t n = getline(&input, &s, input_source); -- if (n == -1) { -- fprintf(stderr, "Error reading input: %s\n", -- feof(input_source) ? "EOF" : -- strerror(ferror(input_source))); -+ char *input = get_input(&resp, input_source); -+ if (!input) { - r = 1; - exit = true; - break; - } -- input[n - 1] = '\0'; // Drop LF - -- new_url = gemini_input_url(url, input); -+ char *new_url = gemini_input_url(url, input); -+ assert(new_url); -+ -+ free(input); - free(url); - url = new_url; -- assert(url); - goto next; - case GEMINI_STATUS_CLASS_REDIRECT: - free(url); diff --git a/sources/cgmnlm.git/commits/7e4e43b05c298aa812027bf1921ce3f224e86bda.patch b/sources/cgmnlm.git/commits/7e4e43b05c298aa812027bf1921ce3f224e86bda.patch @@ -1,20 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index cec330b44ae6e9252e92fbd1a5d74e7592b84e2a..9efe3e231d445d55ea43d444a72eaa79f595ac76 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -475,7 +475,6 @@ } - } else { - browser->opts.client_cert = NULL; - } -- free(host); - } - - while (requesting) { -@@ -600,6 +599,7 @@ if (client_cert.key) { - free(client_cert.key); - } - free(scheme); -+ free(host); - return res; - } - diff --git a/sources/cgmnlm.git/commits/801d9b8f13f6adef25fb14ec2e9acbc6dd4e92a9.patch b/sources/cgmnlm.git/commits/801d9b8f13f6adef25fb14ec2e9acbc6dd4e92a9.patch @@ -1,87 +0,0 @@ -diff --git a/README.md b/README.md -index fa803f99158582d982ef3a01d6b87471fb27f0cb..9aa989fe358ecccd9b984b1aff0df97c6c2ceec0 100644 ---- a/README.md -+++ b/README.md -@@ -29,7 +29,7 @@ - s command to directly search in geminispace (via geminispace.info) - - k command to remove the bookmark for the current page - - e[N] command to open a link in default external program (requires `xdg-open`) - - t[N] command to download the content behind a link to a temporary file --- b & f commands to navigate history can jump multiple entries at once -+- a command to switch between display of preformatted blocks and alt text (if available) - - The actual colors used depend on your terminal palette: - - heading 1: light red -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 8cafaea17de972c240031b0fe6ab88e3621bd871..77d03cb16e1e27c440ecb1c5b7d154db50659dc6 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -44,7 +44,7 @@ struct history *prev, *next; - }; - - struct browser { -- bool pagination, unicode; -+ bool pagination, unicode, alttext; - int max_width; - struct gemini_options opts; - struct gemini_tofu tofu; -@@ -103,6 +103,7 @@ "n\t\tjump to next search match\n" - "d <path>\tDownload page to <path>\n" - "|<prog>\t\tPipe page into program\n" - "[N]|<prog>\tPipe content of Nth link into program\n" -+ "a\t\ttoggle usage of alt text instead of preformatted text\n" - "q\t\tQuit\n" - "\n" - ; -@@ -636,6 +637,11 @@ if (in[1]) break; - set_url(browser, "gemini://geminispace.info/search", &browser->history); - result = PROMPT_ANSWERED; - goto exit; -+ case 'a': -+ browser->alttext = !browser->alttext; -+ fprintf(browser->tty, "Alttext instead of preformatted block is now %s\n", browser->alttext ? "ENABLED" : "DISABLED"); -+ result = PROMPT_AGAIN; -+ goto exit; - case 'b': - if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); - while (historyhops > 0) { -@@ -939,6 +945,7 @@ - fprintf(out, "\n"); - char *text = NULL; - int row = 0, col = 0; -+ bool no_alttext; - struct gemini_token tok; - struct link **next = &browser->links; - while (text != NULL || gemini_parser_next(&p, &tok) == 0) { -@@ -962,13 +969,21 @@ col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED_BEGIN: -- gemini_token_finish(&tok); -+ if (text == NULL && browser->alttext) { -+ if (tok.preformatted == NULL) { -+ no_alttext = true; -+ } else { -+ fprintf(out, " A %s", ANSI_COLOR_GRAY); -+ text = tok.preformatted; -+ } -+ } -+ break; - /* fallthrough */ - case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: -- if (text == NULL) { -- fprintf(out, " %s", ANSI_COLOR_GRAY); -+ if (text == NULL && (!browser->alttext || no_alttext)) { -+ fprintf(out, " P %s", ANSI_COLOR_GRAY); - text = tok.preformatted; - } - break; -@@ -1241,6 +1256,7 @@ main(int argc, char *argv[]) - { - struct browser browser = { - .pagination = true, -+ .alttext = false, - .tofu_mode = TOFU_ASK, - .unicode = true, - .url = curl_url(), diff --git a/sources/cgmnlm.git/commits/84da4b3f2b95bead2c1609eb572a2369576eae77.patch b/sources/cgmnlm.git/commits/84da4b3f2b95bead2c1609eb572a2369576eae77.patch @@ -1,46 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index d22f64ce27dfcf61eed80ed09b2d072efb8d71e0..6ad61939f8dcd6f6cf530f5615e442fcfb92ffde 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -71,13 +71,13 @@ - const char *help_msg = - "The following commands are available:\n\n" - "q\tQuit\n" -- "N\tFollow Nth link (where N is a number)\n" -- "p[N]\tShow URL of Nth link (where N is a number)\n" -+ "[N]\tFollow Nth link (where N is a number)\n" -+ "u[N]\tShow URL of Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" - "H\tView all page history\n" -- "m\tSave bookmark\n" -- "M\tBrowse bookmarks\n" -+ "a\tSave bookmark\n" -+ "B\tBrowse bookmarks\n" - "r\tReload the page\n" - "d <path>\tDownload page to <path>\n" - "|<prog>\tPipe page into program\n" -@@ -549,12 +549,12 @@ cur = cur->next; - } - result = PROMPT_AGAIN; - goto exit; -- case 'm': -+ case 'a': - if (in[1]) break; - save_bookmark(browser); - result = PROMPT_AGAIN; - goto exit; -- case 'M': -+ case 'B': - if (in[1]) break; - open_bookmarks(browser); - result = PROMPT_ANSWERED; -@@ -582,7 +582,7 @@ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); - result = PROMPT_AGAIN; - goto exit; - } -- case 'p': -+ case 'u': - if (!in[1]) break; - struct link *link = browser->links; - char *endptr; diff --git a/sources/cgmnlm.git/commits/84df94447cdb081ec305a5ba9d2b0ef89dd34fc3.patch b/sources/cgmnlm.git/commits/84df94447cdb081ec305a5ba9d2b0ef89dd34fc3.patch @@ -1,13 +0,0 @@ -diff --git a/README.md b/README.md -index 842e238162c33eec93dad8b10abb7bfe21af447b..dfd2e1744003996f1e629c78e459dac70985a430 100644 ---- a/README.md -+++ b/README.md -@@ -5,7 +5,7 @@ - - A CLI utility (like curl): gmni - - A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm - --[![Screenshot of the line-mode browser](https://l.sr.ht/AY7_.png)](https://asciinema.org/a/ldo2gV7qiDoBXvGwuD6x1jbn3) -+[![Screenshot of the line-mode browser](https://l.sr.ht/7kaA.png)](https://asciinema.org/a/ldo2gV7qiDoBXvGwuD6x1jbn3) - - Dependencies: - diff --git a/sources/cgmnlm.git/commits/852bc7198f9d1d838d76d74a006cc2a2e63e4f1c.patch b/sources/cgmnlm.git/commits/852bc7198f9d1d838d76d74a006cc2a2e63e4f1c.patch @@ -1,120 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 2375151c7f5fced09a80e41d26809730827f8925..4b844200eeb4318d694566d7a84eb3e64c7c7668 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -82,6 +82,7 @@ if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { - fprintf(stderr, "Error: invalid URL\n"); - return false; - } -+ curl_url_get(browser->url, CURLUPART_URL, &browser->plain_url, 0); - if (history) { - struct history *next = calloc(1, sizeof(struct history)); - curl_url_get(browser->url, CURLUPART_URL, &next->url, 0); -@@ -118,19 +119,23 @@ case '\0': - result = PROMPT_MORE; - goto exit; - case 'q': -+ if (in[1]) break; - result = PROMPT_QUIT; - goto exit; - case 'b': -+ if (in[1]) break; - if (!browser->history->prev) { - fprintf(stderr, "At beginning of history\n"); - result = PROMPT_AGAIN; - goto exit; - } -+ if (in[1]) break; - browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; - case 'f': -+ if (in[1]) break; - if (!browser->history->next) { - fprintf(stderr, "At end of history\n"); - result = PROMPT_AGAIN; -@@ -141,6 +146,7 @@ set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; - case '/': -+ if (in[1]) break; - if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) { - static char buf[1024]; - r = regerror(r, &browser->regex, buf, sizeof(buf)); -@@ -153,6 +159,7 @@ result = PROMPT_ANSWERED; - } - goto exit_re; - case 'n': -+ if (in[1]) break; - if (browser->searching) { - result = PROMPT_NEXT; - goto exit_re; -@@ -162,6 +169,7 @@ result = PROMPT_AGAIN; - goto exit; - } - case '?': -+ if (in[1]) break; - fprintf(browser->tty, "%s", help_msg); - result = PROMPT_AGAIN; - goto exit; -@@ -465,6 +473,17 @@ } - return input; - } - -+static bool -+has_suffix(char *str, char *suff) -+{ -+ size_t suffl = strlen(suff); -+ size_t strl = strlen(str); -+ if (strl < suffl) { -+ return false; -+ } -+ return strcmp(&str[strl - suffl], suff) == 0; -+} -+ - // Returns true to skip prompting - static bool - do_requests(struct browser *browser, struct gemini_response *resp) -@@ -472,9 +491,40 @@ { - int nredir = 0; - bool requesting = true; - while (requesting) { -+ char *scheme; - CURLUcode uc = curl_url_get(browser->url, -- CURLUPART_URL, &browser->plain_url, 0); -+ CURLUPART_SCHEME, &scheme, 0); - assert(uc == CURLUE_OK); // Invariant -+ if (strcmp(scheme, "file") == 0) { -+ requesting = false; -+ -+ char *path; -+ uc = curl_url_get(browser->url, -+ CURLUPART_PATH, &path, 0); -+ if (uc != CURLUE_OK) { -+ resp->status = GEMINI_STATUS_BAD_REQUEST; -+ break; -+ } -+ -+ FILE *fp = fopen(path, "r"); -+ if (!fp) { -+ resp->status = GEMINI_STATUS_NOT_FOUND; -+ break; -+ } -+ -+ BIO *file = BIO_new_fp(fp, BIO_CLOSE); -+ resp->bio = BIO_new(BIO_f_buffer()); -+ BIO_push(resp->bio, file); -+ if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) { -+ resp->meta = strdup("text/gemini"); -+ } else if (has_suffix(path, ".txt")) { -+ resp->meta = strdup("text/plain"); -+ } else { -+ resp->meta = strdup("application/x-octet-stream"); -+ } -+ resp->status = GEMINI_STATUS_SUCCESS; -+ return display_response(browser, resp); -+ } - - enum gemini_result res = gemini_request(browser->plain_url, - &browser->opts, resp); diff --git a/sources/cgmnlm.git/commits/86b299819c86758f2b537c1de0475a2906f0a4d2.patch b/sources/cgmnlm.git/commits/86b299819c86758f2b537c1de0475a2906f0a4d2.patch @@ -1,28 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 37b4db277d8fcd961eb1746294724783e638109c..3bd2ea9a9b70cc33f6fc5b330234c17a974f80f5 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -684,15 +684,19 @@ goto exit; - case 'H': - if (in[1]) break; - struct history *cur = browser->history; -- while (cur->prev) cur = cur->prev; -+ int hist_count = 0; -+ while (cur->prev) { -+ cur = cur->prev; -+ hist_count++; -+ } - while (cur != browser->history) { -- fprintf(browser->tty, " %s\n", cur->url); -+ fprintf(browser->tty, "b%-3i %s\n", hist_count--, cur->url); - cur = cur->next; - } -- fprintf(browser->tty, "* %s\n", cur->url); -+ fprintf(browser->tty, "* %s\n", cur->url); - cur = cur->next; - while (cur) { -- fprintf(browser->tty, " %s\n", cur->url); -+ fprintf(browser->tty, "f%-3i %s\n", ++hist_count, cur->url); - cur = cur->next; - } - result = PROMPT_AGAIN; diff --git a/sources/cgmnlm.git/commits/8970adc23e0a1bcf29d211f353dbd5ebd68cfe66.patch b/sources/cgmnlm.git/commits/8970adc23e0a1bcf29d211f353dbd5ebd68cfe66.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 9f76188252a30e95868b99d64d57369ac03a7e1c..23b8e6054c6824ebfe4aba19404d7eeb2c3dab35 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -471,7 +471,7 @@ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: - requesting = false; - fprintf(stderr, "Server returned %s %d %s\n", - resp->status / 10 == 4 ? -- "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ "TEMPORARY FAILURE" : "PERMANENT FAILURE", - resp->status, resp->meta); - break; - case GEMINI_STATUS_CLASS_SUCCESS: diff --git a/sources/cgmnlm.git/commits/8a83030e5a390c2151c485b3c091ba28ddebcab7.patch b/sources/cgmnlm.git/commits/8a83030e5a390c2151c485b3c091ba28ddebcab7.patch @@ -1,35 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 23b8e6054c6824ebfe4aba19404d7eeb2c3dab35..c5419243f983c878abff1cfb36633d648f6449f2 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -159,7 +159,7 @@ size_t n; - - n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - assert(n < sizeof(path)); -- strncpy(dname, dirname(path), sizeof(dname)); -+ strncpy(dname, dirname(path), sizeof(dname)-1); - if (mkdirs(dname, 0755) != 0) { - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); -@@ -200,7 +200,7 @@ size_t n; - - n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - assert(n < sizeof(path)); -- strncpy(dname, dirname(path), sizeof(dname)); -+ strncpy(dname, dirname(path), sizeof(dname)-1); - if (mkdirs(dname, 0755) != 0) { - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); -diff --git a/src/tofu.c b/src/tofu.c -index 16548a1cbcebc8c4cdda4a57a4655a04e7f390a3..b9100c77fd61c71ce561f2194b991eee7130e689 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -164,7 +164,7 @@ n = snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); - assert(n < sizeof(tofu->known_hosts_path)); - -- strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)); -+ strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1); - if (mkdirs(dname, 0755) != 0) { - snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); diff --git a/sources/cgmnlm.git/commits/8bb1d81f539f1e223e8fcd79e2d18f58e3c9d28f.patch b/sources/cgmnlm.git/commits/8bb1d81f539f1e223e8fcd79e2d18f58e3c9d28f.patch @@ -1,14 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index def42ac8b1af405da29681d29cd7b9d07b727f11..07d0000babe7cd51876ead407e83c3062adfda56 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -380,6 +380,9 @@ assert(0); // Not supposed to happen - case '\t': - *col = *col + (8 - *col % 8); - break; -+ case '\r': -+ if (!s[i+1]) break; -+ /* fallthrough */ - default: - if (iscntrl(s[i])) { - s[i] = '.'; diff --git a/sources/cgmnlm.git/commits/8c473eda5e4c6537058d0ff1815f2943e7b41498.patch b/sources/cgmnlm.git/commits/8c473eda5e4c6537058d0ff1815f2943e7b41498.patch @@ -1,197 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 26654324b4fb845633f7db2b2ac01e5e3b4635a1..99ecb7d1d76f8b5f0c856ca9dcd2d6abf5dd6de6 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -35,6 +35,7 @@ struct gemini_tofu tofu; - enum tofu_action tofu_mode; - - FILE *tty; -+ char *meta; - char *plain_url; - char *page_title; - struct Curl_URL *url; -@@ -206,6 +207,52 @@ snprintf(url, sizeof(url), "file://%s", path); - set_url(browser, url, &browser->history); - } - -+static void -+print_media_parameters(FILE *out, char *params) -+{ -+ if (params == NULL) { -+ fprintf(out, "No media parameters\n"); -+ return; -+ } -+ for (char *param = strtok(params, ";"); param; -+ param = strtok(NULL, ";")) { -+ char *value = strchr(param, '='); -+ if (value == NULL) { -+ fprintf(out, "Invalid media type parameter '%s'\n", -+ trim_ws(param)); -+ continue; -+ } -+ *value = 0; -+ fprintf(out, "%s: ", trim_ws(param)); -+ *value++ = '='; -+ if (*value != '"') { -+ fprintf(out, "%s\n", value); -+ continue; -+ } -+ while (value++) { -+ switch (*value) { -+ case '\0': -+ if ((value = strtok(NULL, ";")) != NULL) { -+ fprintf(out, ";%c", *value); -+ } -+ break; -+ case '"': -+ value = NULL; -+ break; -+ case '\\': -+ if (value[1] == '\0') { -+ break; -+ } -+ value++; -+ /* fallthrough */ -+ default: -+ putc(*value, out); -+ } -+ } -+ putc('\n', out); -+ } -+} -+ - static enum prompt_result - do_prompts(const char *prompt, struct browser *browser) - { -@@ -329,6 +376,12 @@ case 'r': - if (in[1]) break; - result = PROMPT_ANSWERED; - goto exit; -+ case 'i': -+ if (in[1]) break; -+ print_media_parameters(browser->tty, browser->meta -+ ? strchr(browser->meta, ';') : NULL); -+ result = PROMPT_AGAIN; -+ goto exit; - case '?': - if (in[1]) break; - fprintf(browser->tty, "%s", help_msg); -@@ -539,12 +592,19 @@ ++row; col = 0; - - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; -+ char *end = NULL; -+ if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) { -+ *end = 0; -+ } - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" - "[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "(more) => ", resp->meta, browser->plain_url, - browser->searching ? "[n]ext result; " : "", - browser->history->prev ? "[b]ack; " : "", - browser->history->next ? "[f]orward; " : ""); -+ if (end != NULL) { -+ *end = ';'; -+ } - enum prompt_result result = PROMPT_AGAIN; - while (result == PROMPT_AGAIN) { - result = do_prompts(prompt, browser); -@@ -714,6 +774,7 @@ &browser->opts, resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); - requesting = false; -+ resp->status = 70 + res; - break; - } - -@@ -858,6 +919,7 @@ .tofu_mode = TOFU_ASK, - .unicode = true, - .url = curl_url(), - .tty = fopen("/dev/tty", "w+"), -+ .meta = NULL, - }; - - int c; -@@ -909,38 +971,45 @@ struct gemini_response resp; - browser.running = true; - while (browser.running) { - static char prompt[4096]; -- if (do_requests(&browser, &resp)) { -- // Skip prompts -- gemini_response_finish(&resp); -- goto next; -+ bool skip_prompt = do_requests(&browser, &resp); -+ if (browser.meta) { -+ free(browser.meta); - } -- -- snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" -- "=> ", -- resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", -- browser.plain_url, -- browser.history->prev ? "[b]ack; " : "", -- browser.history->next ? "[f]orward; " : ""); -+ browser.meta = resp.status == GEMINI_STATUS_SUCCESS -+ ? strdup(resp.meta) : NULL; - gemini_response_finish(&resp); -+ if (!skip_prompt) { -+ char *end = NULL; -+ if (browser.meta && (end = strchr(browser.meta, ';')) != NULL) { -+ *end = 0; -+ } -+ snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -+ "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" -+ "=> ", browser.meta ? browser.meta -+ : "[request failed]", browser.plain_url, -+ browser.history->prev ? "[b]ack; " : "", -+ browser.history->next ? "[f]orward; " : ""); -+ if (end != NULL) { -+ *end = ';'; -+ } - -- enum prompt_result result = PROMPT_AGAIN; -- while (result == PROMPT_AGAIN || result == PROMPT_MORE) { -- result = do_prompts(prompt, &browser); -- } -- switch (result) { -- case PROMPT_AGAIN: -- case PROMPT_MORE: -- assert(0); -- case PROMPT_QUIT: -- browser.running = false; -- break; -- case PROMPT_ANSWERED: -- case PROMPT_NEXT: -- break; -+ enum prompt_result result = PROMPT_AGAIN; -+ while (result == PROMPT_AGAIN || result == PROMPT_MORE) { -+ result = do_prompts(prompt, &browser); -+ } -+ switch (result) { -+ case PROMPT_AGAIN: -+ case PROMPT_MORE: -+ assert(0); -+ case PROMPT_QUIT: -+ browser.running = false; -+ break; -+ case PROMPT_ANSWERED: -+ case PROMPT_NEXT: -+ break; -+ } - } - --next:; - struct link *link = browser.links; - while (link) { - struct link *next = link->next; -@@ -961,6 +1030,9 @@ SSL_CTX_free(browser.opts.ssl_ctx); - curl_url_cleanup(browser.url); - free(browser.page_title); - free(browser.plain_url); -+ if (browser.meta != NULL) { -+ free(browser.meta); -+ } - fclose(browser.tty); - return 0; - } diff --git a/sources/cgmnlm.git/commits/8cac260a4b7c0b4df4d1229a5e41e64c3a687173.patch b/sources/cgmnlm.git/commits/8cac260a4b7c0b4df4d1229a5e41e64c3a687173.patch @@ -1,30 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 489a6833f75c9e6a13562c84dc8e3ded24be23c4..930ab19abe717b186070151a184b2a8e98542f77 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -82,6 +82,7 @@ "<Enter>\t\tread more lines (if available)\n" - "<url>\t\tgo to url\n" - "[N]\t\tFollow Nth link (where N is a number)\n" - "p[N]\t\tPrint URL of Nth link (where N is a number)\n" -+ "e\t\tSend current URL of browser to external default program\n" - "e[N]\t\tSend URL of Nth link in external default program\n" - "t[N]\t\tDownload content of Nth link to a temporary file\n" - "b[N]\t\tJump back N entries in history, N is optional, default 1\n" -@@ -645,7 +646,16 @@ } - case 'e': - case 'p': - case 't': -- if (!in[1]) break; -+ if (!in[1]) { -+ if (in[0] == 'e') { -+ char xdgopen[4096]; -+ snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", browser->plain_url); -+ if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); -+ goto exit; -+ } else { -+ break; -+ } -+ } - linksel = (int)strtol(in+1, &endptr, 10); - if (!endptr[0] && linksel >= 0) { - while (linksel > 0 && link) { diff --git a/sources/cgmnlm.git/commits/8d897e4a00be9986209f1ca394ed46befadf6088.patch b/sources/cgmnlm.git/commits/8d897e4a00be9986209f1ca394ed46befadf6088.patch @@ -1,13 +0,0 @@ -diff --git a/Makefile b/Makefile -index 22fed5d19ec4f989ead26d22d3d1887614e4927c..c686cc60d215bc9e6032b429a1bc8babc6509a2f 100644 ---- a/Makefile -+++ b/Makefile -@@ -40,7 +40,7 @@ @printf 'CC\t$@\n' - @touch $(OUTDIR)/cppcache - @grep $< $(OUTDIR)/cppcache >/dev/null || \ - $(CPP) $(CFLAGS) -MM -MT $@ $< >> $(OUTDIR)/cppcache -- @$(CC) -c -fPIC $(CFLAGS) -o $@ $< -+ @$(CC) -c $(CFLAGS) -o $@ $< - - .scd.1: - @printf 'SCDOC\t$@\n' diff --git a/sources/cgmnlm.git/commits/8ddc99fdc336957d7565cd50e329da9cbe9e4de8.patch b/sources/cgmnlm.git/commits/8ddc99fdc336957d7565cd50e329da9cbe9e4de8.patch @@ -1,12 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 77d03cb16e1e27c440ecb1c5b7d154db50659dc6..041096cc5f0117632f80b0427b9a916aaa6401c2 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1085,6 +1085,7 @@ - switch (result) { - case PROMPT_AGAIN: - case PROMPT_MORE: -+ printf("\n"); - break; - case PROMPT_QUIT: - browser->running = false; diff --git a/sources/cgmnlm.git/commits/90995e834f2e87427f2f4bddf26a93258b45aa31.patch b/sources/cgmnlm.git/commits/90995e834f2e87427f2f4bddf26a93258b45aa31.patch @@ -1,50 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 99ecb7d1d76f8b5f0c856ca9dcd2d6abf5dd6de6..85917dfa97c034af41b9ab15e45139d6b95709de 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -834,7 +834,7 @@ } - - static enum tofu_action - tofu_callback(enum tofu_error error, const char *fingerprint, -- struct known_host *host, void *data) -+ struct known_host *khost, void *data) - { - struct browser *browser = data; - if (browser->tofu_mode != TOFU_ASK) { -@@ -852,15 +852,22 @@ "you should not disclose personal information or trust the contents of the page.\n" - "trust [o]nce; [a]bort\n" - "=> "); - break; -- case TOFU_UNTRUSTED_CERT: -+ case TOFU_UNTRUSTED_CERT:; -+ char *host; -+ if (curl_url_get(browser->url, CURLUPART_HOST, &host, 0) != CURLUE_OK) { -+ fprintf(stderr, "Error: invalid URL %s\n", -+ browser->plain_url); -+ return TOFU_FAIL; -+ } - snprintf(prompt, sizeof(prompt), -- "The certificate offered by this server is of unknown trust. " -+ "The certificate offered by %s is of unknown trust. " - "Its fingerprint is: \n" - "%s\n\n" - "If you knew the fingerprint to expect in advance, verify that this matches.\n" - "Otherwise, it should be safe to trust this certificate.\n\n" - "[t]rust always; trust [o]nce; [a]bort\n" -- "=> ", fingerprint); -+ "=> ", host, fingerprint); -+ free(host); - break; - case TOFU_FINGERPRINT_MISMATCH: - snprintf(prompt, sizeof(prompt), -@@ -871,8 +878,8 @@ "%s\n\n" - "The expected fingerprint is:\n" - "%s\n\n" - "If you're certain that this is correct, edit %s:%d\n", -- fingerprint, host->fingerprint, -- browser->tofu.known_hosts_path, host->lineno); -+ fingerprint, khost->fingerprint, -+ browser->tofu.known_hosts_path, khost->lineno); - return TOFU_FAIL; - } - diff --git a/sources/cgmnlm.git/commits/95518992983e6531106b48c82edeb0ce825bf351.patch b/sources/cgmnlm.git/commits/95518992983e6531106b48c82edeb0ce825bf351.patch @@ -1,164 +0,0 @@ -diff --git a/include/client.h b/include/gmni.h -rename from include/client.h -rename to include/gmni.h -index 671de12e7ee53fbbfc03407760723d2a66458c2d..42cfdac95530c9d6a5f4e6e6c3b85635908cc1e6 100644 ---- a/include/client.h -+++ b/include/gmni.h -@@ -4,8 +4,49 @@ #include <netdb.h> - #include <openssl/ssl.h> - #include <sys/socket.h> - -+enum gemini_result { -+ GEMINI_OK, -+ GEMINI_ERR_OOM, -+ GEMINI_ERR_INVALID_URL, -+ GEMINI_ERR_RESOLVE, -+ GEMINI_ERR_CONNECT, -+ GEMINI_ERR_SSL, -+ GEMINI_ERR_IO, -+ GEMINI_ERR_PROTOCOL, -+}; -+ -+enum gemini_status { -+ GEMINI_STATUS_INPUT = 10, -+ GEMINI_STATUS_SENSITIVE_INPUT = 11, -+ GEMINI_STATUS_SUCCESS = 20, -+ GEMINI_STATUS_REDIRECT_TEMPORARY = 30, -+ GEMINI_STATUS_REDIRECT_PERMANENT = 31, -+ GEMINI_STATUS_TEMPORARY_FAILURE = 40, -+ GEMINI_STATUS_SERVER_UNAVAILABLE = 41, -+ GEMINI_STATUS_CGI_ERROR = 42, -+ GEMINI_STATUS_PROXY_ERROR = 43, -+ GEMINI_STATUS_SLOW_DOWN = 44, -+ GEMINI_STATUS_PERMANENT_FAILURE = 50, -+ GEMINI_STATUS_NOT_FOUND = 51, -+ GEMINI_STATUS_GONE = 52, -+ GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, -+ GEMINI_STATUS_BAD_REQUEST = 59, -+ GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, -+ GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, -+ GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, -+}; -+ -+enum gemini_status_class { -+ GEMINI_STATUS_CLASS_INPUT = 10, -+ GEMINI_STATUS_CLASS_SUCCESS = 20, -+ GEMINI_STATUS_CLASS_REDIRECT = 30, -+ GEMINI_STATUS_CLASS_TEMPORARY_FAILURE = 40, -+ GEMINI_STATUS_CLASS_PERMANENT_FAILURE = 50, -+ GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED = 60, -+}; -+ - struct gemini_response { -- int status; -+ enum gemini_status status; - char *meta; - - // Response body may be read from here if appropriate: -@@ -35,17 +76,6 @@ // example, to force IPv4/IPv6. - struct addrinfo *hints; - }; - --enum gemini_result { -- GEMINI_OK, -- GEMINI_ERR_OOM, -- GEMINI_ERR_INVALID_URL, -- GEMINI_ERR_RESOLVE, -- GEMINI_ERR_CONNECT, -- GEMINI_ERR_SSL, -- GEMINI_ERR_IO, -- GEMINI_ERR_PROTOCOL, --}; -- - // Requests the specified URL via the gemini protocol. If options is non-NULL, - // it may specify some additional configuration to adjust client behavior. - // -@@ -67,5 +97,9 @@ - // Returns the given URL with the input response set to the specified value. - // The caller must free the string. - char *gemini_input_url(const char *url, const char *input); -+ -+// Returns the general response class (i.e. with the second digit set to zero) -+// of the given Gemini status code. -+enum gemini_status_class gemini_response_class(enum gemini_status status); - - #endif -diff --git a/src/client.c b/src/client.c -index 59fa1381ab66496900acb4321f22c6ec3675db5e..86152d6183462636cca4eecbd47824569a688cb1 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -9,7 +9,7 @@ #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> - #include <unistd.h> --#include "client.h" -+#include "gmni.h" - #include "url.h" - - static enum gemini_result -@@ -264,3 +264,9 @@ cleanup: - curl_url_cleanup(uri); - return new_url; - } -+ -+enum gemini_status_class -+gemini_response_class(enum gemini_status status) -+{ -+ return status / 10; -+} -diff --git a/src/gmni.c b/src/gmni.c -index bdaa9baaca75e5f167add9a0ebed73cc60eaa647..b4efdc0235dac0279ce284f2c6b19f680a191647 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -11,7 +11,7 @@ #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> - #include <unistd.h> --#include "client.h" -+#include "gmni.h" - - static void - usage(char *argv_0) -@@ -120,8 +120,8 @@ goto next; - } - - char *new_url, *input = NULL; -- switch (resp.status / 10) { -- case 1: // INPUT -+ switch (gemini_response_class(resp.status)) { -+ case GEMINI_STATUS_CLASS_INPUT: - if (input_mode == INPUT_SUPPRESS) { - exit = true; - break; -@@ -149,7 +149,7 @@ free(url); - url = new_url; - assert(url); - goto next; -- case 3: // REDIRECT -+ case GEMINI_STATUS_CLASS_REDIRECT: - free(url); - url = strdup(resp.meta); - if (!follow_redirects) { -@@ -160,10 +160,10 @@ } - exit = true; - } - goto next; -- case 6: // CLIENT CERTIFICATE REQUIRED -+ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: - assert(0); // TODO -- case 4: // TEMPORARY FAILURE -- case 5: // PERMANENT FAILURE -+ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -+ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: - if (header_mode == OMIT_HEADERS) { - fprintf(stderr, "%s: %d %s\n", - resp.status / 10 == 4 ? -@@ -172,7 +172,7 @@ resp.status, resp.meta); - } - exit = true; - break; -- case 2: // SUCCESS -+ case GEMINI_STATUS_CLASS_SUCCESS: - exit = true; - break; - } diff --git a/sources/cgmnlm.git/commits/9551d0a3822312a0a4917ccbe80fdaeb49954d70.patch b/sources/cgmnlm.git/commits/9551d0a3822312a0a4917ccbe80fdaeb49954d70.patch @@ -1,42 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 02d8829598902d93f1dea3d68073f609573370c1..199572c363f9e50acdd052111350f3926d6a350d 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -57,6 +57,7 @@ const char *help_msg = - "The following commands are available:\n\n" - "q\tQuit\n" - "N\tFollow Nth link (where N is a number)\n" -+ "p[N]\tShow URL of Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" - "H\tView all page history\n" -@@ -267,6 +268,29 @@ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); - result = PROMPT_AGAIN; - goto exit; - } -+ case 'p': -+ if (!in[1]) break; -+ struct link *link = browser->links; -+ char *endptr; -+ int linksel = (int)strtol(in+1, &endptr, 10); -+ if (!endptr[0] && linksel >= 0) { -+ while (linksel > 0 && link) { -+ link = link->next; -+ --linksel; -+ } -+ -+ if (!link) { -+ fprintf(stderr, "Error: no such link.\n"); -+ } else { -+ fprintf(browser->tty, "=> %s\n", link->url); -+ result = PROMPT_AGAIN; -+ goto exit; -+ } -+ } else { -+ fprintf(stderr, "Error: invalid argument.\n"); -+ } -+ result = PROMPT_AGAIN; -+ goto exit; - case '?': - if (in[1]) break; - fprintf(browser->tty, "%s", help_msg); diff --git a/sources/cgmnlm.git/commits/955f7524b955e19bc89c6e9f76f3f3ecfb7bfb58.patch b/sources/cgmnlm.git/commits/955f7524b955e19bc89c6e9f76f3f3ecfb7bfb58.patch @@ -1,445 +0,0 @@ -diff --git a/configure b/configure -index 70a19b193d6e33b41b5cc7ef13511bb0f5b4e076..7b55a2580018cc69b2f52c9bc53832cc9faadc4f 100755 ---- a/configure -+++ b/configure -@@ -4,6 +4,7 @@ eval ". $srcdir/config.sh" - - gmni() { - genrules gmni \ -+ src/certs.c \ - src/client.c \ - src/escape.c \ - src/gmni.c \ -diff --git a/doc/gmni.scd b/doc/gmni.scd -index 1f20672f1fb0c8ff6a4f3a97a3324a25d2a0a01d..866dd418a510ccd141d9c23afb4b321b84a5f9ec 100644 ---- a/doc/gmni.scd -+++ b/doc/gmni.scd -@@ -38,11 +38,9 @@ If the server requests user input, _path_ is opened and read, and a - second request is performed with the contents of _path_ as the user - input. - --*-E* _path_[:_password_] -- Sets the path to the client certificate to use (and optionally a -- password). If the filename contains ":" but the certificate does not -- accept a password, append ":" to the path and it will be intepreted as -- an empty password. -+*-E* _path_:_key_ -+ Sets the path to the client certificate and private key file to use, -+ both PEM encoded. - - *-l* - For *text/\** responses, *gmni* normally adds a line feed if stdout is a -diff --git a/include/gmni/certs.h b/include/gmni/certs.h -new file mode 100644 -index 0000000000000000000000000000000000000000..22e226d6b4252dddd8526970cec0a947f12242d1 ---- /dev/null -+++ b/include/gmni/certs.h -@@ -0,0 +1,27 @@ -+#ifndef GEMINI_CERTS_H -+#define GEMINI_CERTS_H -+#include <bearssl.h> -+#include <stdio.h> -+ -+struct gmni_options; -+ -+struct gmni_client_certificate { -+ br_x509_certificate *chain; -+ size_t nchain; -+ struct gmni_private_key *key; -+}; -+ -+struct gmni_private_key { -+ int type; -+ union { -+ br_rsa_private_key rsa; -+ br_ec_private_key ec; -+ }; -+ unsigned char data[]; -+}; -+ -+// Returns nonzero on failure and sets errno -+int gmni_ccert_load(struct gmni_client_certificate *cert, -+ FILE *certin, FILE *skin); -+ -+#endif -diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h -index 16bef51024275bbb6ade9c90a11ae68876df387a..22295e20fb36d492a979c060de8dcdca14df5a34 100644 ---- a/include/gmni/gmni.h -+++ b/include/gmni/gmni.h -@@ -1,6 +1,6 @@ - #ifndef GEMINI_CLIENT_H - #define GEMINI_CLIENT_H --#include <bearssl_ssl.h> -+#include <bearssl.h> - #include <netdb.h> - #include <stdbool.h> - #include <sys/socket.h> -@@ -61,6 +61,8 @@ br_ssl_client_context *sc; - int fd; - }; - -+struct gmni_client_certificate; -+ - struct gemini_options { - // If ai_family != AF_UNSPEC (the default value on most systems), the - // client will connect to this address and skip name resolution. -@@ -69,6 +71,10 @@ - // If non-NULL, these hints are provided to getaddrinfo. Useful, for - // example, to force IPv4/IPv6. - struct addrinfo *hints; -+ -+ // If non-NULL, this will be used as the client certificate for the -+ // request. The other fields must be set as well. -+ struct gmni_client_certificate *client_cert; - }; - - struct gemini_tofu; -diff --git a/include/gmni/tofu.h b/include/gmni/tofu.h -index a0981a5296421541a766846bc36c733dab1dfd1a..51d1d60a3719469a1f8cd295b9d85e21d7affb43 100644 ---- a/include/gmni/tofu.h -+++ b/include/gmni/tofu.h -@@ -1,6 +1,6 @@ - #ifndef GEMINI_TOFU_H - #define GEMINI_TOFU_H --#include <bearssl_x509.h> -+#include <bearssl.h> - #include <limits.h> - - enum tofu_error { -diff --git a/src/certs.c b/src/certs.c -new file mode 100644 -index 0000000000000000000000000000000000000000..f40bfa7d27925baaa2fe6dbdf0d6c18ce4e10014 ---- /dev/null -+++ b/src/certs.c -@@ -0,0 +1,156 @@ -+#include <assert.h> -+#include <bearssl.h> -+#include <errno.h> -+#include <gmni/certs.h> -+#include <gmni/gmni.h> -+#include <stdio.h> -+#include <stdlib.h> -+ -+static void -+crt_append(void *ctx, const void *src, size_t len) -+{ -+ br_x509_certificate *crt = (br_x509_certificate *)ctx; -+ crt->data = realloc(crt->data, crt->data_len + len); -+ assert(crt->data); -+ memcpy(&crt->data[crt->data_len], src, len); -+ crt->data_len += len; -+} -+ -+static void -+key_append(void *ctx, const void *src, size_t len) -+{ -+ br_skey_decoder_context *skctx = (br_skey_decoder_context *)ctx; -+ br_skey_decoder_push(skctx, src, len); -+} -+ -+int -+gmni_ccert_load(struct gmni_client_certificate *cert, FILE *certin, FILE *skin) -+{ -+ // TODO: Better error propagation to caller -+ static unsigned char buf[BUFSIZ]; -+ -+ br_pem_decoder_context pemdec; -+ br_pem_decoder_init(&pemdec); -+ -+ cert->chain = NULL; -+ cert->nchain = 0; -+ -+ static const char *certname = "CERTIFICATE"; -+ while (!feof(certin)) { -+ size_t n = fread(&buf, 1, sizeof(buf), certin); -+ if (ferror(certin)) { -+ goto error; -+ } -+ size_t q = 0; -+ while (q < n) { -+ q += br_pem_decoder_push(&pemdec, &buf[q], n - q); -+ switch (br_pem_decoder_event(&pemdec)) { -+ case BR_PEM_BEGIN_OBJ: -+ if (strcmp(br_pem_decoder_name(&pemdec), certname) != 0) { -+ break; -+ } -+ cert->chain = realloc(cert->chain, -+ sizeof(br_x509_certificate) * (cert->nchain + 1)); -+ memset(&cert->chain[cert->nchain], 0, sizeof(*cert->chain)); -+ br_pem_decoder_setdest(&pemdec, &crt_append, -+ &cert->chain[cert->nchain]); -+ ++cert->nchain; -+ break; -+ case BR_PEM_END_OBJ: -+ break; -+ case BR_PEM_ERROR: -+ fprintf(stderr, "Error decoding PEM certificate\n"); -+ errno = EINVAL; -+ goto error; -+ } -+ } -+ } -+ -+ if (cert->nchain == 0) { -+ fprintf(stderr, "No certificates found in provided client certificate file\n"); -+ errno = EINVAL; -+ goto error; -+ } -+ -+ br_skey_decoder_context skdec = {0}; -+ br_skey_decoder_init(&skdec); -+ br_pem_decoder_init(&pemdec); -+ -+ // TODO: Better validation of PEM file -+ while (!feof(skin)) { -+ size_t n = fread(&buf, 1, sizeof(buf), skin); -+ if (ferror(skin)) { -+ goto error; -+ } -+ size_t q = 0; -+ while (q < n) { -+ q += br_pem_decoder_push(&pemdec, &buf[q], n - q); -+ switch (br_pem_decoder_event(&pemdec)) { -+ case BR_PEM_BEGIN_OBJ: -+ br_pem_decoder_setdest(&pemdec, &key_append, &skdec); -+ break; -+ case BR_PEM_END_OBJ: -+ // no-op -+ break; -+ case BR_PEM_ERROR: -+ fprintf(stderr, "Error decoding PEM private key\n"); -+ errno = EINVAL; -+ goto error; -+ } -+ } -+ } -+ -+ int err = br_skey_decoder_last_error(&skdec); -+ if (err != 0) { -+ fprintf(stderr, "Error loading private key: %d\n", err); -+ errno = EINVAL; -+ goto error; -+ } -+ switch (br_skey_decoder_key_type(&skdec)) { -+ struct gmni_private_key *k; -+ const br_ec_private_key *ec; -+ const br_rsa_private_key *rsa; -+ case BR_KEYTYPE_RSA: -+ rsa = br_skey_decoder_get_rsa(&skdec); -+ cert->key = k = malloc(sizeof(*k) -+ + rsa->plen + rsa->qlen -+ + rsa->dplen + rsa->dqlen -+ + rsa->iqlen); -+ assert(k); -+ k->type = BR_KEYTYPE_RSA; -+ k->rsa = *rsa; -+ k->rsa.p = k->data; -+ k->rsa.q = k->rsa.p + k->rsa.plen; -+ k->rsa.dp = k->rsa.q + k->rsa.qlen; -+ k->rsa.dq = k->rsa.dp + k->rsa.dplen; -+ k->rsa.iq = k->rsa.dq + k->rsa.dqlen; -+ memcpy(k->rsa.p, rsa->p, rsa->plen); -+ memcpy(k->rsa.q, rsa->q, rsa->qlen); -+ memcpy(k->rsa.dp, rsa->dp, rsa->dplen); -+ memcpy(k->rsa.dq, rsa->dq, rsa->dqlen); -+ memcpy(k->rsa.iq, rsa->iq, rsa->iqlen); -+ break; -+ case BR_KEYTYPE_EC: -+ ec = br_skey_decoder_get_ec(&skdec); -+ cert->key = k = malloc(sizeof(*k) + ec->xlen); -+ assert(k); -+ k->type = BR_KEYTYPE_EC; -+ k->ec.curve = ec->curve; -+ k->ec.x = k->data; -+ k->ec.xlen = ec->xlen; -+ memcpy(k->ec.x, ec->x, ec->xlen); -+ break; -+ default: -+ assert(0); -+ } -+ -+ fclose(certin); -+ fclose(skin); -+ return 0; -+ -+error: -+ fclose(certin); -+ fclose(skin); -+ free(cert->chain); -+ return 1; -+} -diff --git a/src/client.c b/src/client.c -index e402cc97d96a904a2a8e9e2db40345b222cb85ae..127a56ca59859e645fea6f1e4ac62431d734e4fd 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -1,13 +1,14 @@ - #include <assert.h> - #include <errno.h> - #include <netdb.h> --#include <bearssl_ssl.h> -+#include <bearssl.h> - #include <stdlib.h> - #include <stdio.h> - #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> - #include <unistd.h> -+#include <gmni/certs.h> - #include <gmni/gmni.h> - #include <gmni/tofu.h> - #include <gmni/url.h> -@@ -169,7 +170,26 @@ } - - // TODO: session reuse - resp->sc = &tofu->sc; -+ if (options->client_cert) { -+ struct gmni_client_certificate *cert = options->client_cert; -+ struct gmni_private_key *key = cert->key; -+ switch (key->type) { -+ case BR_KEYTYPE_RSA: -+ br_ssl_client_set_single_rsa(resp->sc, -+ cert->chain, cert->nchain, &key->rsa, -+ br_rsa_pkcs1_sign_get_default()); -+ break; -+ case BR_KEYTYPE_EC: -+ br_ssl_client_set_single_ec(resp->sc, -+ cert->chain, cert->nchain, &key->ec, -+ BR_KEYTYPE_SIGN, 0, -+ br_ec_get_default(), -+ br_ecdsa_sign_asn1_get_default()); -+ break; -+ } -+ } - br_ssl_client_reset(resp->sc, host, 0); -+ - br_sslio_init(&resp->body, &resp->sc->eng, - sock_read, &resp->fd, sock_write, &resp->fd); - -diff --git a/src/gmni.c b/src/gmni.c -index a8321d06c367128706d19ac283a53e55d95d0a92..f3015ac679eba77397fb4aac5068f3a63e1cdbab 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -1,5 +1,5 @@ - #include <assert.h> --#include <bearssl_ssl.h> -+#include <bearssl.h> - #include <errno.h> - #include <getopt.h> - #include <netdb.h> -@@ -11,6 +11,7 @@ #include <sys/socket.h> - #include <sys/types.h> - #include <termios.h> - #include <unistd.h> -+#include <gmni/certs.h> - #include <gmni/gmni.h> - #include <gmni/tofu.h> - #include <gmni/url.h> -@@ -109,6 +110,45 @@ - return action; - } - -+static struct gmni_client_certificate * -+load_client_cert(char *argv_0, char *path) -+{ -+ char *certpath = strtok(path, ":"); -+ if (!certpath) { -+ usage(argv_0); -+ exit(1); -+ } -+ -+ FILE *certf = fopen(certpath, "r"); -+ if (!certf) { -+ fprintf(stderr, "Failed to open certificate: %s\n", -+ strerror(errno)); -+ exit(1); -+ } -+ -+ char *keypath = strtok(NULL, ":"); -+ if (!keypath) { -+ usage(argv_0); -+ exit(1); -+ } -+ -+ FILE *keyf = fopen(keypath, "r"); -+ if (!keyf) { -+ fprintf(stderr, "Failed to open certificate: %s\n", -+ strerror(errno)); -+ exit(1); -+ } -+ -+ struct gmni_client_certificate *cert = -+ calloc(1, sizeof(struct gmni_client_certificate)); -+ if (gmni_ccert_load(cert, certf, keyf) != 0) { -+ fprintf(stderr, "Failed to load client certificate: %s\n", -+ strerror(errno)); -+ exit(1); -+ } -+ return cert; -+} -+ - int - main(int argc, char *argv[]) - { -@@ -165,7 +205,7 @@ } - } - break; - case 'E': -- assert(0); // TODO: Client certificates -+ opts.client_cert = load_client_cert(argv[0], optarg); - break; - case 'h': - usage(argv[0]); -@@ -226,7 +266,7 @@ - bool exit = false; - struct Curl_URL *url = curl_url(); - -- if(curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { -+ if (curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { - // TODO: Better error - fprintf(stderr, "Error: invalid URL\n"); - return 1; -@@ -238,8 +278,8 @@ char *buf; - curl_url_get(url, CURLUPART_URL, &buf, 0); - - struct gemini_response resp; -- enum gemini_result r = gemini_request( -- buf, &opts, &cfg.tofu, &resp); -+ enum gemini_result r = gemini_request(buf, -+ &opts, &cfg.tofu, &resp); - - free(buf); - -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 0ea492bb85fe024bd250c304808dcb8f0a915f90..aeb0c834d49c799fcdae54acfcbd3925dcedd392 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1,5 +1,5 @@ - #include <assert.h> --#include <bearssl_ssl.h> -+#include <bearssl.h> - #include <ctype.h> - #include <errno.h> - #include <fcntl.h> -diff --git a/src/tofu.c b/src/tofu.c -index 570bd41c885cd9bb007b58bffca957ace6916bea..0acdf33a50bb4957741f4789472ffe56035a7159 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -1,6 +1,5 @@ - #include <assert.h> --#include <bearssl_hash.h> --#include <bearssl_x509.h> -+#include <bearssl.h> - #include <errno.h> - #include <gmni/gmni.h> - #include <gmni/tofu.h> -diff --git a/src/util.c b/src/util.c -index 780d0e8803ab9179683bd9a92cbead05624f2681..1cb0bf42b6319e6bd1b0cf0cc94e28627e4f9c68 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -1,5 +1,5 @@ - #include <assert.h> --#include <bearssl_ssl.h> -+#include <bearssl.h> - #include <errno.h> - #include <gmni/gmni.h> - #include <libgen.h> diff --git a/sources/cgmnlm.git/commits/963700d8d6e31aecfc14e12184637f4c3360f6ed.patch b/sources/cgmnlm.git/commits/963700d8d6e31aecfc14e12184637f4c3360f6ed.patch @@ -1,22 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 7fba942066c9d9870f78311f050ef19dc42f7ce1..def42ac8b1af405da29681d29cd7b9d07b727f11 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -69,7 +69,7 @@ const char *help_msg = - "The following commands are available:\n\n" - "q\tQuit\n" - "N\tFollow Nth link (where N is a number)\n" -- "p[N]\tShow URL of Nth link (where N is a number)\n" -+ "p N\tShow URL of Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" - "H\tView all page history\n" -@@ -303,7 +303,7 @@ result = PROMPT_AGAIN; - goto exit; - } - case 'p': -- if (!in[1]) break; -+ if (!isspace(in[1])) break; - struct link *link = browser->links; - char *endptr; - int linksel = (int)strtol(in+1, &endptr, 10); diff --git a/sources/cgmnlm.git/commits/996bd24225e7a63fd160d1feb9af193225a065b3.patch b/sources/cgmnlm.git/commits/996bd24225e7a63fd160d1feb9af193225a065b3.patch @@ -1,14 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index aefac17cf61674d5c40d9554d0cb9089b1519ba4..50a295870876265849eae7fdf0de926a3ad309fd 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -63,6 +63,9 @@ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { - cc->err = err; - return; - } -+ if (br_x509_decoder_isCA(&cc->decoder) && cc->pkey) { -+ return; -+ } - cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); - br_sha512_out(&cc->sha512, &cc->hash); - } diff --git a/sources/cgmnlm.git/commits/9a195d92566b5790b2b7d3ca848987a095bf3d9c.patch b/sources/cgmnlm.git/commits/9a195d92566b5790b2b7d3ca848987a095bf3d9c.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index a0bc81f851a69ec34c7e9baf643c10728632c7e2..4cd601913caa3fb307de28cfbe861689f933eaf5 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -88,7 +88,7 @@ "b\t\tBack (in the page history)\n" - "f\t\tForward (in the page history)\n" - "H\t\tView all page history\n" - "a\t\tSave bookmark\n" -- "s\tRemove bookmark for current URL\n" -+ "s\t\tRemove bookmark for current URL\n" - "B\t\tBrowse bookmarks\n" - "r\t\tReload the page\n" - "/<text>\t\tsearch for text (POSIX regular expression)\n" diff --git a/sources/cgmnlm.git/commits/9b0006509931c9a3defb64c64f4b0071657f8e61.patch b/sources/cgmnlm.git/commits/9b0006509931c9a3defb64c64f4b0071657f8e61.patch @@ -1,31 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index 5b34850de9e17dcd59fc64a3227cd92bfb0a6cef..570bd41c885cd9bb007b58bffca957ace6916bea 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -25,7 +25,7 @@ static void - xt_start_cert(const br_x509_class **ctx, uint32_t length) - { - struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -- if (cc->err != 0) { -+ if (cc->err != 0 || cc->pkey) { - return; - } - if (length == 0) { -@@ -40,7 +40,7 @@ static void - xt_append(const br_x509_class **ctx, const unsigned char *buf, size_t len) - { - struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -- if (cc->err != 0) { -+ if (cc->err != 0 || cc->pkey) { - return; - } - br_x509_decoder_push(&cc->decoder, buf, len); -@@ -63,7 +63,7 @@ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { - cc->err = err; - return; - } -- if (br_x509_decoder_isCA(&cc->decoder) && cc->pkey) { -+ if (br_x509_decoder_isCA(&cc->decoder)) { - return; - } - cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); diff --git a/sources/cgmnlm.git/commits/9b1a618b4211a029c352c72f6d273e3085c8457d.patch b/sources/cgmnlm.git/commits/9b1a618b4211a029c352c72f6d273e3085c8457d.patch @@ -1,221 +0,0 @@ -diff --git a/include/client.h b/include/client.h -index dbd73234311331ea52650d59dbd9cf535b73298b..f711eea91c3842ac29a884b38e0d7a0ab99cb7fb 100644 ---- a/include/client.h -+++ b/include/client.h -@@ -46,6 +46,7 @@ GEMINI_ERR_CONNECT, - // use SSL_get_error(resp->ssl, resp->status) to get details - GEMINI_ERR_SSL, - GEMINI_ERR_IO, -+ GEMINI_ERR_PROTOCOL, - }; - - // Requests the specified URL via the gemini protocol. If options is non-NULL, -@@ -63,5 +64,8 @@ // allocated during the request. If you intend to re-use the SSL_CTX provided by - // gemini_options, set the ctx pointer to NULL before calling - // gemini_response_finish. - void gemini_response_finish(struct gemini_response *resp); -+ -+// Returns a user-friendly string describing an error. -+const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); - - #endif -diff --git a/src/client.c b/src/client.c -index 5f2debb52c310f88e82ad83ca2251ba233d6a704..67671ccca2a25a460681bdffacb981c7f26c1407 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -2,6 +2,7 @@ #include <assert.h> - #include <errno.h> - #include <netdb.h> - #include <openssl/bio.h> -+#include <openssl/err.h> - #include <openssl/ssl.h> - #include <stdlib.h> - #include <string.h> -@@ -168,6 +169,21 @@ res = GEMINI_ERR_IO; - goto cleanup; - } - -+ if (r < 3 || strcmp(&buf[r - 2], "\r\n") != 0) { -+ res = GEMINI_ERR_PROTOCOL; -+ goto cleanup; -+ } -+ -+ char *endptr; -+ resp->status = (int)strtol(buf, &endptr, 10); -+ if (*endptr != ' ' || resp->status <= 10 || resp->status >= 70) { -+ res = GEMINI_ERR_PROTOCOL; -+ goto cleanup; -+ } -+ resp->meta = calloc(r - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); -+ strncpy(resp->meta, &endptr[1], r - 5); -+ resp->meta[r - 5] = '\0'; -+ - cleanup: - curl_url_cleanup(uri); - return res; -@@ -188,3 +204,29 @@ SSL_free(resp->ssl); - SSL_CTX_free(resp->ssl_ctx); - free(resp->meta); - } -+ -+const char * -+gemini_strerr(enum gemini_result r, struct gemini_response *resp) -+{ -+ switch (r) { -+ case GEMINI_OK: -+ return "OK"; -+ case GEMINI_ERR_OOM: -+ return "Out of memory"; -+ case GEMINI_ERR_INVALID_URL: -+ return "Invalid URL"; -+ case GEMINI_ERR_RESOLVE: -+ return gai_strerror(resp->status); -+ case GEMINI_ERR_CONNECT: -+ return strerror(errno); -+ case GEMINI_ERR_SSL: -+ return ERR_error_string( -+ SSL_get_error(resp->ssl, resp->status), -+ NULL); -+ case GEMINI_ERR_IO: -+ return "I/O error"; -+ case GEMINI_ERR_PROTOCOL: -+ return "Protocol error"; -+ } -+ assert(0); -+} -diff --git a/src/gmnic.c b/src/gmnic.c -index 7b2eb183ebfe67dce0aaf8c6010bf6785b503df6..014211dc3784ba3eadcf1885ef7bcf6fe84d8462 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -1,9 +1,13 @@ - #include <assert.h> -+#include <errno.h> - #include <getopt.h> -+#include <openssl/bio.h> - #include <openssl/err.h> - #include <stdbool.h> - #include <stdio.h> - #include <stdlib.h> -+#include <string.h> -+#include <unistd.h> - #include "client.h" - - static void -@@ -17,11 +21,15 @@ - int - main(int argc, char *argv[]) - { -- bool headers = false, follow_redirect = false; -- char *certificate = NULL, *input = NULL; -+ enum header_mode { -+ OMIT_HEADERS, -+ SHOW_HEADERS, -+ ONLY_HEADERS, -+ }; -+ enum header_mode headers = OMIT_HEADERS; - - int c; -- while ((c = getopt(argc, argv, "46C:d:hLI")) != -1) { -+ while ((c = getopt(argc, argv, "46C:d:hLiI")) != -1) { - switch (c) { - case '4': - assert(0); // TODO -@@ -30,25 +38,29 @@ case '6': - assert(0); // TODO - break; - case 'C': -- certificate = optarg; -+ assert(0); // TODO: Client certificates - break; - case 'd': -- input = optarg; -+ assert(0); // TODO: Input - break; - case 'h': - usage(argv[0]); - return 0; - case 'L': -- follow_redirect = true; -+ assert(0); // TODO: Follow redirects -+ break; -+ case 'i': -+ headers = SHOW_HEADERS; - break; - case 'I': -- headers = true; -+ headers = ONLY_HEADERS; - break; - default: - fprintf(stderr, "fatal: unknown flag %c", c); - return 1; - } - } -+ - if (optind != argc - 1) { - usage(argv[0]); - return 1; -@@ -59,33 +71,41 @@ ERR_load_crypto_strings(); - - struct gemini_response resp; - enum gemini_result r = gemini_request(argv[optind], NULL, &resp); -- switch (r) { -- case GEMINI_OK: -- printf("OK\n"); -- break; -- case GEMINI_ERR_OOM: -- printf("OOM\n"); -- break; -- case GEMINI_ERR_INVALID_URL: -- printf("INVALID_URL\n"); -- break; -- case GEMINI_ERR_RESOLVE: -- printf("RESOLVE\n"); -- break; -- case GEMINI_ERR_CONNECT: -- printf("CONNECT\n"); -+ if (r != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); -+ gemini_response_finish(&resp); -+ return (int)r; -+ } -+ -+ switch (headers) { -+ case ONLY_HEADERS: -+ printf("%d %s\n", resp.status, resp.meta); - break; -- case GEMINI_ERR_SSL: -- fprintf(stderr, "SSL error: %s\n", ERR_error_string( -- SSL_get_error(resp.ssl, resp.status), NULL)); -+ case SHOW_HEADERS: -+ printf("%d %s\n", resp.status, resp.meta); -+ /* fallthrough */ -+ case OMIT_HEADERS: -+ for (int n = 1; n > 0;) { -+ char buf[BUFSIZ]; -+ n = BIO_read(resp.bio, buf, BUFSIZ); -+ if (n == -1) { -+ fprintf(stderr, "Error: read\n"); -+ return 1; -+ } -+ ssize_t w = 0; -+ while (w < (ssize_t)n) { -+ ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); -+ if (x == -1) { -+ fprintf(stderr, "Error: write: %s\n", -+ strerror(errno)); -+ return 1; -+ } -+ w += x; -+ } -+ } - break; - } - - gemini_response_finish(&resp); -- -- (void)headers; -- (void)follow_redirect; -- (void)certificate; -- (void)input; - return 0; - } diff --git a/sources/cgmnlm.git/commits/9bd1a7457ea58ddd568fdbe46a1155c28424e8be.patch b/sources/cgmnlm.git/commits/9bd1a7457ea58ddd568fdbe46a1155c28424e8be.patch @@ -1,12 +0,0 @@ -diff --git a/src/parser.c b/src/parser.c -index ffcc28767be7d638d18609764999d004790aa9a2..eb9aa5ec3d1da4a222e9ec0c76ad19ba004b9a2c 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -58,7 +58,6 @@ if ((end = strstr(p->buf, "\n")) != NULL) { - *end = 0; - } - -- // TODO: Provide whitespace trimming helper function - if (strncmp(p->buf, "=>", 2) == 0) { - tok->token = GEMINI_LINK; - int i = 2; diff --git a/sources/cgmnlm.git/commits/9ddd5c16dae4b556c7aeac88c219568c479d87f2.patch b/sources/cgmnlm.git/commits/9ddd5c16dae4b556c7aeac88c219568c479d87f2.patch @@ -1,363 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index d05180c7a74c88bd7ed8ba551a4a522bb8b74009..9bc95fad89c0f92bc5a68fdd40d272b021c3cc25 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -263,6 +263,167 @@ putc('\n', out); - } - } - -+static char * -+get_input(const struct gemini_response *resp, FILE *source) -+{ -+ int r = 0; -+ struct termios attrs; -+ bool tty = fileno(source) != -1 && isatty(fileno(source)); -+ char *input = NULL; -+ if (tty) { -+ fprintf(stderr, "%s: ", resp->meta); -+ if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { -+ r = tcgetattr(fileno(source), &attrs); -+ struct termios new_attrs; -+ r = tcgetattr(fileno(source), &new_attrs); -+ if (r != -1) { -+ new_attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &new_attrs); -+ } -+ } -+ } -+ size_t s = 0; -+ ssize_t n = getline(&input, &s, source); -+ if (n == -1) { -+ fprintf(stderr, "Error reading input: %s\n", -+ feof(source) ? "EOF" : strerror(ferror(source))); -+ return NULL; -+ } -+ input[n - 1] = '\0'; // Drop LF -+ if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { -+ attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &attrs); -+ } -+ return input; -+} -+ -+static bool -+has_suffix(char *str, char *suff) -+{ -+ size_t suffl = strlen(suff); -+ size_t strl = strlen(str); -+ if (strl < suffl) { -+ return false; -+ } -+ return strcmp(&str[strl - suffl], suff) == 0; -+} -+ -+static enum gemini_result -+do_requests(struct browser *browser, struct gemini_response *resp) -+{ -+ int nredir = 0; -+ bool requesting = true; -+ enum gemini_result res; -+ while (requesting) { -+ char *scheme; -+ CURLUcode uc = curl_url_get(browser->url, -+ CURLUPART_SCHEME, &scheme, 0); -+ assert(uc == CURLUE_OK); // Invariant -+ if (strcmp(scheme, "file") == 0) { -+ free(scheme); -+ requesting = false; -+ -+ char *path; -+ uc = curl_url_get(browser->url, -+ CURLUPART_PATH, &path, 0); -+ if (uc != CURLUE_OK) { -+ resp->status = GEMINI_STATUS_BAD_REQUEST; -+ break; -+ } -+ -+ FILE *fp = fopen(path, "r"); -+ if (!fp) { -+ resp->status = GEMINI_STATUS_NOT_FOUND; -+ /* Make sure members of resp evaluate to false, so that -+ gemini_response_finish does not try to free them. */ -+ resp->bio = NULL; -+ resp->ssl = NULL; -+ resp->ssl_ctx = NULL; -+ resp->meta = NULL; -+ resp->fd = -1; -+ free(path); -+ break; -+ } -+ -+ BIO *file = BIO_new_fp(fp, BIO_CLOSE); -+ resp->bio = BIO_new(BIO_f_buffer()); -+ BIO_push(resp->bio, file); -+ if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) { -+ resp->meta = strdup("text/gemini"); -+ } else if (has_suffix(path, ".txt")) { -+ resp->meta = strdup("text/plain"); -+ } else { -+ resp->meta = strdup("application/x-octet-stream"); -+ } -+ free(path); -+ resp->status = GEMINI_STATUS_SUCCESS; -+ resp->fd = -1; -+ resp->ssl = NULL; -+ resp->ssl_ctx = NULL; -+ return GEMINI_OK; -+ } -+ free(scheme); -+ -+ res = gemini_request(browser->plain_url, &browser->opts, resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); -+ requesting = false; -+ resp->status = 70 + res; -+ break; -+ } -+ -+ char *input; -+ switch (gemini_response_class(resp->status)) { -+ case GEMINI_STATUS_CLASS_INPUT: -+ input = get_input(resp, browser->tty); -+ if (!input) { -+ requesting = false; -+ break; -+ } -+ if (input[0] == '\0' && browser->history->prev) { -+ free(input); -+ browser->history = browser->history->prev; -+ set_url(browser, browser->history->url, NULL); -+ break; -+ } -+ -+ char *new_url = gemini_input_url( -+ browser->plain_url, input); -+ free(input); -+ assert(new_url); -+ set_url(browser, new_url, NULL); -+ free(new_url); -+ break; -+ case GEMINI_STATUS_CLASS_REDIRECT: -+ if (++nredir >= 5) { -+ requesting = false; -+ fprintf(stderr, "Error: maximum redirects (5) exceeded\n"); -+ break; -+ } -+ set_url(browser, resp->meta, NULL); -+ break; -+ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -+ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -+ requesting = false; -+ fprintf(stderr, "Server returned %s %d %s\n", -+ resp->status / 10 == 4 ? -+ "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ resp->status, resp->meta); -+ break; -+ case GEMINI_STATUS_CLASS_SUCCESS: -+ return res; -+ } -+ -+ if (requesting) { -+ gemini_response_finish(resp); -+ } -+ } -+ -+ return res; -+} -+ - static enum prompt_result - do_prompts(const char *prompt, struct browser *browser) - { -@@ -678,6 +839,9 @@ - static bool - display_response(struct browser *browser, struct gemini_response *resp) - { -+ if (gemini_response_class(resp->status) != GEMINI_STATUS_CLASS_SUCCESS) { -+ return false; -+ } - if (strcmp(resp->meta, "text/gemini") == 0 - || strncmp(resp->meta, "text/gemini;", 12) == 0) { - return display_gemini(browser, resp); -@@ -688,170 +852,6 @@ } - assert(0); // TODO: Deal with other mimetypes - } - --static char * --get_input(const struct gemini_response *resp, FILE *source) --{ -- int r = 0; -- struct termios attrs; -- bool tty = fileno(source) != -1 && isatty(fileno(source)); -- char *input = NULL; -- if (tty) { -- fprintf(stderr, "%s: ", resp->meta); -- if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { -- r = tcgetattr(fileno(source), &attrs); -- struct termios new_attrs; -- r = tcgetattr(fileno(source), &new_attrs); -- if (r != -1) { -- new_attrs.c_lflag &= ~ECHO; -- tcsetattr(fileno(source), TCSANOW, &new_attrs); -- } -- } -- } -- size_t s = 0; -- ssize_t n = getline(&input, &s, source); -- if (n == -1) { -- fprintf(stderr, "Error reading input: %s\n", -- feof(source) ? "EOF" : strerror(ferror(source))); -- return NULL; -- } -- input[n - 1] = '\0'; // Drop LF -- if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { -- attrs.c_lflag &= ~ECHO; -- tcsetattr(fileno(source), TCSANOW, &attrs); -- } -- return input; --} -- --static bool --has_suffix(char *str, char *suff) --{ -- size_t suffl = strlen(suff); -- size_t strl = strlen(str); -- if (strl < suffl) { -- return false; -- } -- return strcmp(&str[strl - suffl], suff) == 0; --} -- --// Returns true to skip prompting --static bool --do_requests(struct browser *browser, struct gemini_response *resp) --{ -- int nredir = 0; -- bool requesting = true; -- while (requesting) { -- char *scheme; -- CURLUcode uc = curl_url_get(browser->url, -- CURLUPART_SCHEME, &scheme, 0); -- assert(uc == CURLUE_OK); // Invariant -- if (strcmp(scheme, "file") == 0) { -- free(scheme); -- requesting = false; -- -- char *path; -- uc = curl_url_get(browser->url, -- CURLUPART_PATH, &path, 0); -- if (uc != CURLUE_OK) { -- resp->status = GEMINI_STATUS_BAD_REQUEST; -- break; -- } -- -- FILE *fp = fopen(path, "r"); -- if (!fp) { -- resp->status = GEMINI_STATUS_NOT_FOUND; -- /* Make sure members of resp evaluate to false, so that -- gemini_response_finish does not try to free them. */ -- resp->bio = NULL; -- resp->ssl = NULL; -- resp->ssl_ctx = NULL; -- resp->meta = NULL; -- resp->fd = -1; -- free(path); -- break; -- } -- -- BIO *file = BIO_new_fp(fp, BIO_CLOSE); -- resp->bio = BIO_new(BIO_f_buffer()); -- BIO_push(resp->bio, file); -- if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) { -- resp->meta = strdup("text/gemini"); -- } else if (has_suffix(path, ".txt")) { -- resp->meta = strdup("text/plain"); -- } else { -- resp->meta = strdup("application/x-octet-stream"); -- } -- free(path); -- resp->status = GEMINI_STATUS_SUCCESS; -- resp->fd = -1; -- resp->ssl = NULL; -- resp->ssl_ctx = NULL; -- return display_response(browser, resp); -- } -- free(scheme); -- -- enum gemini_result res = gemini_request(browser->plain_url, -- &browser->opts, resp); -- if (res != GEMINI_OK) { -- fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); -- requesting = false; -- resp->status = 70 + res; -- break; -- } -- -- char *input; -- switch (gemini_response_class(resp->status)) { -- case GEMINI_STATUS_CLASS_INPUT: -- input = get_input(resp, browser->tty); -- if (!input) { -- requesting = false; -- break; -- } -- if (input[0] == '\0' && browser->history->prev) { -- free(input); -- browser->history = browser->history->prev; -- set_url(browser, browser->history->url, NULL); -- break; -- } -- -- char *new_url = gemini_input_url( -- browser->plain_url, input); -- free(input); -- assert(new_url); -- set_url(browser, new_url, NULL); -- free(new_url); -- break; -- case GEMINI_STATUS_CLASS_REDIRECT: -- if (++nredir >= 5) { -- requesting = false; -- fprintf(stderr, "Error: maximum redirects (5) exceeded\n"); -- break; -- } -- fprintf(stderr, "Following redirect to %s\n", resp->meta); -- set_url(browser, resp->meta, NULL); -- break; -- case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -- assert(0); // TODO -- case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -- case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -- requesting = false; -- fprintf(stderr, "Server returned %s %d %s\n", -- resp->status / 10 == 4 ? -- "TEMPORARY FAILURE" : "PERMANENT FALIURE", -- resp->status, resp->meta); -- break; -- case GEMINI_STATUS_CLASS_SUCCESS: -- requesting = false; -- return display_response(browser, resp); -- } -- -- if (requesting) { -- gemini_response_finish(resp); -- } -- } -- -- return false; --} -- - static enum tofu_action - tofu_callback(enum tofu_error error, const char *fingerprint, - struct known_host *khost, void *data) -@@ -1001,7 +1001,8 @@ struct gemini_response resp; - browser.running = true; - while (browser.running) { - static char prompt[4096]; -- bool skip_prompt = do_requests(&browser, &resp); -+ bool skip_prompt = do_requests(&browser, &resp) == GEMINI_OK -+ && display_response(&browser, &resp); - if (browser.meta) { - free(browser.meta); - } diff --git a/sources/cgmnlm.git/commits/9ef33fb102426d0bf56e93ceebcd81eb24171a9e.patch b/sources/cgmnlm.git/commits/9ef33fb102426d0bf56e93ceebcd81eb24171a9e.patch @@ -1,108 +0,0 @@ -diff --git a/README.md b/README.md -index 80695023b563475331438ce368d70f77ff6f24ca..eabc77ea5a6dd0f73bd908fa5a52b03fd53ddcce 100644 ---- a/README.md -+++ b/README.md -@@ -28,7 +28,7 @@ - heading 3: light green - - gemini link on same capsule: light cyan - - gemini link to another capsule: dark cyan - - non-gemini link: light magenta --- quote: light gray -+- preformatted text: light gray - - Besides this rendering adjustments i'll try to keep track of upstream changes or send patches to upstream. - -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 8e85d091e54e3c3c11dbd6980ae660be6838c7c8..66db54c7e0cf7dd6a1f58872621cc28e69944134 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -23,7 +23,7 @@ #define ANSI_COLOR_RED "\x1b[91m" - #define ANSI_COLOR_GREEN "\x1b[92m" - #define ANSI_COLOR_YELLOW "\x1b[93m" - #define ANSI_COLOR_BLUE "\x1b[94m" --#define ANSI_COLOR_MAGENTA "\x1b[95m" -+#define ANSI_COLOR_MAGENTA "\x1b[35m" - #define ANSI_COLOR_CYAN "\x1b[36m" - #define ANSI_COLOR_LCYAN "\x1b[96m" - #define ANSI_COLOR_GRAY "\x1b[37m" -@@ -70,7 +70,7 @@ - const char *default_bookmarks = - "# Welcome to cgmnlm\n\n" - "Links:\n\n" -- "=> https://src.clttr.info/rwa/cgmnlm The cgmnlm browser\n" -+ "=> https://gmn.clttr.info/cgmnln.gmi The colorful line mode client\n" - "=> gemini://gemini.circumlunar.space The gemini protocol\n\n" - "This file can be found at %s and may be edited at your pleasure.\n\n" - "Bookmarks:\n" -@@ -809,20 +809,20 @@ while (text != NULL || gemini_parser_next(&p, &tok) == 0) { - repeat: - switch (tok.token) { - case GEMINI_TEXT: -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - if (text == NULL) { - text = tok.text; - } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_MAGENTA)); -+ col += fprintf(out, "%3d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_MAGENTA)); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); - next = &(*next)->next; - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED_BEGIN: -@@ -832,7 +832,7 @@ case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: - if (text == NULL) { -- fprintf(out, " "); -+ fprintf(out, " %s", ANSI_COLOR_GRAY); - text = tok.preformatted; - } - break; -@@ -843,32 +843,31 @@ } - if (text == NULL) { - switch (tok.heading.level) { - case 1: -- col += fprintf(out, "%s%s", " # ", ANSI_COLOR_RED); -+ col += fprintf(out, " # %s", ANSI_COLOR_RED); - break; - case 2: -- col += fprintf(out, "%s%s", " ## ", ANSI_COLOR_YELLOW); -+ col += fprintf(out, " ## %s", ANSI_COLOR_YELLOW); - break; - case 3: -- col += fprintf(out, "%s%s", "### ", ANSI_COLOR_GREEN); -+ col += fprintf(out, " ### %s", ANSI_COLOR_GREEN); - break; - } - text = trim_ws(tok.heading.title); - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_LIST_ITEM: - if (text == NULL) { -- col += fprintf(out, " %s ", -+ col += fprintf(out, " %s ", - browser->unicode ? "•" : "*"); - text = trim_ws(tok.list_item); - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: -- col += fprintf(out, " %s%s %s", ANSI_COLOR_RESET, -- browser->unicode ? "┃" : ">", ANSI_COLOR_GRAY); -+ col += fprintf(out, " %s ", browser->unicode ? "┃" : ">"); - if (text == NULL) { - text = trim_ws(tok.quote_text); - } diff --git a/sources/cgmnlm.git/commits/9f98e013a6cd966cf4dc2d98187d6f0ba6f7fb5c.patch b/sources/cgmnlm.git/commits/9f98e013a6cd966cf4dc2d98187d6f0ba6f7fb5c.patch @@ -1,27 +0,0 @@ -diff --git a/config.sh b/config.sh -index 52931ab241c3177285b56258bf9b67ac4b63a7ea..2a94bc6feada62bcfda616fa573d0a12db0a4502 100644 ---- a/config.sh -+++ b/config.sh -@@ -88,6 +88,8 @@ CFLAGS="$CFLAGS $(pkg-config --cflags "$pc")" - LIBS="$LIBS $(pkg-config --libs "$pc")" - } - -+docs() { true; } -+ - run_configure() { - mkdir -p $outdir - -@@ -133,8 +135,11 @@ CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' - - all: ${all} - EOF -- gmni >>"$outdir"/config.mk -- gmnlm >>"$outdir"/config.mk -+ -+ for target in $all -+ do -+ $target >>"$outdir"/config.mk -+ done - echo done - - touch $outdir/cppcache diff --git a/sources/cgmnlm.git/commits/a3d5169d71f181efaa59a619e7362911a6c048b7.patch b/sources/cgmnlm.git/commits/a3d5169d71f181efaa59a619e7362911a6c048b7.patch @@ -1,28 +0,0 @@ -diff --git a/README.md b/README.md -new file mode 100644 -index 0000000000000000000000000000000000000000..c08d5b2e62d4ec740ffda4acfccbc198ad9726d4 ---- /dev/null -+++ b/README.md -@@ -0,0 +1,22 @@ -+# gmni - A Gemini client -+ -+This is a [Gemini](https://gemini.circumlunar.space/) client. -+ -+Dependencies: -+ -+- A POSIX-like system and a C11 compiler -+- OpenSSL -+- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) -+ -+## Compiling -+ -+``` -+$ mkdir build && cd build -+$ ../configure -+$ make -+# make install -+``` -+ -+## Usage -+ -+See `gmni(1)` for the CLI usage. diff --git a/sources/cgmnlm.git/commits/a5eae7ea6b35f7b2540fefdf4613a86916f0a0b0.patch b/sources/cgmnlm.git/commits/a5eae7ea6b35f7b2540fefdf4613a86916f0a0b0.patch @@ -1,20 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index b4f20de66d50fe9f0287c74b9a9292fe8ddd48d3..0546486340b7299be3c97266dd534a1354bf31a3 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -188,6 +188,15 @@ { - char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ if (mkdirs(dirname(path), 0755) != 0) { -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); -+ fprintf(stderr, "Error creating directory %s: %s\n", -+ dirname(path), strerror(errno)); -+ return; -+ } -+ -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - - struct stat buf; diff --git a/sources/cgmnlm.git/commits/a61a75f837239bed3aa74331699d301fb93d9da8.patch b/sources/cgmnlm.git/commits/a61a75f837239bed3aa74331699d301fb93d9da8.patch @@ -1,84 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 5d6ce3cc5ff97884bb10c2e9e7f6e870b5d2f54a..e6910607646fdd4f4d5a04a052715eda35147c30 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -146,6 +146,45 @@ for (; *in && isspace(*in); ++in); - return in; - } - -+static int -+wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col) -+{ -+ if (!s[0]) { -+ fprintf(f, "\n"); -+ return 0; -+ } -+ for (int i = 0; s[i]; ++i) { -+ // TODO: Other control sequences, and eat ANSI escapes before -+ // they become a problem -+ switch (s[i]) { -+ case '\n': -+ assert(0); // Not supposed to happen -+ case '\t': -+ *col = *col + (8 - *col % 8); -+ break; -+ default: -+ *col += 1; -+ break; -+ } -+ -+ if (*col >= ws->ws_col) { -+ int j = i--; -+ while (&s[i] != s && !isspace(s[i])) --i; -+ if (&s[i] == s) { -+ i = j; -+ } -+ char c = s[i]; -+ s[i] = 0; -+ int n = fprintf(f, "%s\n", s); -+ s[i] = c; -+ *row += 1; -+ *col = 0; -+ return n; -+ } -+ } -+ return fprintf(f, "%s\n", s) - 1; -+} -+ - static bool - display_gemini(struct browser *browser, struct gemini_response *resp) - { -@@ -157,15 +196,29 @@ - struct winsize ws; - ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - -+ char *text = NULL; - int row = 0, col = 0; - struct gemini_token tok; - struct link **next = &browser->links; -- while (gemini_parser_next(&p, &tok) == 0) { -+ while (text != NULL || gemini_parser_next(&p, &tok) == 0) { - switch (tok.token) { - case GEMINI_TEXT: -- // TODO: word wrap -- col += fprintf(browser->tty, " %s\n", -- trim_ws(tok.text)); -+ if (text == NULL) { -+ text = tok.text; -+ } -+ -+ do { -+ col += fprintf(browser->tty, " "); -+ int w = wrap(browser->tty, text, &ws, &row, &col); -+ text += w; -+ if (row >= ws.ws_row - 4) { -+ break; -+ } -+ } while (text[0]); -+ -+ if (!text[0]) { -+ text = NULL; -+ } - break; - case GEMINI_LINK: - col += fprintf(browser->tty, "%d) %s\n", nlinks++, diff --git a/sources/cgmnlm.git/commits/a6e0326291eee1e1f8ee723ac1e8467ed0561e86.patch b/sources/cgmnlm.git/commits/a6e0326291eee1e1f8ee723ac1e8467ed0561e86.patch @@ -1,37 +0,0 @@ -diff --git a/README.md b/README.md -index 9564df898ce60d9895b818f95ae020c39edb83af..80695023b563475331438ce368d70f77ff6f24ca 100644 ---- a/README.md -+++ b/README.md -@@ -25,7 +25,8 @@ The actual colors used depend on your terminal palette: - - heading 1: light red - - heading 2: light yellow - - heading 3: light green --- gemini link: light cyan -+- gemini link on same capsule: light cyan -+- gemini link to another capsule: dark cyan - - non-gemini link: light magenta - - quote: light gray - -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index 39be8ad24bdf97f87c3e51f4b5ce92e8bf1b421b..a021a97298bb41bff8cdbbceca172f561b64c7a5 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -24,7 +24,8 @@ #define ANSI_COLOR_GREEN "\x1b[92m" - #define ANSI_COLOR_YELLOW "\x1b[93m" - #define ANSI_COLOR_BLUE "\x1b[94m" - #define ANSI_COLOR_MAGENTA "\x1b[95m" --#define ANSI_COLOR_CYAN "\x1b[96m" -+#define ANSI_COLOR_CYAN "\x1b[36m" -+#define ANSI_COLOR_LCYAN "\x1b[96m" - #define ANSI_COLOR_GRAY "\x1b[37m" - #define ANSI_COLOR_RESET "\x1b[0m" - -@@ -815,7 +816,7 @@ } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9) || strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_CYAN : ANSI_COLOR_MAGENTA); -+ col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_MAGENTA)); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); diff --git a/sources/cgmnlm.git/commits/ab66dd2be92931bef04cbccdb3aa008615bd8eba.patch b/sources/cgmnlm.git/commits/ab66dd2be92931bef04cbccdb3aa008615bd8eba.patch @@ -1,19 +0,0 @@ -diff --git a/src/util.c b/src/util.c -index 573e8a717efbe843fce003126cb932928b15f716..2f62c29ce40ac012993e1d208d65491b56d204aa 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -19,13 +19,8 @@ - assert(strlen(path) <= PATH_MAX); - - strcpy(p, path); -- t = dirname(path); -+ t = dirname(p); - memmove(dname, t, strlen(t) + 1); -- -- /* restore the path if dirname worked in-place */ -- if (t == path && path != dname) { -- strcpy(path, p); -- } - } - - /** Make directory and all of its parents */ diff --git a/sources/cgmnlm.git/commits/abcb9caf86020a7cdd9f502fe01eb5db3c70c685.patch b/sources/cgmnlm.git/commits/abcb9caf86020a7cdd9f502fe01eb5db3c70c685.patch @@ -1,2412 +0,0 @@ -diff --git a/config.sh b/config.sh -index fc1134c0c98d9f57cb2cfdbb3b9d6faf35001dfd..70c4489bf5fd48fc0eb636bf739a5d82dcff8663 100644 ---- a/config.sh -+++ b/config.sh -@@ -5,6 +5,7 @@ AS=${AS:-as} - CC=${CC:-cc} - CFLAGS=${CFLAGS:-} - LD=${LD:-ld} -+LIBSSL= - - for arg - do -@@ -12,6 +13,9 @@ # TODO: Add args for install directories - case "$arg" in - --prefix=*) - PREFIX=${arg#*=} -+ ;; -+ --with-libssl=*) -+ LIBSSL=${arg#*=} - ;; - esac - done -@@ -72,12 +76,27 @@ return 1 - fi - } - -+find_library() { -+ name="$1" -+ pc="$2" -+ printf "Checking for %s... " "$name" -+ if ! pkg-config "$pc" 2>/dev/null -+ then -+ printf "NOT FOUND\n" -+ printf "Tried pkg-config %s\n" "$pc" -+ return 1 -+ fi -+ printf "OK\n" -+ CFLAGS="$CFLAGS $(pkg-config --cflags "$pc")" -+ LIBS="$LIBS $(pkg-config --libs "$pc")" -+} -+ - run_configure() { - mkdir -p $outdir - - for flag in -g -std=c11 -D_XOPEN_SOURCE=700 -Wall -Wextra -Werror -pedantic - do -- printf "Checking for $flag... " -+ printf "Checking for %s... " "$flag" - if test_cflags "$flag" - then - echo yes -@@ -86,9 +105,13 @@ echo no - fi - done - -+ find_library OpenSSL libssl -+ find_library OpenSSL libcrypto -+ - printf "Creating $outdir/config.mk... " - cat <<-EOF > "$outdir"/config.mk - CC=$CC -+ LIBS=$LIBS - PREFIX=${PREFIX:-/usr/local} - OUTDIR=${outdir} - _INSTDIR=\$(DESTDIR)\$(PREFIX) -diff --git a/configure b/configure -index 7b1a48b785b7ab1a8811cb36ea9ee00a68fc9ff8..680b57fc9e319c2707e0cc2ded0bc22597544d78 100755 ---- a/configure -+++ b/configure -@@ -4,7 +4,10 @@ eval ". $srcdir/config.sh" - - gmni() { - genrules gmnic \ -- src/gmnic.c -+ src/client.c \ -+ src/escape.c \ -+ src/gmnic.c \ -+ src/url.c - } - - all="gmnic" -diff --git a/include/client.h b/include/client.h -new file mode 100644 -index 0000000000000000000000000000000000000000..dbd73234311331ea52650d59dbd9cf535b73298b ---- /dev/null -+++ b/include/client.h -@@ -0,0 +1,67 @@ -+#ifndef GEMINI_CLIENT_H -+#define GEMINI_CLIENT_H -+#include <netdb.h> -+#include <openssl/ssl.h> -+#include <sys/socket.h> -+ -+struct gemini_response { -+ int status; -+ char *meta; -+ -+ // Response body may be read from here if appropriate: -+ BIO *bio; -+ -+ // Connection state -+ SSL_CTX *ssl_ctx; -+ SSL *ssl; -+ int fd; -+}; -+ -+struct gemini_options { -+ // If NULL, an SSL context will be created. If unset, the ssl field -+ // must also be NULL. -+ SSL_CTX *ssl_ctx; -+ -+ // If NULL, an SSL connection will be established. If set, it is -+ // presumed that the caller pre-established the SSL connection. -+ SSL *ssl; -+ -+ // If ai_family != AF_UNSPEC (the default value on most systems), the -+ // client will connect to this address and skip name resolution. -+ struct addrinfo *addr; -+ -+ // If non-NULL, these hints are provided to getaddrinfo. Useful, for -+ // example, to force IPv4/IPv6. -+ struct addrinfo *hints; -+}; -+ -+enum gemini_result { -+ GEMINI_OK, -+ GEMINI_ERR_OOM, -+ GEMINI_ERR_INVALID_URL, -+ // status is set to the return value from getaddrinfo -+ GEMINI_ERR_RESOLVE, -+ // status is set to errno -+ GEMINI_ERR_CONNECT, -+ // use SSL_get_error(resp->ssl, resp->status) to get details -+ GEMINI_ERR_SSL, -+ GEMINI_ERR_IO, -+}; -+ -+// Requests the specified URL via the gemini protocol. If options is non-NULL, -+// it may specify some additional configuration to adjust client behavior. -+// -+// Returns a value indicating the success of the request. If GEMINI_OK is -+// returned, the response details shall be written to the gemini_response -+// argument. -+enum gemini_result gemini_request(const char *url, -+ struct gemini_options *options, -+ struct gemini_response *resp); -+ -+// Must be called after gemini_request in order to free up the resources -+// allocated during the request. If you intend to re-use the SSL_CTX provided by -+// gemini_options, set the ctx pointer to NULL before calling -+// gemini_response_finish. -+void gemini_response_finish(struct gemini_response *resp); -+ -+#endif -diff --git a/include/escape.h b/include/escape.h -new file mode 100644 -index 0000000000000000000000000000000000000000..a1184ba3141b2992b0b18e44105bdc4474a7d8c9 ---- /dev/null -+++ b/include/escape.h -@@ -0,0 +1,175 @@ -+#ifndef ESCAPE_H -+#define ESCAPE_H -+/*************************************************************************** -+ * _ _ ____ _ -+ * Project ___| | | | _ \| | -+ * / __| | | | |_) | | -+ * | (__| |_| | _ <| |___ -+ * \___|\___/|_| \_\_____| -+ * -+ * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -+ * -+ * This software is licensed as described in the file COPYING, which -+ * you should have received as part of this distribution. The terms -+ * are also available at https://curl.haxx.se/docs/copyright.html. -+ * -+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell -+ * copies of the Software, and permit persons to whom the Software is -+ * furnished to do so, under the terms of the COPYING file. -+ * -+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -+ * KIND, either express or implied. -+ * -+ ***************************************************************************/ -+ -+/* from curl.h */ -+typedef enum { -+ CURLE_OK = 0, -+ CURLE_UNSUPPORTED_PROTOCOL, /* 1 */ -+ CURLE_FAILED_INIT, /* 2 */ -+ CURLE_URL_MALFORMAT, /* 3 */ -+ CURLE_NOT_BUILT_IN, /* 4 - [was obsoleted in August 2007 for -+ 7.17.0, reused in April 2011 for 7.21.5] */ -+ CURLE_COULDNT_RESOLVE_PROXY, /* 5 */ -+ CURLE_COULDNT_RESOLVE_HOST, /* 6 */ -+ CURLE_COULDNT_CONNECT, /* 7 */ -+ CURLE_WEIRD_SERVER_REPLY, /* 8 */ -+ CURLE_REMOTE_ACCESS_DENIED, /* 9 a service was denied by the server -+ due to lack of access - when login fails -+ this is not returned. */ -+ CURLE_FTP_ACCEPT_FAILED, /* 10 - [was obsoleted in April 2006 for -+ 7.15.4, reused in Dec 2011 for 7.24.0]*/ -+ CURLE_FTP_WEIRD_PASS_REPLY, /* 11 */ -+ CURLE_FTP_ACCEPT_TIMEOUT, /* 12 - timeout occurred accepting server -+ [was obsoleted in August 2007 for 7.17.0, -+ reused in Dec 2011 for 7.24.0]*/ -+ CURLE_FTP_WEIRD_PASV_REPLY, /* 13 */ -+ CURLE_FTP_WEIRD_227_FORMAT, /* 14 */ -+ CURLE_FTP_CANT_GET_HOST, /* 15 */ -+ CURLE_HTTP2, /* 16 - A problem in the http2 framing layer. -+ [was obsoleted in August 2007 for 7.17.0, -+ reused in July 2014 for 7.38.0] */ -+ CURLE_FTP_COULDNT_SET_TYPE, /* 17 */ -+ CURLE_PARTIAL_FILE, /* 18 */ -+ CURLE_FTP_COULDNT_RETR_FILE, /* 19 */ -+ CURLE_OBSOLETE20, /* 20 - NOT USED */ -+ CURLE_QUOTE_ERROR, /* 21 - quote command failure */ -+ CURLE_HTTP_RETURNED_ERROR, /* 22 */ -+ CURLE_WRITE_ERROR, /* 23 */ -+ CURLE_OBSOLETE24, /* 24 - NOT USED */ -+ CURLE_UPLOAD_FAILED, /* 25 - failed upload "command" */ -+ CURLE_READ_ERROR, /* 26 - couldn't open/read from file */ -+ CURLE_OUT_OF_MEMORY, /* 27 */ -+ /* Note: CURLE_OUT_OF_MEMORY may sometimes indicate a conversion error -+ instead of a memory allocation error if CURL_DOES_CONVERSIONS -+ is defined -+ */ -+ CURLE_OPERATION_TIMEDOUT, /* 28 - the timeout time was reached */ -+ CURLE_OBSOLETE29, /* 29 - NOT USED */ -+ CURLE_FTP_PORT_FAILED, /* 30 - FTP PORT operation failed */ -+ CURLE_FTP_COULDNT_USE_REST, /* 31 - the REST command failed */ -+ CURLE_OBSOLETE32, /* 32 - NOT USED */ -+ CURLE_RANGE_ERROR, /* 33 - RANGE "command" didn't work */ -+ CURLE_HTTP_POST_ERROR, /* 34 */ -+ CURLE_SSL_CONNECT_ERROR, /* 35 - wrong when connecting with SSL */ -+ CURLE_BAD_DOWNLOAD_RESUME, /* 36 - couldn't resume download */ -+ CURLE_FILE_COULDNT_READ_FILE, /* 37 */ -+ CURLE_LDAP_CANNOT_BIND, /* 38 */ -+ CURLE_LDAP_SEARCH_FAILED, /* 39 */ -+ CURLE_OBSOLETE40, /* 40 - NOT USED */ -+ CURLE_FUNCTION_NOT_FOUND, /* 41 - NOT USED starting with 7.53.0 */ -+ CURLE_ABORTED_BY_CALLBACK, /* 42 */ -+ CURLE_BAD_FUNCTION_ARGUMENT, /* 43 */ -+ CURLE_OBSOLETE44, /* 44 - NOT USED */ -+ CURLE_INTERFACE_FAILED, /* 45 - CURLOPT_INTERFACE failed */ -+ CURLE_OBSOLETE46, /* 46 - NOT USED */ -+ CURLE_TOO_MANY_REDIRECTS, /* 47 - catch endless re-direct loops */ -+ CURLE_UNKNOWN_OPTION, /* 48 - User specified an unknown option */ -+ CURLE_TELNET_OPTION_SYNTAX, /* 49 - Malformed telnet option */ -+ CURLE_OBSOLETE50, /* 50 - NOT USED */ -+ CURLE_OBSOLETE51, /* 51 - NOT USED */ -+ CURLE_GOT_NOTHING, /* 52 - when this is a specific error */ -+ CURLE_SSL_ENGINE_NOTFOUND, /* 53 - SSL crypto engine not found */ -+ CURLE_SSL_ENGINE_SETFAILED, /* 54 - can not set SSL crypto engine as -+ default */ -+ CURLE_SEND_ERROR, /* 55 - failed sending network data */ -+ CURLE_RECV_ERROR, /* 56 - failure in receiving network data */ -+ CURLE_OBSOLETE57, /* 57 - NOT IN USE */ -+ CURLE_SSL_CERTPROBLEM, /* 58 - problem with the local certificate */ -+ CURLE_SSL_CIPHER, /* 59 - couldn't use specified cipher */ -+ CURLE_PEER_FAILED_VERIFICATION, /* 60 - peer's certificate or fingerprint -+ wasn't verified fine */ -+ CURLE_BAD_CONTENT_ENCODING, /* 61 - Unrecognized/bad encoding */ -+ CURLE_LDAP_INVALID_URL, /* 62 - Invalid LDAP URL */ -+ CURLE_FILESIZE_EXCEEDED, /* 63 - Maximum file size exceeded */ -+ CURLE_USE_SSL_FAILED, /* 64 - Requested FTP SSL level failed */ -+ CURLE_SEND_FAIL_REWIND, /* 65 - Sending the data requires a rewind -+ that failed */ -+ CURLE_SSL_ENGINE_INITFAILED, /* 66 - failed to initialise ENGINE */ -+ CURLE_LOGIN_DENIED, /* 67 - user, password or similar was not -+ accepted and we failed to login */ -+ CURLE_TFTP_NOTFOUND, /* 68 - file not found on server */ -+ CURLE_TFTP_PERM, /* 69 - permission problem on server */ -+ CURLE_REMOTE_DISK_FULL, /* 70 - out of disk space on server */ -+ CURLE_TFTP_ILLEGAL, /* 71 - Illegal TFTP operation */ -+ CURLE_TFTP_UNKNOWNID, /* 72 - Unknown transfer ID */ -+ CURLE_REMOTE_FILE_EXISTS, /* 73 - File already exists */ -+ CURLE_TFTP_NOSUCHUSER, /* 74 - No such user */ -+ CURLE_CONV_FAILED, /* 75 - conversion failed */ -+ CURLE_CONV_REQD, /* 76 - caller must register conversion -+ callbacks using curl_easy_setopt options -+ CURLOPT_CONV_FROM_NETWORK_FUNCTION, -+ CURLOPT_CONV_TO_NETWORK_FUNCTION, and -+ CURLOPT_CONV_FROM_UTF8_FUNCTION */ -+ CURLE_SSL_CACERT_BADFILE, /* 77 - could not load CACERT file, missing -+ or wrong format */ -+ CURLE_REMOTE_FILE_NOT_FOUND, /* 78 - remote file not found */ -+ CURLE_SSH, /* 79 - error from the SSH layer, somewhat -+ generic so the error message will be of -+ interest when this has happened */ -+ -+ CURLE_SSL_SHUTDOWN_FAILED, /* 80 - Failed to shut down the SSL -+ connection */ -+ CURLE_AGAIN, /* 81 - socket is not ready for send/recv, -+ wait till it's ready and try again (Added -+ in 7.18.2) */ -+ CURLE_SSL_CRL_BADFILE, /* 82 - could not load CRL file, missing or -+ wrong format (Added in 7.19.0) */ -+ CURLE_SSL_ISSUER_ERROR, /* 83 - Issuer check failed. (Added in -+ 7.19.0) */ -+ CURLE_FTP_PRET_FAILED, /* 84 - a PRET command failed */ -+ CURLE_RTSP_CSEQ_ERROR, /* 85 - mismatch of RTSP CSeq numbers */ -+ CURLE_RTSP_SESSION_ERROR, /* 86 - mismatch of RTSP Session Ids */ -+ CURLE_FTP_BAD_FILE_LIST, /* 87 - unable to parse FTP file list */ -+ CURLE_CHUNK_FAILED, /* 88 - chunk callback reported error */ -+ CURLE_NO_CONNECTION_AVAILABLE, /* 89 - No connection available, the -+ session will be queued */ -+ CURLE_SSL_PINNEDPUBKEYNOTMATCH, /* 90 - specified pinned public key did not -+ match */ -+ CURLE_SSL_INVALIDCERTSTATUS, /* 91 - invalid certificate status */ -+ CURLE_HTTP2_STREAM, /* 92 - stream error in HTTP/2 framing layer -+ */ -+ CURLE_RECURSIVE_API_CALL, /* 93 - an api function was called from -+ inside a callback */ -+ CURL_LAST /* never use! */ -+} CURLcode; -+ -+/* Escape and unescape URL encoding in strings. The functions return a new -+ * allocated string or NULL if an error occurred. */ -+ -+bool Curl_isunreserved(unsigned char in); -+CURLcode Curl_urldecode(const char *string, size_t length, -+ char **ostring, size_t *olen, -+ bool reject_crlf); -+ -+char *curl_easy_escape(const char *string, int length); -+ -+char *curl_escape(const char *string, int length); -+ -+char *curl_easy_unescape(const char *string, -+ int length, int *outlength); -+ -+char *curl_unescape(const char *string, int length); -+ -+ -+#endif /* HEADER_CURL_ESCAPE_H */ -diff --git a/include/url.h b/include/url.h -new file mode 100644 -index 0000000000000000000000000000000000000000..155fd55740dbe47a498062713aca217ab259734f ---- /dev/null -+++ b/include/url.h -@@ -0,0 +1,103 @@ -+#ifndef URLAPI_H -+#define URLAPI_H -+/*************************************************************************** -+ * _ _ ____ _ -+ * Project ___| | | | _ \| | -+ * / __| | | | |_) | | -+ * | (__| |_| | _ <| |___ -+ * \___|\___/|_| \_\_____| -+ * -+ * Copyright (C) 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -+ * -+ * This software is licensed as described in the file COPYING, which -+ * you should have received as part of this distribution. The terms -+ * are also available at https://curl.haxx.se/docs/copyright.html. -+ * -+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell -+ * copies of the Software, and permit persons to whom the Software is -+ * furnished to do so, under the terms of the COPYING file. -+ * -+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -+ * KIND, either express or implied. -+ * -+ ***************************************************************************/ -+ -+/* the error codes for the URL API */ -+typedef enum { -+ CURLUE_OK, -+ CURLUE_BAD_HANDLE, /* 1 */ -+ CURLUE_BAD_PARTPOINTER, /* 2 */ -+ CURLUE_MALFORMED_INPUT, /* 3 */ -+ CURLUE_BAD_PORT_NUMBER, /* 4 */ -+ CURLUE_UNSUPPORTED_SCHEME, /* 5 */ -+ CURLUE_URLDECODE, /* 6 */ -+ CURLUE_OUT_OF_MEMORY, /* 7 */ -+ CURLUE_USER_NOT_ALLOWED, /* 8 */ -+ CURLUE_UNKNOWN_PART, /* 9 */ -+ CURLUE_NO_SCHEME, /* 10 */ -+ CURLUE_NO_USER, /* 11 */ -+ CURLUE_NO_PASSWORD, /* 12 */ -+ CURLUE_NO_OPTIONS, /* 13 */ -+ CURLUE_NO_HOST, /* 14 */ -+ CURLUE_NO_PORT, /* 15 */ -+ CURLUE_NO_QUERY, /* 16 */ -+ CURLUE_NO_FRAGMENT /* 17 */ -+} CURLUcode; -+ -+typedef enum { -+ CURLUPART_URL, -+ CURLUPART_SCHEME, -+ CURLUPART_USER, -+ CURLUPART_PASSWORD, -+ CURLUPART_OPTIONS, -+ CURLUPART_HOST, -+ CURLUPART_PORT, -+ CURLUPART_PATH, -+ CURLUPART_QUERY, -+ CURLUPART_FRAGMENT -+} CURLUPart; -+ -+#define CURLU_PATH_AS_IS (1<<4) /* leave dot sequences */ -+#define CURLU_DISALLOW_USER (1<<5) /* no user+password allowed */ -+#define CURLU_URLDECODE (1<<6) /* URL decode on get */ -+#define CURLU_URLENCODE (1<<7) /* URL encode on set */ -+#define CURLU_APPENDQUERY (1<<8) /* append a form style part */ -+ -+typedef struct Curl_URL CURLU; -+ -+/* -+ * curl_url() creates a new CURLU handle and returns a pointer to it. -+ * Must be freed with curl_url_cleanup(). -+ */ -+struct Curl_URL *curl_url(void); -+ -+/* -+ * curl_url_cleanup() frees the CURLU handle and related resources used for -+ * the URL parsing. It will not free strings previously returned with the URL -+ * API. -+ */ -+void curl_url_cleanup(struct Curl_URL *handle); -+ -+/* -+ * curl_url_dup() duplicates a CURLU handle and returns a new copy. The new -+ * handle must also be freed with curl_url_cleanup(). -+ */ -+struct Curl_URL *curl_url_dup(struct Curl_URL *in); -+ -+/* -+ * curl_url_get() extracts a specific part of the URL from a CURLU -+ * handle. Returns error code. The returned pointer MUST be freed with -+ * free() afterwards. -+ */ -+CURLUcode curl_url_get(struct Curl_URL *handle, CURLUPart what, -+ char **part, unsigned int flags); -+ -+/* -+ * curl_url_set() sets a specific part of the URL in a CURLU handle. Returns -+ * error code. The passed in string will be copied. Passing a NULL instead of -+ * a part string, clears that part. -+ */ -+CURLUcode curl_url_set(struct Curl_URL *handle, CURLUPart what, -+ const char *part, unsigned int flags); -+ -+#endif -diff --git a/src/client.c b/src/client.c -new file mode 100644 -index 0000000000000000000000000000000000000000..5f2debb52c310f88e82ad83ca2251ba233d6a704 ---- /dev/null -+++ b/src/client.c -@@ -0,0 +1,190 @@ -+#include <assert.h> -+#include <errno.h> -+#include <netdb.h> -+#include <openssl/bio.h> -+#include <openssl/ssl.h> -+#include <stdlib.h> -+#include <string.h> -+#include <sys/socket.h> -+#include <sys/types.h> -+#include <unistd.h> -+#include "client.h" -+#include "url.h" -+ -+static enum gemini_result -+gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options, -+ struct gemini_response *resp, struct addrinfo **addr) -+{ -+ int port = 1965; -+ char *uport; -+ if (curl_url_get(uri, CURLUPART_PORT, &uport, 0) == CURLUE_OK) { -+ port = (int)strtol(uport, NULL, 10); -+ free(uport); -+ } -+ -+ if (options && options->addr->ai_family != AF_UNSPEC) { -+ *addr = options->addr; -+ } else { -+ struct addrinfo hints = {0}; -+ if (options && options->hints) { -+ hints = *options->hints; -+ } else { -+ hints.ai_family = AF_UNSPEC; -+ hints.ai_socktype = SOCK_STREAM; -+ } -+ -+ char pbuf[7]; -+ snprintf(pbuf, sizeof(pbuf), "%d", port); -+ -+ char *domain; -+ CURLUcode uc = curl_url_get(uri, CURLUPART_HOST, &domain, 0); -+ assert(uc == CURLUE_OK); -+ -+ int r = getaddrinfo(domain, pbuf, &hints, addr); -+ free(domain); -+ if (r != 0) { -+ resp->status = r; -+ return GEMINI_ERR_RESOLVE; -+ } -+ } -+ -+ return GEMINI_OK; -+} -+ -+static enum gemini_result -+gemini_connect(struct Curl_URL *uri, struct gemini_options *options, -+ struct gemini_response *resp, int *sfd) -+{ -+ struct addrinfo *addr; -+ enum gemini_result res = gemini_get_addrinfo(uri, options, resp, &addr); -+ if (res != GEMINI_OK) { -+ goto cleanup; -+ } -+ -+ struct addrinfo *rp; -+ for (rp = addr; rp != NULL; rp = rp->ai_next) { -+ *sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); -+ if (*sfd == -1) { -+ continue; -+ } -+ if (connect(*sfd, rp->ai_addr, rp->ai_addrlen) != -1) { -+ break; -+ } -+ close(*sfd); -+ } -+ if (rp == NULL) { -+ resp->status = errno; -+ res = GEMINI_ERR_CONNECT; -+ return res; -+ } -+ -+cleanup: -+ if (!options || !options->addr) { -+ freeaddrinfo(addr); -+ } -+ return res; -+} -+ -+#define GEMINI_META_MAXLEN 1024 -+#define GEMINI_STATUS_MAXLEN 2 -+ -+enum gemini_result -+gemini_request(const char *url, struct gemini_options *options, -+ struct gemini_response *resp) -+{ -+ assert(url); -+ assert(resp); -+ resp->meta = NULL; -+ if (strlen(url) > 1024) { -+ return GEMINI_ERR_INVALID_URL; -+ } -+ -+ struct Curl_URL *uri = curl_url(); -+ if (!uri) { -+ return GEMINI_ERR_OOM; -+ } -+ if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { -+ return GEMINI_ERR_INVALID_URL; -+ } -+ -+ enum gemini_result res = GEMINI_OK; -+ if (options && options->ssl_ctx) { -+ resp->ssl_ctx = options->ssl_ctx; -+ SSL_CTX_up_ref(options->ssl_ctx); -+ } else { -+ resp->ssl_ctx = SSL_CTX_new(TLS_method()); -+ assert(resp->ssl_ctx); -+ } -+ -+ BIO *sbio = BIO_new(BIO_f_ssl()); -+ if (options && options->ssl) { -+ resp->ssl = options->ssl; -+ SSL_up_ref(resp->ssl); -+ BIO_set_ssl(sbio, resp->ssl, 0); -+ resp->fd = -1; -+ } else { -+ res = gemini_connect(uri, options, resp, &resp->fd); -+ if (res != GEMINI_OK) { -+ goto cleanup; -+ } -+ -+ resp->ssl = SSL_new(resp->ssl_ctx); -+ assert(resp->ssl); -+ int r = SSL_set_fd(resp->ssl, resp->fd); -+ if (r != 1) { -+ resp->status = r; -+ res = GEMINI_ERR_SSL; -+ goto cleanup; -+ } -+ r = SSL_connect(resp->ssl); -+ if (r != 1) { -+ resp->status = r; -+ res = GEMINI_ERR_SSL; -+ goto cleanup; -+ } -+ BIO_set_ssl(sbio, resp->ssl, 0); -+ } -+ -+ resp->bio = BIO_new(BIO_f_buffer()); -+ BIO_push(resp->bio, sbio); -+ -+ char req[1024 + 3]; -+ int r = snprintf(req, sizeof(req), "%s\r\n", url); -+ assert(r > 0); -+ -+ r = BIO_puts(sbio, req); -+ if (r == -1) { -+ res = GEMINI_ERR_IO; -+ goto cleanup; -+ } -+ assert(r == (int)strlen(req)); -+ -+ char buf[GEMINI_META_MAXLEN -+ + GEMINI_STATUS_MAXLEN -+ + 2 /* CRLF */ + 1 /* NUL */]; -+ r = BIO_gets(resp->bio, buf, sizeof(buf)); -+ if (r == -1) { -+ res = GEMINI_ERR_IO; -+ goto cleanup; -+ } -+ -+cleanup: -+ curl_url_cleanup(uri); -+ return res; -+} -+ -+void -+gemini_response_finish(struct gemini_response *resp) -+{ -+ if (!resp) { -+ return; -+ } -+ if (resp->fd != -1) { -+ close(resp->fd); -+ } -+ BIO_free(BIO_pop(resp->bio)); // ssl bio -+ BIO_free(resp->bio); // buffered bio -+ SSL_free(resp->ssl); -+ SSL_CTX_free(resp->ssl_ctx); -+ free(resp->meta); -+} -diff --git a/src/escape.c b/src/escape.c -new file mode 100644 -index 0000000000000000000000000000000000000000..d083da699d96f01a94a6ce8a86e41f0a47c54ffe ---- /dev/null -+++ b/src/escape.c -@@ -0,0 +1,213 @@ -+/*************************************************************************** -+ * _ _ ____ _ -+ * Project ___| | | | _ \| | -+ * / __| | | | |_) | | -+ * | (__| |_| | _ <| |___ -+ * \___|\___/|_| \_\_____| -+ * -+ * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -+ * -+ * This software is licensed as described in the file COPYING, which -+ * you should have received as part of this distribution. The terms -+ * are also available at https://curl.haxx.se/docs/copyright.html. -+ * -+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell -+ * copies of the Software, and permit persons to whom the Software is -+ * furnished to do so, under the terms of the COPYING file. -+ * -+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -+ * KIND, either express or implied. -+ * -+ ***************************************************************************/ -+ -+/* Escape and unescape URL encoding in strings. The functions return a new -+ * allocated string or NULL if an error occurred. */ -+#include <ctype.h> -+#include <limits.h> -+#include <stdbool.h> -+#include <stddef.h> -+#include <stdio.h> -+#include <stdlib.h> -+#include <string.h> -+#include "escape.h" -+ -+/* Portable character check (remember EBCDIC). Do not use isalnum() because -+ its behavior is altered by the current locale. -+ See https://tools.ietf.org/html/rfc3986#section-2.3 -+*/ -+bool Curl_isunreserved(unsigned char in) -+{ -+ switch(in) { -+ case '0': case '1': case '2': case '3': case '4': -+ case '5': case '6': case '7': case '8': case '9': -+ case 'a': case 'b': case 'c': case 'd': case 'e': -+ case 'f': case 'g': case 'h': case 'i': case 'j': -+ case 'k': case 'l': case 'm': case 'n': case 'o': -+ case 'p': case 'q': case 'r': case 's': case 't': -+ case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': -+ case 'A': case 'B': case 'C': case 'D': case 'E': -+ case 'F': case 'G': case 'H': case 'I': case 'J': -+ case 'K': case 'L': case 'M': case 'N': case 'O': -+ case 'P': case 'Q': case 'R': case 'S': case 'T': -+ case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': -+ case '-': case '.': case '_': case '~': -+ return true; -+ default: -+ break; -+ } -+ return false; -+} -+ -+/* for ABI-compatibility with previous versions */ -+char *curl_escape(const char *string, int inlength) -+{ -+ return curl_easy_escape(string, inlength); -+} -+ -+/* for ABI-compatibility with previous versions */ -+char *curl_unescape(const char *string, int length) -+{ -+ return curl_easy_unescape(string, length, NULL); -+} -+ -+char *curl_easy_escape(const char *string, int inlength) -+{ -+ size_t alloc; -+ char *ns; -+ char *testing_ptr = NULL; -+ size_t newlen; -+ size_t strindex = 0; -+ size_t length; -+ -+ if(inlength < 0) -+ return NULL; -+ -+ alloc = (inlength?(size_t)inlength:strlen(string)) + 1; -+ newlen = alloc; -+ -+ ns = malloc(alloc); -+ if(!ns) -+ return NULL; -+ -+ length = alloc-1; -+ while(length--) { -+ unsigned char in = *string; /* we need to treat the characters unsigned */ -+ -+ if(Curl_isunreserved(in)) -+ /* just copy this */ -+ ns[strindex++] = in; -+ else { -+ /* encode it */ -+ newlen += 2; /* the size grows with two, since this'll become a %XX */ -+ if(newlen > alloc) { -+ alloc *= 2; -+ testing_ptr = realloc(ns, alloc); -+ if(!testing_ptr) -+ return NULL; -+ ns = testing_ptr; -+ } -+ -+ snprintf(&ns[strindex], 4, "%%%02X", in); -+ -+ strindex += 3; -+ } -+ string++; -+ } -+ ns[strindex] = 0; /* terminate it */ -+ return ns; -+} -+ -+/* -+ * Curl_urldecode() URL decodes the given string. -+ * -+ * Optionally detects control characters (byte codes lower than 32) in the -+ * data and rejects such data. -+ * -+ * Returns a pointer to a malloced string in *ostring with length given in -+ * *olen. If length == 0, the length is assumed to be strlen(string). -+ */ -+CURLcode Curl_urldecode(const char *string, size_t length, -+ char **ostring, size_t *olen, -+ bool reject_ctrl) -+{ -+ size_t alloc = (length?length:strlen(string)) + 1; -+ char *ns = malloc(alloc); -+ size_t strindex = 0; -+ unsigned long hex; -+ -+ if(!ns) -+ return CURLE_OUT_OF_MEMORY; -+ -+ while(--alloc > 0) { -+ unsigned char in = *string; -+ if(('%' == in) && (alloc > 2) && -+ isxdigit(string[1]) && isxdigit(string[2])) { -+ /* this is two hexadecimal digits following a '%' */ -+ char hexstr[3]; -+ char *ptr; -+ hexstr[0] = string[1]; -+ hexstr[1] = string[2]; -+ hexstr[2] = 0; -+ -+ hex = strtoul(hexstr, &ptr, 16); -+ -+ in = (unsigned char)hex; /* this long is never bigger than 255 anyway */ -+ -+ string += 2; -+ alloc -= 2; -+ } -+ -+ if(reject_ctrl && (in < 0x20)) { -+ free(ns); -+ return CURLE_URL_MALFORMAT; -+ } -+ -+ ns[strindex++] = in; -+ string++; -+ } -+ ns[strindex] = 0; /* terminate it */ -+ -+ if(olen) -+ /* store output size */ -+ *olen = strindex; -+ -+ /* store output string */ -+ *ostring = ns; -+ -+ return CURLE_OK; -+} -+ -+/* -+ * Unescapes the given URL escaped string of given length. Returns a -+ * pointer to a malloced string with length given in *olen. -+ * If length == 0, the length is assumed to be strlen(string). -+ * If olen == NULL, no output length is stored. -+ */ -+char *curl_easy_unescape(const char *string, int length, int *olen) -+{ -+ char *str = NULL; -+ if(length >= 0) { -+ size_t inputlen = length; -+ size_t outputlen; -+ CURLcode res = Curl_urldecode(string, inputlen, &str, &outputlen, false); -+ if(res) -+ return NULL; -+ -+ if(olen) { -+ if(outputlen <= (size_t) INT_MAX) -+ *olen = (int)outputlen; -+ else -+ /* too large to return in an int, fail! */ -+ free(str); -+ } -+ } -+ return str; -+} -+ -+/* For operating systems/environments that use different malloc/free -+ systems for the app and for this library, we provide a free that uses -+ the library's memory system */ -+void curl_free(void *p) -+{ -+ free(p); -+} -diff --git a/src/gmnic.c b/src/gmnic.c -index 35a49493cc6af2486d055c9bde5a576942c2e40a..7b2eb183ebfe67dce0aaf8c6010bf6785b503df6 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -1,8 +1,91 @@ -+#include <assert.h> -+#include <getopt.h> -+#include <openssl/err.h> -+#include <stdbool.h> - #include <stdio.h> -+#include <stdlib.h> -+#include "client.h" -+ -+static void -+usage(char *argv_0) -+{ -+ fprintf(stderr, -+ "usage: %s [-LI] [-C cert] [-d input] gemini://...\n", -+ argv_0); -+} - - int --main(int argc, char *argv[]) { -- (void)argc; (void)argv; -- printf("Hello, world!\n"); -+main(int argc, char *argv[]) -+{ -+ bool headers = false, follow_redirect = false; -+ char *certificate = NULL, *input = NULL; -+ -+ int c; -+ while ((c = getopt(argc, argv, "46C:d:hLI")) != -1) { -+ switch (c) { -+ case '4': -+ assert(0); // TODO -+ break; -+ case '6': -+ assert(0); // TODO -+ break; -+ case 'C': -+ certificate = optarg; -+ break; -+ case 'd': -+ input = optarg; -+ break; -+ case 'h': -+ usage(argv[0]); -+ return 0; -+ case 'L': -+ follow_redirect = true; -+ break; -+ case 'I': -+ headers = true; -+ break; -+ default: -+ fprintf(stderr, "fatal: unknown flag %c", c); -+ return 1; -+ } -+ } -+ if (optind != argc - 1) { -+ usage(argv[0]); -+ return 1; -+ } -+ -+ SSL_load_error_strings(); -+ ERR_load_crypto_strings(); -+ -+ struct gemini_response resp; -+ enum gemini_result r = gemini_request(argv[optind], NULL, &resp); -+ switch (r) { -+ case GEMINI_OK: -+ printf("OK\n"); -+ break; -+ case GEMINI_ERR_OOM: -+ printf("OOM\n"); -+ break; -+ case GEMINI_ERR_INVALID_URL: -+ printf("INVALID_URL\n"); -+ break; -+ case GEMINI_ERR_RESOLVE: -+ printf("RESOLVE\n"); -+ break; -+ case GEMINI_ERR_CONNECT: -+ printf("CONNECT\n"); -+ break; -+ case GEMINI_ERR_SSL: -+ fprintf(stderr, "SSL error: %s\n", ERR_error_string( -+ SSL_get_error(resp.ssl, resp.status), NULL)); -+ break; -+ } -+ -+ gemini_response_finish(&resp); -+ -+ (void)headers; -+ (void)follow_redirect; -+ (void)certificate; -+ (void)input; - return 0; - } -diff --git a/src/url.c b/src/url.c -new file mode 100644 -index 0000000000000000000000000000000000000000..47e31b5fcbeeecb5bc3962585311b20e3518bb73 ---- /dev/null -+++ b/src/url.c -@@ -0,0 +1,1448 @@ -+/*************************************************************************** -+ * _ _ ____ _ -+ * Project ___| | | | _ \| | -+ * / __| | | | |_) | | -+ * | (__| |_| | _ <| |___ -+ * \___|\___/|_| \_\_____| -+ * -+ * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -+ * -+ * This software is licensed as described in the file COPYING, which -+ * you should have received as part of this distribution. The terms -+ * are also available at https://curl.haxx.se/docs/copyright.html. -+ * -+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell -+ * copies of the Software, and permit persons to whom the Software is -+ * furnished to do so, under the terms of the COPYING file. -+ * -+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -+ * KIND, either express or implied. -+ * -+ ***************************************************************************/ -+ -+#define MAX_SCHEME_LEN 8 -+ -+#include <assert.h> -+#include <ctype.h> -+#include <stdarg.h> -+#include <stdbool.h> -+#include <stdio.h> -+#include <stdlib.h> -+#include <string.h> -+#include <strings.h> -+#include "escape.h" -+#include "url.h" -+ -+/* Provided by gmni */ -+static char * -+aprintf(const char *fmt, ...) -+{ -+ va_list ap; -+ va_start(ap, fmt); -+ int n = vsnprintf(NULL, 0, fmt, ap); -+ va_end(ap); -+ -+ char *strp = calloc(n + 1, 1); -+ assert(strp); -+ -+ va_start(ap, fmt); -+ n = vsnprintf(strp, n + 1, fmt, ap); -+ va_end(ap); -+ return strp; -+} -+ -+/* via lib/dotdot.c */ -+char *Curl_dedotdotify(const char *input) -+{ -+ size_t inlen = strlen(input); -+ char *clone; -+ size_t clen = inlen; /* the length of the cloned input */ -+ char *out = malloc(inlen + 1); -+ char *outptr; -+ char *orgclone; -+ char *queryp; -+ if(!out) -+ return NULL; /* out of memory */ -+ -+ *out = 0; /* zero terminates, for inputs like "./" */ -+ -+ /* get a cloned copy of the input */ -+ clone = strdup(input); -+ if(!clone) { -+ free(out); -+ return NULL; -+ } -+ orgclone = clone; -+ outptr = out; -+ -+ if(!*clone) { -+ /* zero length string, return that */ -+ free(out); -+ return clone; -+ } -+ -+ /* -+ * To handle query-parts properly, we must find it and remove it during the -+ * dotdot-operation and then append it again at the end to the output -+ * string. -+ */ -+ queryp = strchr(clone, '?'); -+ if(queryp) -+ *queryp = 0; -+ -+ do { -+ -+ /* A. If the input buffer begins with a prefix of "../" or "./", then -+ remove that prefix from the input buffer; otherwise, */ -+ -+ if(!strncmp("./", clone, 2)) { -+ clone += 2; -+ clen -= 2; -+ } -+ else if(!strncmp("../", clone, 3)) { -+ clone += 3; -+ clen -= 3; -+ } -+ -+ /* B. if the input buffer begins with a prefix of "/./" or "/.", where -+ "." is a complete path segment, then replace that prefix with "/" in -+ the input buffer; otherwise, */ -+ else if(!strncmp("/./", clone, 3)) { -+ clone += 2; -+ clen -= 2; -+ } -+ else if(!strcmp("/.", clone)) { -+ clone[1]='/'; -+ clone++; -+ clen -= 1; -+ } -+ -+ /* C. if the input buffer begins with a prefix of "/../" or "/..", where -+ ".." is a complete path segment, then replace that prefix with "/" in -+ the input buffer and remove the last segment and its preceding "/" (if -+ any) from the output buffer; otherwise, */ -+ -+ else if(!strncmp("/../", clone, 4)) { -+ clone += 3; -+ clen -= 3; -+ /* remove the last segment from the output buffer */ -+ while(outptr > out) { -+ outptr--; -+ if(*outptr == '/') -+ break; -+ } -+ *outptr = 0; /* zero-terminate where it stops */ -+ } -+ else if(!strcmp("/..", clone)) { -+ clone[2]='/'; -+ clone += 2; -+ clen -= 2; -+ /* remove the last segment from the output buffer */ -+ while(outptr > out) { -+ outptr--; -+ if(*outptr == '/') -+ break; -+ } -+ *outptr = 0; /* zero-terminate where it stops */ -+ } -+ -+ /* D. if the input buffer consists only of "." or "..", then remove -+ that from the input buffer; otherwise, */ -+ -+ else if(!strcmp(".", clone) || !strcmp("..", clone)) { -+ *clone = 0; -+ *out = 0; -+ } -+ -+ else { -+ /* E. move the first path segment in the input buffer to the end of -+ the output buffer, including the initial "/" character (if any) and -+ any subsequent characters up to, but not including, the next "/" -+ character or the end of the input buffer. */ -+ -+ do { -+ *outptr++ = *clone++; -+ clen--; -+ } while(*clone && (*clone != '/')); -+ *outptr = 0; -+ } -+ -+ } while(*clone); -+ -+ if(queryp) { -+ size_t qlen; -+ /* There was a query part, append that to the output. The 'clone' string -+ may now have been altered so we copy from the original input string -+ from the correct index. */ -+ size_t oindex = queryp - orgclone; -+ qlen = strlen(&input[oindex]); -+ memcpy(outptr, &input[oindex], qlen + 1); /* include the end zero byte */ -+ } -+ -+ free(orgclone); -+ return out; -+} -+ -+/* via lib/url.c */ -+CURLcode Curl_parse_login_details(const char *login, const size_t len, -+ char **userp, char **passwdp, -+ char **optionsp) -+{ -+ CURLcode result = CURLE_OK; -+ char *ubuf = NULL; -+ char *pbuf = NULL; -+ char *obuf = NULL; -+ const char *psep = NULL; -+ const char *osep = NULL; -+ size_t ulen; -+ size_t plen; -+ size_t olen; -+ -+ /* Attempt to find the password separator */ -+ if(passwdp) { -+ psep = strchr(login, ':'); -+ -+ /* Within the constraint of the login string */ -+ if(psep >= login + len) -+ psep = NULL; -+ } -+ -+ /* Attempt to find the options separator */ -+ if(optionsp) { -+ osep = strchr(login, ';'); -+ -+ /* Within the constraint of the login string */ -+ if(osep >= login + len) -+ osep = NULL; -+ } -+ -+ /* Calculate the portion lengths */ -+ ulen = (psep ? -+ (size_t)(osep && psep > osep ? osep - login : psep - login) : -+ (osep ? (size_t)(osep - login) : len)); -+ plen = (psep ? -+ (osep && osep > psep ? (size_t)(osep - psep) : -+ (size_t)(login + len - psep)) - 1 : 0); -+ olen = (osep ? -+ (psep && psep > osep ? (size_t)(psep - osep) : -+ (size_t)(login + len - osep)) - 1 : 0); -+ -+ /* Allocate the user portion buffer */ -+ if(userp && ulen) { -+ ubuf = malloc(ulen + 1); -+ if(!ubuf) -+ result = CURLE_OUT_OF_MEMORY; -+ } -+ -+ /* Allocate the password portion buffer */ -+ if(!result && passwdp && plen) { -+ pbuf = malloc(plen + 1); -+ if(!pbuf) { -+ free(ubuf); -+ result = CURLE_OUT_OF_MEMORY; -+ } -+ } -+ -+ /* Allocate the options portion buffer */ -+ if(!result && optionsp && olen) { -+ obuf = malloc(olen + 1); -+ if(!obuf) { -+ free(pbuf); -+ free(ubuf); -+ result = CURLE_OUT_OF_MEMORY; -+ } -+ } -+ -+ if(!result) { -+ /* Store the user portion if necessary */ -+ if(ubuf) { -+ memcpy(ubuf, login, ulen); -+ ubuf[ulen] = '\0'; -+ free(*userp); -+ *userp = ubuf; -+ } -+ -+ /* Store the password portion if necessary */ -+ if(pbuf) { -+ memcpy(pbuf, psep + 1, plen); -+ pbuf[plen] = '\0'; -+ free(*passwdp); -+ *passwdp = pbuf; -+ } -+ -+ /* Store the options portion if necessary */ -+ if(obuf) { -+ memcpy(obuf, osep + 1, olen); -+ obuf[olen] = '\0'; -+ free(*optionsp); -+ *optionsp = obuf; -+ } -+ } -+ -+ return result; -+} -+ -+/* Internal representation of CURLU. Point to URL-encoded strings. */ -+struct Curl_URL { -+ char *scheme; -+ char *user; -+ char *password; -+ char *options; /* IMAP only? */ -+ char *host; -+ char *port; -+ char *path; -+ char *query; -+ char *fragment; -+ -+ char *scratch; /* temporary scratch area */ -+ long portnum; /* the numerical version */ -+}; -+ -+#define DEFAULT_SCHEME "https" -+ -+static void free_urlhandle(struct Curl_URL *u) -+{ -+ free(u->scheme); -+ free(u->user); -+ free(u->password); -+ free(u->options); -+ free(u->host); -+ free(u->port); -+ free(u->path); -+ free(u->query); -+ free(u->fragment); -+ free(u->scratch); -+} -+ -+/* move the full contents of one handle onto another and -+ free the original */ -+static void mv_urlhandle(struct Curl_URL *from, -+ struct Curl_URL *to) -+{ -+ free_urlhandle(to); -+ *to = *from; -+ free(from); -+} -+ -+/* -+ * Find the separator at the end of the host name, or the '?' in cases like -+ * http://www.url.com?id=2380 -+ */ -+static const char *find_host_sep(const char *url) -+{ -+ const char *sep; -+ const char *query; -+ -+ /* Find the start of the hostname */ -+ sep = strstr(url, "//"); -+ if(!sep) -+ sep = url; -+ else -+ sep += 2; -+ -+ query = strchr(sep, '?'); -+ sep = strchr(sep, '/'); -+ -+ if(!sep) -+ sep = url + strlen(url); -+ -+ if(!query) -+ query = url + strlen(url); -+ -+ return sep < query ? sep : query; -+} -+ -+/* -+ * Decide in an encoding-independent manner whether a character in an -+ * URL must be escaped. The same criterion must be used in strlen_url() -+ * and strcpy_url(). -+ */ -+static bool urlchar_needs_escaping(int c) -+{ -+ return !(iscntrl(c) || isspace(c) || isgraph(c)); -+} -+ -+/* -+ * strlen_url() returns the length of the given URL if the spaces within the -+ * URL were properly URL encoded. -+ * URL encoding should be skipped for host names, otherwise IDN resolution -+ * will fail. -+ */ -+size_t Curl_strlen_url(const char *url, bool relative) -+{ -+ const unsigned char *ptr; -+ size_t newlen = 0; -+ bool left = true; /* left side of the ? */ -+ const unsigned char *host_sep = (const unsigned char *) url; -+ -+ if(!relative) -+ host_sep = (const unsigned char *) find_host_sep(url); -+ -+ for(ptr = (unsigned char *)url; *ptr; ptr++) { -+ -+ if(ptr < host_sep) { -+ ++newlen; -+ continue; -+ } -+ -+ switch(*ptr) { -+ case '?': -+ left = false; -+ /* FALLTHROUGH */ -+ default: -+ if(urlchar_needs_escaping(*ptr)) -+ newlen += 2; -+ newlen++; -+ break; -+ case ' ': -+ if(left) -+ newlen += 3; -+ else -+ newlen++; -+ break; -+ } -+ } -+ return newlen; -+} -+ -+/* strcpy_url() copies a url to a output buffer and URL-encodes the spaces in -+ * the source URL accordingly. -+ * URL encoding should be skipped for host names, otherwise IDN resolution -+ * will fail. -+ */ -+void Curl_strcpy_url(char *output, const char *url, bool relative) -+{ -+ /* we must add this with whitespace-replacing */ -+ bool left = true; -+ const unsigned char *iptr; -+ char *optr = output; -+ const unsigned char *host_sep = (const unsigned char *) url; -+ -+ if(!relative) -+ host_sep = (const unsigned char *) find_host_sep(url); -+ -+ for(iptr = (unsigned char *)url; /* read from here */ -+ *iptr; /* until zero byte */ -+ iptr++) { -+ -+ if(iptr < host_sep) { -+ *optr++ = *iptr; -+ continue; -+ } -+ -+ switch(*iptr) { -+ case '?': -+ left = false; -+ /* FALLTHROUGH */ -+ default: -+ if(urlchar_needs_escaping(*iptr)) { -+ snprintf(optr, 4, "%%%02x", *iptr); -+ optr += 3; -+ } -+ else -+ *optr++=*iptr; -+ break; -+ case ' ': -+ if(left) { -+ *optr++='%'; /* add a '%' */ -+ *optr++='2'; /* add a '2' */ -+ *optr++='0'; /* add a '0' */ -+ } -+ else -+ *optr++='+'; /* add a '+' here */ -+ break; -+ } -+ } -+ *optr = 0; /* zero terminate output buffer */ -+ -+} -+ -+/* -+ * Returns true if the given URL is absolute (as opposed to relative) within -+ * the buffer size. Returns the scheme in the buffer if true and 'buf' is -+ * non-NULL. -+ */ -+bool Curl_is_absolute_url(const char *url, char *buf, size_t buflen) -+{ -+ size_t i; -+ for(i = 0; i < buflen && url[i]; ++i) { -+ char s = url[i]; -+ if((s == ':') && (url[i + 1] == '/')) { -+ if(buf) -+ buf[i] = 0; -+ return true; -+ } -+ /* RFC 3986 3.1 explains: -+ scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) -+ */ -+ else if(isalnum(s) || (s == '+') || (s == '-') || (s == '.') ) { -+ if(buf) -+ buf[i] = (char)tolower(s); -+ } -+ else -+ break; -+ } -+ return false; -+} -+ -+/* -+ * Concatenate a relative URL to a base URL making it absolute. -+ * URL-encodes any spaces. -+ * The returned pointer must be freed by the caller unless NULL -+ * (returns NULL on out of memory). -+ */ -+char *Curl_concat_url(const char *base, const char *relurl) -+{ -+ /*** -+ TRY to append this new path to the old URL -+ to the right of the host part. Oh crap, this is doomed to cause -+ problems in the future... -+ */ -+ char *newest; -+ char *protsep; -+ char *pathsep; -+ size_t newlen; -+ bool host_changed = false; -+ -+ const char *useurl = relurl; -+ size_t urllen; -+ -+ /* we must make our own copy of the URL to play with, as it may -+ point to read-only data */ -+ char *url_clone = strdup(base); -+ -+ if(!url_clone) -+ return NULL; /* skip out of this NOW */ -+ -+ /* protsep points to the start of the host name */ -+ protsep = strstr(url_clone, "//"); -+ if(!protsep) -+ protsep = url_clone; -+ else -+ protsep += 2; /* pass the slashes */ -+ -+ if('/' != relurl[0]) { -+ int level = 0; -+ -+ /* First we need to find out if there's a ?-letter in the URL, -+ and cut it and the right-side of that off */ -+ pathsep = strchr(protsep, '?'); -+ if(pathsep) -+ *pathsep = 0; -+ -+ /* we have a relative path to append to the last slash if there's one -+ available, or if the new URL is just a query string (starts with a -+ '?') we append the new one at the end of the entire currently worked -+ out URL */ -+ if(useurl[0] != '?') { -+ pathsep = strrchr(protsep, '/'); -+ if(pathsep) -+ *pathsep = 0; -+ } -+ -+ /* Check if there's any slash after the host name, and if so, remember -+ that position instead */ -+ pathsep = strchr(protsep, '/'); -+ if(pathsep) -+ protsep = pathsep + 1; -+ else -+ protsep = NULL; -+ -+ /* now deal with one "./" or any amount of "../" in the newurl -+ and act accordingly */ -+ -+ if((useurl[0] == '.') && (useurl[1] == '/')) -+ useurl += 2; /* just skip the "./" */ -+ -+ while((useurl[0] == '.') && -+ (useurl[1] == '.') && -+ (useurl[2] == '/')) { -+ level++; -+ useurl += 3; /* pass the "../" */ -+ } -+ -+ if(protsep) { -+ while(level--) { -+ /* cut off one more level from the right of the original URL */ -+ pathsep = strrchr(protsep, '/'); -+ if(pathsep) -+ *pathsep = 0; -+ else { -+ *protsep = 0; -+ break; -+ } -+ } -+ } -+ } -+ else { -+ /* We got a new absolute path for this server */ -+ -+ if((relurl[0] == '/') && (relurl[1] == '/')) { -+ /* the new URL starts with //, just keep the protocol part from the -+ original one */ -+ *protsep = 0; -+ useurl = &relurl[2]; /* we keep the slashes from the original, so we -+ skip the new ones */ -+ host_changed = true; -+ } -+ else { -+ /* cut off the original URL from the first slash, or deal with URLs -+ without slash */ -+ pathsep = strchr(protsep, '/'); -+ if(pathsep) { -+ /* When people use badly formatted URLs, such as -+ "http://www.url.com?dir=/home/daniel" we must not use the first -+ slash, if there's a ?-letter before it! */ -+ char *sep = strchr(protsep, '?'); -+ if(sep && (sep < pathsep)) -+ pathsep = sep; -+ *pathsep = 0; -+ } -+ else { -+ /* There was no slash. Now, since we might be operating on a badly -+ formatted URL, such as "http://www.url.com?id=2380" which doesn't -+ use a slash separator as it is supposed to, we need to check for a -+ ?-letter as well! */ -+ pathsep = strchr(protsep, '?'); -+ if(pathsep) -+ *pathsep = 0; -+ } -+ } -+ } -+ -+ /* If the new part contains a space, this is a mighty stupid redirect -+ but we still make an effort to do "right". To the left of a '?' -+ letter we replace each space with %20 while it is replaced with '+' -+ on the right side of the '?' letter. -+ */ -+ newlen = Curl_strlen_url(useurl, !host_changed); -+ -+ urllen = strlen(url_clone); -+ -+ newest = malloc(urllen + 1 + /* possible slash */ -+ newlen + 1 /* zero byte */); -+ -+ if(!newest) { -+ free(url_clone); /* don't leak this */ -+ return NULL; -+ } -+ -+ /* copy over the root url part */ -+ memcpy(newest, url_clone, urllen); -+ -+ /* check if we need to append a slash */ -+ if(('/' == useurl[0]) || (protsep && !*protsep) || ('?' == useurl[0])) -+ ; -+ else -+ newest[urllen++]='/'; -+ -+ /* then append the new piece on the right side */ -+ Curl_strcpy_url(&newest[urllen], useurl, !host_changed); -+ -+ free(url_clone); -+ -+ return newest; -+} -+ -+/* -+ * parse_hostname_login() -+ * -+ * Parse the login details (user name, password and options) from the URL and -+ * strip them out of the host name -+ * -+ */ -+static CURLUcode parse_hostname_login(struct Curl_URL *u, -+ char **hostname, -+ unsigned int flags) -+{ -+ CURLUcode result = CURLUE_OK; -+ CURLcode ccode; -+ char *userp = NULL; -+ char *passwdp = NULL; -+ char *optionsp = NULL; -+ -+ /* At this point, we're hoping all the other special cases have -+ * been taken care of, so conn->host.name is at most -+ * [user[:password][;options]]@]hostname -+ * -+ * We need somewhere to put the embedded details, so do that first. -+ */ -+ -+ char *ptr = strchr(*hostname, '@'); -+ char *login = *hostname; -+ -+ if(!ptr) -+ goto out; -+ -+ /* We will now try to extract the -+ * possible login information in a string like: -+ * ftp://user:password@ftp.my.site:8021/README */ -+ *hostname = ++ptr; -+ -+ /* We could use the login information in the URL so extract it. Only parse -+ options if the handler says we should. Note that 'h' might be NULL! */ -+ ccode = Curl_parse_login_details(login, ptr - login - 1, -+ &userp, &passwdp, NULL); -+ if(ccode) { -+ result = CURLUE_MALFORMED_INPUT; -+ goto out; -+ } -+ -+ if(userp) { -+ if(flags & CURLU_DISALLOW_USER) { -+ /* Option DISALLOW_USER is set and url contains username. */ -+ result = CURLUE_USER_NOT_ALLOWED; -+ goto out; -+ } -+ -+ u->user = userp; -+ } -+ -+ if(passwdp) -+ u->password = passwdp; -+ -+ if(optionsp) -+ u->options = optionsp; -+ -+ return CURLUE_OK; -+ out: -+ -+ free(userp); -+ free(passwdp); -+ free(optionsp); -+ -+ return result; -+} -+ -+static CURLUcode parse_port(struct Curl_URL *u, char *hostname) -+{ -+ char *portptr; -+ char endbracket; -+ int len; -+ -+ if((1 == sscanf(hostname, "[%*45[0123456789abcdefABCDEF:.%%]%c%n", -+ &endbracket, &len)) && -+ (']' == endbracket)) { -+ /* this is a RFC2732-style specified IP-address */ -+ portptr = &hostname[len]; -+ if(*portptr) { -+ if(*portptr != ':') -+ return CURLUE_MALFORMED_INPUT; -+ } -+ else -+ portptr = NULL; -+ } -+ else -+ portptr = strchr(hostname, ':'); -+ -+ if(portptr) { -+ char *rest; -+ long port; -+ char portbuf[7]; -+ -+ if(!isdigit(portptr[1])) -+ return CURLUE_BAD_PORT_NUMBER; -+ -+ port = strtol(portptr + 1, &rest, 10); /* Port number must be decimal */ -+ -+ if((port <= 0) || (port > 0xffff)) -+ /* Single unix standard says port numbers are 16 bits long, but we don't -+ treat port zero as OK. */ -+ return CURLUE_BAD_PORT_NUMBER; -+ -+ if(rest[0]) -+ return CURLUE_BAD_PORT_NUMBER; -+ -+ if(rest != &portptr[1]) { -+ *portptr++ = '\0'; /* cut off the name there */ -+ *rest = 0; -+ /* generate a new to get rid of leading zeroes etc */ -+ snprintf(portbuf, sizeof(portbuf), "%ld", port); -+ u->portnum = port; -+ u->port = strdup(portbuf); -+ if(!u->port) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ else { -+ /* Browser behavior adaptation. If there's a colon with no digits after, -+ just cut off the name there which makes us ignore the colon and just -+ use the default port. Firefox and Chrome both do that. */ -+ *portptr = '\0'; -+ } -+ } -+ -+ return CURLUE_OK; -+} -+ -+/* scan for byte values < 31 or 127 */ -+static CURLUcode junkscan(char *part) -+{ -+ char badbytes[]={ -+ /* */ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, -+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, -+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, -+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, -+ 0x7f, -+ 0x00 /* zero terminate */ -+ }; -+ if(part) { -+ size_t n = strlen(part); -+ size_t nfine = strcspn(part, badbytes); -+ if(nfine != n) -+ /* since we don't know which part is scanned, return a generic error -+ code */ -+ return CURLUE_MALFORMED_INPUT; -+ } -+ return CURLUE_OK; -+} -+ -+static CURLUcode hostname_check(char *hostname, unsigned int flags) -+{ -+ const char *l = NULL; /* accepted characters */ -+ size_t len; -+ size_t hlen = strlen(hostname); -+ (void)flags; -+ -+ if(hostname[0] == '[') { -+ hostname++; -+ l = "0123456789abcdefABCDEF::.%"; -+ hlen -= 2; -+ } -+ -+ if(l) { -+ /* only valid letters are ok */ -+ len = strspn(hostname, l); -+ if(hlen != len) -+ /* hostname with bad content */ -+ return CURLUE_MALFORMED_INPUT; -+ } -+ else { -+ /* letters from the second string is not ok */ -+ len = strcspn(hostname, " "); -+ if(hlen != len) -+ /* hostname with bad content */ -+ return CURLUE_MALFORMED_INPUT; -+ } -+ return CURLUE_OK; -+} -+ -+#define HOSTNAME_END(x) (((x) == '/') || ((x) == '?') || ((x) == '#')) -+ -+static CURLUcode seturl(const char *url, struct Curl_URL *u, unsigned int flags) -+{ -+ char *path; -+ bool path_alloced = false; -+ char *hostname; -+ char *query = NULL; -+ char *fragment = NULL; -+ CURLUcode result; -+ bool url_has_scheme = false; -+ char schemebuf[MAX_SCHEME_LEN]; -+ char *schemep = NULL; -+ size_t schemelen = 0; -+ size_t urllen; -+ -+ if(!url) -+ return CURLUE_MALFORMED_INPUT; -+ -+ /************************************************************* -+ * Parse the URL. -+ ************************************************************/ -+ /* allocate scratch area */ -+ urllen = strlen(url); -+ path = u->scratch = malloc(urllen * 2 + 2); -+ if(!path) -+ return CURLUE_OUT_OF_MEMORY; -+ -+ hostname = &path[urllen + 1]; -+ hostname[0] = 0; -+ -+ if(Curl_is_absolute_url(url, schemebuf, sizeof(schemebuf))) { -+ url_has_scheme = true; -+ schemelen = strlen(schemebuf); -+ } -+ -+ /* handle the file: scheme */ -+ if(url_has_scheme && strcasecmp(schemebuf, "file") == 0) { -+ /* path has been allocated large enough to hold this */ -+ strcpy(path, &url[5]); -+ -+ hostname = NULL; /* no host for file: URLs */ -+ u->scheme = strdup("file"); -+ if(!u->scheme) -+ return CURLUE_OUT_OF_MEMORY; -+ -+ /* Extra handling URLs with an authority component (i.e. that start with -+ * "file://") -+ * -+ * We allow omitted hostname (e.g. file:/<path>) -- valid according to -+ * RFC 8089, but not the (current) WHAT-WG URL spec. -+ */ -+ if(path[0] == '/' && path[1] == '/') { -+ /* swallow the two slashes */ -+ char *ptr = &path[2]; -+ path = ptr; -+ } -+ } -+ else { -+ /* clear path */ -+ const char *p; -+ const char *hostp; -+ size_t len; -+ path[0] = 0; -+ -+ if(url_has_scheme) { -+ int i = 0; -+ p = &url[schemelen + 1]; -+ while(p && (*p == '/') && (i < 4)) { -+ p++; -+ i++; -+ } -+ if((i < 1) || (i>3)) -+ /* less than one or more than three slashes */ -+ return CURLUE_MALFORMED_INPUT; -+ -+ schemep = schemebuf; -+ if(junkscan(schemep)) -+ return CURLUE_MALFORMED_INPUT; -+ } -+ else { -+ /* no scheme! */ -+ return CURLUE_MALFORMED_INPUT; -+ } -+ hostp = p; /* host name starts here */ -+ -+ while(*p && !HOSTNAME_END(*p)) /* find end of host name */ -+ p++; -+ -+ len = p - hostp; -+ if(!len) -+ return CURLUE_MALFORMED_INPUT; -+ -+ memcpy(hostname, hostp, len); -+ hostname[len] = 0; -+ -+ len = strlen(p); -+ memcpy(path, p, len); -+ path[len] = 0; -+ -+ u->scheme = strdup(schemep); -+ if(!u->scheme) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ -+ if(junkscan(path)) -+ return CURLUE_MALFORMED_INPUT; -+ -+ query = strchr(path, '?'); -+ if(query) -+ *query++ = 0; -+ -+ fragment = strchr(query?query:path, '#'); -+ if(fragment) -+ *fragment++ = 0; -+ -+ if(!path[0]) -+ /* if there's no path set, unset */ -+ path = NULL; -+ else if(!(flags & CURLU_PATH_AS_IS)) { -+ /* sanitise paths and remove ../ and ./ sequences according to RFC3986 */ -+ char *newp = Curl_dedotdotify(path); -+ if(!newp) -+ return CURLUE_OUT_OF_MEMORY; -+ -+ if(strcmp(newp, path)) { -+ /* if we got a new version */ -+ path = newp; -+ path_alloced = true; -+ } -+ else -+ free(newp); -+ } -+ if(path) { -+ u->path = path_alloced?path:strdup(path); -+ if(!u->path) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ -+ if(hostname) { -+ /* -+ * Parse the login details and strip them out of the host name. -+ */ -+ if(junkscan(hostname)) -+ return CURLUE_MALFORMED_INPUT; -+ -+ result = parse_hostname_login(u, &hostname, flags); -+ if(result) -+ return result; -+ -+ result = parse_port(u, hostname); -+ if(result) -+ return result; -+ -+ result = hostname_check(hostname, flags); -+ if(result) -+ return result; -+ -+ u->host = strdup(hostname); -+ if(!u->host) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ -+ if(query && query[0]) { -+ u->query = strdup(query); -+ if(!u->query) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ if(fragment && fragment[0]) { -+ u->fragment = strdup(fragment); -+ if(!u->fragment) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ -+ free(u->scratch); -+ u->scratch = NULL; -+ -+ return CURLUE_OK; -+} -+ -+/* -+ * Parse the URL and set the relevant members of the Curl_URL struct. -+ */ -+static CURLUcode parseurl(const char *url, struct Curl_URL *u, unsigned int flags) -+{ -+ CURLUcode result = seturl(url, u, flags); -+ if(result) { -+ free_urlhandle(u); -+ memset(u, 0, sizeof(struct Curl_URL)); -+ } -+ return result; -+} -+ -+/* -+ */ -+struct Curl_URL *curl_url(void) -+{ -+ return calloc(sizeof(struct Curl_URL), 1); -+} -+ -+void curl_url_cleanup(struct Curl_URL *u) -+{ -+ if(u) { -+ free_urlhandle(u); -+ free(u); -+ } -+} -+ -+#define DUP(dest, src, name) \ -+ if(src->name) { \ -+ dest->name = strdup(src->name); \ -+ if(!dest->name) \ -+ goto fail; \ -+ } -+ -+struct Curl_URL *curl_url_dup(struct Curl_URL *in) -+{ -+ struct Curl_URL *u = calloc(sizeof(struct Curl_URL), 1); -+ if(u) { -+ DUP(u, in, scheme); -+ DUP(u, in, user); -+ DUP(u, in, password); -+ DUP(u, in, options); -+ DUP(u, in, host); -+ DUP(u, in, port); -+ DUP(u, in, path); -+ DUP(u, in, query); -+ DUP(u, in, fragment); -+ u->portnum = in->portnum; -+ } -+ return u; -+ fail: -+ curl_url_cleanup(u); -+ return NULL; -+} -+ -+CURLUcode curl_url_get(struct Curl_URL *u, CURLUPart what, -+ char **part, unsigned int flags) -+{ -+ char *ptr; -+ CURLUcode ifmissing = CURLUE_UNKNOWN_PART; -+ bool urldecode = (flags & CURLU_URLDECODE)?1:0; -+ bool plusdecode = false; -+ (void)flags; -+ if(!u) -+ return CURLUE_BAD_HANDLE; -+ if(!part) -+ return CURLUE_BAD_PARTPOINTER; -+ *part = NULL; -+ -+ switch(what) { -+ case CURLUPART_SCHEME: -+ ptr = u->scheme; -+ ifmissing = CURLUE_NO_SCHEME; -+ urldecode = false; /* never for schemes */ -+ break; -+ case CURLUPART_USER: -+ ptr = u->user; -+ ifmissing = CURLUE_NO_USER; -+ break; -+ case CURLUPART_PASSWORD: -+ ptr = u->password; -+ ifmissing = CURLUE_NO_PASSWORD; -+ break; -+ case CURLUPART_OPTIONS: -+ ptr = u->options; -+ ifmissing = CURLUE_NO_OPTIONS; -+ break; -+ case CURLUPART_HOST: -+ ptr = u->host; -+ ifmissing = CURLUE_NO_HOST; -+ break; -+ case CURLUPART_PORT: -+ ptr = u->port; -+ ifmissing = CURLUE_NO_PORT; -+ urldecode = false; /* never for port */ -+ break; -+ case CURLUPART_PATH: -+ ptr = u->path; -+ if(!ptr) { -+ ptr = u->path = strdup("/"); -+ if(!u->path) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ break; -+ case CURLUPART_QUERY: -+ ptr = u->query; -+ ifmissing = CURLUE_NO_QUERY; -+ plusdecode = urldecode; -+ break; -+ case CURLUPART_FRAGMENT: -+ ptr = u->fragment; -+ ifmissing = CURLUE_NO_FRAGMENT; -+ break; -+ case CURLUPART_URL: { -+ char *url; -+ char *scheme; -+ char *options = u->options; -+ char *port = u->port; -+ if(u->scheme && strcasecmp("file", u->scheme) == 0) { -+ url = aprintf("file://%s%s%s", -+ u->path, -+ u->fragment? "#": "", -+ u->fragment? u->fragment : ""); -+ } -+ else if(!u->host) -+ return CURLUE_NO_HOST; -+ else { -+ if(u->scheme) -+ scheme = u->scheme; -+ else -+ return CURLUE_NO_SCHEME; -+ -+ options = NULL; -+ -+ url = aprintf("%s://%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", -+ scheme, -+ u->user ? u->user : "", -+ u->password ? ":": "", -+ u->password ? u->password : "", -+ options ? ";" : "", -+ options ? options : "", -+ (u->user || u->password || options) ? "@": "", -+ u->host, -+ port ? ":": "", -+ port ? port : "", -+ (u->path && (u->path[0] != '/')) ? "/": "", -+ u->path ? u->path : "/", -+ u->query? "?": "", -+ u->query? u->query : "", -+ u->fragment? "#": "", -+ u->fragment? u->fragment : ""); -+ } -+ if(!url) -+ return CURLUE_OUT_OF_MEMORY; -+ *part = url; -+ return CURLUE_OK; -+ break; -+ } -+ default: -+ ptr = NULL; -+ } -+ if(ptr) { -+ *part = strdup(ptr); -+ if(!*part) -+ return CURLUE_OUT_OF_MEMORY; -+ if(plusdecode) { -+ /* convert + to space */ -+ char *plus; -+ for(plus = *part; *plus; ++plus) { -+ if(*plus == '+') -+ *plus = ' '; -+ } -+ } -+ if(urldecode) { -+ char *decoded; -+ size_t dlen; -+ CURLcode res = Curl_urldecode(*part, 0, &decoded, &dlen, true); -+ free(*part); -+ if(res) { -+ *part = NULL; -+ return CURLUE_URLDECODE; -+ } -+ *part = decoded; -+ } -+ return CURLUE_OK; -+ } -+ else -+ return ifmissing; -+} -+ -+CURLUcode curl_url_set(struct Curl_URL *u, CURLUPart what, -+ const char *part, unsigned int flags) -+{ -+ char **storep = NULL; -+ long port = 0; -+ bool urlencode = (flags & CURLU_URLENCODE)? 1 : 0; -+ bool plusencode = false; -+ bool urlskipslash = false; -+ bool appendquery = false; -+ bool equalsencode = false; -+ -+ if(!u) -+ return CURLUE_BAD_HANDLE; -+ if(!part) { -+ /* setting a part to NULL clears it */ -+ switch(what) { -+ case CURLUPART_URL: -+ break; -+ case CURLUPART_SCHEME: -+ storep = &u->scheme; -+ break; -+ case CURLUPART_USER: -+ storep = &u->user; -+ break; -+ case CURLUPART_PASSWORD: -+ storep = &u->password; -+ break; -+ case CURLUPART_OPTIONS: -+ storep = &u->options; -+ break; -+ case CURLUPART_HOST: -+ storep = &u->host; -+ break; -+ case CURLUPART_PORT: -+ storep = &u->port; -+ break; -+ case CURLUPART_PATH: -+ storep = &u->path; -+ break; -+ case CURLUPART_QUERY: -+ storep = &u->query; -+ break; -+ case CURLUPART_FRAGMENT: -+ storep = &u->fragment; -+ break; -+ default: -+ return CURLUE_UNKNOWN_PART; -+ } -+ if(storep && *storep) { -+ free(*storep); -+ *storep = NULL; -+ } -+ return CURLUE_OK; -+ } -+ -+ switch(what) { -+ case CURLUPART_SCHEME: -+ storep = &u->scheme; -+ urlencode = false; /* never */ -+ break; -+ case CURLUPART_USER: -+ storep = &u->user; -+ break; -+ case CURLUPART_PASSWORD: -+ storep = &u->password; -+ break; -+ case CURLUPART_OPTIONS: -+ storep = &u->options; -+ break; -+ case CURLUPART_HOST: -+ storep = &u->host; -+ break; -+ case CURLUPART_PORT: -+ urlencode = false; /* never */ -+ port = strtol(part, NULL, 10); /* Port number must be decimal */ -+ if((port <= 0) || (port > 0xffff)) -+ return CURLUE_BAD_PORT_NUMBER; -+ storep = &u->port; -+ break; -+ case CURLUPART_PATH: -+ urlskipslash = true; -+ storep = &u->path; -+ break; -+ case CURLUPART_QUERY: -+ plusencode = urlencode; -+ appendquery = (flags & CURLU_APPENDQUERY)?1:0; -+ equalsencode = appendquery; -+ storep = &u->query; -+ break; -+ case CURLUPART_FRAGMENT: -+ storep = &u->fragment; -+ break; -+ case CURLUPART_URL: { -+ /* -+ * Allow a new URL to replace the existing (if any) contents. -+ * -+ * If the existing contents is enough for a URL, allow a relative URL to -+ * replace it. -+ */ -+ CURLUcode result; -+ char *oldurl; -+ char *redired_url; -+ struct Curl_URL *handle2; -+ -+ if(Curl_is_absolute_url(part, NULL, MAX_SCHEME_LEN)) { -+ handle2 = curl_url(); -+ if(!handle2) -+ return CURLUE_OUT_OF_MEMORY; -+ result = parseurl(part, handle2, flags); -+ if(!result) -+ mv_urlhandle(handle2, u); -+ else -+ curl_url_cleanup(handle2); -+ return result; -+ } -+ /* extract the full "old" URL to do the redirect on */ -+ result = curl_url_get(u, CURLUPART_URL, &oldurl, flags); -+ if(result) { -+ /* couldn't get the old URL, just use the new! */ -+ handle2 = curl_url(); -+ if(!handle2) -+ return CURLUE_OUT_OF_MEMORY; -+ result = parseurl(part, handle2, flags); -+ if(!result) -+ mv_urlhandle(handle2, u); -+ else -+ curl_url_cleanup(handle2); -+ return result; -+ } -+ -+ /* apply the relative part to create a new URL */ -+ redired_url = Curl_concat_url(oldurl, part); -+ free(oldurl); -+ if(!redired_url) -+ return CURLUE_OUT_OF_MEMORY; -+ -+ /* now parse the new URL */ -+ handle2 = curl_url(); -+ if(!handle2) { -+ free(redired_url); -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ result = parseurl(redired_url, handle2, flags); -+ free(redired_url); -+ if(!result) -+ mv_urlhandle(handle2, u); -+ else -+ curl_url_cleanup(handle2); -+ return result; -+ } -+ default: -+ return CURLUE_UNKNOWN_PART; -+ } -+ if(storep) { -+ const char *newp = part; -+ size_t nalloc = strlen(part); -+ -+ if(urlencode) { -+ const char *i; -+ char *o; -+ bool free_part = false; -+ char *enc = malloc(nalloc * 3 + 1); /* for worst case! */ -+ if(!enc) -+ return CURLUE_OUT_OF_MEMORY; -+ if(plusencode) { -+ /* space to plus */ -+ i = part; -+ for(o = enc; *i; ++o, ++i) -+ *o = (*i == ' ') ? '+' : *i; -+ *o = 0; /* zero terminate */ -+ part = strdup(enc); -+ if(!part) { -+ free(enc); -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ free_part = true; -+ } -+ for(i = part, o = enc; *i; i++) { -+ if(Curl_isunreserved(*i) || -+ ((*i == '/') && urlskipslash) || -+ ((*i == '=') && equalsencode) || -+ ((*i == '+') && plusencode)) { -+ if((*i == '=') && equalsencode) -+ /* only skip the first equals sign */ -+ equalsencode = false; -+ *o = *i; -+ o++; -+ } -+ else { -+ snprintf(o, 4, "%%%02x", *i); -+ o += 3; -+ } -+ } -+ *o = 0; /* zero terminate */ -+ newp = enc; -+ if(free_part) -+ free((char *)part); -+ } -+ else { -+ char *p; -+ newp = strdup(part); -+ if(!newp) -+ return CURLUE_OUT_OF_MEMORY; -+ p = (char *)newp; -+ while(*p) { -+ /* make sure percent encoded are lower case */ -+ if((*p == '%') && isxdigit(p[1]) && isxdigit(p[2]) && -+ (isupper(p[1]) || isupper(p[2]))) { -+ p[1] = (char)tolower(p[1]); -+ p[2] = (char)tolower(p[2]); -+ p += 3; -+ } -+ else -+ p++; -+ } -+ } -+ -+ if(appendquery) { -+ /* Append the string onto the old query. Add a '&' separator if none is -+ present at the end of the exsting query already */ -+ size_t querylen = u->query ? strlen(u->query) : 0; -+ bool addamperand = querylen && (u->query[querylen -1] != '&'); -+ if(querylen) { -+ size_t newplen = strlen(newp); -+ char *p = malloc(querylen + addamperand + newplen + 1); -+ if(!p) { -+ free((char *)newp); -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ strcpy(p, u->query); /* original query */ -+ if(addamperand) -+ p[querylen] = '&'; /* ampersand */ -+ strcpy(&p[querylen + addamperand], newp); /* new suffix */ -+ free((char *)newp); -+ free(*storep); -+ *storep = p; -+ return CURLUE_OK; -+ } -+ } -+ -+ free(*storep); -+ *storep = (char *)newp; -+ } -+ /* set after the string, to make it not assigned if the allocation above -+ fails */ -+ if(port) -+ u->portnum = port; -+ return CURLUE_OK; -+} diff --git a/sources/cgmnlm.git/commits/ac86b2f9fece0e39be57b81e02cb9946a10df570.patch b/sources/cgmnlm.git/commits/ac86b2f9fece0e39be57b81e02cb9946a10df570.patch @@ -1,35 +0,0 @@ -diff --git a/src/util.c b/src/util.c -index 0a479af3ea734ce25d0806aa086e61bef6b8953b..573e8a717efbe843fce003126cb932928b15f716 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -68,15 +68,15 @@ int - download_resp(FILE *out, struct gemini_response resp, const char *path, - char *url) - { -- char buf[PATH_MAX]; -+ char path_buf[PATH_MAX]; - assert(path); - if (path[0] == '\0') { - path = "./"; - } - if (path[strlen(path)-1] == '/') { -- strncat(strncpy(&buf[0], path, sizeof(buf)), basename(url), -- sizeof(buf)); -- path = &buf[0]; -+ int n = snprintf(path_buf, sizeof(path_buf), "%s%s", path, basename(url)); -+ assert((size_t)n < sizeof(path_buf)); -+ path = path_buf; - } - FILE *f = fopen(path, "w"); - if (f == NULL) { -@@ -85,8 +85,9 @@ path, strerror(errno)); - return 1; - } - fprintf(out, "Downloading %s to %s\n", url, path); -+ char buf[BUFSIZ]; - for (int n = 1; n > 0;) { -- n = BIO_read(resp.bio, buf, BUFSIZ); -+ n = BIO_read(resp.bio, buf, sizeof(buf)); - if (n == -1) { - fprintf(stderr, "Error: read\n"); - return 1; diff --git a/sources/cgmnlm.git/commits/ae43b9190e1a18796222b94ec1e78b35f5826964.patch b/sources/cgmnlm.git/commits/ae43b9190e1a18796222b94ec1e78b35f5826964.patch @@ -1,111 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 82d7abc15562ad7ee4fe5f7e5349be477f719ae5..cb7e2843798f66f596e071823d9e41153a555fcc 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -19,6 +19,14 @@ #include <gmni/tofu.h> - #include <gmni/url.h> - #include "util.h" - -+#define ANSI_COLOR_RED "\x1b[31m" -+#define ANSI_COLOR_GREEN "\x1b[32m" -+#define ANSI_COLOR_YELLOW "\x1b[33m" -+#define ANSI_COLOR_BLUE "\x1b[34m" -+#define ANSI_COLOR_MAGENTA "\x1b[35m" -+#define ANSI_COLOR_CYAN "\x1b[36m" -+#define ANSI_COLOR_RESET "\x1b[0m" -+ - struct link { - char *url; - struct link *next; -@@ -767,7 +775,7 @@ if (searching) { - out = fopen("/dev/null", "w+"); - } - -- fprintf(out, "\n\n"); -+ fprintf(out, "\n"); - char *text = NULL; - int row = 0, col = 0; - struct gemini_token tok; -@@ -776,20 +784,20 @@ while (text != NULL || gemini_parser_next(&p, &tok) == 0) { - repeat: - switch (tok.token) { - case GEMINI_TEXT: -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - if (text == NULL) { - text = tok.text; - } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(out, "%d) ", nlinks++); -+ col += fprintf(out, "%2d> %s", nlinks++, ANSI_COLOR_CYAN); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); - next = &(*next)->next; - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED_BEGIN: -@@ -799,6 +807,7 @@ case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: - if (text == NULL) { -+ fprintf(out, " "); - text = tok.preformatted; - } - break; -@@ -807,34 +816,33 @@ if (!browser->page_title) { - browser->page_title = strdup(tok.heading.title); - } - if (text == NULL) { -- for (int n = tok.heading.level; n; --n) { -- col += fprintf(out, "#"); -- } - switch (tok.heading.level) { - case 1: -- col += fprintf(out, " "); -+ col += fprintf(out, "%s%s", " # ", ANSI_COLOR_RED); - break; - case 2: -+ col += fprintf(out, "%s%s", " ## ", ANSI_COLOR_YELLOW); -+ break; - case 3: -- col += fprintf(out, " "); -+ col += fprintf(out, "%s%s", "### ", ANSI_COLOR_GREEN); - break; - } - text = trim_ws(tok.heading.title); - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_LIST_ITEM: - if (text == NULL) { -- col += fprintf(out, " %s ", -+ col += fprintf(out, " %s ", - browser->unicode ? "•" : "*"); - text = trim_ws(tok.list_item); - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: -- col += fprintf(out, " %s ", -+ col += fprintf(out, " %s ", - browser->unicode ? "┃" : ">"); - if (text == NULL) { - text = trim_ws(tok.quote_text); -@@ -878,6 +886,7 @@ ++row; - } - ++row; col = 0; - -+ fprintf(out, ANSI_COLOR_RESET); - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; - char *end = NULL; diff --git a/sources/cgmnlm.git/commits/afab58cb64f205ce9f469a328a7477b808b0c76c.patch b/sources/cgmnlm.git/commits/afab58cb64f205ce9f469a328a7477b808b0c76c.patch @@ -1,30 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 5d7892b83f7914da46b08100014db6211e36173a..245ec85fa59fbcabde7e49c7d4d6e978aefa5f9a 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -908,10 +908,21 @@ int row = 0, col = 0; - ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - - char buf[BUFSIZ]; -- int n; -- while ((n = BIO_read(resp->bio, buf, sizeof(buf)) != 0)) { -- while (n) { -- n -= fwrite(buf, 1, n, browser->tty); -+ for (int n = 1; n > 0;) { -+ n = BIO_read(resp->bio, buf, BUFSIZ); -+ if (n == -1) { -+ fprintf(stderr, "Error: read\n"); -+ return 1; -+ } -+ ssize_t w = 0; -+ while (w < (ssize_t)n) { -+ ssize_t x = fwrite(&buf[w], 1, n - w, browser->tty); -+ if (ferror(browser->tty)) { -+ fprintf(stderr, "Error: write: %s\n", -+ strerror(errno)); -+ return 1; -+ } -+ w += x; - } - } - diff --git a/sources/cgmnlm.git/commits/b050b9e467589561b1203f99e9f58c990c824b1a.patch b/sources/cgmnlm.git/commits/b050b9e467589561b1203f99e9f58c990c824b1a.patch @@ -1,24 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 92e866a2a58a222f56993e57849c348ead658ab4..25afbaf0ce2621c4559fd34a035a9f944485aa3d 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -696,11 +696,19 @@ if (!input) { - requesting = false; - break; - } -+ if (input[0] == '\0' && browser->history->prev) { -+ free(input); -+ browser->history = browser->history->prev; -+ set_url(browser, browser->history->url, NULL); -+ break; -+ } - - char *new_url = gemini_input_url( - browser->plain_url, input); -+ free(input); - assert(new_url); - set_url(browser, new_url, NULL); -+ free(new_url); - break; - case GEMINI_STATUS_CLASS_REDIRECT: - if (++nredir >= 5) { diff --git a/sources/cgmnlm.git/commits/b136eea98a33ffcaf2d965e907dd6799078d2110.patch b/sources/cgmnlm.git/commits/b136eea98a33ffcaf2d965e907dd6799078d2110.patch @@ -1,144 +0,0 @@ -diff --git a/Makefile b/Makefile -index 091f426bd85ae2880fb07f3df9d8e2fa63621412..0070b703ea0af36743878d2264259bc04edb6b70 100644 ---- a/Makefile -+++ b/Makefile -@@ -8,7 +8,9 @@ gmnic: $(gmnic_objects) - @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnic_objects) - --.SUFFIXES: .c .o -+doc/gmnic.1: doc/gmnic.scd -+ -+.SUFFIXES: .c .o .scd .1 - - .c.o: - @printf 'CC\t$@\n' -@@ -17,10 +19,16 @@ @grep $< $(OUTDIR)/cppcache >/dev/null || \ - $(CPP) $(CFLAGS) -MM -MT $@ $< >> $(OUTDIR)/cppcache - @$(CC) -c $(CFLAGS) -o $@ $< - -+.scd.1: -+ @printf 'SCDOC\t$@\n' -+ @$(SCDOC) < $< > $@ -+ -+docs: doc/gmnic.1 -+ - clean: - @rm -f gmnic - - distclean: clean - @rm -rf "$(OUTDIR)" - --.PHONY: clean distclean -+.PHONY: clean distclean docs -diff --git a/config.sh b/config.sh -index ef533ec79238744458fb2e69b95e3d34333c0e5a..b93815ada4a25ec508e7a86cc79b9e9be3eba428 100644 ---- a/config.sh -+++ b/config.sh -@@ -5,6 +5,7 @@ AS=${AS:-as} - CC=${CC:-cc} - CFLAGS=${CFLAGS:-} - LD=${LD:-ld} -+SCDOC=${SCDOC:-scdoc} - - for arg - do -@@ -104,9 +105,19 @@ - find_library OpenSSL libssl - find_library OpenSSL libcrypto - -+ printf "Checking for scdoc... " -+ if scdoc -v >/dev/null 2>&1 -+ then -+ echo yes -+ all="$all docs" -+ else -+ echo no -+ fi -+ - printf "Creating $outdir/config.mk... " - cat <<-EOF > "$outdir"/config.mk - CC=$CC -+ SCDOC=$SCDOC - LIBS=$LIBS - PREFIX=${PREFIX:-/usr/local} - OUTDIR=${outdir} -@@ -153,6 +164,7 @@ - printf "Populating build dir... " - populate "$srcdir/include" - populate "$srcdir/src" -+ populate "$srcdir/doc" - ln -sf "$srcdir"/Makefile ./ - echo done - } -diff --git a/doc/gmnic.scd b/doc/gmnic.scd -new file mode 100644 -index 0000000000000000000000000000000000000000..9eec29cfddf50c2678ef7ae080275fbfbe3fcaa1 ---- /dev/null -+++ b/doc/gmnic.scd -@@ -0,0 +1,65 @@ -+gmnic(1) -+ -+# NAME -+ -+gmnic - Gemini client -+ -+# SYNPOSIS -+ -+*gmnic* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ -+ -+# DESCRIPTION -+ -+*gmnic* executes a gemini request and, if successful, prints the response body -+to stdout. -+ -+If an error is returned, information is printed to stderr and the process exits -+with a non-zero exit status equal to the response status. If a response is -+returned which is neither successful or an error, the response status and meta -+text are printed to stderr. -+ -+If the server requests user input, a prompt is shown and a second request is -+performed with the user's input supplied to the server. -+ -+# OPTIONS -+ -+*-4* -+ Force the connection to use IPv4. -+ -+*-6* -+ Force the connection to use IPv6. -+ -+*-d* _input_ -+ If the server requests user input, a second request is performed with -+ the given input string as the user input. -+ -+*-D* _path_ -+ If the server requests user input, _path_ is opened and read, and a -+ second request is performed with the contents of _path_ as the user -+ input. -+ -+*-E* _path_[:_password_] -+ Sets the path to the client certificate to use (and optionally a -+ password). If the filename contains ":" but the certificate does not -+ accept a password, append ":" to the path and it will be intepreted as -+ an empty password. -+ -+*-l* -+ For *text/\** responses, gmnic normally adds a line feed if stdout is a -+ TTY and the response body does not include one. This flag suppresses -+ this behavior. -+ -+*-L* -+ Follow redirects. -+ -+*-i* -+ Print the response status and meta text to stdout. -+ -+*-I* -+ Print the response status and meta text to stdout, and suppress the -+ printing of the response body to stdout. -+ -+*-N* -+ Suppress the input prompt if the server requests an input, and instead -+ print a diagnostic message and exit with a zero (successful) status -+ code. diff --git a/sources/cgmnlm.git/commits/b25b4576e3fd889b3edadb51ff9a387ef0bae653.patch b/sources/cgmnlm.git/commits/b25b4576e3fd889b3edadb51ff9a387ef0bae653.patch @@ -1,15 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 756520c728baad52206866a3af64b0ffebbac3cd..02d8829598902d93f1dea3d68073f609573370c1 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -797,7 +797,9 @@ } - } - - if (optind == argc - 1) { -- set_url(&browser, argv[optind], &browser.history); -+ if (!set_url(&browser, argv[optind], &browser.history)) { -+ return 1; -+ } - } else { - usage(argv[0]); - return 1; diff --git a/sources/cgmnlm.git/commits/b298fadb216bfbac8c84e05363e508b3b3a314a5.patch b/sources/cgmnlm.git/commits/b298fadb216bfbac8c84e05363e508b3b3a314a5.patch @@ -1,30 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index 18b5115371bc6b13b9b796a85f7ad4f826eef973..398e133b182af7f2fbe907be83cd5ecdc1b3a671 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -94,8 +94,7 @@ struct gemini_response *resp) - { - assert(url); - assert(resp); -- resp->meta = NULL; -- resp->bio = NULL; -+ memset(resp, 0, sizeof(*resp)); - if (strlen(url) > 1024) { - return GEMINI_ERR_INVALID_URL; - } -@@ -206,6 +205,7 @@ goto cleanup; - } - - if (r < 3 || strcmp(&buf[r - 2], "\r\n") != 0) { -+ fprintf(stderr, "invalid line %d '%s'\n", r, buf); - res = GEMINI_ERR_PROTOCOL; - goto cleanup; - } -@@ -213,6 +213,7 @@ - char *endptr; - resp->status = (enum gemini_status)strtol(buf, &endptr, 10); - if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) { -+ fprintf(stderr, "invalid status\n"); - res = GEMINI_ERR_PROTOCOL; - goto cleanup; - } diff --git a/sources/cgmnlm.git/commits/b39e196040623a80bf9f1a0a05c3da8523e26ee3.patch b/sources/cgmnlm.git/commits/b39e196040623a80bf9f1a0a05c3da8523e26ee3.patch @@ -1,65 +0,0 @@ -diff --git a/README.md b/README.md -index 9e1c9831127d8395d3fa8173f4ecb775c27e31b1..ea1f81f04aac1b93f73fca7d984b6e3f02415a2b 100644 ---- a/README.md -+++ b/README.md -@@ -19,6 +19,16 @@ It includes the following modifications: - - default 4 char indenting - - colored headings & links - -+The actual colors used depend on your terminal palette: -+- heading 1: light red -+- heading 2: light yellow -+- heading 3: light green -+- gemini link: light cyan -+- non-gemini link: light magenta -+- quote: light gray -+ -+Besides this rendering adjustments i'll try to keep track of upstream changes or send patches to upstream. -+ - ## Dependencies: - - - A POSIX-like system and a C11 compiler -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index 38ac821cff06602aba88c5e1c414aeccf3eb1e1f..6f0b8773c16001ab95edaec345879874483114a9 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -19,12 +19,13 @@ #include <gmni/tofu.h> - #include <gmni/url.h> - #include "util.h" - --#define ANSI_COLOR_RED "\x1b[31m" --#define ANSI_COLOR_GREEN "\x1b[32m" --#define ANSI_COLOR_YELLOW "\x1b[33m" --#define ANSI_COLOR_BLUE "\x1b[34m" --#define ANSI_COLOR_MAGENTA "\x1b[35m" --#define ANSI_COLOR_CYAN "\x1b[36m" -+#define ANSI_COLOR_RED "\x1b[91m" -+#define ANSI_COLOR_GREEN "\x1b[92m" -+#define ANSI_COLOR_YELLOW "\x1b[93m" -+#define ANSI_COLOR_BLUE "\x1b[94m" -+#define ANSI_COLOR_MAGENTA "\x1b[95m" -+#define ANSI_COLOR_CYAN "\x1b[96m" -+#define ANSI_COLOR_GRAY "\x1b[37m" - #define ANSI_COLOR_RESET "\x1b[0m" - - struct link { -@@ -791,7 +792,7 @@ } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9) || strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_CYAN : ANSI_COLOR_MAGENTA ); -+ col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9) || strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_CYAN : ANSI_COLOR_MAGENTA); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); -@@ -842,8 +843,8 @@ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: -- col += fprintf(out, " %s ", -- browser->unicode ? "┃" : ">"); -+ col += fprintf(out, " %s%s %s", ANSI_COLOR_RESET, -+ browser->unicode ? "┃" : ">", ANSI_COLOR_GRAY); - if (text == NULL) { - text = trim_ws(tok.quote_text); - } diff --git a/sources/cgmnlm.git/commits/b4fc0c0993229b8fc8242e314e701f33a8102688.patch b/sources/cgmnlm.git/commits/b4fc0c0993229b8fc8242e314e701f33a8102688.patch @@ -1,15 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index 4c25d5eb50a7dcb92d2990364527b04bdb66d588..6f12a4e0dfafa563de2836b13da910ae2fd65895 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -26,8 +26,8 @@ // If the answer to the latter is "no", then we give the user an - // opportunity to explicitly agree to trust the certificate before - // rejecting it. - // -- // If you're reading this code with the intent to re-use it, think -- // twice. -+ // If you're reading this code with the intent to re-use it for -+ // something unrelated to Gemini, think twice. - struct gemini_tofu *tofu = (struct gemini_tofu *)data; - X509 *cert = X509_STORE_CTX_get0_cert(ctx); - struct known_host *host = NULL; diff --git a/sources/cgmnlm.git/commits/b54a100d7156ea279641a9e779b7658c42300fe9.patch b/sources/cgmnlm.git/commits/b54a100d7156ea279641a9e779b7658c42300fe9.patch @@ -1,33 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 041096cc5f0117632f80b0427b9a916aaa6401c2..c56f3a2d393b7d475f3b859a9a8ed4ca922186a3 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -948,6 +948,10 @@ int row = 0, col = 0; - bool no_alttext; - struct gemini_token tok; - struct link **next = &browser->links; -+ // When your screen is too narrow, more lines will be used for helptext and URL. -+ // 87 is the maximum width of the prompt. -+ int info_rows = (ws.ws_col >= 87) ? 4 : 6; -+ - while (text != NULL || gemini_parser_next(&p, &tok) == 0) { - repeat: - switch (tok.token) { -@@ -1043,7 +1047,7 @@ - if (text) { - int w = wrap(out, text, &ws, &row, &col); - text += w; -- if (text[0] && row < ws.ws_row - 4) { -+ if (text[0] && row < ws.ws_row - info_rows) { - continue; - } - -@@ -1062,7 +1066,7 @@ } - ++row; col = 0; - - fprintf(out, ANSI_COLOR_RESET); -- if (browser->pagination && row >= ws.ws_row - 4) { -+ if (browser->pagination && row >= ws.ws_row - info_rows) { - char prompt[4096]; - char *end = NULL; - if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) { diff --git a/sources/cgmnlm.git/commits/b64d3d5ac9121bd3c5df1c48defe5fdc209e467d.patch b/sources/cgmnlm.git/commits/b64d3d5ac9121bd3c5df1c48defe5fdc209e467d.patch @@ -1,18 +0,0 @@ -diff --git a/Makefile b/Makefile -index 863d9f585607869a43c9c678cd72b2f21f724ee0..c5bc1ed76a0a61d70c2bfe3506a868f0d7eace5e 100644 ---- a/Makefile -+++ b/Makefile -@@ -67,4 +67,13 @@ install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc - install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 - install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 - -+uninstall: -+ rm -f $(BINDIR)/gmni -+ rm -f $(BINDIR)/gmnlm -+ rm -f $(LIBDIR)/libgmni.a -+ rm -rf $(INCLUDEDIR)/gmni -+ rm -f $(LIBDIR)/pkgconfig/libgmni.pc -+ rm -f $(MANDIR)/man1/gmni.1 -+ rm -f $(MANDIR)/man1/gmnlm.1 -+ - .PHONY: clean distclean docs install diff --git a/sources/cgmnlm.git/commits/b90888e71878eea7727cd507503198d134d91ab7.patch b/sources/cgmnlm.git/commits/b90888e71878eea7727cd507503198d134d91ab7.patch @@ -1,21 +0,0 @@ -diff --git a/README.md b/README.md -index c08d5b2e62d4ec740ffda4acfccbc198ad9726d4..44cb015b64c3d0fbe5d898668d8d423045d7f609 100644 ---- a/README.md -+++ b/README.md -@@ -1,6 +1,9 @@ - # gmni - A Gemini client - --This is a [Gemini](https://gemini.circumlunar.space/) client. -+This is a [Gemini](https://gemini.circumlunar.space/) client. Included are: -+ -+- A CLI utility (like curl): gmni -+- A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm - - Dependencies: - -@@ -19,4 +22,4 @@ ``` - - ## Usage - --See `gmni(1)` for the CLI usage. -+See `gmni(1)`, `gmnlm(1)`. diff --git a/sources/cgmnlm.git/commits/bb696e6e2823d38bf6ad2f5106f3808555c48b18.patch b/sources/cgmnlm.git/commits/bb696e6e2823d38bf6ad2f5106f3808555c48b18.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 5cc2e18a06dc99f2258d6bfc875c2c01ceb265c2..5e5d828f6f246e0baa3917177acd8cb6482387f8 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -164,7 +164,7 @@ - struct link *link = browser->links; - char *endptr; - int linksel = (int)strtol(in, &endptr, 10); -- if (endptr[0] == '\n' && linksel >= 0) { -+ if (!endptr[0] && linksel >= 0) { - while (linksel > 0 && link) { - link = link->next; - --linksel; diff --git a/sources/cgmnlm.git/commits/be0cf0dfd1e83a1ba4f6b27fb9464c4e95d10752.patch b/sources/cgmnlm.git/commits/be0cf0dfd1e83a1ba4f6b27fb9464c4e95d10752.patch @@ -1,13 +0,0 @@ -diff --git a/Makefile b/Makefile -index 9a51be9ccf5eeb34c1481db6b75bea42c73adead..c842ac46d9b0340fd16ef5e19d75e6977e4547e9 100644 ---- a/Makefile -+++ b/Makefile -@@ -31,7 +31,7 @@ - docs: doc/gmni.1 doc/gmnlm.1 - - clean: -- @rm -f gmni doc/gmni.1 doc/gmnlm.1 -+ @rm -f gmni doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) - - distclean: clean - @rm -rf "$(OUTDIR)" diff --git a/sources/cgmnlm.git/commits/c036a43801d60b620262687e4bb6d98f97e23dbd.patch b/sources/cgmnlm.git/commits/c036a43801d60b620262687e4bb6d98f97e23dbd.patch @@ -1,12 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 2fba84e1f3c9e312ac53417d1b873158ceaf8c73..da43dbe2ce10637c2f267e2cc75c0a7a6f3cea92 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -629,6 +629,7 @@ res = do_requests(browser, &resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", - gemini_strerr(res, &resp)); -+ result = PROMPT_AGAIN; - goto exit; - } - pipe_resp(browser->tty, resp, &in[1]); diff --git a/sources/cgmnlm.git/commits/c08e934c449c7a030fb1ebacd3166820b23faeb3.patch b/sources/cgmnlm.git/commits/c08e934c449c7a030fb1ebacd3166820b23faeb3.patch @@ -1,42 +0,0 @@ -diff --git a/src/gmnic.c b/src/gmnic.c -index 2a06a180ebd5478e25b97cacea8468c86d11df00..ab0908de5ec9682865504e940a5489a43c41b12e 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -17,7 +17,7 @@ static void - usage(char *argv_0) - { - fprintf(stderr, -- "usage: %s [-46lLiIN] [-C cert] [-d input] [-D path] gemini://...\n", -+ "usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n", - argv_0); - } - -@@ -44,7 +44,7 @@ .hints = &hints, - }; - - int c; -- while ((c = getopt(argc, argv, "46C:d:D:hlLiIN")) != -1) { -+ while ((c = getopt(argc, argv, "46d:D:E:hlLiIN")) != -1) { - switch (c) { - case '4': - hints.ai_family = AF_INET; -@@ -52,9 +52,6 @@ break; - case '6': - hints.ai_family = AF_INET6; - break; -- case 'C': -- assert(0); // TODO: Client certificates -- break; - case 'd': - input_mode = INPUT_READ; - input_source = fmemopen(optarg, strlen(optarg), "r"); -@@ -71,6 +68,9 @@ optarg, strerror(errno)); - return 1; - } - } -+ break; -+ case 'E': -+ assert(0); // TODO: Client certificates - break; - case 'h': - usage(argv[0]); diff --git a/sources/cgmnlm.git/commits/c0c891f87b101db98c589da9a60e4a39bf048f0d.patch b/sources/cgmnlm.git/commits/c0c891f87b101db98c589da9a60e4a39bf048f0d.patch @@ -1,12 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 6ad61939f8dcd6f6cf530f5615e442fcfb92ffde..82d7abc15562ad7ee4fe5f7e5349be477f719ae5 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -767,6 +767,7 @@ if (searching) { - out = fopen("/dev/null", "w+"); - } - -+ fprintf(out, "\n\n"); - char *text = NULL; - int row = 0, col = 0; - struct gemini_token tok; diff --git a/sources/cgmnlm.git/commits/c3aa884144bb173073c6b973835a266bae27bf1e.patch b/sources/cgmnlm.git/commits/c3aa884144bb173073c6b973835a266bae27bf1e.patch @@ -1,50 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index c56f3a2d393b7d475f3b859a9a8ed4ca922186a3..f93ae642c8c641c9718f8ee849b0f666bdcd2869 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -639,7 +639,7 @@ result = PROMPT_ANSWERED; - goto exit; - case 'a': - browser->alttext = !browser->alttext; -- fprintf(browser->tty, "Alttext instead of preformatted block is now %s\n", browser->alttext ? "ENABLED" : "DISABLED"); -+ fprintf(browser->tty, "Alttext instead of preformatted block is now %s\n\n", browser->alttext ? "ENABLED" : "DISABLED"); - result = PROMPT_AGAIN; - goto exit; - case 'b': -@@ -945,7 +945,7 @@ - fprintf(out, "\n"); - char *text = NULL; - int row = 0, col = 0; -- bool no_alttext; -+ bool alttext_printed; - struct gemini_token tok; - struct link **next = &browser->links; - // When your screen is too narrow, more lines will be used for helptext and URL. -@@ -973,20 +973,19 @@ col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED_BEGIN: -- if (text == NULL && browser->alttext) { -- if (tok.preformatted == NULL) { -- no_alttext = true; -- } else { -- fprintf(out, " A %s", ANSI_COLOR_GRAY); -- text = tok.preformatted; -- } -+ alttext_printed = false; -+ if (text == NULL && browser->alttext && tok.preformatted != NULL) { -+ fprintf(out, " A %s", ANSI_COLOR_GRAY); -+ text = trim_ws(tok.preformatted); -+ alttext_printed = true; - } - break; - /* fallthrough */ - case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: -- if (text == NULL && (!browser->alttext || no_alttext)) { -+ if (alttext_printed) continue; -+ if (text == NULL) { - fprintf(out, " P %s", ANSI_COLOR_GRAY); - text = tok.preformatted; - } diff --git a/sources/cgmnlm.git/commits/c414388c0f2c020c0e22d53f6df577e1fbeb32fc.patch b/sources/cgmnlm.git/commits/c414388c0f2c020c0e22d53f6df577e1fbeb32fc.patch @@ -1,13 +0,0 @@ -diff --git a/README.md b/README.md -index 44cb015b64c3d0fbe5d898668d8d423045d7f609..842e238162c33eec93dad8b10abb7bfe21af447b 100644 ---- a/README.md -+++ b/README.md -@@ -5,6 +5,8 @@ - - A CLI utility (like curl): gmni - - A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm - -+[![Screenshot of the line-mode browser](https://l.sr.ht/AY7_.png)](https://asciinema.org/a/ldo2gV7qiDoBXvGwuD6x1jbn3) -+ - Dependencies: - - - A POSIX-like system and a C11 compiler diff --git a/sources/cgmnlm.git/commits/c7592c6a5c2ff7b390af89f297d5c9e1c34c9414.patch b/sources/cgmnlm.git/commits/c7592c6a5c2ff7b390af89f297d5c9e1c34c9414.patch @@ -1,28 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index d567a958702bed9413c746bd5b9f3024526fa92e..69b9a75ca1caab6f7e5f74685e550539be149ab6 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1,6 +1,7 @@ - #include <assert.h> - #include <ctype.h> - #include <getopt.h> -+#include <libgen.h> - #include <limits.h> - #include <openssl/bio.h> - #include <openssl/err.h> -@@ -127,8 +128,14 @@ { - const char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -- mkdirs(path, 0755); -+ if (mkdirs(dirname(path), 0755) != 0) { -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ fprintf(stderr, "Error creating directory %s: %s\n", -+ dirname(path), strerror(errno)); -+ return; -+ } - -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - FILE *f = fopen(path, "a"); - if (!f) { - fprintf(stderr, "Error opening %s for writing: %s\n", diff --git a/sources/cgmnlm.git/commits/c8041a15ac7d36ecc2e1c34dcaa14c51e62de788.patch b/sources/cgmnlm.git/commits/c8041a15ac7d36ecc2e1c34dcaa14c51e62de788.patch @@ -1,92 +0,0 @@ -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index 5bf7dfdbea9a60cd610d76b82bb8e9bc3d5ffa1d..39be8ad24bdf97f87c3e51f4b5ce92e8bf1b421b 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -67,11 +67,9 @@ PROMPT_NEXT, - }; - - const char *default_bookmarks = -- "# Welcome to gmni\n\n" -+ "# Welcome to cgmnlm\n\n" - "Links:\n\n" -- // TODO: sub out the URL for the appropriate geminispace version once -- // sr.ht supports gemini -- "=> https://sr.ht/~sircmpwn/gmni The gmni browser\n" -+ "=> https://src.clttr.info/rwa/cgmnlm The cgmnlm browser\n" - "=> gemini://gemini.circumlunar.space The gemini protocol\n\n" - "This file can be found at %s and may be edited at your pleasure.\n\n" - "Bookmarks:\n" -@@ -79,24 +77,25 @@ ; - - const char *help_msg = - "The following commands are available:\n\n" -- "q\tQuit\n" -- "[N]\tFollow Nth link (where N is a number)\n" -- "u[N]\tShow URL of Nth link (where N is a number)\n" -- "e[N]\tSend URL of Nth link in external default program\n" -- "b\tBack (in the page history)\n" -- "f\tForward (in the page history)\n" -- "H\tView all page history\n" -- "a\tSave bookmark\n" -- "B\tBrowse bookmarks\n" -- "r\tReload the page\n" -+ "<Enter>\t\tread more lines (if available)\n" -+ "<url>\t\tgo to url\n" -+ "[N]\t\tFollow Nth link (where N is a number)\n" -+ "u[N]\t\tShow URL of Nth link (where N is a number)\n" -+ "e[N]\t\tSend URL of Nth link in external default program\n" -+ "t[N]\t\tDownload content of Nth link to a temporary file\n" -+ "b\t\tBack (in the page history)\n" -+ "f\t\tForward (in the page history)\n" -+ "H\t\tView all page history\n" -+ "a\t\tSave bookmark\n" -+ "B\t\tBrowse bookmarks\n" -+ "r\t\tReload the page\n" -+ "/<text>\t\tsearch for text (POSIX regular expression)\n" -+ "n\t\tjump to next search match\n" - "d <path>\tDownload page to <path>\n" -- "|<prog>\tPipe page into program\n" -+ "|<prog>\t\tPipe page into program\n" - "[N]|<prog>\tPipe content of Nth link into program\n" -+ "q\t\tQuit\n" - "\n" -- "Other commands include:\n\n" -- "<Enter>\tread more lines\n" -- "<url>\tgo to url\n" -- "/<text>\tsearch for text (POSIX regular expression)\n" - ; - - static void -@@ -598,6 +597,7 @@ goto exit; - } - case 'e': - case 'u': -+ case 't': - if (!in[1]) break; - linksel = (int)strtol(in+1, &endptr, 10); - if (!endptr[0] && linksel >= 0) { -@@ -614,6 +614,24 @@ if (in[0] == 'e') { - char xdgopen[4096]; - snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", link->url); - if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); -+ } -+ if (in[0] == 't') { -+ struct gemini_response resp; -+ char url[1024] = {0}; -+ strncpy(&url[0], browser->plain_url, sizeof(link->url)-1); -+ set_url(browser, link->url, &browser->history); -+ // XXX: may affect history, do we care? -+ enum gemini_result res = do_requests(browser, &resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", -+ gemini_strerr(res, &resp)); -+ } else { -+ char tempfile[] = "/tmp/cgmnlm_XXXXXX"; -+ mkstemp(tempfile); -+ download_resp(browser->tty, resp, tempfile, link->url); -+ } -+ gemini_response_finish(&resp); -+ set_url(browser, url, NULL); - } - fprintf(browser->tty, "\n"); - } diff --git a/sources/cgmnlm.git/commits/cb63b8ddf093711607ff98e933d4bd04154a854b.patch b/sources/cgmnlm.git/commits/cb63b8ddf093711607ff98e933d4bd04154a854b.patch @@ -1,67 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 2eb906e1312a346fe2ad479fa5f49b1d155927b8..9f76188252a30e95868b99d64d57369ac03a7e1c 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -154,8 +154,13 @@ save_bookmark(struct browser *browser) - { - char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; -- snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -- if (mkdirs(dirname(path), 0755) != 0) { -+ static char dname[PATH_MAX+1]; -+ size_t n; -+ -+ n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ assert(n < sizeof(path)); -+ strncpy(dname, dirname(path), sizeof(dname)); -+ if (mkdirs(dname, 0755) != 0) { - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - fprintf(stderr, "Error creating directory %s: %s\n", -@@ -190,8 +195,13 @@ open_bookmarks(struct browser *browser) - { - char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; -- snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -- if (mkdirs(dirname(path), 0755) != 0) { -+ static char dname[PATH_MAX+1]; -+ size_t n; -+ -+ n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ assert(n < sizeof(path)); -+ strncpy(dname, dirname(path), sizeof(dname)); -+ if (mkdirs(dname, 0755) != 0) { - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - fprintf(stderr, "Error creating directory %s: %s\n", -diff --git a/src/tofu.c b/src/tofu.c -index 863efc644a691370ee58ef0fb92291e9471adeb9..16548a1cbcebc8c4cdda4a57a4655a04e7f390a3 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -157,10 +157,15 @@ {.var = "XDG_DATA_HOME", .path = "/gemini/%s"}, - {.var = "HOME", .path = "/.local/share/gemini/%s"} - }; - char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); -- snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -+ char dname[PATH_MAX+1]; -+ size_t n = 0; -+ -+ n = snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); -+ assert(n < sizeof(tofu->known_hosts_path)); - -- if (mkdirs(dirname(tofu->known_hosts_path), 0755) != 0) { -+ strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)); -+ if (mkdirs(dname, 0755) != 0) { - snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); - fprintf(stderr, "Error creating directory %s: %s\n", -@@ -182,7 +187,7 @@ FILE *f = fopen(tofu->known_hosts_path, "r"); - if (!f) { - return; - } -- size_t n = 0; -+ n = 0; - char *line = NULL; - while (getline(&line, &n, f) != -1) { - struct known_host *host = calloc(1, sizeof(struct known_host)); diff --git a/sources/cgmnlm.git/commits/cc3f9a5eea25ad350039d7e552bec944b3a121b0.patch b/sources/cgmnlm.git/commits/cc3f9a5eea25ad350039d7e552bec944b3a121b0.patch @@ -1,16 +0,0 @@ -diff --git a/cgmnlm b/cgmnlm -index 5c05cd3ecca44b577a75544d7e2119421a5da102..a7237df4a6c75847b8bfaee4dc4d1df687975936 100755 -Binary files a/cgmnlm and b/cgmnlm differ -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index 95b85fedd9fb84b50bf1c8a89be94daaeb99a4a6..cb0bc10b924eb6afa057d41158815b133fd9372f 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -795,7 +795,7 @@ } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(out, "%2d) %s", nlinks++, ANSI_COLOR_CYAN); -+ col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9) || strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_CYAN : ANSI_COLOR_MAGENTA ); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); diff --git a/sources/cgmnlm.git/commits/ccec255833fa27789ffb75551b29528b10f8c62a.patch b/sources/cgmnlm.git/commits/ccec255833fa27789ffb75551b29528b10f8c62a.patch @@ -1,900 +0,0 @@ -diff --git a/.gitignore b/.gitignore -new file mode 100644 -index 0000000000000000000000000000000000000000..7b35cd8ef5366f89f8db24b1f62f0a1f2c3e336d ---- /dev/null -+++ b/.gitignore -@@ -0,0 +1,5 @@ -+.build -+build -+gmnic -+*.1 -+*.o -diff --git a/COPYING b/COPYING -new file mode 100644 -index 0000000000000000000000000000000000000000..f288702d2fa16d3cdf0035b15a9fcbc552cd88e7 ---- /dev/null -+++ b/COPYING -@@ -0,0 +1,674 @@ -+ GNU GENERAL PUBLIC LICENSE -+ Version 3, 29 June 2007 -+ -+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> -+ Everyone is permitted to copy and distribute verbatim copies -+ of this license document, but changing it is not allowed. -+ -+ Preamble -+ -+ The GNU General Public License is a free, copyleft license for -+software and other kinds of works. -+ -+ The licenses for most software and other practical works are designed -+to take away your freedom to share and change the works. By contrast, -+the GNU General Public License is intended to guarantee your freedom to -+share and change all versions of a program--to make sure it remains free -+software for all its users. We, the Free Software Foundation, use the -+GNU General Public License for most of our software; it applies also to -+any other work released this way by its authors. You can apply it to -+your programs, too. -+ -+ When we speak of free software, we are referring to freedom, not -+price. Our General Public Licenses are designed to make sure that you -+have the freedom to distribute copies of free software (and charge for -+them if you wish), that you receive source code or can get it if you -+want it, that you can change the software or use pieces of it in new -+free programs, and that you know you can do these things. -+ -+ To protect your rights, we need to prevent others from denying you -+these rights or asking you to surrender the rights. Therefore, you have -+certain responsibilities if you distribute copies of the software, or if -+you modify it: responsibilities to respect the freedom of others. -+ -+ For example, if you distribute copies of such a program, whether -+gratis or for a fee, you must pass on to the recipients the same -+freedoms that you received. You must make sure that they, too, receive -+or can get the source code. And you must show them these terms so they -+know their rights. -+ -+ Developers that use the GNU GPL protect your rights with two steps: -+(1) assert copyright on the software, and (2) offer you this License -+giving you legal permission to copy, distribute and/or modify it. -+ -+ For the developers' and authors' protection, the GPL clearly explains -+that there is no warranty for this free software. For both users' and -+authors' sake, the GPL requires that modified versions be marked as -+changed, so that their problems will not be attributed erroneously to -+authors of previous versions. -+ -+ Some devices are designed to deny users access to install or run -+modified versions of the software inside them, although the manufacturer -+can do so. This is fundamentally incompatible with the aim of -+protecting users' freedom to change the software. The systematic -+pattern of such abuse occurs in the area of products for individuals to -+use, which is precisely where it is most unacceptable. Therefore, we -+have designed this version of the GPL to prohibit the practice for those -+products. If such problems arise substantially in other domains, we -+stand ready to extend this provision to those domains in future versions -+of the GPL, as needed to protect the freedom of users. -+ -+ Finally, every program is threatened constantly by software patents. -+States should not allow patents to restrict development and use of -+software on general-purpose computers, but in those that do, we wish to -+avoid the special danger that patents applied to a free program could -+make it effectively proprietary. To prevent this, the GPL assures that -+patents cannot be used to render the program non-free. -+ -+ The precise terms and conditions for copying, distribution and -+modification follow. -+ -+ TERMS AND CONDITIONS -+ -+ 0. Definitions. -+ -+ "This License" refers to version 3 of the GNU General Public License. -+ -+ "Copyright" also means copyright-like laws that apply to other kinds of -+works, such as semiconductor masks. -+ -+ "The Program" refers to any copyrightable work licensed under this -+License. Each licensee is addressed as "you". "Licensees" and -+"recipients" may be individuals or organizations. -+ -+ To "modify" a work means to copy from or adapt all or part of the work -+in a fashion requiring copyright permission, other than the making of an -+exact copy. The resulting work is called a "modified version" of the -+earlier work or a work "based on" the earlier work. -+ -+ A "covered work" means either the unmodified Program or a work based -+on the Program. -+ -+ To "propagate" a work means to do anything with it that, without -+permission, would make you directly or secondarily liable for -+infringement under applicable copyright law, except executing it on a -+computer or modifying a private copy. Propagation includes copying, -+distribution (with or without modification), making available to the -+public, and in some countries other activities as well. -+ -+ To "convey" a work means any kind of propagation that enables other -+parties to make or receive copies. Mere interaction with a user through -+a computer network, with no transfer of a copy, is not conveying. -+ -+ An interactive user interface displays "Appropriate Legal Notices" -+to the extent that it includes a convenient and prominently visible -+feature that (1) displays an appropriate copyright notice, and (2) -+tells the user that there is no warranty for the work (except to the -+extent that warranties are provided), that licensees may convey the -+work under this License, and how to view a copy of this License. If -+the interface presents a list of user commands or options, such as a -+menu, a prominent item in the list meets this criterion. -+ -+ 1. Source Code. -+ -+ The "source code" for a work means the preferred form of the work -+for making modifications to it. "Object code" means any non-source -+form of a work. -+ -+ A "Standard Interface" means an interface that either is an official -+standard defined by a recognized standards body, or, in the case of -+interfaces specified for a particular programming language, one that -+is widely used among developers working in that language. -+ -+ The "System Libraries" of an executable work include anything, other -+than the work as a whole, that (a) is included in the normal form of -+packaging a Major Component, but which is not part of that Major -+Component, and (b) serves only to enable use of the work with that -+Major Component, or to implement a Standard Interface for which an -+implementation is available to the public in source code form. A -+"Major Component", in this context, means a major essential component -+(kernel, window system, and so on) of the specific operating system -+(if any) on which the executable work runs, or a compiler used to -+produce the work, or an object code interpreter used to run it. -+ -+ The "Corresponding Source" for a work in object code form means all -+the source code needed to generate, install, and (for an executable -+work) run the object code and to modify the work, including scripts to -+control those activities. However, it does not include the work's -+System Libraries, or general-purpose tools or generally available free -+programs which are used unmodified in performing those activities but -+which are not part of the work. For example, Corresponding Source -+includes interface definition files associated with source files for -+the work, and the source code for shared libraries and dynamically -+linked subprograms that the work is specifically designed to require, -+such as by intimate data communication or control flow between those -+subprograms and other parts of the work. -+ -+ The Corresponding Source need not include anything that users -+can regenerate automatically from other parts of the Corresponding -+Source. -+ -+ The Corresponding Source for a work in source code form is that -+same work. -+ -+ 2. Basic Permissions. -+ -+ All rights granted under this License are granted for the term of -+copyright on the Program, and are irrevocable provided the stated -+conditions are met. This License explicitly affirms your unlimited -+permission to run the unmodified Program. The output from running a -+covered work is covered by this License only if the output, given its -+content, constitutes a covered work. This License acknowledges your -+rights of fair use or other equivalent, as provided by copyright law. -+ -+ You may make, run and propagate covered works that you do not -+convey, without conditions so long as your license otherwise remains -+in force. You may convey covered works to others for the sole purpose -+of having them make modifications exclusively for you, or provide you -+with facilities for running those works, provided that you comply with -+the terms of this License in conveying all material for which you do -+not control copyright. Those thus making or running the covered works -+for you must do so exclusively on your behalf, under your direction -+and control, on terms that prohibit them from making any copies of -+your copyrighted material outside their relationship with you. -+ -+ Conveying under any other circumstances is permitted solely under -+the conditions stated below. Sublicensing is not allowed; section 10 -+makes it unnecessary. -+ -+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law. -+ -+ No covered work shall be deemed part of an effective technological -+measure under any applicable law fulfilling obligations under article -+11 of the WIPO copyright treaty adopted on 20 December 1996, or -+similar laws prohibiting or restricting circumvention of such -+measures. -+ -+ When you convey a covered work, you waive any legal power to forbid -+circumvention of technological measures to the extent such circumvention -+is effected by exercising rights under this License with respect to -+the covered work, and you disclaim any intention to limit operation or -+modification of the work as a means of enforcing, against the work's -+users, your or third parties' legal rights to forbid circumvention of -+technological measures. -+ -+ 4. Conveying Verbatim Copies. -+ -+ You may convey verbatim copies of the Program's source code as you -+receive it, in any medium, provided that you conspicuously and -+appropriately publish on each copy an appropriate copyright notice; -+keep intact all notices stating that this License and any -+non-permissive terms added in accord with section 7 apply to the code; -+keep intact all notices of the absence of any warranty; and give all -+recipients a copy of this License along with the Program. -+ -+ You may charge any price or no price for each copy that you convey, -+and you may offer support or warranty protection for a fee. -+ -+ 5. Conveying Modified Source Versions. -+ -+ You may convey a work based on the Program, or the modifications to -+produce it from the Program, in the form of source code under the -+terms of section 4, provided that you also meet all of these conditions: -+ -+ a) The work must carry prominent notices stating that you modified -+ it, and giving a relevant date. -+ -+ b) The work must carry prominent notices stating that it is -+ released under this License and any conditions added under section -+ 7. This requirement modifies the requirement in section 4 to -+ "keep intact all notices". -+ -+ c) You must license the entire work, as a whole, under this -+ License to anyone who comes into possession of a copy. This -+ License will therefore apply, along with any applicable section 7 -+ additional terms, to the whole of the work, and all its parts, -+ regardless of how they are packaged. This License gives no -+ permission to license the work in any other way, but it does not -+ invalidate such permission if you have separately received it. -+ -+ d) If the work has interactive user interfaces, each must display -+ Appropriate Legal Notices; however, if the Program has interactive -+ interfaces that do not display Appropriate Legal Notices, your -+ work need not make them do so. -+ -+ A compilation of a covered work with other separate and independent -+works, which are not by their nature extensions of the covered work, -+and which are not combined with it such as to form a larger program, -+in or on a volume of a storage or distribution medium, is called an -+"aggregate" if the compilation and its resulting copyright are not -+used to limit the access or legal rights of the compilation's users -+beyond what the individual works permit. Inclusion of a covered work -+in an aggregate does not cause this License to apply to the other -+parts of the aggregate. -+ -+ 6. Conveying Non-Source Forms. -+ -+ You may convey a covered work in object code form under the terms -+of sections 4 and 5, provided that you also convey the -+machine-readable Corresponding Source under the terms of this License, -+in one of these ways: -+ -+ a) Convey the object code in, or embodied in, a physical product -+ (including a physical distribution medium), accompanied by the -+ Corresponding Source fixed on a durable physical medium -+ customarily used for software interchange. -+ -+ b) Convey the object code in, or embodied in, a physical product -+ (including a physical distribution medium), accompanied by a -+ written offer, valid for at least three years and valid for as -+ long as you offer spare parts or customer support for that product -+ model, to give anyone who possesses the object code either (1) a -+ copy of the Corresponding Source for all the software in the -+ product that is covered by this License, on a durable physical -+ medium customarily used for software interchange, for a price no -+ more than your reasonable cost of physically performing this -+ conveying of source, or (2) access to copy the -+ Corresponding Source from a network server at no charge. -+ -+ c) Convey individual copies of the object code with a copy of the -+ written offer to provide the Corresponding Source. This -+ alternative is allowed only occasionally and noncommercially, and -+ only if you received the object code with such an offer, in accord -+ with subsection 6b. -+ -+ d) Convey the object code by offering access from a designated -+ place (gratis or for a charge), and offer equivalent access to the -+ Corresponding Source in the same way through the same place at no -+ further charge. You need not require recipients to copy the -+ Corresponding Source along with the object code. If the place to -+ copy the object code is a network server, the Corresponding Source -+ may be on a different server (operated by you or a third party) -+ that supports equivalent copying facilities, provided you maintain -+ clear directions next to the object code saying where to find the -+ Corresponding Source. Regardless of what server hosts the -+ Corresponding Source, you remain obligated to ensure that it is -+ available for as long as needed to satisfy these requirements. -+ -+ e) Convey the object code using peer-to-peer transmission, provided -+ you inform other peers where the object code and Corresponding -+ Source of the work are being offered to the general public at no -+ charge under subsection 6d. -+ -+ A separable portion of the object code, whose source code is excluded -+from the Corresponding Source as a System Library, need not be -+included in conveying the object code work. -+ -+ A "User Product" is either (1) a "consumer product", which means any -+tangible personal property which is normally used for personal, family, -+or household purposes, or (2) anything designed or sold for incorporation -+into a dwelling. In determining whether a product is a consumer product, -+doubtful cases shall be resolved in favor of coverage. For a particular -+product received by a particular user, "normally used" refers to a -+typical or common use of that class of product, regardless of the status -+of the particular user or of the way in which the particular user -+actually uses, or expects or is expected to use, the product. A product -+is a consumer product regardless of whether the product has substantial -+commercial, industrial or non-consumer uses, unless such uses represent -+the only significant mode of use of the product. -+ -+ "Installation Information" for a User Product means any methods, -+procedures, authorization keys, or other information required to install -+and execute modified versions of a covered work in that User Product from -+a modified version of its Corresponding Source. The information must -+suffice to ensure that the continued functioning of the modified object -+code is in no case prevented or interfered with solely because -+modification has been made. -+ -+ If you convey an object code work under this section in, or with, or -+specifically for use in, a User Product, and the conveying occurs as -+part of a transaction in which the right of possession and use of the -+User Product is transferred to the recipient in perpetuity or for a -+fixed term (regardless of how the transaction is characterized), the -+Corresponding Source conveyed under this section must be accompanied -+by the Installation Information. But this requirement does not apply -+if neither you nor any third party retains the ability to install -+modified object code on the User Product (for example, the work has -+been installed in ROM). -+ -+ The requirement to provide Installation Information does not include a -+requirement to continue to provide support service, warranty, or updates -+for a work that has been modified or installed by the recipient, or for -+the User Product in which it has been modified or installed. Access to a -+network may be denied when the modification itself materially and -+adversely affects the operation of the network or violates the rules and -+protocols for communication across the network. -+ -+ Corresponding Source conveyed, and Installation Information provided, -+in accord with this section must be in a format that is publicly -+documented (and with an implementation available to the public in -+source code form), and must require no special password or key for -+unpacking, reading or copying. -+ -+ 7. Additional Terms. -+ -+ "Additional permissions" are terms that supplement the terms of this -+License by making exceptions from one or more of its conditions. -+Additional permissions that are applicable to the entire Program shall -+be treated as though they were included in this License, to the extent -+that they are valid under applicable law. If additional permissions -+apply only to part of the Program, that part may be used separately -+under those permissions, but the entire Program remains governed by -+this License without regard to the additional permissions. -+ -+ When you convey a copy of a covered work, you may at your option -+remove any additional permissions from that copy, or from any part of -+it. (Additional permissions may be written to require their own -+removal in certain cases when you modify the work.) You may place -+additional permissions on material, added by you to a covered work, -+for which you have or can give appropriate copyright permission. -+ -+ Notwithstanding any other provision of this License, for material you -+add to a covered work, you may (if authorized by the copyright holders of -+that material) supplement the terms of this License with terms: -+ -+ a) Disclaiming warranty or limiting liability differently from the -+ terms of sections 15 and 16 of this License; or -+ -+ b) Requiring preservation of specified reasonable legal notices or -+ author attributions in that material or in the Appropriate Legal -+ Notices displayed by works containing it; or -+ -+ c) Prohibiting misrepresentation of the origin of that material, or -+ requiring that modified versions of such material be marked in -+ reasonable ways as different from the original version; or -+ -+ d) Limiting the use for publicity purposes of names of licensors or -+ authors of the material; or -+ -+ e) Declining to grant rights under trademark law for use of some -+ trade names, trademarks, or service marks; or -+ -+ f) Requiring indemnification of licensors and authors of that -+ material by anyone who conveys the material (or modified versions of -+ it) with contractual assumptions of liability to the recipient, for -+ any liability that these contractual assumptions directly impose on -+ those licensors and authors. -+ -+ All other non-permissive additional terms are considered "further -+restrictions" within the meaning of section 10. If the Program as you -+received it, or any part of it, contains a notice stating that it is -+governed by this License along with a term that is a further -+restriction, you may remove that term. If a license document contains -+a further restriction but permits relicensing or conveying under this -+License, you may add to a covered work material governed by the terms -+of that license document, provided that the further restriction does -+not survive such relicensing or conveying. -+ -+ If you add terms to a covered work in accord with this section, you -+must place, in the relevant source files, a statement of the -+additional terms that apply to those files, or a notice indicating -+where to find the applicable terms. -+ -+ Additional terms, permissive or non-permissive, may be stated in the -+form of a separately written license, or stated as exceptions; -+the above requirements apply either way. -+ -+ 8. Termination. -+ -+ You may not propagate or modify a covered work except as expressly -+provided under this License. Any attempt otherwise to propagate or -+modify it is void, and will automatically terminate your rights under -+this License (including any patent licenses granted under the third -+paragraph of section 11). -+ -+ However, if you cease all violation of this License, then your -+license from a particular copyright holder is reinstated (a) -+provisionally, unless and until the copyright holder explicitly and -+finally terminates your license, and (b) permanently, if the copyright -+holder fails to notify you of the violation by some reasonable means -+prior to 60 days after the cessation. -+ -+ Moreover, your license from a particular copyright holder is -+reinstated permanently if the copyright holder notifies you of the -+violation by some reasonable means, this is the first time you have -+received notice of violation of this License (for any work) from that -+copyright holder, and you cure the violation prior to 30 days after -+your receipt of the notice. -+ -+ Termination of your rights under this section does not terminate the -+licenses of parties who have received copies or rights from you under -+this License. If your rights have been terminated and not permanently -+reinstated, you do not qualify to receive new licenses for the same -+material under section 10. -+ -+ 9. Acceptance Not Required for Having Copies. -+ -+ You are not required to accept this License in order to receive or -+run a copy of the Program. Ancillary propagation of a covered work -+occurring solely as a consequence of using peer-to-peer transmission -+to receive a copy likewise does not require acceptance. However, -+nothing other than this License grants you permission to propagate or -+modify any covered work. These actions infringe copyright if you do -+not accept this License. Therefore, by modifying or propagating a -+covered work, you indicate your acceptance of this License to do so. -+ -+ 10. Automatic Licensing of Downstream Recipients. -+ -+ Each time you convey a covered work, the recipient automatically -+receives a license from the original licensors, to run, modify and -+propagate that work, subject to this License. You are not responsible -+for enforcing compliance by third parties with this License. -+ -+ An "entity transaction" is a transaction transferring control of an -+organization, or substantially all assets of one, or subdividing an -+organization, or merging organizations. If propagation of a covered -+work results from an entity transaction, each party to that -+transaction who receives a copy of the work also receives whatever -+licenses to the work the party's predecessor in interest had or could -+give under the previous paragraph, plus a right to possession of the -+Corresponding Source of the work from the predecessor in interest, if -+the predecessor has it or can get it with reasonable efforts. -+ -+ You may not impose any further restrictions on the exercise of the -+rights granted or affirmed under this License. For example, you may -+not impose a license fee, royalty, or other charge for exercise of -+rights granted under this License, and you may not initiate litigation -+(including a cross-claim or counterclaim in a lawsuit) alleging that -+any patent claim is infringed by making, using, selling, offering for -+sale, or importing the Program or any portion of it. -+ -+ 11. Patents. -+ -+ A "contributor" is a copyright holder who authorizes use under this -+License of the Program or a work on which the Program is based. The -+work thus licensed is called the contributor's "contributor version". -+ -+ A contributor's "essential patent claims" are all patent claims -+owned or controlled by the contributor, whether already acquired or -+hereafter acquired, that would be infringed by some manner, permitted -+by this License, of making, using, or selling its contributor version, -+but do not include claims that would be infringed only as a -+consequence of further modification of the contributor version. For -+purposes of this definition, "control" includes the right to grant -+patent sublicenses in a manner consistent with the requirements of -+this License. -+ -+ Each contributor grants you a non-exclusive, worldwide, royalty-free -+patent license under the contributor's essential patent claims, to -+make, use, sell, offer for sale, import and otherwise run, modify and -+propagate the contents of its contributor version. -+ -+ In the following three paragraphs, a "patent license" is any express -+agreement or commitment, however denominated, not to enforce a patent -+(such as an express permission to practice a patent or covenant not to -+sue for patent infringement). To "grant" such a patent license to a -+party means to make such an agreement or commitment not to enforce a -+patent against the party. -+ -+ If you convey a covered work, knowingly relying on a patent license, -+and the Corresponding Source of the work is not available for anyone -+to copy, free of charge and under the terms of this License, through a -+publicly available network server or other readily accessible means, -+then you must either (1) cause the Corresponding Source to be so -+available, or (2) arrange to deprive yourself of the benefit of the -+patent license for this particular work, or (3) arrange, in a manner -+consistent with the requirements of this License, to extend the patent -+license to downstream recipients. "Knowingly relying" means you have -+actual knowledge that, but for the patent license, your conveying the -+covered work in a country, or your recipient's use of the covered work -+in a country, would infringe one or more identifiable patents in that -+country that you have reason to believe are valid. -+ -+ If, pursuant to or in connection with a single transaction or -+arrangement, you convey, or propagate by procuring conveyance of, a -+covered work, and grant a patent license to some of the parties -+receiving the covered work authorizing them to use, propagate, modify -+or convey a specific copy of the covered work, then the patent license -+you grant is automatically extended to all recipients of the covered -+work and works based on it. -+ -+ A patent license is "discriminatory" if it does not include within -+the scope of its coverage, prohibits the exercise of, or is -+conditioned on the non-exercise of one or more of the rights that are -+specifically granted under this License. You may not convey a covered -+work if you are a party to an arrangement with a third party that is -+in the business of distributing software, under which you make payment -+to the third party based on the extent of your activity of conveying -+the work, and under which the third party grants, to any of the -+parties who would receive the covered work from you, a discriminatory -+patent license (a) in connection with copies of the covered work -+conveyed by you (or copies made from those copies), or (b) primarily -+for and in connection with specific products or compilations that -+contain the covered work, unless you entered into that arrangement, -+or that patent license was granted, prior to 28 March 2007. -+ -+ Nothing in this License shall be construed as excluding or limiting -+any implied license or other defenses to infringement that may -+otherwise be available to you under applicable patent law. -+ -+ 12. No Surrender of Others' Freedom. -+ -+ If conditions are imposed on you (whether by court order, agreement or -+otherwise) that contradict the conditions of this License, they do not -+excuse you from the conditions of this License. If you cannot convey a -+covered work so as to satisfy simultaneously your obligations under this -+License and any other pertinent obligations, then as a consequence you may -+not convey it at all. For example, if you agree to terms that obligate you -+to collect a royalty for further conveying from those to whom you convey -+the Program, the only way you could satisfy both those terms and this -+License would be to refrain entirely from conveying the Program. -+ -+ 13. Use with the GNU Affero General Public License. -+ -+ Notwithstanding any other provision of this License, you have -+permission to link or combine any covered work with a work licensed -+under version 3 of the GNU Affero General Public License into a single -+combined work, and to convey the resulting work. The terms of this -+License will continue to apply to the part which is the covered work, -+but the special requirements of the GNU Affero General Public License, -+section 13, concerning interaction through a network will apply to the -+combination as such. -+ -+ 14. Revised Versions of this License. -+ -+ The Free Software Foundation may publish revised and/or new versions of -+the GNU General Public License from time to time. Such new versions will -+be similar in spirit to the present version, but may differ in detail to -+address new problems or concerns. -+ -+ Each version is given a distinguishing version number. If the -+Program specifies that a certain numbered version of the GNU General -+Public License "or any later version" applies to it, you have the -+option of following the terms and conditions either of that numbered -+version or of any later version published by the Free Software -+Foundation. If the Program does not specify a version number of the -+GNU General Public License, you may choose any version ever published -+by the Free Software Foundation. -+ -+ If the Program specifies that a proxy can decide which future -+versions of the GNU General Public License can be used, that proxy's -+public statement of acceptance of a version permanently authorizes you -+to choose that version for the Program. -+ -+ Later license versions may give you additional or different -+permissions. However, no additional obligations are imposed on any -+author or copyright holder as a result of your choosing to follow a -+later version. -+ -+ 15. Disclaimer of Warranty. -+ -+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -+ALL NECESSARY SERVICING, REPAIR OR CORRECTION. -+ -+ 16. Limitation of Liability. -+ -+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -+SUCH DAMAGES. -+ -+ 17. Interpretation of Sections 15 and 16. -+ -+ If the disclaimer of warranty and limitation of liability provided -+above cannot be given local legal effect according to their terms, -+reviewing courts shall apply local law that most closely approximates -+an absolute waiver of all civil liability in connection with the -+Program, unless a warranty or assumption of liability accompanies a -+copy of the Program in return for a fee. -+ -+ END OF TERMS AND CONDITIONS -+ -+ How to Apply These Terms to Your New Programs -+ -+ If you develop a new program, and you want it to be of the greatest -+possible use to the public, the best way to achieve this is to make it -+free software which everyone can redistribute and change under these terms. -+ -+ To do so, attach the following notices to the program. It is safest -+to attach them to the start of each source file to most effectively -+state the exclusion of warranty; and each file should have at least -+the "copyright" line and a pointer to where the full notice is found. -+ -+ <one line to give the program's name and a brief idea of what it does.> -+ Copyright (C) <year> <name of author> -+ -+ This program is free software: you can redistribute it and/or modify -+ it under the terms of the GNU General Public License as published by -+ the Free Software Foundation, either version 3 of the License, or -+ (at your option) any later version. -+ -+ This program is distributed in the hope that it will be useful, -+ but WITHOUT ANY WARRANTY; without even the implied warranty of -+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ GNU General Public License for more details. -+ -+ You should have received a copy of the GNU General Public License -+ along with this program. If not, see <https://www.gnu.org/licenses/>. -+ -+Also add information on how to contact you by electronic and paper mail. -+ -+ If the program does terminal interaction, make it output a short -+notice like this when it starts in an interactive mode: -+ -+ <program> Copyright (C) <year> <name of author> -+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. -+ This is free software, and you are welcome to redistribute it -+ under certain conditions; type `show c' for details. -+ -+The hypothetical commands `show w' and `show c' should show the appropriate -+parts of the General Public License. Of course, your program's commands -+might be different; for a GUI interface, you would use an "about box". -+ -+ You should also get your employer (if you work as a programmer) or school, -+if any, to sign a "copyright disclaimer" for the program, if necessary. -+For more information on this, and how to apply and follow the GNU GPL, see -+<https://www.gnu.org/licenses/>. -+ -+ The GNU General Public License does not permit incorporating your program -+into proprietary programs. If your program is a subroutine library, you -+may consider it more useful to permit linking proprietary applications with -+the library. If this is what you want to do, use the GNU Lesser General -+Public License instead of this License. But first, please read -+<https://www.gnu.org/licenses/why-not-lgpl.html>. -diff --git a/Makefile b/Makefile -new file mode 100644 -index 0000000000000000000000000000000000000000..091f426bd85ae2880fb07f3df9d8e2fa63621412 ---- /dev/null -+++ b/Makefile -@@ -0,0 +1,26 @@ -+.POSIX: -+.SUFFIXES: -+OUTDIR=.build -+include $(OUTDIR)/config.mk -+include $(OUTDIR)/cppcache -+ -+gmnic: $(gmnic_objects) -+ @printf 'CCLD\t$@\n' -+ @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnic_objects) -+ -+.SUFFIXES: .c .o -+ -+.c.o: -+ @printf 'CC\t$@\n' -+ @touch $(OUTDIR)/cppcache -+ @grep $< $(OUTDIR)/cppcache >/dev/null || \ -+ $(CPP) $(CFLAGS) -MM -MT $@ $< >> $(OUTDIR)/cppcache -+ @$(CC) -c $(CFLAGS) -o $@ $< -+ -+clean: -+ @rm -f gmnic -+ -+distclean: clean -+ @rm -rf "$(OUTDIR)" -+ -+.PHONY: clean distclean -diff --git a/config.sh b/config.sh -new file mode 100644 -index 0000000000000000000000000000000000000000..fc1134c0c98d9f57cb2cfdbb3b9d6faf35001dfd ---- /dev/null -+++ b/config.sh -@@ -0,0 +1,139 @@ -+outdir=${OUTDIR:-.build} -+srcdir=${SRCDIR:-$(dirname "$0")} -+AR=${AR:-ar} -+AS=${AS:-as} -+CC=${CC:-cc} -+CFLAGS=${CFLAGS:-} -+LD=${LD:-ld} -+ -+for arg -+do -+ # TODO: Add args for install directories -+ case "$arg" in -+ --prefix=*) -+ PREFIX=${arg#*=} -+ ;; -+ esac -+done -+ -+subdir() { -+ eval ". $srcdir/$1/configure" -+} -+ -+genrules() { -+ target="$1" -+ shift -+ printf '# Begin generated rules for %s\n' "$target" -+ for file in "$@" -+ do -+ ext="${file#*.}" -+ file="${file%.*}" -+ deps= -+ printf '%s.o: %s.%s%s\n' "$file" "$file" "$ext" "$deps" -+ done -+ printf '%s_objects=\\\n' "$target" -+ n=0 -+ for file in "$@" -+ do -+ file="${file%.*}" -+ n=$((n+1)) -+ if [ $n -eq $# ] -+ then -+ printf '\t%s.o\n' "$file" -+ else -+ printf '\t%s.o \\\n' "$file" -+ fi -+ done -+ printf '# End generated rules for %s\n' "$target" -+} -+ -+append_cflags() { -+ for flag -+ do -+ CFLAGS="$(printf '%s \\\n\t%s' "$CFLAGS" "$flag")" -+ done -+} -+ -+test_cflags() { -+ [ ! -e "$outdir"/check.c ] && cat <<-EOF > "$outdir"/check.c -+ int main(void) { return 0; } -+ EOF -+ werror="" -+ case "$CFLAGS" in -+ *-Werror*) -+ werror="-Werror" -+ ;; -+ esac -+ if $CC $werror "$@" -o /dev/null "$outdir"/check.c >/dev/null 2>&1 -+ then -+ append_cflags "$@" -+ else -+ return 1 -+ fi -+} -+ -+run_configure() { -+ mkdir -p $outdir -+ -+ for flag in -g -std=c11 -D_XOPEN_SOURCE=700 -Wall -Wextra -Werror -pedantic -+ do -+ printf "Checking for $flag... " -+ if test_cflags "$flag" -+ then -+ echo yes -+ else -+ echo no -+ fi -+ done -+ -+ printf "Creating $outdir/config.mk... " -+ cat <<-EOF > "$outdir"/config.mk -+ CC=$CC -+ PREFIX=${PREFIX:-/usr/local} -+ OUTDIR=${outdir} -+ _INSTDIR=\$(DESTDIR)\$(PREFIX) -+ BINDIR?=${BINDIR:-\$(_INSTDIR)/bin} -+ LIBDIR?=${LIBDIR:-\$(_INSTDIR)/lib} -+ MANDIR?=${MANDIR:-\$(_INSTDIR)/share/man} -+ CACHE=\$(OUTDIR)/cache -+ CFLAGS=${CFLAGS} -+ CFLAGS+=-Iinclude -I\$(OUTDIR) -+ CFLAGS+=-DPREFIX='"\$(PREFIX)"' -+ CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' -+ -+ all: ${all} -+ EOF -+ gmni >>"$outdir"/config.mk -+ echo done -+ -+ touch $outdir/cppcache -+ -+ if [ "$srcdir" = "." ] -+ then -+ return -+ fi -+ -+ populate() ( -+ path="$1" -+ mkdir -p "${path#$srcdir/}" -+ fullpath() ( cd "$1" && pwd ) -+ for orig in "$path"/* -+ do -+ link="${orig#$srcdir/}" -+ if [ -d "$orig" ] -+ then -+ mkdir -p $link -+ populate "$orig" -+ elif [ -f "$orig" ] -+ then -+ ln -sf "$(fullpath "$path")"/"$(basename "$orig")" "$link" -+ fi -+ done -+ ) -+ -+ printf "Populating build dir... " -+ populate "$srcdir/include" -+ populate "$srcdir/src" -+ ln -sf "$srcdir"/Makefile ./ -+ echo done -+} -diff --git a/configure b/configure -new file mode 100755 -index 0000000000000000000000000000000000000000..7b1a48b785b7ab1a8811cb36ea9ee00a68fc9ff8 ---- /dev/null -+++ b/configure -@@ -0,0 +1,12 @@ -+#!/bin/sh -e -+srcdir=${SRCDIR:-$(dirname "$0")} -+eval ". $srcdir/config.sh" -+ -+gmni() { -+ genrules gmnic \ -+ src/gmnic.c -+} -+ -+all="gmnic" -+ -+run_configure -diff --git a/src/gmnic.c b/src/gmnic.c -new file mode 100644 -index 0000000000000000000000000000000000000000..35a49493cc6af2486d055c9bde5a576942c2e40a ---- /dev/null -+++ b/src/gmnic.c -@@ -0,0 +1,8 @@ -+#include <stdio.h> -+ -+int -+main(int argc, char *argv[]) { -+ (void)argc; (void)argv; -+ printf("Hello, world!\n"); -+ return 0; -+} diff --git a/sources/cgmnlm.git/commits/ce1a524642e25da8a66a18797e9afc8f54f20903.patch b/sources/cgmnlm.git/commits/ce1a524642e25da8a66a18797e9afc8f54f20903.patch @@ -1,13 +0,0 @@ -diff --git a/Makefile b/Makefile -index c686cc60d215bc9e6032b429a1bc8babc6509a2f..863d9f585607869a43c9c678cd72b2f21f724ee0 100644 ---- a/Makefile -+++ b/Makefile -@@ -63,7 +63,7 @@ install -Dm755 libgmni.a $(LIBDIR)/libgmni.a - install -Dm644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h - install -Dm644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h - install -Dm644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h -- install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig -+ install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc - install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 - install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 - diff --git a/sources/cgmnlm.git/commits/ce1ef1abde0d5519e0464f9326edea01b73a845f.patch b/sources/cgmnlm.git/commits/ce1ef1abde0d5519e0464f9326edea01b73a845f.patch @@ -1,34 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index 4eeb7fd0eb8df7423fe8fd8f106e2fe433fa8716..4c25d5eb50a7dcb92d2990364527b04bdb66d588 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -6,6 +6,7 @@ #include <openssl/asn1.h> - #include <openssl/evp.h> - #include <openssl/ssl.h> - #include <openssl/x509.h> -+#include <openssl/x509v3.h> - #include <stdio.h> - #include <string.h> - #include <time.h> -@@ -27,8 +28,6 @@ // rejecting it. - // - // If you're reading this code with the intent to re-use it, think - // twice. -- // -- // TODO: Check that the subject name is valid for the requested URL. - struct gemini_tofu *tofu = (struct gemini_tofu *)data; - X509 *cert = X509_STORE_CTX_get0_cert(ctx); - struct known_host *host = NULL; -@@ -70,6 +69,12 @@ SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, - SSL_get_ex_data_X509_STORE_CTX_idx()); - const char *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); - if (!servername) { -+ rc = X509_V_ERR_HOSTNAME_MISMATCH; -+ goto invalid_cert; -+ } -+ -+ rc = X509_check_host(cert, servername, strlen(servername), 0, NULL); -+ if (rc != 1) { - rc = X509_V_ERR_HOSTNAME_MISMATCH; - goto invalid_cert; - } diff --git a/sources/cgmnlm.git/commits/d06c4cda5c5538ea71401bbfb9ff0bb6657d8413.patch b/sources/cgmnlm.git/commits/d06c4cda5c5538ea71401bbfb9ff0bb6657d8413.patch @@ -1,34 +0,0 @@ -diff --git a/doc/cgmnlm.scd b/doc/cgmnlm.scd -index 1ed6e620cb39e04ade8b98fa9bab7e7c0bacc5e9..0e50aa7bfe8eed0814a18456ef101cd30180fb14 100644 ---- a/doc/cgmnlm.scd -+++ b/doc/cgmnlm.scd -@@ -28,3 +28,6 @@ features. - - *-W* _width_ - Sets the maximum width, in columns, of Gemtext pages. -+ -+*-A* -+ Enables alternate text instead of preformatted text by default. -diff --git a/src/gmnlm.c b/src/gmnlm.c -index f93ae642c8c641c9718f8ee849b0f666bdcd2869..cec330b44ae6e9252e92fbd1a5d74e7592b84e2a 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1269,7 +1269,7 @@ .meta = NULL, - }; - - int c; -- while ((c = getopt(argc, argv, "hj:PUW:")) != -1) { -+ while ((c = getopt(argc, argv, "hj:PAUW:")) != -1) { - switch (c) { - case 'h': - usage(argv[0]); -@@ -1285,6 +1285,9 @@ } else { - usage(argv[0]); - return 1; - } -+ break; -+ case 'A': -+ browser.alttext = true; - break; - case 'P': - browser.pagination = false; diff --git a/sources/cgmnlm.git/commits/d0acd0f4d08a0d5e8f6c729f53de2f381b270202.patch b/sources/cgmnlm.git/commits/d0acd0f4d08a0d5e8f6c729f53de2f381b270202.patch @@ -1,59 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 3598e3fb1770aca2bd646d7a3f2bf642b54c2ab9..81923101e320b8517405ef2ed7efdbc626241201 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -23,7 +23,7 @@ struct history *prev, *next; - }; - - struct browser { -- bool pagination; -+ bool pagination, unicode; - struct gemini_options opts; - - FILE *tty; -@@ -184,13 +184,13 @@ col += fprintf(browser->tty, " %s\n", - trim_ws(tok.heading.title)); - break; - case GEMINI_LIST_ITEM: -- // TODO: Option to disable Unicode -- col += fprintf(browser->tty, " • %s\n", -+ col += fprintf(browser->tty, " %s %s\n", -+ browser->unicode ? "•" : "*", - trim_ws(tok.list_item)); - break; - case GEMINI_QUOTE: -- // TODO: Option to disable Unicode -- col += fprintf(browser->tty, " | %s\n", -+ col += fprintf(browser->tty, " %s %s\n", -+ browser->unicode ? "|" : "|", - trim_ws(tok.quote_text)); - break; - } -@@ -368,19 +368,23 @@ main(int argc, char *argv[]) - { - struct browser browser = { - .pagination = true, -+ .unicode = true, - .url = curl_url(), - .tty = fopen("/dev/tty", "w+"), - }; - - int c; -- while ((c = getopt(argc, argv, "hP")) != -1) { -+ while ((c = getopt(argc, argv, "hPU")) != -1) { - switch (c) { -+ case 'h': -+ usage(argv[0]); -+ return 0; - case 'P': - browser.pagination = false; - break; -- case 'h': -- usage(argv[0]); -- return 0; -+ case 'U': -+ browser.unicode = false; -+ break; - default: - fprintf(stderr, "fatal: unknown flag %c\n", c); - return 1; diff --git a/sources/cgmnlm.git/commits/d2fa1b4567aa841020f4d4bbdb1298b310534c98.patch b/sources/cgmnlm.git/commits/d2fa1b4567aa841020f4d4bbdb1298b310534c98.patch @@ -1,96 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index e80b38b6521ef0eccd5629a1f0b16d2406d8cac1..129324718d13f8311f38cc3c80b7f0f3ccb3bd09 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -82,37 +82,45 @@ - static enum prompt_result - do_prompts(const char *prompt, struct browser *browser) - { -+ enum prompt_result result; - fprintf(browser->tty, "%s", prompt); - - size_t l = 0; - char *in = NULL; - ssize_t n = getline(&in, &l, browser->tty); - if (n == -1 && feof(browser->tty)) { -- return PROMPT_QUIT; -+ result = PROMPT_QUIT; -+ goto exit; - } - if (strcmp(in, "\n") == 0) { -- return PROMPT_MORE; -+ result = PROMPT_MORE; -+ goto exit; - } - if (strcmp(in, "q\n") == 0) { -- return PROMPT_QUIT; -+ result = PROMPT_QUIT; -+ goto exit; - } - if (strcmp(in, "b\n") == 0) { - if (!browser->history->prev) { - fprintf(stderr, "At beginning of history\n"); -- return PROMPT_AGAIN; -+ result = PROMPT_AGAIN; -+ goto exit; - } - browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); -- return PROMPT_ANSWERED; -+ result = PROMPT_ANSWERED; -+ goto exit; - } - if (strcmp(in, "f\n") == 0) { - if (!browser->history->next) { - fprintf(stderr, "At end of history\n"); -- return PROMPT_AGAIN; -+ result = PROMPT_AGAIN; -+ goto exit; - } - browser->history = browser->history->next; - set_url(browser, browser->history->url, NULL); -- return PROMPT_ANSWERED; -+ result = PROMPT_ANSWERED; -+ goto exit; - } - - struct link *link = browser->links; -@@ -128,12 +136,17 @@ if (!link) { - fprintf(stderr, "Error: no such link.\n"); - } else { - set_url(browser, link->url, &browser->history); -- return PROMPT_ANSWERED; -+ result = PROMPT_ANSWERED; -+ goto exit; - } - } -+ -+ in[n - 1] = 0; // Remove LF -+ set_url(browser, in, &browser->history); -+ result = PROMPT_ANSWERED; -+exit: - free(in); -- -- return PROMPT_AGAIN; -+ return result; - } - - static char * -@@ -278,7 +291,7 @@ - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit\n" -+ "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit; or type a URL\n" - "(more) => ", resp->meta, browser->plain_url, - browser->history->prev ? "[b]ack; " : "", - browser->history->next ? "[f]orward; " : ""); -@@ -487,7 +500,7 @@ goto next; - } - - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[N]: follow Nth link; %s%s[q]uit\n" -+ "[N]: follow Nth link; %s%s[q]uit; or type a URL\n" - "=> ", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", - browser.plain_url, diff --git a/sources/cgmnlm.git/commits/d371589381e57835d37796f1d349638b806e43b4.patch b/sources/cgmnlm.git/commits/d371589381e57835d37796f1d349638b806e43b4.patch @@ -1,14 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index 0acdf33a50bb4957741f4789472ffe56035a7159..54183a79278de45bfbe5a1f8ddbcbf10be1c01c4 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -62,9 +62,6 @@ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { - cc->err = err; - return; - } -- if (br_x509_decoder_isCA(&cc->decoder)) { -- return; -- } - cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); - br_sha512_out(&cc->sha512, &cc->hash); - } diff --git a/sources/cgmnlm.git/commits/d3afac098e2bf0a4ed9fad89f6a748f6288ae3bc.patch b/sources/cgmnlm.git/commits/d3afac098e2bf0a4ed9fad89f6a748f6288ae3bc.patch @@ -1,16 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index f31222b3ee2c495ed42c614bbd3c5100b52ae767..2c35f76f8ee4b6dfa6af5bbe8af790a3e98afbbc 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -70,8 +70,9 @@ - const char *default_bookmarks = - "# Welcome to cgmnlm\n\n" - "Links:\n\n" -- "=> gemini://gmn.clttr.info/cgmnln.gmi The colorful line mode client\n" -- "=> gemini://gemini.circumlunar.space The gemini protocol\n\n" -+ "=> gemini://gmn.clttr.info/cgmnln.gmi The colorful gemini line mode client\n" -+ "=> gemini://gemini.circumlunar.space The gemini protocol\n" -+ "=> gemini://geminispace.info/search/ search in geminispace\n\n" - "This file can be found at %s and may be edited at your pleasure.\n\n" - "Bookmarks:\n" - ; diff --git a/sources/cgmnlm.git/commits/d5936353392a17ae6bac3303d506a7e79855da2d.patch b/sources/cgmnlm.git/commits/d5936353392a17ae6bac3303d506a7e79855da2d.patch @@ -1,18 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 8fc92928ba0d8532a5f94f8e637d0b0aa025bc09..2375151c7f5fced09a80e41d26809730827f8925 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -322,12 +322,9 @@ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: -+ col += fprintf(out, "> "); - if (text == NULL) { -- col += fprintf(out, " %s ", -- browser->unicode ? "|" : "|"); - text = trim_ws(tok.quote_text); -- } else { -- col += fprintf(out, " "); - } - break; - } diff --git a/sources/cgmnlm.git/commits/d6777ec2788f9ece56a6201fb091dba4a15f739a.patch b/sources/cgmnlm.git/commits/d6777ec2788f9ece56a6201fb091dba4a15f739a.patch @@ -1,15 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index 07460f917b153b0d368eb2a98f368aa6a5df56a4..2d39f56cb8e45f7b708e5a5dea1ec6fa84e188c9 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -240,7 +240,9 @@ BIO_free(resp->bio); // buffered bio - resp->bio = NULL; - } - -- SSL_free(resp->ssl); -+ if (resp->ssl) { -+ SSL_free(resp->ssl); -+ } - SSL_CTX_free(resp->ssl_ctx); - free(resp->meta); - diff --git a/sources/cgmnlm.git/commits/d754f34e7eb5f700b6de13c6c4692837d0a123f4.patch b/sources/cgmnlm.git/commits/d754f34e7eb5f700b6de13c6c4692837d0a123f4.patch @@ -1,22 +0,0 @@ -diff --git a/config.sh b/config.sh -index 2a94bc6feada62bcfda616fa573d0a12db0a4502..b03dfffbc5976e69d2d878dbf3b6c545ebcad377 100644 ---- a/config.sh -+++ b/config.sh -@@ -9,8 +9,16 @@ SCDOC=${SCDOC:-scdoc} - - for arg - do -- # TODO: Add args for install directories - case "$arg" in -+ --bindir=*) -+ BINDIR=${arg#*=} -+ ;; -+ --libdir=*) -+ LIBDIR=${arg#*=} -+ ;; -+ --mandir=*) -+ MANDIR=${arg#*=} -+ ;; - --prefix=*) - PREFIX=${arg#*=} - ;; diff --git a/sources/cgmnlm.git/commits/d84ee77e249eae11c4b240294c6d37851f1ba11f.patch b/sources/cgmnlm.git/commits/d84ee77e249eae11c4b240294c6d37851f1ba11f.patch @@ -1,61 +0,0 @@ -diff --git a/doc/gmnlm.scd b/doc/gmnlm.scd -index b11f3612e044ad0667d8c4d306445ae86e9f73d4..e0a368f8921253c76219790d35380848e486720d 100644 ---- a/doc/gmnlm.scd -+++ b/doc/gmnlm.scd -@@ -6,7 +6,7 @@ gmnlm - Gemini line-mode browser - - # SYNPOSIS - --*gmnlm* [-PU] [-j _mode_] _gemini://..._ -+*gmnlm* [-PU] [-j _mode_] [-W _width_] _gemini://..._ - - # DESCRIPTION - -@@ -25,3 +25,6 @@ - *-U* - Disable conservative use of Unicode symbols to render Gemini layout - features. -+ -+*-W* _width_ -+ Sets the maximum width, in columns, of Gemtext pages. -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 85917dfa97c034af41b9ab15e45139d6b95709de..b4f20de66d50fe9f0287c74b9a9292fe8ddd48d3 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -30,6 +30,7 @@ }; - - struct browser { - bool pagination, unicode; -+ int max_width; - struct gemini_options opts; - struct gemini_tofu tofu; - enum tofu_action tofu_mode; -@@ -473,6 +474,9 @@ browser->page_title = NULL; - - struct winsize ws; - ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); -+ if (browser->max_width != 0 && ws.ws_col > browser->max_width) { -+ ws.ws_col = browser->max_width; -+ } - - FILE *out = browser->tty; - bool searching = browser->searching; -@@ -930,7 +934,7 @@ .meta = NULL, - }; - - int c; -- while ((c = getopt(argc, argv, "hj:PU")) != -1) { -+ while ((c = getopt(argc, argv, "hj:PUW:")) != -1) { - switch (c) { - case 'h': - usage(argv[0]); -@@ -952,6 +956,9 @@ browser.pagination = false; - break; - case 'U': - browser.unicode = false; -+ break; -+ case 'W': -+ browser.max_width = strtoul(optarg, NULL, 10); - break; - default: - fprintf(stderr, "fatal: unknown flag %c\n", c); diff --git a/sources/cgmnlm.git/commits/d8f0870446c471a42612d6a8e853ad9b723a6d39.patch b/sources/cgmnlm.git/commits/d8f0870446c471a42612d6a8e853ad9b723a6d39.patch @@ -1,93 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 1c3e5f2bebddf5b606d11acd8924562cabde9a6f..49abb8524a012c27249006dc45f4688a846e63c3 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -14,6 +14,7 @@ #include <termios.h> - #include <unistd.h> - #include <gmni/gmni.h> - #include <gmni/tofu.h> -+#include <gmni/url.h> - #include "util.h" - - static void -@@ -227,12 +228,24 @@ opts.ssl_ctx = SSL_CTX_new(TLS_method()); - gemini_tofu_init(&cfg.tofu, opts.ssl_ctx, &tofu_callback, &cfg); - - bool exit = false; -- char *url = strdup(argv[optind]); -+ struct Curl_URL *url = curl_url(); -+ -+ if(curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { -+ // TODO: Better error -+ fprintf(stderr, "Error: invalid URL\n"); -+ return 1; -+ } - - int ret = 0, nredir = 0; - while (!exit) { -+ char *buf; -+ curl_url_get(url, CURLUPART_URL, &buf, 0); -+ - struct gemini_response resp; -- enum gemini_result r = gemini_request(url, &opts, &resp); -+ enum gemini_result r = gemini_request(buf, &opts, &resp); -+ -+ free(buf); -+ - if (r != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); - ret = (int)r; -@@ -254,12 +267,16 @@ exit = true; - break; - } - -- char *new_url = gemini_input_url(url, input); -+ char *buf; -+ curl_url_get(url, CURLUPART_URL, &buf, 0); -+ -+ char *new_url = gemini_input_url(buf, input); - assert(new_url); - - free(input); -- free(url); -- url = new_url; -+ free(buf); -+ -+ curl_url_set(url, CURLUPART_URL, new_url, 0); - goto next; - case GEMINI_STATUS_CLASS_REDIRECT: - if (++nredir >= max_redirect) { -@@ -270,8 +287,8 @@ exit = true; - goto next; - } - -- free(url); -- url = strdup(resp.meta); -+ curl_url_set(url, CURLUPART_URL, resp.meta, 0); -+ - if (!follow_redirects) { - if (header_mode == OMIT_HEADERS) { - fprintf(stderr, "REDIRECT: %d %s\n", -@@ -311,7 +328,12 @@ break; - } - - if (output_file != NULL) { -- ret = download_resp(stderr, resp, output_file, url); -+ char *buf; -+ curl_url_get(url, CURLUPART_URL, &buf, 0); -+ -+ ret = download_resp(stderr, resp, output_file, buf); -+ free(buf); -+ - break; - } - -@@ -349,7 +371,7 @@ gemini_response_finish(&resp); - } - - SSL_CTX_free(opts.ssl_ctx); -- free(url); -+ curl_url_cleanup(url); - gemini_tofu_finish(&cfg.tofu); - return ret; - } diff --git a/sources/cgmnlm.git/commits/dbc726616e6675ae82d9bb55be5693371255ed2f.patch b/sources/cgmnlm.git/commits/dbc726616e6675ae82d9bb55be5693371255ed2f.patch @@ -1,1502 +0,0 @@ -diff --git a/README.md b/README.md -index aa9f811a9bfd9cfb7b6654954e33fc62e868f8c4..fa803f99158582d982ef3a01d6b87471fb27f0cb 100644 ---- a/README.md -+++ b/README.md -@@ -10,6 +10,7 @@ - - Page history - - Regex searches - - Bookmarks -+- basic Client Certificate support (no autocreation of client certs currently) - - ## Non-Features: - -@@ -61,6 +62,5 @@ - ### Dependencies: - - - A POSIX-like system and a C11 compiler --- OpenSSL -+- [BearSSL](https://www.bearssl.org/index.html) - - [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) -- -diff --git a/config.sh b/config.sh -index 424cbda346869c7476859d55a600c414c86bbb46..87888929b98a46887dbcb25378385183e4ef2566 100644 ---- a/config.sh -+++ b/config.sh -@@ -117,8 +117,8 @@ echo no - fi - done - -- find_library OpenSSL libssl -- find_library OpenSSL libcrypto -+ # XXX: Asked the maintainer to provide a .pc file -+ LIBS="$LIBS -lbearssl" - - printf "Checking for scdoc... " - if scdoc -v >/dev/null 2>&1 -diff --git a/configure b/configure -index 70a19b193d6e33b41b5cc7ef13511bb0f5b4e076..6abccdd18d0b85e4b2d4b527d49727d360dec5c3 100755 ---- a/configure -+++ b/configure -@@ -4,6 +4,7 @@ eval ". $srcdir/config.sh" - - gmni() { - genrules gmni \ -+ src/certs.c \ - src/client.c \ - src/escape.c \ - src/gmni.c \ -@@ -14,6 +15,7 @@ } - - cgmnlm() { - genrules cgmnlm \ -+ src/certs.c \ - src/client.c \ - src/escape.c \ - src/gmnlm.c \ -@@ -25,6 +27,7 @@ } - - libgmni_a() { - genrules libgmni.a \ -+ src/certs.c \ - src/client.c \ - src/escape.c \ - src/tofu.c \ -diff --git a/doc/gmni.scd b/doc/gmni.scd -index 1f20672f1fb0c8ff6a4f3a97a3324a25d2a0a01d..866dd418a510ccd141d9c23afb4b321b84a5f9ec 100644 ---- a/doc/gmni.scd -+++ b/doc/gmni.scd -@@ -38,11 +38,9 @@ If the server requests user input, _path_ is opened and read, and a - second request is performed with the contents of _path_ as the user - input. - --*-E* _path_[:_password_] -- Sets the path to the client certificate to use (and optionally a -- password). If the filename contains ":" but the certificate does not -- accept a password, append ":" to the path and it will be intepreted as -- an empty password. -+*-E* _path_:_key_ -+ Sets the path to the client certificate and private key file to use, -+ both PEM encoded. - - *-l* - For *text/\** responses, *gmni* normally adds a line feed if stdout is a -diff --git a/include/gmni/certs.h b/include/gmni/certs.h -new file mode 100644 -index 0000000000000000000000000000000000000000..53b8aad27fa50649ddfdacbd08724d5d0b033120 ---- /dev/null -+++ b/include/gmni/certs.h -@@ -0,0 +1,27 @@ -+#ifndef GEMINI_CERTS_H -+#define GEMINI_CERTS_H -+#include <bearssl.h> -+#include <stdio.h> -+ -+struct gmni_options; -+ -+struct gmni_client_certificate { -+ br_x509_certificate *chain; -+ size_t nchain; -+ struct gmni_private_key *key; -+}; -+ -+struct gmni_private_key { -+ int type; -+ union { -+ br_rsa_private_key rsa; -+ br_ec_private_key ec; -+ }; -+ unsigned char data[]; -+}; -+ -+// Returns nonzero on failure and sets errno. Closes both files. -+int gmni_ccert_load(struct gmni_client_certificate *cert, -+ FILE *certin, FILE *skin); -+ -+#endif -diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h -index 7e27b489d71fd3a43ca60292b17d56cab3caa5f8..22295e20fb36d492a979c060de8dcdca14df5a34 100644 ---- a/include/gmni/gmni.h -+++ b/include/gmni/gmni.h -@@ -1,7 +1,7 @@ - #ifndef GEMINI_CLIENT_H - #define GEMINI_CLIENT_H -+#include <bearssl.h> - #include <netdb.h> --#include <openssl/ssl.h> - #include <stdbool.h> - #include <sys/socket.h> - -@@ -52,20 +52,18 @@ struct gemini_response { - enum gemini_status status; - char *meta; - -+ // TODO: Make these private - // Response body may be read from here if appropriate: -- BIO *bio; -+ br_sslio_context body; - - // Connection state -- SSL_CTX *ssl_ctx; -- SSL *ssl; -+ br_ssl_client_context *sc; - int fd; - }; - --struct gemini_options { -- // If NULL, an SSL context will be created. If unset, the ssl field -- // must also be NULL. -- SSL_CTX *ssl_ctx; -+struct gmni_client_certificate; - -+struct gemini_options { - // If ai_family != AF_UNSPEC (the default value on most systems), the - // client will connect to this address and skip name resolution. - struct addrinfo *addr; -@@ -73,7 +71,13 @@ - // If non-NULL, these hints are provided to getaddrinfo. Useful, for - // example, to force IPv4/IPv6. - struct addrinfo *hints; -+ -+ // If non-NULL, this will be used as the client certificate for the -+ // request. The other fields must be set as well. -+ struct gmni_client_certificate *client_cert; - }; -+ -+struct gemini_tofu; - - // Requests the specified URL via the gemini protocol. If options is non-NULL, - // it may specify some additional configuration to adjust client behavior. -@@ -84,6 +88,7 @@ // Caller must call gemini_response_finish afterwards to clean up resources - // before exiting or re-using it for another request. - enum gemini_result gemini_request(const char *url, - struct gemini_options *options, -+ struct gemini_tofu *tofu, - struct gemini_response *resp); - - // Must be called after gemini_request in order to free up the resources -@@ -137,15 +142,20 @@ }; - }; - - struct gemini_parser { -- BIO *f; -+ int (*read)(void *state, void *buf, size_t nbyte); -+ void *state; - char *buf; - size_t bufsz; - size_t bufln; - bool preformatted; - }; - --// Initializes a text/gemini parser which reads from the specified BIO. --void gemini_parser_init(struct gemini_parser *p, BIO *f); -+// Initializes a text/gemini parser. The provided "read" function will be called -+// with the provided "state" value in order to obtain more gemtext data. The -+// read function should behave like read(3). -+void gemini_parser_init(struct gemini_parser *p, -+ int (*read)(void *state, void *buf, size_t nbyte), -+ void *state); - - // Finishes this text/gemini parser and frees up its resources. - void gemini_parser_finish(struct gemini_parser *p); -diff --git a/include/gmni/tofu.h b/include/gmni/tofu.h -index a88167ba0fb6606b2b170e5005c55131f1861972..51d1d60a3719469a1f8cd295b9d85e21d7affb43 100644 ---- a/include/gmni/tofu.h -+++ b/include/gmni/tofu.h -@@ -1,9 +1,7 @@ - #ifndef GEMINI_TOFU_H - #define GEMINI_TOFU_H -+#include <bearssl.h> - #include <limits.h> --#include <openssl/ssl.h> --#include <openssl/x509.h> --#include <time.h> - - enum tofu_error { - TOFU_VALID, -@@ -24,7 +22,6 @@ }; - - struct known_host { - char *host, *fingerprint; -- time_t expires; - int lineno; - struct known_host *next; - }; -@@ -34,7 +31,23 @@ // certificate. Return true to trust this certificate. - typedef enum tofu_action (tofu_callback_t)(enum tofu_error error, - const char *fingerprint, struct known_host *host, void *data); - -+struct gemini_tofu; -+ -+struct x509_tofu_context { -+ const br_x509_class *vtable; -+ br_x509_decoder_context decoder; -+ br_x509_pkey *pkey; -+ br_sha512_context sha512; -+ unsigned char hash[64]; -+ struct gemini_tofu *store; -+ const char *server_name; -+ int err; -+}; -+ - struct gemini_tofu { -+ struct x509_tofu_context x509_ctx; -+ br_ssl_client_context sc; -+ unsigned char iobuf[BR_SSL_BUFSIZE_BIDI]; - char known_hosts_path[PATH_MAX+1]; - struct known_host *known_hosts; - int lineno; -@@ -42,8 +55,7 @@ tofu_callback_t *callback; - void *cb_data; - }; - --void gemini_tofu_init(struct gemini_tofu *tofu, -- SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); -+void gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *data); - void gemini_tofu_finish(struct gemini_tofu *tofu); - - #endif -diff --git a/include/util.h b/include/util.h -index 6193fdf48c0ac6d313de7f35a1d09ecba62e3283..6c241f41a175c3d27390ecb6ca5b041bd637cb2b 100644 ---- a/include/util.h -+++ b/include/util.h -@@ -1,5 +1,7 @@ - #ifndef GEMINI_UTIL_H - #define GEMINI_UTIL_H -+#include <stdio.h> -+#include <sys/types.h> - - struct pathspec { - const char *var; -@@ -7,6 +9,7 @@ const char *path; - }; - - char *getpath(const struct pathspec *paths, size_t npaths); -+void posix_dirname(char *path, char *dname); - int mkdirs(char *path, mode_t mode); - int download_resp(FILE *out, struct gemini_response resp, const char *path, - char *url); -diff --git a/src/certs.c b/src/certs.c -new file mode 100644 -index 0000000000000000000000000000000000000000..f40bfa7d27925baaa2fe6dbdf0d6c18ce4e10014 ---- /dev/null -+++ b/src/certs.c -@@ -0,0 +1,156 @@ -+#include <assert.h> -+#include <bearssl.h> -+#include <errno.h> -+#include <gmni/certs.h> -+#include <gmni/gmni.h> -+#include <stdio.h> -+#include <stdlib.h> -+ -+static void -+crt_append(void *ctx, const void *src, size_t len) -+{ -+ br_x509_certificate *crt = (br_x509_certificate *)ctx; -+ crt->data = realloc(crt->data, crt->data_len + len); -+ assert(crt->data); -+ memcpy(&crt->data[crt->data_len], src, len); -+ crt->data_len += len; -+} -+ -+static void -+key_append(void *ctx, const void *src, size_t len) -+{ -+ br_skey_decoder_context *skctx = (br_skey_decoder_context *)ctx; -+ br_skey_decoder_push(skctx, src, len); -+} -+ -+int -+gmni_ccert_load(struct gmni_client_certificate *cert, FILE *certin, FILE *skin) -+{ -+ // TODO: Better error propagation to caller -+ static unsigned char buf[BUFSIZ]; -+ -+ br_pem_decoder_context pemdec; -+ br_pem_decoder_init(&pemdec); -+ -+ cert->chain = NULL; -+ cert->nchain = 0; -+ -+ static const char *certname = "CERTIFICATE"; -+ while (!feof(certin)) { -+ size_t n = fread(&buf, 1, sizeof(buf), certin); -+ if (ferror(certin)) { -+ goto error; -+ } -+ size_t q = 0; -+ while (q < n) { -+ q += br_pem_decoder_push(&pemdec, &buf[q], n - q); -+ switch (br_pem_decoder_event(&pemdec)) { -+ case BR_PEM_BEGIN_OBJ: -+ if (strcmp(br_pem_decoder_name(&pemdec), certname) != 0) { -+ break; -+ } -+ cert->chain = realloc(cert->chain, -+ sizeof(br_x509_certificate) * (cert->nchain + 1)); -+ memset(&cert->chain[cert->nchain], 0, sizeof(*cert->chain)); -+ br_pem_decoder_setdest(&pemdec, &crt_append, -+ &cert->chain[cert->nchain]); -+ ++cert->nchain; -+ break; -+ case BR_PEM_END_OBJ: -+ break; -+ case BR_PEM_ERROR: -+ fprintf(stderr, "Error decoding PEM certificate\n"); -+ errno = EINVAL; -+ goto error; -+ } -+ } -+ } -+ -+ if (cert->nchain == 0) { -+ fprintf(stderr, "No certificates found in provided client certificate file\n"); -+ errno = EINVAL; -+ goto error; -+ } -+ -+ br_skey_decoder_context skdec = {0}; -+ br_skey_decoder_init(&skdec); -+ br_pem_decoder_init(&pemdec); -+ -+ // TODO: Better validation of PEM file -+ while (!feof(skin)) { -+ size_t n = fread(&buf, 1, sizeof(buf), skin); -+ if (ferror(skin)) { -+ goto error; -+ } -+ size_t q = 0; -+ while (q < n) { -+ q += br_pem_decoder_push(&pemdec, &buf[q], n - q); -+ switch (br_pem_decoder_event(&pemdec)) { -+ case BR_PEM_BEGIN_OBJ: -+ br_pem_decoder_setdest(&pemdec, &key_append, &skdec); -+ break; -+ case BR_PEM_END_OBJ: -+ // no-op -+ break; -+ case BR_PEM_ERROR: -+ fprintf(stderr, "Error decoding PEM private key\n"); -+ errno = EINVAL; -+ goto error; -+ } -+ } -+ } -+ -+ int err = br_skey_decoder_last_error(&skdec); -+ if (err != 0) { -+ fprintf(stderr, "Error loading private key: %d\n", err); -+ errno = EINVAL; -+ goto error; -+ } -+ switch (br_skey_decoder_key_type(&skdec)) { -+ struct gmni_private_key *k; -+ const br_ec_private_key *ec; -+ const br_rsa_private_key *rsa; -+ case BR_KEYTYPE_RSA: -+ rsa = br_skey_decoder_get_rsa(&skdec); -+ cert->key = k = malloc(sizeof(*k) -+ + rsa->plen + rsa->qlen -+ + rsa->dplen + rsa->dqlen -+ + rsa->iqlen); -+ assert(k); -+ k->type = BR_KEYTYPE_RSA; -+ k->rsa = *rsa; -+ k->rsa.p = k->data; -+ k->rsa.q = k->rsa.p + k->rsa.plen; -+ k->rsa.dp = k->rsa.q + k->rsa.qlen; -+ k->rsa.dq = k->rsa.dp + k->rsa.dplen; -+ k->rsa.iq = k->rsa.dq + k->rsa.dqlen; -+ memcpy(k->rsa.p, rsa->p, rsa->plen); -+ memcpy(k->rsa.q, rsa->q, rsa->qlen); -+ memcpy(k->rsa.dp, rsa->dp, rsa->dplen); -+ memcpy(k->rsa.dq, rsa->dq, rsa->dqlen); -+ memcpy(k->rsa.iq, rsa->iq, rsa->iqlen); -+ break; -+ case BR_KEYTYPE_EC: -+ ec = br_skey_decoder_get_ec(&skdec); -+ cert->key = k = malloc(sizeof(*k) + ec->xlen); -+ assert(k); -+ k->type = BR_KEYTYPE_EC; -+ k->ec.curve = ec->curve; -+ k->ec.x = k->data; -+ k->ec.xlen = ec->xlen; -+ memcpy(k->ec.x, ec->x, ec->xlen); -+ break; -+ default: -+ assert(0); -+ } -+ -+ fclose(certin); -+ fclose(skin); -+ return 0; -+ -+error: -+ fclose(certin); -+ fclose(skin); -+ free(cert->chain); -+ return 1; -+} -diff --git a/src/client.c b/src/client.c -index a8a12f3cfbe1e98fd2045eec257cfaf95c319910..127a56ca59859e645fea6f1e4ac62431d734e4fd 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -1,15 +1,16 @@ - #include <assert.h> - #include <errno.h> - #include <netdb.h> --#include <openssl/bio.h> --#include <openssl/err.h> --#include <openssl/ssl.h> -+#include <bearssl.h> - #include <stdlib.h> -+#include <stdio.h> - #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> - #include <unistd.h> -+#include <gmni/certs.h> - #include <gmni/gmni.h> -+#include <gmni/tofu.h> - #include <gmni/url.h> - - static enum gemini_result -@@ -88,9 +89,41 @@ - #define GEMINI_META_MAXLEN 1024 - #define GEMINI_STATUS_MAXLEN 2 - -+static int -+sock_read(void *ctx, unsigned char *buf, size_t len) -+{ -+ for (;;) { -+ ssize_t rlen; -+ rlen = read(*(int *)ctx, buf, len); -+ if (rlen <= 0) { -+ if (rlen < 0 && errno == EINTR) { -+ continue; -+ } -+ return -1; -+ } -+ return (int)rlen; -+ } -+} -+ -+static int -+sock_write(void *ctx, const unsigned char *buf, size_t len) -+{ -+ for (;;) { -+ ssize_t wlen; -+ wlen = write(*(int *)ctx, buf, len); -+ if (wlen <= 0) { -+ if (wlen < 0 && errno == EINTR) { -+ continue; -+ } -+ return -1; -+ } -+ return (int)wlen; -+ } -+} -+ - enum gemini_result - gemini_request(const char *url, struct gemini_options *options, -- struct gemini_response *resp) -+ struct gemini_tofu *tofu, struct gemini_response *resp) - { - assert(url); - assert(resp); -@@ -128,84 +161,69 @@ free(host); - goto cleanup; - } - -- if (options && options->ssl_ctx) { -- resp->ssl_ctx = options->ssl_ctx; -- SSL_CTX_up_ref(options->ssl_ctx); -- } else { -- resp->ssl_ctx = SSL_CTX_new(TLS_method()); -- assert(resp->ssl_ctx); -- SSL_CTX_set_verify(resp->ssl_ctx, SSL_VERIFY_PEER, NULL); -- } -- - int r; -- BIO *sbio = BIO_new(BIO_f_ssl()); - res = gemini_connect(uri, options, resp, &resp->fd); - if (res != GEMINI_OK) { - free(host); - goto cleanup; - } - -- resp->ssl = SSL_new(resp->ssl_ctx); -- assert(resp->ssl); -- SSL_set_connect_state(resp->ssl); -- if ((r = SSL_set1_host(resp->ssl, host)) != 1) { -- free(host); -- goto ssl_error; -- } -- if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) { -- free(host); -- goto ssl_error; -- } -- free(host); -- if ((r = SSL_set_fd(resp->ssl, resp->fd)) != 1) { -- goto ssl_error; -- } -- if ((r = SSL_connect(resp->ssl)) != 1) { -- goto ssl_error; -- } -- -- X509 *cert = SSL_get_peer_certificate(resp->ssl); -- if (!cert) { -- resp->status = X509_V_ERR_UNSPECIFIED; -- res = GEMINI_ERR_SSL_VERIFY; -- goto cleanup; -- } -- X509_free(cert); -- -- long vr = SSL_get_verify_result(resp->ssl); -- if (vr != X509_V_OK) { -- resp->status = vr; -- res = GEMINI_ERR_SSL_VERIFY; -- goto cleanup; -+ // TODO: session reuse -+ resp->sc = &tofu->sc; -+ if (options->client_cert) { -+ struct gmni_client_certificate *cert = options->client_cert; -+ struct gmni_private_key *key = cert->key; -+ switch (key->type) { -+ case BR_KEYTYPE_RSA: -+ br_ssl_client_set_single_rsa(resp->sc, -+ cert->chain, cert->nchain, &key->rsa, -+ br_rsa_pkcs1_sign_get_default()); -+ break; -+ case BR_KEYTYPE_EC: -+ br_ssl_client_set_single_ec(resp->sc, -+ cert->chain, cert->nchain, &key->ec, -+ BR_KEYTYPE_SIGN, 0, -+ br_ec_get_default(), -+ br_ecdsa_sign_asn1_get_default()); -+ break; -+ } - } -+ br_ssl_client_reset(resp->sc, host, 0); - -- BIO_set_ssl(sbio, resp->ssl, 0); -- -- resp->bio = BIO_new(BIO_f_buffer()); -- BIO_push(resp->bio, sbio); -+ br_sslio_init(&resp->body, &resp->sc->eng, -+ sock_read, &resp->fd, sock_write, &resp->fd); - - char req[1024 + 3]; - r = snprintf(req, sizeof(req), "%s\r\n", url); - assert(r > 0); - -- r = BIO_puts(sbio, req); -- if (r == -1) { -- res = GEMINI_ERR_IO; -- goto cleanup; -- } -- assert(r == (int)strlen(req)); -+ br_sslio_write_all(&resp->body, req, r); -+ br_sslio_flush(&resp->body); - -+ // The SSL engine maintains an internal buffer, so this shouldn't be as -+ // inefficient as it looks. It's necessary to do this one byte at a time -+ // to avoid consuming any of the response body buffer. - char buf[GEMINI_META_MAXLEN - + GEMINI_STATUS_MAXLEN - + 2 /* CRLF */ + 1 /* NUL */]; -- r = BIO_gets(resp->bio, buf, sizeof(buf)); -- if (r == -1) { -- res = GEMINI_ERR_IO; -- goto cleanup; -+ memset(buf, 0, sizeof(buf)); -+ size_t l; -+ for (l = 0; l < 2 || memcmp(&buf[l-2], "\r\n", 2) != 0; ++l) { -+ r = br_sslio_read(&resp->body, &buf[l], 1); -+ if (r < 0) { -+ break; -+ } - } - -- if (r < 3 || strcmp(&buf[r - 2], "\r\n") != 0) { -- fprintf(stderr, "invalid line %d '%s'\n", r, buf); -+ int err = br_ssl_engine_last_error(&resp->sc->eng); -+ if (err != 0) { -+ // TODO: Bubble this up properly -+ fprintf(stderr, "SSL error %d\n", err); -+ goto ssl_error; -+ } -+ -+ if (l < 3 || strcmp(&buf[l-2], "\r\n") != 0) { -+ fprintf(stderr, "invalid line '%s'\n", buf); - res = GEMINI_ERR_PROTOCOL; - goto cleanup; - } -@@ -217,9 +235,9 @@ fprintf(stderr, "invalid status\n"); - res = GEMINI_ERR_PROTOCOL; - goto cleanup; - } -- resp->meta = calloc(r - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); -- strncpy(resp->meta, &endptr[1], r - 5); -- resp->meta[r - 5] = '\0'; -+ resp->meta = calloc(l - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); -+ strncpy(resp->meta, &endptr[1], l - 5); -+ resp->meta[l - 5] = '\0'; - - cleanup: - curl_url_cleanup(uri); -@@ -237,26 +255,18 @@ if (!resp) { - return; - } - -- if (resp->bio) { -- BIO_free_all(resp->bio); -- resp->bio = NULL; -+ if (resp->fd != -1) { -+ close(resp->fd); -+ resp->fd = -1; - } - -- if (resp->ssl) { -- SSL_free(resp->ssl); -- } -- if (resp->ssl_ctx) { -- SSL_CTX_free(resp->ssl_ctx); -- } - free(resp->meta); - -- if (resp->fd != -1) { -- close(resp->fd); -- resp->fd = -1; -+ if (resp->sc) { -+ br_sslio_close(&resp->body); - } - -- resp->ssl = NULL; -- resp->ssl_ctx = NULL; -+ resp->sc = NULL; - resp->meta = NULL; - } - -@@ -277,11 +287,11 @@ return gai_strerror(resp->status); - case GEMINI_ERR_CONNECT: - return strerror(errno); - case GEMINI_ERR_SSL: -- return ERR_error_string( -- SSL_get_error(resp->ssl, resp->status), -- NULL); -+ // TODO: more specific -+ return "SSL error"; - case GEMINI_ERR_SSL_VERIFY: -- return X509_verify_cert_error_string(resp->status); -+ // TODO: more specific -+ return "X.509 certificate not trusted"; - case GEMINI_ERR_IO: - return "I/O error"; - case GEMINI_ERR_PROTOCOL: -diff --git a/src/gmni.c b/src/gmni.c -index 49abb8524a012c27249006dc45f4688a846e63c3..f3015ac679eba77397fb4aac5068f3a63e1cdbab 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -1,9 +1,8 @@ - #include <assert.h> -+#include <bearssl.h> - #include <errno.h> - #include <getopt.h> - #include <netdb.h> --#include <openssl/bio.h> --#include <openssl/err.h> - #include <stdbool.h> - #include <stdio.h> - #include <stdlib.h> -@@ -12,6 +11,7 @@ #include <sys/socket.h> - #include <sys/types.h> - #include <termios.h> - #include <unistd.h> -+#include <gmni/certs.h> - #include <gmni/gmni.h> - #include <gmni/tofu.h> - #include <gmni/url.h> -@@ -110,6 +110,45 @@ - return action; - } - -+static struct gmni_client_certificate * -+load_client_cert(char *argv_0, char *path) -+{ -+ char *certpath = strtok(path, ":"); -+ if (!certpath) { -+ usage(argv_0); -+ exit(1); -+ } -+ -+ FILE *certf = fopen(certpath, "r"); -+ if (!certf) { -+ fprintf(stderr, "Failed to open certificate: %s\n", -+ strerror(errno)); -+ exit(1); -+ } -+ -+ char *keypath = strtok(NULL, ":"); -+ if (!keypath) { -+ usage(argv_0); -+ exit(1); -+ } -+ -+ FILE *keyf = fopen(keypath, "r"); -+ if (!keyf) { -+ fprintf(stderr, "Failed to open certificate: %s\n", -+ strerror(errno)); -+ exit(1); -+ } -+ -+ struct gmni_client_certificate *cert = -+ calloc(1, sizeof(struct gmni_client_certificate)); -+ if (gmni_ccert_load(cert, certf, keyf) != 0) { -+ fprintf(stderr, "Failed to load client certificate: %s\n", -+ strerror(errno)); -+ exit(1); -+ } -+ return cert; -+} -+ - int - main(int argc, char *argv[]) - { -@@ -166,7 +205,7 @@ } - } - break; - case 'E': -- assert(0); // TODO: Client certificates -+ opts.client_cert = load_client_cert(argv[0], optarg); - break; - case 'h': - usage(argv[0]); -@@ -222,15 +261,12 @@ usage(argv[0]); - return 1; - } - -- SSL_load_error_strings(); -- ERR_load_crypto_strings(); -- opts.ssl_ctx = SSL_CTX_new(TLS_method()); -- gemini_tofu_init(&cfg.tofu, opts.ssl_ctx, &tofu_callback, &cfg); -+ gemini_tofu_init(&cfg.tofu, &tofu_callback, &cfg); - - bool exit = false; - struct Curl_URL *url = curl_url(); - -- if(curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { -+ if (curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { - // TODO: Better error - fprintf(stderr, "Error: invalid URL\n"); - return 1; -@@ -242,7 +278,8 @@ char *buf; - curl_url_get(url, CURLUPART_URL, &buf, 0); - - struct gemini_response resp; -- enum gemini_result r = gemini_request(buf, &opts, &resp); -+ enum gemini_result r = gemini_request(buf, -+ &opts, &cfg.tofu, &resp); - - free(buf); - -@@ -340,11 +377,8 @@ - char last = 0; - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { -- n = BIO_read(resp.bio, buf, BUFSIZ); -- if (n == -1) { -- fprintf(stderr, "Error: read\n"); -- return 1; -- } else if (n != 0) { -+ n = br_sslio_read(&resp.body, buf, BUFSIZ); -+ if (n > 0) { - last = buf[n - 1]; - } - ssize_t w = 0; -@@ -370,7 +404,6 @@ next: - gemini_response_finish(&resp); - } - -- SSL_CTX_free(opts.ssl_ctx); - curl_url_cleanup(url); - gemini_tofu_finish(&cfg.tofu); - return ret; -diff --git a/src/gmnlm.c b/src/gmnlm.c -index e28bf70afc5d6fd4c5c6eff85d85198d62a127e8..ef4c97950495e369e4fd3b1256c185fdb01f8cf1 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1,22 +1,26 @@ - #include <assert.h> -+#include <bearssl.h> - #include <ctype.h> -+#include <errno.h> -+#include <fcntl.h> - #include <getopt.h> -+#include <gmni/certs.h> -+#include <gmni/gmni.h> -+#include <gmni/tofu.h> -+#include <gmni/url.h> - #include <libgen.h> - #include <limits.h> --#include <openssl/bio.h> --#include <openssl/err.h> - #include <regex.h> - #include <stdbool.h> - #include <stdio.h> -+#include <stdlib.h> - #include <string.h> - #include <sys/ioctl.h> - #include <sys/stat.h> -+#include <sys/types.h> - #include <sys/wait.h> - #include <termios.h> - #include <unistd.h> --#include <gmni/gmni.h> --#include <gmni/tofu.h> --#include <gmni/url.h> - #include "util.h" - - #define ANSI_COLOR_RED "\x1b[91m" -@@ -400,10 +404,13 @@ close(pfd[0]); - FILE *f = fdopen(pfd[1], "w"); - // XXX: may affect history, do we care? - for (int n = 1; n > 0;) { -- n = BIO_read(resp.bio, buf, BUFSIZ); -- if (n == -1) { -- fprintf(stderr, "Error: read\n"); -- return; -+ if (resp.sc) { -+ n = br_sslio_read(&resp.body, buf, BUFSIZ); -+ } else { -+ n = read(resp.fd, buf, BUFSIZ); -+ } -+ if (n < 0) { -+ n = 0; - } - ssize_t w = 0; - while (w < (ssize_t)n) { -@@ -430,13 +437,50 @@ { - int nredir = 0; - bool requesting = true; - enum gemini_result res; -- while (requesting) { -- char *scheme; -+ -+ char *scheme; -+ CURLUcode uc = curl_url_get(browser->url, -+ CURLUPART_SCHEME, &scheme, 0); -+ assert(uc == CURLUE_OK); // Invariant -+ -+ char *host = NULL; -+ struct gmni_client_certificate client_cert = {0}; -+ const struct pathspec paths[] = { -+ {.var = "GMNIDATA", .path = "/certs/%s.%s"}, -+ {.var = "XDG_DATA_HOME", .path = "/gmni/certs/%s.%s"}, -+ {.var = "HOME", .path = "/.local/share/gmni/certs/%s.%s"} -+ }; -+ char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); -+ char certpath[PATH_MAX+1], keypath[PATH_MAX+1]; -+ size_t n = 0; -+ -+ if (strcmp(scheme, "gemini") == 0) { - CURLUcode uc = curl_url_get(browser->url, -- CURLUPART_SCHEME, &scheme, 0); -- assert(uc == CURLUE_OK); // Invariant -+ CURLUPART_HOST, &host, 0); -+ assert(uc == CURLUE_OK); -+ -+ n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); -+ assert(n < sizeof(certpath)); -+ FILE *certin = fopen(certpath, "r"); -+ if (certin) { -+ n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); -+ assert(n < sizeof(keypath)); -+ -+ FILE *skin = fopen(keypath, "r"); -+ if (gmni_ccert_load(&client_cert, certin, skin)) { -+ browser->opts.client_cert = NULL; -+ fprintf(stderr, "Unable to load client certificate for host %s", host); -+ } else { -+ browser->opts.client_cert = &client_cert; -+ } -+ } else { -+ browser->opts.client_cert = NULL; -+ } -+ free(host); -+ } -+ -+ while (requesting) { - if (strcmp(scheme, "file") == 0) { -- free(scheme); - requesting = false; - - char *path; -@@ -447,23 +491,19 @@ resp->status = GEMINI_STATUS_BAD_REQUEST; - break; - } - -- FILE *fp = fopen(path, "r"); -- if (!fp) { -+ int fd = open(path, O_RDONLY); -+ if (fd < 0) { - resp->status = GEMINI_STATUS_NOT_FOUND; -- /* Make sure members of resp evaluate to false, so that -- gemini_response_finish does not try to free them. */ -- resp->bio = NULL; -- resp->ssl = NULL; -- resp->ssl_ctx = NULL; -+ // Make sure members of resp evaluate to false, -+ // so that gemini_response_finish does not try -+ // to free them. -+ resp->sc = NULL; - resp->meta = NULL; - resp->fd = -1; - free(path); - break; - } - -- BIO *file = BIO_new_fp(fp, BIO_CLOSE); -- resp->bio = BIO_new(BIO_f_buffer()); -- BIO_push(resp->bio, file); - if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) { - resp->meta = strdup("text/gemini"); - } else if (has_suffix(path, ".txt")) { -@@ -473,14 +513,14 @@ resp->meta = strdup("application/x-octet-stream"); - } - free(path); - resp->status = GEMINI_STATUS_SUCCESS; -- resp->fd = -1; -- resp->ssl = NULL; -- resp->ssl_ctx = NULL; -- return GEMINI_OK; -+ resp->fd = fd; -+ resp->sc = NULL; -+ res = GEMINI_OK; -+ goto out; - } -- free(scheme); - -- res = gemini_request(browser->plain_url, &browser->opts, resp); -+ res = gemini_request(browser->plain_url, &browser->opts, -+ &browser->tofu, resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); - requesting = false; -@@ -519,7 +559,26 @@ } - set_url(browser, resp->meta, NULL); - break; - case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -- assert(0); // TODO -+ requesting = false; -+ assert(host); -+ n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); -+ assert(n < sizeof(certpath)); -+ n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); -+ char dname[PATH_MAX + 1]; -+ posix_dirname(certpath, dname); -+ if (mkdirs(dname, 0755) != 0) { -+ fprintf(stderr, "Error creating directory %s: %s\n", -+ dname, strerror(errno)); -+ break; -+ } -+ assert(n < sizeof(keypath)); -+ fprintf(stderr, "The server requested a client certificate.\n" -+ "Presently, this process is not automated.\n" -+ "The following OpenSSL command will generate a certificate for this host:\n\n" -+ "openssl req -x509 -newkey rsa:4096 \\\n\t-keyout %s \\\n\t-out %s \\\n\t-days 36500 -nodes\n\n" -+ "Use the 'r' command to reload the page after creating this certificate.\n", -+ keypath, certpath); -+ break; - case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: - case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: - requesting = false; -@@ -529,7 +588,7 @@ "TEMPORARY FAILURE" : "PERMANENT FAILURE", - resp->status, resp->meta); - break; - case GEMINI_STATUS_CLASS_SUCCESS: -- return res; -+ goto out; - } - - if (requesting) { -@@ -537,6 +596,11 @@ gemini_response_finish(resp); - } - } - -+out: -+ if (client_cert.key) { -+ free(client_cert.key); -+ } -+ free(scheme); - return res; - } - -@@ -842,12 +906,23 @@ } - return fprintf(f, "%s\n", s) - 1; - } - -+static int -+resp_read(void *state, void *buf, size_t nbyte) -+{ -+ struct gemini_response *resp = state; -+ if (resp->sc) { -+ return br_sslio_read(&resp->body, buf, nbyte); -+ } else { -+ return read(resp->fd, buf, nbyte); -+ } -+} -+ - static bool - display_gemini(struct browser *browser, struct gemini_response *resp) - { - int nlinks = 0; - struct gemini_parser p; -- gemini_parser_init(&p, resp->bio); -+ gemini_parser_init(&p, &resp_read, resp); - free(browser->page_title); - browser->page_title = NULL; - -@@ -1036,10 +1111,13 @@ ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { -- n = BIO_read(resp->bio, buf, BUFSIZ); -- if (n == -1) { -- fprintf(stderr, "Error: read\n"); -- return 1; -+ if (resp->sc) { -+ n = br_sslio_read(&resp->body, buf, BUFSIZ); -+ } else { -+ n = read(resp->fd, buf, BUFSIZ); -+ } -+ if (n < 0) { -+ n = 0; - } - ssize_t w = 0; - while (w < (ssize_t)n) { -@@ -1214,11 +1292,7 @@ } else { - open_bookmarks(&browser); - } - -- SSL_load_error_strings(); -- ERR_load_crypto_strings(); -- browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); -- gemini_tofu_init(&browser.tofu, browser.opts.ssl_ctx, -- &tofu_callback, &browser); -+ gemini_tofu_init(&browser.tofu, &tofu_callback, &browser); - - struct gemini_response resp; - browser.running = true; -@@ -1280,7 +1354,6 @@ while (hist && hist->prev) { - hist = hist->prev; - } - history_free(hist); -- SSL_CTX_free(browser.opts.ssl_ctx); - curl_url_cleanup(browser.url); - free(browser.page_title); - free(browser.plain_url); -diff --git a/src/parser.c b/src/parser.c -index ad2c0e6306872f8dde450063ba8815e2ee7e314a..6f12456ddbe5248b59ea146464a1d76365505c56 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -1,21 +1,23 @@ - #include <assert.h> - #include <ctype.h> --#include <openssl/bio.h> - #include <stdbool.h> - #include <stddef.h> -+#include <stdio.h> - #include <stdlib.h> - #include <string.h> - #include <gmni/gmni.h> - - void --gemini_parser_init(struct gemini_parser *p, BIO *f) -+gemini_parser_init(struct gemini_parser *p, -+ int (*read)(void *state, void *buf, size_t nbyte), -+ void *state) - { -- p->f = f; -+ p->read = read; -+ p->state = state; - p->bufln = 0; - p->bufsz = BUFSIZ; - p->buf = malloc(p->bufsz + 1); - p->buf[0] = 0; -- BIO_up_ref(p->f); - p->preformatted = false; - } - -@@ -25,7 +27,6 @@ { - if (!p) { - return; - } -- BIO_free(p->f); - free(p->buf); - } - -@@ -42,7 +43,7 @@ p->buf = realloc(p->buf, p->bufsz); - assert(p->buf); - } - -- ssize_t n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln - 1); -+ int n = p->read(p->state, &p->buf[p->bufln], p->bufsz - p->bufln - 1); - if (n == -1) { - return -1; - } else if (n == 0) { -diff --git a/src/tofu.c b/src/tofu.c -index 48395c08cdbf68b31c5defd15b360f1eb2897a3f..54183a79278de45bfbe5a1f8ddbcbf10be1c01c4 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -1,95 +1,92 @@ - #include <assert.h> -+#include <bearssl.h> - #include <errno.h> -+#include <gmni/gmni.h> -+#include <gmni/tofu.h> - #include <libgen.h> - #include <limits.h> --#include <openssl/asn1.h> --#include <openssl/evp.h> --#include <openssl/ssl.h> --#include <openssl/x509.h> --#include <openssl/x509v3.h> -+#include <stdint.h> - #include <stdio.h> -+#include <stdlib.h> - #include <string.h> --#include <time.h> --#include <gmni/gmni.h> --#include <gmni/tofu.h> - #include "util.h" - --static int --verify_callback(X509_STORE_CTX *ctx, void *data) -+static void -+xt_start_chain(const br_x509_class **ctx, const char *server_name) - { -- // Gemini clients handle TLS verification differently from the rest of -- // the internet. We use a TOFU system, so trust is based on two factors: -- // -- // - Is the certificate valid at the time of the request? -- // - Has the user trusted this certificate yet? -- // -- // If the answer to the latter is "no", then we give the user an -- // opportunity to explicitly agree to trust the certificate before -- // rejecting it. -- // -- // If you're reading this code with the intent to re-use it for -- // something unrelated to Gemini, think twice. -- struct gemini_tofu *tofu = (struct gemini_tofu *)data; -- X509 *cert = X509_STORE_CTX_get0_cert(ctx); -- struct known_host *host = NULL; -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ cc->server_name = server_name; -+ cc->err = 0; -+ cc->pkey = NULL; -+} - -- int rc; -- int day, sec; -- const ASN1_TIME *notBefore = X509_get0_notBefore(cert); -- const ASN1_TIME *notAfter = X509_get0_notAfter(cert); -- if (!ASN1_TIME_diff(&day, &sec, NULL, notBefore)) { -- rc = X509_V_ERR_UNSPECIFIED; -- goto invalid_cert; -+static void -+xt_start_cert(const br_x509_class **ctx, uint32_t length) -+{ -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ if (cc->err != 0 || cc->pkey) { -+ return; - } -- if (day > 0 || sec > 0) { -- rc = X509_V_ERR_CERT_NOT_YET_VALID; -- goto invalid_cert; -+ if (length == 0) { -+ cc->err = BR_ERR_X509_TRUNCATED; -+ return; - } -- if (!ASN1_TIME_diff(&day, &sec, NULL, notAfter)) { -- rc = X509_V_ERR_UNSPECIFIED; -- goto invalid_cert; -+ br_x509_decoder_init(&cc->decoder, NULL, NULL); -+ br_sha512_init(&cc->sha512); -+} -+ -+static void -+xt_append(const br_x509_class **ctx, const unsigned char *buf, size_t len) -+{ -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ if (cc->err != 0 || cc->pkey) { -+ return; - } -- if (day < 0 || sec < 0) { -- rc = X509_V_ERR_CERT_HAS_EXPIRED; -- goto invalid_cert; -+ br_x509_decoder_push(&cc->decoder, buf, len); -+ int err = br_x509_decoder_last_error(&cc->decoder); -+ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { -+ cc->err = err; - } -+ br_sha512_update(&cc->sha512, buf, len); -+} - -- unsigned char md[512 / 8]; -- const EVP_MD *sha512 = EVP_sha512(); -- unsigned int len = sizeof(md); -- rc = X509_digest(cert, sha512, md, &len); -- assert(rc == 1); -- -- char fingerprint[512 / 8 * 3]; -- for (size_t i = 0; i < sizeof(md); ++i) { -- snprintf(&fingerprint[i * 3], 4, "%02X%s", -- md[i], i + 1 == sizeof(md) ? "" : ":"); -+static void -+xt_end_cert(const br_x509_class **ctx) -+{ -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ if (cc->err != 0) { -+ return; -+ } -+ int err = br_x509_decoder_last_error(&cc->decoder); -+ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { -+ cc->err = err; -+ return; - } -+ cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); -+ br_sha512_out(&cc->sha512, &cc->hash); -+} - -- SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, -- SSL_get_ex_data_X509_STORE_CTX_idx()); -- const char *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); -- if (!servername) { -- rc = X509_V_ERR_HOSTNAME_MISMATCH; -- goto invalid_cert; -+static unsigned -+xt_end_chain(const br_x509_class **ctx) -+{ -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ if (cc->err != 0) { -+ return (unsigned)cc->err; -+ } -+ if (!cc->pkey) { -+ return BR_ERR_X509_EMPTY_CHAIN; - } - -- rc = X509_check_host(cert, servername, strlen(servername), 0, NULL); -- if (rc != 1) { -- rc = X509_V_ERR_HOSTNAME_MISMATCH; -- goto invalid_cert; -+ char fingerprint[512 / 8 * 3]; -+ for (size_t i = 0; i < sizeof(cc->hash); ++i) { -+ snprintf(&fingerprint[i * 3], 4, "%02X%s", -+ cc->hash[i], i + 1 == sizeof(cc->hash) ? "" : ":"); - } - -- time_t now; -- time(&now); -- - enum tofu_error error = TOFU_UNTRUSTED_CERT; -- host = tofu->known_hosts; -+ struct known_host *host = cc->store->known_hosts; - while (host) { -- if (host->expires < now) { -- goto next; -- } -- if (strcmp(host->host, servername) != 0) { -+ if (strcmp(host->host, cc->server_name) != 0) { - goto next; - } - if (strcmp(host->fingerprint, fingerprint) == 0) { -@@ -102,66 +99,84 @@ next: - host = host->next; - } - -- rc = X509_V_ERR_CERT_UNTRUSTED; -- --callback: -- switch (tofu->callback(error, fingerprint, host, tofu->cb_data)) { -+ switch (cc->store->callback(error, fingerprint, -+ host, cc->store->cb_data)) { - case TOFU_ASK: - assert(0); // Invariant - case TOFU_FAIL: -- X509_STORE_CTX_set_error(ctx, rc); -- break; -+ return BR_ERR_X509_NOT_TRUSTED; - case TOFU_TRUST_ONCE: - // No further action necessary - return 0; - case TOFU_TRUST_ALWAYS:; -- FILE *f = fopen(tofu->known_hosts_path, "a"); -+ FILE *f = fopen(cc->store->known_hosts_path, "a"); - if (!f) { - fprintf(stderr, "Error opening %s for writing: %s\n", -- tofu->known_hosts_path, strerror(errno)); -+ cc->store->known_hosts_path, strerror(errno)); - break; - }; -- struct tm expires_tm; -- ASN1_TIME_to_tm(notAfter, &expires_tm); -- time_t expires = mktime(&expires_tm); -- fprintf(f, "%s %s %s %jd\n", servername, -- "SHA-512", fingerprint, (intmax_t)expires); -+ fprintf(f, "%s %s %s\n", cc->server_name, -+ "SHA-512", fingerprint); - fclose(f); - - host = calloc(1, sizeof(struct known_host)); -- host->host = strdup(servername); -+ host->host = strdup(cc->server_name); - host->fingerprint = strdup(fingerprint); -- host->expires = expires; -- host->lineno = ++tofu->lineno; -- host->next = tofu->known_hosts; -- tofu->known_hosts = host; -+ host->lineno = ++cc->store->lineno; -+ host->next = cc->store->known_hosts; -+ cc->store->known_hosts = host; - return 0; - } - -- X509_STORE_CTX_set_error(ctx, rc); -- return 0; -+ assert(0); // Unreachable -+} - --invalid_cert: -- error = TOFU_INVALID_CERT; -- goto callback; -+static const br_x509_pkey * -+xt_get_pkey(const br_x509_class *const *ctx, unsigned *usages) -+{ -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ if (cc->err != 0) { -+ return NULL; -+ } -+ if (usages) { -+ // XXX: BearSSL doesn't pull the usages out of the X.509 for us -+ *usages = BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN; -+ } -+ return cc->pkey; - } - -+const br_x509_class xt_vtable = { -+ sizeof(struct x509_tofu_context), -+ xt_start_chain, -+ xt_start_cert, -+ xt_append, -+ xt_end_cert, -+ xt_end_chain, -+ xt_get_pkey, -+}; -+ -+static void -+x509_init_tofu(struct x509_tofu_context *ctx, struct gemini_tofu *store) -+{ -+ ctx->vtable = &xt_vtable; -+ ctx->store = store; -+} - - void --gemini_tofu_init(struct gemini_tofu *tofu, -- SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data) -+gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *cb_data) - { - const struct pathspec paths[] = { - {.var = "GMNIDATA", .path = "/%s"}, -- {.var = "XDG_DATA_HOME", .path = "/gemini/%s"}, -- {.var = "HOME", .path = "/.local/share/gemini/%s"} -+ {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, -+ {.var = "HOME", .path = "/.local/share/gmni/%s"} - }; - char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); - char dname[PATH_MAX+1]; - size_t n = 0; - -- n = snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -- path_fmt, "known_hosts"); -+ n = snprintf(tofu->known_hosts_path, -+ sizeof(tofu->known_hosts_path), -+ path_fmt, "known_hosts"); - assert(n < sizeof(tofu->known_hosts_path)); - - strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1); -@@ -179,10 +194,17 @@ free(path_fmt); - - tofu->callback = cb; - tofu->cb_data = cb_data; -- SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu); - - tofu->known_hosts = NULL; - -+ x509_init_tofu(&tofu->x509_ctx, tofu); -+ -+ br_x509_minimal_context _; // Discarded -+ br_ssl_client_init_full(&tofu->sc, &_, NULL, 0); -+ br_ssl_engine_set_x509(&tofu->sc.eng, &tofu->x509_ctx.vtable); -+ br_ssl_engine_set_buffer(&tofu->sc.eng, -+ &tofu->iobuf, sizeof(tofu->iobuf), 1); -+ - FILE *f = fopen(tofu->known_hosts_path, "r"); - if (!f) { - return; -@@ -191,6 +213,11 @@ n = 0; - int lineno = 1; - char *line = NULL; - while (getline(&line, &n, f) != -1) { -+ int ln = strlen(line); -+ if (line[ln-1] == '\n') { -+ line[ln-1] = 0; -+ } -+ - struct known_host *host = calloc(1, sizeof(struct known_host)); - char *tok = strtok(line, " "); - assert(tok); -@@ -207,10 +234,6 @@ - tok = strtok(NULL, " "); - assert(tok); - host->fingerprint = strdup(tok); -- -- tok = strtok(NULL, " "); -- assert(tok); -- host->expires = strtoul(tok, NULL, 10); - - host->lineno = lineno++; - -diff --git a/src/util.c b/src/util.c -index 2f62c29ce40ac012993e1d208d65491b56d204aa..8441b584ff199ffd81472539a59acce5d0a18f42 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -1,5 +1,7 @@ - #include <assert.h> -+#include <bearssl.h> - #include <errno.h> -+#include <gmni/gmni.h> - #include <libgen.h> - #include <limits.h> - #include <stdint.h> -@@ -7,10 +9,9 @@ #include <stdio.h> - #include <stdlib.h> - #include <string.h> - #include <sys/stat.h> --#include <gmni/gmni.h> - #include "util.h" - --static void -+void - posix_dirname(char *path, char *dname) - { - char p[PATH_MAX+1]; -@@ -82,7 +83,7 @@ } - fprintf(out, "Downloading %s to %s\n", url, path); - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { -- n = BIO_read(resp.bio, buf, sizeof(buf)); -+ n = br_sslio_read(&resp.body, buf, sizeof(buf)); - if (n == -1) { - fprintf(stderr, "Error: read\n"); - return 1; diff --git a/sources/cgmnlm.git/commits/dcc0484a8c238acdbe988a898cf2deeac4f34ae5.patch b/sources/cgmnlm.git/commits/dcc0484a8c238acdbe988a898cf2deeac4f34ae5.patch @@ -1,20 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index 6f12a4e0dfafa563de2836b13da910ae2fd65895..de3746563ed3e0c515a5c52a053f3a535fda5e09 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -53,13 +53,13 @@ rc = X509_V_ERR_CERT_HAS_EXPIRED; - goto invalid_cert; - } - -- unsigned char md[256 / 8]; -+ unsigned char md[512 / 8]; - const EVP_MD *sha512 = EVP_sha512(); - unsigned int len = sizeof(md); - rc = X509_digest(cert, sha512, md, &len); - assert(rc == 1); - -- char fingerprint[256 / 8 * 3]; -+ char fingerprint[512 / 8 * 3]; - for (size_t i = 0; i < sizeof(md); ++i) { - snprintf(&fingerprint[i * 3], 4, "%02X%s", - md[i], i + 1 == sizeof(md) ? "" : ":"); diff --git a/sources/cgmnlm.git/commits/dcc2b34d434d8e0f695e43a1f775759846e60417.patch b/sources/cgmnlm.git/commits/dcc2b34d434d8e0f695e43a1f775759846e60417.patch @@ -1,102 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index ce58ea1c707078abfdc2e39fb310b6f7f55f5585..273423b8261a9608c89505b4e154899806f994f4 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -217,49 +217,65 @@ struct gemini_response resp; - while (run) { - struct link *links; - static char prompt[4096]; -- - char *plain_url; -- CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); -- assert(uc == CURLUE_OK); // Invariant - -- snprintf(prompt, sizeof(prompt), "\nat %s\n" -- "[n]: follow Nth link; [o <url>]: open URL; " -- "[b]ack; [f]orward; " -- "[q]uit\n" -- "=> ", plain_url); -+ int nredir = 0; -+ bool requesting = true; -+ while (requesting) { -+ CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); -+ assert(uc == CURLUE_OK); // Invariant - -- enum gemini_result res = gemini_request(plain_url, &opts, &resp); -- if (res != GEMINI_OK) { -- fprintf(stderr, "Error: %s\n", gemini_strerr(res, &resp)); -- assert(0); // TODO: Prompt -+ enum gemini_result res = gemini_request( -+ plain_url, &opts, &resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", -+ gemini_strerr(res, &resp)); -+ requesting = false; -+ break; -+ } -+ -+ switch (gemini_response_class(resp.status)) { -+ case GEMINI_STATUS_CLASS_INPUT: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_REDIRECT: -+ if (++nredir >= 5) { -+ requesting = false; -+ fprintf(stderr, "Error: maximum redirects (5) exceeded"); -+ break; -+ } -+ fprintf(stderr, -+ "Following redirect to %s\n", resp.meta); -+ set_url(url, resp.meta, NULL); -+ break; -+ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -+ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -+ requesting = false; -+ fprintf(stderr, "Server returned %s %d %s\n", -+ resp.status / 10 == 4 ? -+ "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ resp.status, resp.meta); -+ break; -+ case GEMINI_STATUS_CLASS_SUCCESS: -+ requesting = false; -+ display_response(tty, &resp, &links, pagination); -+ break; -+ } -+ -+ if (requesting) { -+ gemini_response_finish(&resp); -+ } - } - -- snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -+ snprintf(prompt, sizeof(prompt), "\n%s%s at %s\n" - "[n]: follow Nth link; [o <url>]: open URL; " - "[b]ack; [f]orward; " - "[q]uit\n" - "=> ", -+ resp.status == GEMINI_STATUS_SUCCESS ? " " : "", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", - plain_url); -- -- switch (gemini_response_class(resp.status)) { -- case GEMINI_STATUS_CLASS_INPUT: -- assert(0); // TODO -- case GEMINI_STATUS_CLASS_REDIRECT: -- assert(0); // TODO -- case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -- assert(0); // TODO -- case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -- case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -- fprintf(stderr, "Server returned %s %d %s\n", -- resp.status / 10 == 4 ? -- "TEMPORARY FAILURE" : "PERMANENT FALIURE", -- resp.status, resp.meta); -- break; -- case GEMINI_STATUS_CLASS_SUCCESS: -- display_response(tty, &resp, &links, pagination); -- break; -- } - - gemini_response_finish(&resp); - diff --git a/sources/cgmnlm.git/commits/dde8799e758b1e6c3985492a8205c189d4d47b9c.patch b/sources/cgmnlm.git/commits/dde8799e758b1e6c3985492a8205c189d4d47b9c.patch @@ -1,121 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 66db54c7e0cf7dd6a1f58872621cc28e69944134..a0bc81f851a69ec34c7e9baf643c10728632c7e2 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -88,6 +88,7 @@ "b\t\tBack (in the page history)\n" - "f\t\tForward (in the page history)\n" - "H\t\tView all page history\n" - "a\t\tSave bookmark\n" -+ "s\tRemove bookmark for current URL\n" - "B\t\tBrowse bookmarks\n" - "r\t\tReload the page\n" - "/<text>\t\tsearch for text (POSIX regular expression)\n" -@@ -169,18 +170,16 @@ static char dname[PATH_MAX+1]; - size_t n; - - n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); - assert(n < sizeof(path)); -- strncpy(dname, dirname(path), sizeof(dname)-1); -+ strncpy(dname, path, sizeof(dname)-1); -+ dirname(dname); - if (mkdirs(dname, 0755) != 0) { -- snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -- free(path_fmt); - fprintf(stderr, "Error creating directory %s: %s\n", -- dirname(path), strerror(errno)); -+ dname, strerror(errno)); - return; - } - -- snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -- free(path_fmt); - FILE *f = fopen(path, "a"); - if (!f) { - fprintf(stderr, "Error opening %s for writing: %s\n", -@@ -202,6 +201,49 @@ title ? title : browser->plain_url); - } - - static void -+remove_bookmark(struct browser *browser) -+{ -+ char *path_fmt = get_data_pathfmt(); -+ static char path[PATH_MAX+1]; -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); -+ -+ static char tempfile[PATH_MAX+2]; -+ snprintf(tempfile, sizeof(tempfile), "%s2", path); -+ FILE *fi = fopen(path, "r"); -+ FILE *fo = fopen(tempfile, "w"); -+ if(fi == NULL) { -+ fprintf(stderr, "Bookmark file not available!\n"); -+ return; -+ } -+ if(fo == NULL) { -+ fprintf(stderr, "tempfile not available!\n"); -+ return; -+ } -+ -+ char *line = NULL; -+ size_t len = 0; -+ size_t n = 0; -+ -+ static char url[1024]; -+ n = snprintf(url, sizeof(url), "=> %s ", browser->plain_url); -+ while(getline(&line, &len, fi) != -1) { -+ if (strncmp(line, url, n)==0) { -+ fprintf(browser->tty, "Bookmark removed!\n"); -+ } else { -+ fprintf(fo, line); -+ } -+ } -+ -+ fclose(fi); -+ fclose(fo); -+ free(line); -+ if ( rename(tempfile, path) != 0) { -+ fprintf(browser->tty, "Failed to udpate bookmarks: %s\n", strerror(errno)); -+ } -+} -+ -+static void - open_bookmarks(struct browser *browser) - { - char *path_fmt = get_data_pathfmt(); -@@ -210,18 +252,16 @@ static char dname[PATH_MAX+1]; - size_t n; - - n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); -+ - assert(n < sizeof(path)); -- strncpy(dname, dirname(path), sizeof(dname)-1); -+ strncpy(dname, path, sizeof(dname)-1); -+ dirname(dname); - if (mkdirs(dname, 0755) != 0) { -- snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -- free(path_fmt); - fprintf(stderr, "Error creating directory %s: %s\n", -- dirname(path), strerror(errno)); -+ dname, strerror(errno)); - return; - } -- -- snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -- free(path_fmt); - - struct stat buf; - if (stat(path, &buf) == -1 && errno == ENOENT) { -@@ -566,6 +606,11 @@ goto exit; - case 'a': - if (in[1]) break; - save_bookmark(browser); -+ result = PROMPT_AGAIN; -+ goto exit; -+ case 's': -+ if (in[1]) break; -+ remove_bookmark(browser); - result = PROMPT_AGAIN; - goto exit; - case 'B': diff --git a/sources/cgmnlm.git/commits/df2e4f8ab2f151eac61702b27f8c5cfe4145912e.patch b/sources/cgmnlm.git/commits/df2e4f8ab2f151eac61702b27f8c5cfe4145912e.patch @@ -1,14 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index de2bc14ce6a7c269cd430c2beebd64d6b493913b..aefac17cf61674d5c40d9554d0cb9089b1519ba4 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -63,9 +63,6 @@ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { - cc->err = err; - return; - } -- if (br_x509_decoder_isCA(&cc->decoder)) { -- return; -- } - cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); - br_sha512_out(&cc->sha512, &cc->hash); - } diff --git a/sources/cgmnlm.git/commits/e1d4e9a07ec31664e61aa72ac9a6f7ab2efea6b9.patch b/sources/cgmnlm.git/commits/e1d4e9a07ec31664e61aa72ac9a6f7ab2efea6b9.patch @@ -1,13 +0,0 @@ -diff --git a/Makefile b/Makefile -index c842ac46d9b0340fd16ef5e19d75e6977e4547e9..5ac44dbda9d629823f99124f67ace8125e9bc697 100644 ---- a/Makefile -+++ b/Makefile -@@ -31,7 +31,7 @@ - docs: doc/gmni.1 doc/gmnlm.1 - - clean: -- @rm -f gmni doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) -+ @rm -f gmni gmnlm doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) - - distclean: clean - @rm -rf "$(OUTDIR)" diff --git a/sources/cgmnlm.git/commits/e1d9773742b9832a87e912b44d4ad9e2e34364ef.patch b/sources/cgmnlm.git/commits/e1d9773742b9832a87e912b44d4ad9e2e34364ef.patch @@ -1,14 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 6d77264cabb0db317ddf0807953d327f2379d9be..5d6ce3cc5ff97884bb10c2e9e7f6e870b5d2f54a 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -180,6 +180,9 @@ case GEMINI_HEADING: - for (int n = tok.heading.level; n; --n) { - col += fprintf(browser->tty, "#"); - } -+ for (int n = 3 - tok.heading.level; n > 1; --n) { -+ col += fprintf(browser->tty, " "); -+ } - col += fprintf(browser->tty, " %s\n", - trim_ws(tok.heading.title)); - break; diff --git a/sources/cgmnlm.git/commits/e299aec4a10ba3aaf01c50263cbcabe8d39a5214.patch b/sources/cgmnlm.git/commits/e299aec4a10ba3aaf01c50263cbcabe8d39a5214.patch @@ -1,91 +0,0 @@ -diff --git a/README.md b/README.md -index eabc77ea5a6dd0f73bd908fa5a52b03fd53ddcce..d68381569be5736bac9887345b5c053e69393b5a 100644 ---- a/README.md -+++ b/README.md -@@ -11,15 +11,22 @@ - Page history - - Regex searches - - Bookmarks - -+## Non-Features: -+ -+- no inlinig of any link type -+- no caching of page content -+- no persistent history across sessions -+ - ### Modifications compared to upstream - - This project is of fork of https://git.sr.ht/~sircmpwn/gmni - - It includes the following modifications: -+- colored headings & links - - default 4 char indenting -+- k command to remove the bookmark for the current page - - e[N] command to open a link in default external program (requires `xdg-open`) - - t[N] command to download the content behind a link to a temporary file --- colored headings & links - - The actual colors used depend on your terminal palette: - - heading 1: light red -@@ -36,7 +43,7 @@ ## Dependencies: - - - A POSIX-like system and a C11 compiler - - OpenSSL --- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) -+- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional, build only) - - ## Compiling - -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 4cd601913caa3fb307de28cfbe861689f933eaf5..cc429cdfe6d482e549afee2bd9b05520985e92f9 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -81,15 +81,15 @@ "The following commands are available:\n\n" - "<Enter>\t\tread more lines (if available)\n" - "<url>\t\tgo to url\n" - "[N]\t\tFollow Nth link (where N is a number)\n" -- "u[N]\t\tShow URL of Nth link (where N is a number)\n" -+ "p[N]\t\tPrint URL of Nth link (where N is a number)\n" - "e[N]\t\tSend URL of Nth link in external default program\n" - "t[N]\t\tDownload content of Nth link to a temporary file\n" - "b\t\tBack (in the page history)\n" - "f\t\tForward (in the page history)\n" - "H\t\tView all page history\n" -- "a\t\tSave bookmark\n" -- "s\t\tRemove bookmark for current URL\n" -- "B\t\tBrowse bookmarks\n" -+ "m\t\tSave bookmark\n" -+ "M\t\tBrowse bookmarks\n" -+ "k\t\tRemove bookmark for current page\n" - "r\t\tReload the page\n" - "/<text>\t\tsearch for text (POSIX regular expression)\n" - "n\t\tjump to next search match\n" -@@ -603,17 +603,17 @@ cur = cur->next; - } - result = PROMPT_AGAIN; - goto exit; -- case 'a': -+ case 'm': - if (in[1]) break; - save_bookmark(browser); - result = PROMPT_AGAIN; - goto exit; -- case 's': -+ case 'k': - if (in[1]) break; - remove_bookmark(browser); - result = PROMPT_AGAIN; - goto exit; -- case 'B': -+ case 'M': - if (in[1]) break; - open_bookmarks(browser); - result = PROMPT_ANSWERED; -@@ -642,7 +642,7 @@ result = PROMPT_AGAIN; - goto exit; - } - case 'e': -- case 'u': -+ case 'p': - case 't': - if (!in[1]) break; - linksel = (int)strtol(in+1, &endptr, 10); diff --git a/sources/cgmnlm.git/commits/e7b2013160db902df5b833ff2e49eaff90807514.patch b/sources/cgmnlm.git/commits/e7b2013160db902df5b833ff2e49eaff90807514.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 1292bb6b8f526f923e9783ba8c0a014c2743af05..f31222b3ee2c495ed42c614bbd3c5100b52ae767 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -819,7 +819,7 @@ *col += 1; - break; - } - -- if (*col >= ws->ws_col) { -+ if (*col >= ws->ws_col - 4) { - int j = i--; - while (&s[i] != s && !isspace(s[i])) --i; - if (&s[i] == s) i = j; diff --git a/sources/cgmnlm.git/commits/e80d852a1ba62c8877dab40324ac90a14a0b7eea.patch b/sources/cgmnlm.git/commits/e80d852a1ba62c8877dab40324ac90a14a0b7eea.patch @@ -1,13 +0,0 @@ -diff --git a/Makefile b/Makefile -index 4dc668c893cfeb03020f992bf92c7ae46953d299..9a51be9ccf5eeb34c1481db6b75bea42c73adead 100644 ---- a/Makefile -+++ b/Makefile -@@ -6,7 +6,7 @@ include $(OUTDIR)/cppcache - - gmni: $(gmni_objects) - @printf 'CCLD\t$@\n' -- $(CC) $(LDFLAGS) -o $@ $(gmni_objects) $(LIBS) -+ @$(CC) $(LDFLAGS) -o $@ $(gmni_objects) $(LIBS) - - gmnlm: $(gmnlm_objects) - @printf 'CCLD\t$@\n' diff --git a/sources/cgmnlm.git/commits/ea595cdb2ba9d1e443c7ccdccf3842e49e7162a8.patch b/sources/cgmnlm.git/commits/ea595cdb2ba9d1e443c7ccdccf3842e49e7162a8.patch @@ -1,77 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 94e9933e514438c633663ae8871d4fba3e64c349..ce58ea1c707078abfdc2e39fb310b6f7f55f5585 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -72,7 +72,7 @@ } - - static void - display_gemini(FILE *tty, struct gemini_response *resp, -- struct link **next, bool pagination) -+ struct link **next, bool pagination) - { - int nlinks = 0; - struct gemini_parser p; -@@ -141,6 +141,39 @@ - gemini_parser_finish(&p); - } - -+static void -+display_plaintext(FILE *tty, struct gemini_response *resp, bool pagination) -+{ -+ struct winsize ws; -+ int row = 0, col = 0; -+ ioctl(fileno(tty), TIOCGWINSZ, &ws); -+ -+ char buf[BUFSIZ]; -+ int n; -+ while ((n = BIO_read(resp->bio, buf, sizeof(buf)) != 0)) { -+ while (n) { -+ n -= fwrite(buf, 1, n, tty); -+ } -+ } -+ -+ (void)pagination; (void)row; (void)col; // TODO: generalize pagination -+} -+ -+static void -+display_response(FILE *tty, struct gemini_response *resp, -+ struct link **next, bool pagination) -+{ -+ if (strcmp(resp->meta, "text/gemini") == 0 -+ || strncmp(resp->meta, "text/gemini;", 12) == 0) { -+ display_gemini(tty, resp, next, pagination); -+ return; -+ } -+ if (strncmp(resp->meta, "text/", 5) == 0) { -+ display_plaintext(tty, resp, pagination); -+ return; -+ } -+} -+ - int - main(int argc, char *argv[]) - { -@@ -201,6 +234,14 @@ fprintf(stderr, "Error: %s\n", gemini_strerr(res, &resp)); - assert(0); // TODO: Prompt - } - -+ snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -+ "[n]: follow Nth link; [o <url>]: open URL; " -+ "[b]ack; [f]orward; " -+ "[q]uit\n" -+ "=> ", -+ resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", -+ plain_url); -+ - switch (gemini_response_class(resp.status)) { - case GEMINI_STATUS_CLASS_INPUT: - assert(0); // TODO -@@ -216,7 +257,7 @@ "TEMPORARY FAILURE" : "PERMANENT FALIURE", - resp.status, resp.meta); - break; - case GEMINI_STATUS_CLASS_SUCCESS: -- display_gemini(tty, &resp, &links, pagination); -+ display_response(tty, &resp, &links, pagination); - break; - } - diff --git a/sources/cgmnlm.git/commits/eb01fde6007ea86b61227f690f8ee7ff0047bfeb.patch b/sources/cgmnlm.git/commits/eb01fde6007ea86b61227f690f8ee7ff0047bfeb.patch @@ -1,64 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index c252c9d9ab1783dd462343ce346cc74611e9ae85..4ec75ba1f132e2b80e0267e0e195bd10a215b918 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -23,7 +23,7 @@ port = (int)strtol(uport, NULL, 10); - free(uport); - } - -- if (options && options->addr->ai_family != AF_UNSPEC) { -+ if (options && options->addr && options->addr->ai_family != AF_UNSPEC) { - *addr = options->addr; - } else { - struct addrinfo hints = {0}; -diff --git a/src/gmnic.c b/src/gmnic.c -index 3fcfa059fe344aa4dc96bd90c4f313a4b6e755cd..fd6add8a61546a662a3fe7bb4f3beb9848a19452 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -1,12 +1,15 @@ - #include <assert.h> - #include <errno.h> - #include <getopt.h> -+#include <netdb.h> - #include <openssl/bio.h> - #include <openssl/err.h> - #include <stdbool.h> - #include <stdio.h> - #include <stdlib.h> - #include <string.h> -+#include <sys/socket.h> -+#include <sys/types.h> - #include <unistd.h> - #include "client.h" - -@@ -35,15 +38,19 @@ }; - enum input_mode input_mode = INPUT_READ; - FILE *input_source = stdin; - bool follow_redirects = false, linefeed = true; -+ struct addrinfo hints = {0}; -+ struct gemini_options opts = { -+ .hints = &hints, -+ }; - - int c; - while ((c = getopt(argc, argv, "46C:d:D:hlLiIN")) != -1) { - switch (c) { - case '4': -- assert(0); // TODO -+ hints.ai_family = AF_INET; - break; - case '6': -- assert(0); // TODO -+ hints.ai_family = AF_INET6; - break; - case 'C': - assert(0); // TODO: Client certificates -@@ -104,7 +111,7 @@ - int ret = 0; - while (!exit) { - struct gemini_response resp; -- enum gemini_result r = gemini_request(url, NULL, &resp); -+ enum gemini_result r = gemini_request(url, &opts, &resp); - if (r != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); - ret = (int)r; diff --git a/sources/cgmnlm.git/commits/eb2873b2ebe1f436d8fb4cd7c336889ad0ddddfd.patch b/sources/cgmnlm.git/commits/eb2873b2ebe1f436d8fb4cd7c336889ad0ddddfd.patch @@ -1,12 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index c5d5da36ffad8772315928e8a69b785d994511e4..d567a958702bed9413c746bd5b9f3024526fa92e 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1,6 +1,7 @@ - #include <assert.h> - #include <ctype.h> - #include <getopt.h> -+#include <limits.h> - #include <openssl/bio.h> - #include <openssl/err.h> - #include <regex.h> diff --git a/sources/cgmnlm.git/commits/eb6a4e9740237bd1bd71c115476e187520a2fc8c.patch b/sources/cgmnlm.git/commits/eb6a4e9740237bd1bd71c115476e187520a2fc8c.patch @@ -1,14 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index 4ec75ba1f132e2b80e0267e0e195bd10a215b918..59fa1381ab66496900acb4321f22c6ec3675db5e 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -31,8 +31,8 @@ if (options && options->hints) { - hints = *options->hints; - } else { - hints.ai_family = AF_UNSPEC; -- hints.ai_socktype = SOCK_STREAM; - } -+ hints.ai_socktype = SOCK_STREAM; - - char pbuf[7]; - snprintf(pbuf, sizeof(pbuf), "%d", port); diff --git a/sources/cgmnlm.git/commits/ec88f4558c48b3eece906143867dfba6d81e5e49.patch b/sources/cgmnlm.git/commits/ec88f4558c48b3eece906143867dfba6d81e5e49.patch @@ -1,22 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index da43dbe2ce10637c2f267e2cc75c0a7a6f3cea92..2eb906e1312a346fe2ad479fa5f49b1d155927b8 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -609,7 +609,7 @@ case 'd': - if (in[1] != '\0' && !isspace(in[1])) break; - struct gemini_response resp; - char url[1024] = {0}; -- strncpy(&url[0], browser->plain_url, sizeof(url)); -+ strncpy(&url[0], browser->plain_url, sizeof(url)-1); - // XXX: may affect history, do we care? - enum gemini_result res = do_requests(browser, &resp); - if (res != GEMINI_OK) { -@@ -624,7 +624,7 @@ gemini_response_finish(&resp); - result = PROMPT_AGAIN; - goto exit; - case '|': -- strncpy(&url[0], browser->plain_url, sizeof(url)); -+ strncpy(&url[0], browser->plain_url, sizeof(url)-1); - res = do_requests(browser, &resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", diff --git a/sources/cgmnlm.git/commits/ee6fe47e44bfb40cd92d6668e85fd893df3a12b8.patch b/sources/cgmnlm.git/commits/ee6fe47e44bfb40cd92d6668e85fd893df3a12b8.patch @@ -1,40 +0,0 @@ -diff --git a/src/gmnic.c b/src/gmnic.c -index 60d5c68530fffacbeea62c1a8b78497857e02a02..4ddcb1d83bf03fba67fd2231c81c38c6e38b2136 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -171,13 +171,15 @@ case OMIT_HEADERS: - if (resp.status / 10 != 2) { - break; - } -+ char last; - char buf[BUFSIZ]; -- int n; -- for (n = 1; n > 0;) { -+ for (int n = 1; n > 0;) { - n = BIO_read(resp.bio, buf, BUFSIZ); - if (n == -1) { - fprintf(stderr, "Error: read\n"); - return 1; -+ } else if (n != 0) { -+ last = buf[n - 1]; - } - ssize_t w = 0; - while (w < (ssize_t)n) { -@@ -191,8 +193,7 @@ w += x; - } - } - if (strncmp(resp.meta, "text/", 5) == 0 -- && linefeed -- && buf[n - 1] != '\n') { -+ && linefeed && last != '\n') { - printf("\n"); - } - break; -@@ -202,7 +203,6 @@ next: - gemini_response_finish(&resp); - } - -- (void)input_mode; - free(url); - return ret; - } diff --git a/sources/cgmnlm.git/commits/f4a4be2513580809c01212a08a5284f9cf16ad5f.patch b/sources/cgmnlm.git/commits/f4a4be2513580809c01212a08a5284f9cf16ad5f.patch @@ -1,264 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index b7cc12dbbb391dd3e72e5e30d21a7e7957872f60..5cc2e18a06dc99f2258d6bfc875c2c01ceb265c2 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -3,6 +3,7 @@ #include <ctype.h> - #include <getopt.h> - #include <openssl/bio.h> - #include <openssl/err.h> -+#include <regex.h> - #include <stdbool.h> - #include <stdio.h> - #include <string.h> -@@ -32,6 +33,9 @@ struct Curl_URL *url; - struct link *links; - struct history *history; - bool running; -+ -+ bool searching; -+ regex_t regex; - }; - - enum prompt_result { -@@ -39,14 +43,20 @@ PROMPT_AGAIN, - PROMPT_MORE, - PROMPT_QUIT, - PROMPT_ANSWERED, -+ PROMPT_NEXT, - }; - - const char *help_msg = - "The following commands are available:\n\n" -- "q: Quit\n" -- "N: Follow Nth link (where N is a number)\n" -- "b: Back (in the page history)\n" -- "f: Forward (in the page history)\n" -+ "q\tQuit\n" -+ "N\tFollow Nth link (where N is a number)\n" -+ "b\tBack (in the page history)\n" -+ "f\tForward (in the page history)\n" -+ "\n" -+ "Other commands include:\n\n" -+ "<Enter>\tread more lines\n" -+ "<url>\tgo to url\n" -+ "/<text>\tsearch for text (POSIX regular expression)\n" - ; - - static void -@@ -100,20 +110,17 @@ if (n == -1 && feof(browser->tty)) { - result = PROMPT_QUIT; - goto exit; - } -- if (strcmp(in, "\n") == 0) { -+ in[n - 1] = 0; // Remove LF -+ -+ int r; -+ switch (in[0]) { -+ case '\0': - result = PROMPT_MORE; - goto exit; -- } -- if (strcmp(in, "q\n") == 0) { -+ case 'q': - result = PROMPT_QUIT; - goto exit; -- } -- if (strcmp(in, "?\n") == 0) { -- fprintf(browser->tty, "%s", help_msg); -- result = PROMPT_AGAIN; -- goto exit; -- } -- if (strcmp(in, "b\n") == 0) { -+ case 'b': - if (!browser->history->prev) { - fprintf(stderr, "At beginning of history\n"); - result = PROMPT_AGAIN; -@@ -123,8 +130,7 @@ browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; -- } -- if (strcmp(in, "f\n") == 0) { -+ case 'f': - if (!browser->history->next) { - fprintf(stderr, "At end of history\n"); - result = PROMPT_AGAIN; -@@ -134,6 +140,25 @@ browser->history = browser->history->next; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; -+ case '/': -+ if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) { -+ static char buf[1024]; -+ r = regerror(r, &browser->regex, buf, sizeof(buf)); -+ assert(r < (int)sizeof(buf)); -+ fprintf(stderr, "Error: %s\n", buf); -+ result = PROMPT_AGAIN; -+ } else { -+ browser->searching = true; -+ result = PROMPT_ANSWERED; -+ } -+ goto exit_re; -+ case 'n': -+ result = PROMPT_NEXT; -+ goto exit_re; -+ case '?': -+ fprintf(browser->tty, "%s", help_msg); -+ result = PROMPT_AGAIN; -+ goto exit; - } - - struct link *link = browser->links; -@@ -154,10 +179,14 @@ goto exit; - } - } - -- in[n - 1] = 0; // Remove LF - set_url(browser, in, &browser->history); - result = PROMPT_ANSWERED; - exit: -+ if (browser->searching) { -+ browser->searching = false; -+ regfree(&browser->regex); -+ } -+exit_re: - free(in); - return result; - } -@@ -219,27 +248,34 @@ - struct winsize ws; - ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - -+ FILE *out = browser->tty; -+ bool searching = browser->searching; -+ if (searching) { -+ out = fopen("/dev/null", "w+"); -+ } -+ - char *text = NULL; - int row = 0, col = 0; - struct gemini_token tok; - struct link **next = &browser->links; - while (text != NULL || gemini_parser_next(&p, &tok) == 0) { -+repeat: - switch (tok.token) { - case GEMINI_TEXT: -- col += fprintf(browser->tty, " "); -+ col += fprintf(out, " "); - if (text == NULL) { - text = tok.text; - } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(browser->tty, "%d) ", nlinks++); -+ col += fprintf(out, "%d) ", nlinks++); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); - next = &(*next)->next; - } else { -- col += fprintf(browser->tty, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED: -@@ -247,44 +283,59 @@ continue; // TODO - case GEMINI_HEADING: - if (text == NULL) { - for (int n = tok.heading.level; n; --n) { -- col += fprintf(browser->tty, "#"); -+ col += fprintf(out, "#"); - } - switch (tok.heading.level) { - case 1: -- col += fprintf(browser->tty, " "); -+ col += fprintf(out, " "); - break; - case 2: - case 3: -- col += fprintf(browser->tty, " "); -+ col += fprintf(out, " "); - break; - } - text = trim_ws(tok.heading.title); - } else { -- col += fprintf(browser->tty, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_LIST_ITEM: - if (text == NULL) { -- col += fprintf(browser->tty, " %s ", -+ col += fprintf(out, " %s ", - browser->unicode ? "•" : "*"); - text = trim_ws(tok.list_item); - } else { -- col += fprintf(browser->tty, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: - if (text == NULL) { -- col += fprintf(browser->tty, " %s ", -+ col += fprintf(out, " %s ", - browser->unicode ? "|" : "|"); - text = trim_ws(tok.quote_text); - } else { -- col += fprintf(browser->tty, " "); -+ col += fprintf(out, " "); - } - break; - } - -+ if (text && searching) { -+ int r = regexec(&browser->regex, text, 0, NULL, 0); -+ if (r != 0) { -+ text = NULL; -+ continue; -+ } else { -+ fclose(out); -+ row = col = 0; -+ out = browser->tty; -+ text = NULL; -+ searching = false; -+ goto repeat; -+ } -+ } -+ - if (text) { -- int w = wrap(browser->tty, text, &ws, &row, &col); -+ int w = wrap(out, text, &ws, &row, &col); - text += w; - if (text[0] && row < ws.ws_row - 4) { - continue; -@@ -304,8 +355,9 @@ - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" -+ "[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "(more) => ", resp->meta, browser->plain_url, -+ browser->searching ? "[n]ext result; " : "", - browser->history->prev ? "[b]ack; " : "", - browser->history->next ? "[f]orward; " : ""); - enum prompt_result result = PROMPT_AGAIN; -@@ -322,6 +374,10 @@ browser->running = false; - return true; - case PROMPT_ANSWERED: - return true; -+ case PROMPT_NEXT: -+ searching = true; -+ out = fopen("/dev/null", "w"); -+ break; - } - - row = col = 0; -@@ -533,6 +589,7 @@ case PROMPT_QUIT: - browser.running = false; - break; - case PROMPT_ANSWERED: -+ case PROMPT_NEXT: - break; - } - diff --git a/sources/cgmnlm.git/commits/f5d540bc5d0112895376aebe6bf54adb32545d6e.patch b/sources/cgmnlm.git/commits/f5d540bc5d0112895376aebe6bf54adb32545d6e.patch @@ -1,40 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index c5419243f983c878abff1cfb36633d648f6449f2..d22f64ce27dfcf61eed80ed09b2d072efb8d71e0 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -657,7 +657,7 @@ - struct link *link = browser->links; - char *endptr; - int linksel = (int)strtol(in, &endptr, 10); -- if (!endptr[0] && linksel >= 0) { -+ if ((endptr[0] == '\0' || endptr[0] == '|') && linksel >= 0) { - while (linksel > 0 && link) { - link = link->next; - --linksel; -@@ -665,7 +665,26 @@ } - - if (!link) { - fprintf(stderr, "Error: no such link.\n"); -+ } else if (endptr[0] == '|') { -+ char url[1024] = {0}; -+ struct gemini_response resp; -+ strncpy(url, browser->plain_url, sizeof(url) - 1); -+ set_url(browser, link->url, &browser->history); -+ enum gemini_result res = do_requests(browser, &resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", -+ gemini_strerr(res, &resp)); -+ set_url(browser, url, NULL); -+ result = PROMPT_AGAIN; -+ goto exit; -+ } -+ pipe_resp(browser->tty, resp, &endptr[1]); -+ gemini_response_finish(&resp); -+ set_url(browser, url, NULL); -+ result = PROMPT_AGAIN; -+ goto exit; - } else { -+ assert(endptr[0] == '\0'); - set_url(browser, link->url, &browser->history); - result = PROMPT_ANSWERED; - goto exit; diff --git a/sources/cgmnlm.git/commits/f6643cf1b5ecbdd030420fb504ae2edcb9102410.patch b/sources/cgmnlm.git/commits/f6643cf1b5ecbdd030420fb504ae2edcb9102410.patch @@ -1,346 +0,0 @@ -diff --git a/.gitignore b/.gitignore -index 4a40b62507e740b789fdb6d2d4a63c41e3b102c8..d622c7ceba259e04d4a2e8c2ee4c91de45e13187 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -1,6 +1,6 @@ - .build - build --gmni -+/gmni - gmnlm - *.1 - *.o -diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h -new file mode 100644 -index 0000000000000000000000000000000000000000..7e27b489d71fd3a43ca60292b17d56cab3caa5f8 ---- /dev/null -+++ b/include/gmni/gmni.h -@@ -0,0 +1,164 @@ -+#ifndef GEMINI_CLIENT_H -+#define GEMINI_CLIENT_H -+#include <netdb.h> -+#include <openssl/ssl.h> -+#include <stdbool.h> -+#include <sys/socket.h> -+ -+enum gemini_result { -+ GEMINI_OK, -+ GEMINI_ERR_OOM, -+ GEMINI_ERR_INVALID_URL, -+ GEMINI_ERR_NOT_GEMINI, -+ GEMINI_ERR_RESOLVE, -+ GEMINI_ERR_CONNECT, -+ GEMINI_ERR_SSL, -+ GEMINI_ERR_SSL_VERIFY, -+ GEMINI_ERR_IO, -+ GEMINI_ERR_PROTOCOL, -+}; -+ -+enum gemini_status { -+ GEMINI_STATUS_INPUT = 10, -+ GEMINI_STATUS_SENSITIVE_INPUT = 11, -+ GEMINI_STATUS_SUCCESS = 20, -+ GEMINI_STATUS_REDIRECT_TEMPORARY = 30, -+ GEMINI_STATUS_REDIRECT_PERMANENT = 31, -+ GEMINI_STATUS_TEMPORARY_FAILURE = 40, -+ GEMINI_STATUS_SERVER_UNAVAILABLE = 41, -+ GEMINI_STATUS_CGI_ERROR = 42, -+ GEMINI_STATUS_PROXY_ERROR = 43, -+ GEMINI_STATUS_SLOW_DOWN = 44, -+ GEMINI_STATUS_PERMANENT_FAILURE = 50, -+ GEMINI_STATUS_NOT_FOUND = 51, -+ GEMINI_STATUS_GONE = 52, -+ GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, -+ GEMINI_STATUS_BAD_REQUEST = 59, -+ GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, -+ GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, -+ GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, -+}; -+ -+enum gemini_status_class { -+ GEMINI_STATUS_CLASS_INPUT = 10, -+ GEMINI_STATUS_CLASS_SUCCESS = 20, -+ GEMINI_STATUS_CLASS_REDIRECT = 30, -+ GEMINI_STATUS_CLASS_TEMPORARY_FAILURE = 40, -+ GEMINI_STATUS_CLASS_PERMANENT_FAILURE = 50, -+ GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED = 60, -+}; -+ -+struct gemini_response { -+ enum gemini_status status; -+ char *meta; -+ -+ // Response body may be read from here if appropriate: -+ BIO *bio; -+ -+ // Connection state -+ SSL_CTX *ssl_ctx; -+ SSL *ssl; -+ int fd; -+}; -+ -+struct gemini_options { -+ // If NULL, an SSL context will be created. If unset, the ssl field -+ // must also be NULL. -+ SSL_CTX *ssl_ctx; -+ -+ // If ai_family != AF_UNSPEC (the default value on most systems), the -+ // client will connect to this address and skip name resolution. -+ struct addrinfo *addr; -+ -+ // If non-NULL, these hints are provided to getaddrinfo. Useful, for -+ // example, to force IPv4/IPv6. -+ struct addrinfo *hints; -+}; -+ -+// Requests the specified URL via the gemini protocol. If options is non-NULL, -+// it may specify some additional configuration to adjust client behavior. -+// -+// Returns a value indicating the success of the request. -+// -+// Caller must call gemini_response_finish afterwards to clean up resources -+// before exiting or re-using it for another request. -+enum gemini_result gemini_request(const char *url, -+ struct gemini_options *options, -+ struct gemini_response *resp); -+ -+// Must be called after gemini_request in order to free up the resources -+// allocated during the request. -+void gemini_response_finish(struct gemini_response *resp); -+ -+// Returns a user-friendly string describing an error. -+const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); -+ -+// Returns the given URL with the input response set to the specified value. -+// The caller must free the string. -+char *gemini_input_url(const char *url, const char *input); -+ -+// Returns the general response class (i.e. with the second digit set to zero) -+// of the given Gemini status code. -+enum gemini_status_class gemini_response_class(enum gemini_status status); -+ -+enum gemini_tok { -+ GEMINI_TEXT, -+ GEMINI_LINK, -+ GEMINI_PREFORMATTED_BEGIN, -+ GEMINI_PREFORMATTED_END, -+ GEMINI_PREFORMATTED_TEXT, -+ GEMINI_HEADING, -+ GEMINI_LIST_ITEM, -+ GEMINI_QUOTE, -+}; -+ -+struct gemini_token { -+ enum gemini_tok token; -+ -+ // The token field determines which of the union members is valid. -+ union { -+ char *text; -+ -+ struct { -+ char *text; -+ char *url; // May be NULL -+ } link; -+ -+ char *preformatted; -+ -+ struct { -+ char *title; -+ int level; // 1, 2, or 3 -+ } heading; -+ -+ char *list_item; -+ char *quote_text; -+ }; -+}; -+ -+struct gemini_parser { -+ BIO *f; -+ char *buf; -+ size_t bufsz; -+ size_t bufln; -+ bool preformatted; -+}; -+ -+// Initializes a text/gemini parser which reads from the specified BIO. -+void gemini_parser_init(struct gemini_parser *p, BIO *f); -+ -+// Finishes this text/gemini parser and frees up its resources. -+void gemini_parser_finish(struct gemini_parser *p); -+ -+// Reads the next token from a text/gemini file. -+// -+// Returns 0 on success, 1 on EOF, and -1 on failure. -+// -+// Caller must call gemini_token_finish before exiting or re-using the token -+// parameter. -+int gemini_parser_next(struct gemini_parser *p, struct gemini_token *token); -+ -+// Must be called after gemini_next to free up resources for the next token. -+void gemini_token_finish(struct gemini_token *token); -+ -+#endif -diff --git a/include/gmni/tofu.h b/include/gmni/tofu.h -new file mode 100644 -index 0000000000000000000000000000000000000000..a88167ba0fb6606b2b170e5005c55131f1861972 ---- /dev/null -+++ b/include/gmni/tofu.h -@@ -0,0 +1,49 @@ -+#ifndef GEMINI_TOFU_H -+#define GEMINI_TOFU_H -+#include <limits.h> -+#include <openssl/ssl.h> -+#include <openssl/x509.h> -+#include <time.h> -+ -+enum tofu_error { -+ TOFU_VALID, -+ // Expired, wrong CN, etc. -+ TOFU_INVALID_CERT, -+ // Cert is valid but we haven't seen it before -+ TOFU_UNTRUSTED_CERT, -+ // Cert is valid but we already trust another cert for this host -+ TOFU_FINGERPRINT_MISMATCH, -+}; -+ -+enum tofu_action { -+ TOFU_ASK, -+ TOFU_FAIL, -+ TOFU_TRUST_ONCE, -+ TOFU_TRUST_ALWAYS, -+}; -+ -+struct known_host { -+ char *host, *fingerprint; -+ time_t expires; -+ int lineno; -+ struct known_host *next; -+}; -+ -+// Called when the user needs to be prompted to agree to trust an unknown -+// certificate. Return true to trust this certificate. -+typedef enum tofu_action (tofu_callback_t)(enum tofu_error error, -+ const char *fingerprint, struct known_host *host, void *data); -+ -+struct gemini_tofu { -+ char known_hosts_path[PATH_MAX+1]; -+ struct known_host *known_hosts; -+ int lineno; -+ tofu_callback_t *callback; -+ void *cb_data; -+}; -+ -+void gemini_tofu_init(struct gemini_tofu *tofu, -+ SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); -+void gemini_tofu_finish(struct gemini_tofu *tofu); -+ -+#endif -diff --git a/include/gmni/url.h b/include/gmni/url.h -new file mode 100644 -index 0000000000000000000000000000000000000000..155fd55740dbe47a498062713aca217ab259734f ---- /dev/null -+++ b/include/gmni/url.h -@@ -0,0 +1,103 @@ -+#ifndef URLAPI_H -+#define URLAPI_H -+/*************************************************************************** -+ * _ _ ____ _ -+ * Project ___| | | | _ \| | -+ * / __| | | | |_) | | -+ * | (__| |_| | _ <| |___ -+ * \___|\___/|_| \_\_____| -+ * -+ * Copyright (C) 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -+ * -+ * This software is licensed as described in the file COPYING, which -+ * you should have received as part of this distribution. The terms -+ * are also available at https://curl.haxx.se/docs/copyright.html. -+ * -+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell -+ * copies of the Software, and permit persons to whom the Software is -+ * furnished to do so, under the terms of the COPYING file. -+ * -+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -+ * KIND, either express or implied. -+ * -+ ***************************************************************************/ -+ -+/* the error codes for the URL API */ -+typedef enum { -+ CURLUE_OK, -+ CURLUE_BAD_HANDLE, /* 1 */ -+ CURLUE_BAD_PARTPOINTER, /* 2 */ -+ CURLUE_MALFORMED_INPUT, /* 3 */ -+ CURLUE_BAD_PORT_NUMBER, /* 4 */ -+ CURLUE_UNSUPPORTED_SCHEME, /* 5 */ -+ CURLUE_URLDECODE, /* 6 */ -+ CURLUE_OUT_OF_MEMORY, /* 7 */ -+ CURLUE_USER_NOT_ALLOWED, /* 8 */ -+ CURLUE_UNKNOWN_PART, /* 9 */ -+ CURLUE_NO_SCHEME, /* 10 */ -+ CURLUE_NO_USER, /* 11 */ -+ CURLUE_NO_PASSWORD, /* 12 */ -+ CURLUE_NO_OPTIONS, /* 13 */ -+ CURLUE_NO_HOST, /* 14 */ -+ CURLUE_NO_PORT, /* 15 */ -+ CURLUE_NO_QUERY, /* 16 */ -+ CURLUE_NO_FRAGMENT /* 17 */ -+} CURLUcode; -+ -+typedef enum { -+ CURLUPART_URL, -+ CURLUPART_SCHEME, -+ CURLUPART_USER, -+ CURLUPART_PASSWORD, -+ CURLUPART_OPTIONS, -+ CURLUPART_HOST, -+ CURLUPART_PORT, -+ CURLUPART_PATH, -+ CURLUPART_QUERY, -+ CURLUPART_FRAGMENT -+} CURLUPart; -+ -+#define CURLU_PATH_AS_IS (1<<4) /* leave dot sequences */ -+#define CURLU_DISALLOW_USER (1<<5) /* no user+password allowed */ -+#define CURLU_URLDECODE (1<<6) /* URL decode on get */ -+#define CURLU_URLENCODE (1<<7) /* URL encode on set */ -+#define CURLU_APPENDQUERY (1<<8) /* append a form style part */ -+ -+typedef struct Curl_URL CURLU; -+ -+/* -+ * curl_url() creates a new CURLU handle and returns a pointer to it. -+ * Must be freed with curl_url_cleanup(). -+ */ -+struct Curl_URL *curl_url(void); -+ -+/* -+ * curl_url_cleanup() frees the CURLU handle and related resources used for -+ * the URL parsing. It will not free strings previously returned with the URL -+ * API. -+ */ -+void curl_url_cleanup(struct Curl_URL *handle); -+ -+/* -+ * curl_url_dup() duplicates a CURLU handle and returns a new copy. The new -+ * handle must also be freed with curl_url_cleanup(). -+ */ -+struct Curl_URL *curl_url_dup(struct Curl_URL *in); -+ -+/* -+ * curl_url_get() extracts a specific part of the URL from a CURLU -+ * handle. Returns error code. The returned pointer MUST be freed with -+ * free() afterwards. -+ */ -+CURLUcode curl_url_get(struct Curl_URL *handle, CURLUPart what, -+ char **part, unsigned int flags); -+ -+/* -+ * curl_url_set() sets a specific part of the URL in a CURLU handle. Returns -+ * error code. The passed in string will be copied. Passing a NULL instead of -+ * a part string, clears that part. -+ */ -+CURLUcode curl_url_set(struct Curl_URL *handle, CURLUPart what, -+ const char *part, unsigned int flags); -+ -+#endif diff --git a/sources/cgmnlm.git/commits/f80e4037c21c87b11b7b4bfefba5d33a0fcc1ea6.patch b/sources/cgmnlm.git/commits/f80e4037c21c87b11b7b4bfefba5d33a0fcc1ea6.patch @@ -1,23 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index a2717fb84c76fd50a000ceda540af54c60396252..f218f0a43c1d32223e6bb73ecc5069c5cd0cf21a 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -63,6 +63,7 @@ "f\tForward (in the page history)\n" - "H\tView all page history\n" - "m\tSave bookmark\n" - "M\tBrowse bookmarks\n" -+ "r\tReload the page\n" - "\n" - "Other commands include:\n\n" - "<Enter>\tread more lines\n" -@@ -299,6 +300,10 @@ } else { - fprintf(stderr, "Error: invalid argument.\n"); - } - result = PROMPT_AGAIN; -+ goto exit; -+ case 'r': -+ if (in[1]) break; -+ result = PROMPT_ANSWERED; - goto exit; - case '?': - if (in[1]) break; diff --git a/sources/cgmnlm.git/commits/f8f6c5a12beeaa4ad614cc349a04e2984c06c83b.patch b/sources/cgmnlm.git/commits/f8f6c5a12beeaa4ad614cc349a04e2984c06c83b.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index eaa34c055cbb92564ebd279fcf06189dc577360d..489a6833f75c9e6a13562c84dc8e3ded24be23c4 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -70,7 +70,7 @@ - const char *default_bookmarks = - "# Welcome to cgmnlm\n\n" - "Links:\n\n" -- "=> https://gmn.clttr.info/cgmnln.gmi The colorful line mode client\n" -+ "=> gemini://gmn.clttr.info/cgmnln.gmi The colorful line mode client\n" - "=> gemini://gemini.circumlunar.space The gemini protocol\n\n" - "This file can be found at %s and may be edited at your pleasure.\n\n" - "Bookmarks:\n" diff --git a/sources/cgmnlm.git/commits/fa78663748958eebb442f03d0406f42f6190522a.patch b/sources/cgmnlm.git/commits/fa78663748958eebb442f03d0406f42f6190522a.patch @@ -1,200 +0,0 @@ -diff --git a/doc/gmni.scd b/doc/gmni.scd -index 2c1dc54272aaf3a4cbd6b1c13e29323f418f199b..1f20672f1fb0c8ff6a4f3a97a3324a25d2a0a01d 100644 ---- a/doc/gmni.scd -+++ b/doc/gmni.scd -@@ -68,3 +68,7 @@ *-N* - Suppress the input prompt if the server requests an input, and instead - print a diagnostic message and exit with a zero (successful) status - code. -+ -+*-o* _path_ -+ Write output to _path_. If _path_ ends with a '/', the basename of the URL -+ will be appended. If _path_ is the empty string, "./" will be presumed. -diff --git a/include/util.h b/include/util.h -index cf731bfdc75a750df6f18873f48dd859786587c7..6193fdf48c0ac6d313de7f35a1d09ecba62e3283 100644 ---- a/include/util.h -+++ b/include/util.h -@@ -8,5 +8,7 @@ }; - - char *getpath(const struct pathspec *paths, size_t npaths); - int mkdirs(char *path, mode_t mode); -+int download_resp(FILE *out, struct gemini_response resp, const char *path, -+ char *url); - - #endif -diff --git a/src/gmni.c b/src/gmni.c -index 171e1407ba2dfa3341f0693ae8d60bb9cedb6564..000a56550a686928f84e2c301c9017a8c48b3d68 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -14,6 +14,7 @@ #include <termios.h> - #include <unistd.h> - #include "gmni.h" - #include "tofu.h" -+#include "util.h" - - static void - usage(const char *argv_0) -@@ -125,6 +126,8 @@ }; - enum input_mode input_mode = INPUT_READ; - FILE *input_source = stdin; - -+ char *output_file = NULL; -+ - bool follow_redirects = false, linefeed = true; - int max_redirect = 5; - -@@ -136,7 +139,7 @@ struct tofu_config cfg; - cfg.action = TOFU_ASK; - - int c; -- while ((c = getopt(argc, argv, "46d:D:E:hj:lLiINR:")) != -1) { -+ while ((c = getopt(argc, argv, "46d:D:E:hj:lLiINR:o:")) != -1) { - switch (c) { - case '4': - hints.ai_family = AF_INET; -@@ -204,6 +207,9 @@ fprintf(stderr, "Error: -R expects numeric argument\n"); - return 1; - } - break; -+ case 'o': -+ output_file = optarg; -+ break; - default: - fprintf(stderr, "fatal: unknown flag %c\n", c); - return 1; -@@ -301,6 +307,11 @@ /* fallthrough */ - case OMIT_HEADERS: - if (gemini_response_class(resp.status) != - GEMINI_STATUS_CLASS_SUCCESS) { -+ break; -+ } -+ -+ if (output_file != NULL) { -+ ret = download_resp(stderr, resp, output_file, url); - break; - } - -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 9bc95fad89c0f92bc5a68fdd40d272b021c3cc25..d5cb2dc29f9ceb448622d5c36b7b73f0ca985bcb 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -78,6 +78,7 @@ "H\tView all page history\n" - "m\tSave bookmark\n" - "M\tBrowse bookmarks\n" - "r\tReload the page\n" -+ "d <path>\tDownload page to <path>\n" - "\n" - "Other commands include:\n\n" - "<Enter>\tread more lines\n" -@@ -553,6 +554,24 @@ print_media_parameters(browser->tty, browser->meta - ? strchr(browser->meta, ';') : NULL); - result = PROMPT_AGAIN; - goto exit; -+ case 'd': -+ if (in[1] != '\0' && !isspace(in[1])) break; -+ struct gemini_response resp; -+ char url[1024] = {0}; -+ strncpy(&url[0], browser->plain_url, sizeof(url)); -+ // XXX: may affect history, do we care? -+ enum gemini_result res = do_requests(browser, &resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", -+ gemini_strerr(res, &resp)); -+ result = PROMPT_AGAIN; -+ goto exit; -+ } -+ set_url(browser, url, NULL); -+ download_resp(browser->tty, resp, trim_ws(&in[1]), url); -+ gemini_response_finish(&resp); -+ result = PROMPT_AGAIN; -+ goto exit; - case '?': - if (in[1]) break; - fprintf(browser->tty, "%s", help_msg); -@@ -849,7 +868,9 @@ } - if (strncmp(resp->meta, "text/", 5) == 0) { - return display_plaintext(browser, resp); - } -- assert(0); // TODO: Deal with other mimetypes -+ fprintf(stderr, "Media type %s is unsupported, use \"d <path>\" to download this page\n", -+ resp->meta); -+ return false; - } - - static enum tofu_action -diff --git a/src/tofu.c b/src/tofu.c -index af5d9f20d78558b3ff7dc9253bef53f2deef1d25..48a627fe131996070d991bcc54bb7bcb40fddce4 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -10,6 +10,7 @@ #include <openssl/x509v3.h> - #include <stdio.h> - #include <string.h> - #include <time.h> -+#include "gmni.h" - #include "tofu.h" - #include "util.h" - -diff --git a/src/util.c b/src/util.c -index 26e7283a26cc8e593bb3e3fd53d4b7a9bc646e3f..360c99ac95de9dcfce860610aeef85e8986e99c2 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -2,9 +2,12 @@ #include <assert.h> - #include <errno.h> - #include <libgen.h> - #include <limits.h> -+#include <stdint.h> -+#include <stdio.h> - #include <stdlib.h> - #include <string.h> - #include <sys/stat.h> -+#include "gmni.h" - #include "util.h" - - static void -@@ -60,3 +63,46 @@ } - } - return NULL; - } -+ -+int -+download_resp(FILE *out, struct gemini_response resp, const char *path, -+ char *url) -+{ -+ char buf[PATH_MAX]; -+ assert(path); -+ if (path[0] == '\0') { -+ path = "./"; -+ } -+ if (path[strlen(path)-1] == '/') { -+ strncat(strncpy(&buf[0], path, sizeof(buf)), basename(url), -+ sizeof(buf)); -+ path = &buf[0]; -+ } -+ FILE *f = fopen(path, "w"); -+ if (f == NULL) { -+ fprintf(stderr, "Could not open %s for writing: %s\n", -+ path, strerror(errno)); -+ return 1; -+ } -+ fprintf(out, "Downloading %s to %s\n", url, path); -+ for (int n = 1; n > 0;) { -+ n = BIO_read(resp.bio, buf, BUFSIZ); -+ if (n == -1) { -+ fprintf(stderr, "Error: read\n"); -+ return 1; -+ } -+ ssize_t w = 0; -+ while (w < (ssize_t)n) { -+ ssize_t x = fwrite(&buf[w], 1, n - w, f); -+ if (ferror(f)) { -+ fprintf(stderr, "Error: write: %s\n", -+ strerror(errno)); -+ return 1; -+ } -+ w += x; -+ } -+ } -+ fprintf(out, "Finished download\n"); -+ fclose(f); -+ return 0; -+} diff --git a/sources/cgmnlm.git/commits/fc0bf889a3e5ce270600811875ef9a50729c4135.patch b/sources/cgmnlm.git/commits/fc0bf889a3e5ce270600811875ef9a50729c4135.patch @@ -1,1043 +0,0 @@ -diff --git a/README.md b/README.md -index 3c999cee8bbcb5b0bfbc6732f9ecea0ba231d043..6dcd2f7b6f5e9d408b03fa2aae5c386fd3121aa1 100644 ---- a/README.md -+++ b/README.md -@@ -60,6 +60,6 @@ - ### Dependencies: - - - A POSIX-like system and a C11 compiler --- OpenSSL -+- [BearSSL](https://www.bearssl.org/index.html) - - [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) - -diff --git a/config.sh b/config.sh -index 424cbda346869c7476859d55a600c414c86bbb46..87888929b98a46887dbcb25378385183e4ef2566 100644 ---- a/config.sh -+++ b/config.sh -@@ -117,8 +117,8 @@ echo no - fi - done - -- find_library OpenSSL libssl -- find_library OpenSSL libcrypto -+ # XXX: Asked the maintainer to provide a .pc file -+ LIBS="$LIBS -lbearssl" - - printf "Checking for scdoc... " - if scdoc -v >/dev/null 2>&1 -diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h -index 7e27b489d71fd3a43ca60292b17d56cab3caa5f8..16bef51024275bbb6ade9c90a11ae68876df387a 100644 ---- a/include/gmni/gmni.h -+++ b/include/gmni/gmni.h -@@ -1,7 +1,7 @@ - #ifndef GEMINI_CLIENT_H - #define GEMINI_CLIENT_H -+#include <bearssl_ssl.h> - #include <netdb.h> --#include <openssl/ssl.h> - #include <stdbool.h> - #include <sys/socket.h> - -@@ -52,20 +52,16 @@ struct gemini_response { - enum gemini_status status; - char *meta; - -+ // TODO: Make these private - // Response body may be read from here if appropriate: -- BIO *bio; -+ br_sslio_context body; - - // Connection state -- SSL_CTX *ssl_ctx; -- SSL *ssl; -+ br_ssl_client_context *sc; - int fd; - }; - - struct gemini_options { -- // If NULL, an SSL context will be created. If unset, the ssl field -- // must also be NULL. -- SSL_CTX *ssl_ctx; -- - // If ai_family != AF_UNSPEC (the default value on most systems), the - // client will connect to this address and skip name resolution. - struct addrinfo *addr; -@@ -75,6 +71,8 @@ // example, to force IPv4/IPv6. - struct addrinfo *hints; - }; - -+struct gemini_tofu; -+ - // Requests the specified URL via the gemini protocol. If options is non-NULL, - // it may specify some additional configuration to adjust client behavior. - // -@@ -84,6 +82,7 @@ // Caller must call gemini_response_finish afterwards to clean up resources - // before exiting or re-using it for another request. - enum gemini_result gemini_request(const char *url, - struct gemini_options *options, -+ struct gemini_tofu *tofu, - struct gemini_response *resp); - - // Must be called after gemini_request in order to free up the resources -@@ -137,15 +136,20 @@ }; - }; - - struct gemini_parser { -- BIO *f; -+ int (*read)(void *state, void *buf, size_t nbyte); -+ void *state; - char *buf; - size_t bufsz; - size_t bufln; - bool preformatted; - }; - --// Initializes a text/gemini parser which reads from the specified BIO. --void gemini_parser_init(struct gemini_parser *p, BIO *f); -+// Initializes a text/gemini parser. The provided "read" function will be called -+// with the provided "state" value in order to obtain more gemtext data. The -+// read function should behave like read(3). -+void gemini_parser_init(struct gemini_parser *p, -+ int (*read)(void *state, void *buf, size_t nbyte), -+ void *state); - - // Finishes this text/gemini parser and frees up its resources. - void gemini_parser_finish(struct gemini_parser *p); -diff --git a/include/gmni/tofu.h b/include/gmni/tofu.h -index a88167ba0fb6606b2b170e5005c55131f1861972..a0981a5296421541a766846bc36c733dab1dfd1a 100644 ---- a/include/gmni/tofu.h -+++ b/include/gmni/tofu.h -@@ -1,9 +1,7 @@ - #ifndef GEMINI_TOFU_H - #define GEMINI_TOFU_H -+#include <bearssl_x509.h> - #include <limits.h> --#include <openssl/ssl.h> --#include <openssl/x509.h> --#include <time.h> - - enum tofu_error { - TOFU_VALID, -@@ -24,7 +22,6 @@ }; - - struct known_host { - char *host, *fingerprint; -- time_t expires; - int lineno; - struct known_host *next; - }; -@@ -34,7 +31,23 @@ // certificate. Return true to trust this certificate. - typedef enum tofu_action (tofu_callback_t)(enum tofu_error error, - const char *fingerprint, struct known_host *host, void *data); - -+struct gemini_tofu; -+ -+struct x509_tofu_context { -+ const br_x509_class *vtable; -+ br_x509_decoder_context decoder; -+ br_x509_pkey *pkey; -+ br_sha512_context sha512; -+ unsigned char hash[64]; -+ struct gemini_tofu *store; -+ const char *server_name; -+ int err; -+}; -+ - struct gemini_tofu { -+ struct x509_tofu_context x509_ctx; -+ br_ssl_client_context sc; -+ unsigned char iobuf[BR_SSL_BUFSIZE_BIDI]; - char known_hosts_path[PATH_MAX+1]; - struct known_host *known_hosts; - int lineno; -@@ -42,8 +55,7 @@ tofu_callback_t *callback; - void *cb_data; - }; - --void gemini_tofu_init(struct gemini_tofu *tofu, -- SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); -+void gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *data); - void gemini_tofu_finish(struct gemini_tofu *tofu); - - #endif -diff --git a/include/util.h b/include/util.h -index 6193fdf48c0ac6d313de7f35a1d09ecba62e3283..8ec9ac5d94092a75813a3e083140fbb88079c29d 100644 ---- a/include/util.h -+++ b/include/util.h -@@ -1,5 +1,7 @@ - #ifndef GEMINI_UTIL_H - #define GEMINI_UTIL_H -+#include <stdio.h> -+#include <sys/types.h> - - struct pathspec { - const char *var; -diff --git a/src/client.c b/src/client.c -index a8a12f3cfbe1e98fd2045eec257cfaf95c319910..e402cc97d96a904a2a8e9e2db40345b222cb85ae 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -1,15 +1,15 @@ - #include <assert.h> - #include <errno.h> - #include <netdb.h> --#include <openssl/bio.h> --#include <openssl/err.h> --#include <openssl/ssl.h> -+#include <bearssl_ssl.h> - #include <stdlib.h> -+#include <stdio.h> - #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> - #include <unistd.h> - #include <gmni/gmni.h> -+#include <gmni/tofu.h> - #include <gmni/url.h> - - static enum gemini_result -@@ -88,9 +88,41 @@ - #define GEMINI_META_MAXLEN 1024 - #define GEMINI_STATUS_MAXLEN 2 - -+static int -+sock_read(void *ctx, unsigned char *buf, size_t len) -+{ -+ for (;;) { -+ ssize_t rlen; -+ rlen = read(*(int *)ctx, buf, len); -+ if (rlen <= 0) { -+ if (rlen < 0 && errno == EINTR) { -+ continue; -+ } -+ return -1; -+ } -+ return (int)rlen; -+ } -+} -+ -+static int -+sock_write(void *ctx, const unsigned char *buf, size_t len) -+{ -+ for (;;) { -+ ssize_t wlen; -+ wlen = write(*(int *)ctx, buf, len); -+ if (wlen <= 0) { -+ if (wlen < 0 && errno == EINTR) { -+ continue; -+ } -+ return -1; -+ } -+ return (int)wlen; -+ } -+} -+ - enum gemini_result - gemini_request(const char *url, struct gemini_options *options, -- struct gemini_response *resp) -+ struct gemini_tofu *tofu, struct gemini_response *resp) - { - assert(url); - assert(resp); -@@ -128,84 +160,50 @@ free(host); - goto cleanup; - } - -- if (options && options->ssl_ctx) { -- resp->ssl_ctx = options->ssl_ctx; -- SSL_CTX_up_ref(options->ssl_ctx); -- } else { -- resp->ssl_ctx = SSL_CTX_new(TLS_method()); -- assert(resp->ssl_ctx); -- SSL_CTX_set_verify(resp->ssl_ctx, SSL_VERIFY_PEER, NULL); -- } -- - int r; -- BIO *sbio = BIO_new(BIO_f_ssl()); - res = gemini_connect(uri, options, resp, &resp->fd); - if (res != GEMINI_OK) { - free(host); - goto cleanup; - } - -- resp->ssl = SSL_new(resp->ssl_ctx); -- assert(resp->ssl); -- SSL_set_connect_state(resp->ssl); -- if ((r = SSL_set1_host(resp->ssl, host)) != 1) { -- free(host); -- goto ssl_error; -- } -- if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) { -- free(host); -- goto ssl_error; -- } -- free(host); -- if ((r = SSL_set_fd(resp->ssl, resp->fd)) != 1) { -- goto ssl_error; -- } -- if ((r = SSL_connect(resp->ssl)) != 1) { -- goto ssl_error; -- } -- -- X509 *cert = SSL_get_peer_certificate(resp->ssl); -- if (!cert) { -- resp->status = X509_V_ERR_UNSPECIFIED; -- res = GEMINI_ERR_SSL_VERIFY; -- goto cleanup; -- } -- X509_free(cert); -- -- long vr = SSL_get_verify_result(resp->ssl); -- if (vr != X509_V_OK) { -- resp->status = vr; -- res = GEMINI_ERR_SSL_VERIFY; -- goto cleanup; -- } -- -- BIO_set_ssl(sbio, resp->ssl, 0); -- -- resp->bio = BIO_new(BIO_f_buffer()); -- BIO_push(resp->bio, sbio); -+ // TODO: session reuse -+ resp->sc = &tofu->sc; -+ br_ssl_client_reset(resp->sc, host, 0); -+ br_sslio_init(&resp->body, &resp->sc->eng, -+ sock_read, &resp->fd, sock_write, &resp->fd); - - char req[1024 + 3]; - r = snprintf(req, sizeof(req), "%s\r\n", url); - assert(r > 0); - -- r = BIO_puts(sbio, req); -- if (r == -1) { -- res = GEMINI_ERR_IO; -- goto cleanup; -- } -- assert(r == (int)strlen(req)); -+ br_sslio_write_all(&resp->body, req, r); -+ br_sslio_flush(&resp->body); - -+ // The SSL engine maintains an internal buffer, so this shouldn't be as -+ // inefficient as it looks. It's necessary to do this one byte at a time -+ // to avoid consuming any of the response body buffer. - char buf[GEMINI_META_MAXLEN - + GEMINI_STATUS_MAXLEN - + 2 /* CRLF */ + 1 /* NUL */]; -- r = BIO_gets(resp->bio, buf, sizeof(buf)); -- if (r == -1) { -- res = GEMINI_ERR_IO; -- goto cleanup; -+ memset(buf, 0, sizeof(buf)); -+ size_t l; -+ for (l = 0; l < 2 || memcmp(&buf[l-2], "\r\n", 2) != 0; ++l) { -+ r = br_sslio_read(&resp->body, &buf[l], 1); -+ if (r < 0) { -+ break; -+ } - } - -- if (r < 3 || strcmp(&buf[r - 2], "\r\n") != 0) { -- fprintf(stderr, "invalid line %d '%s'\n", r, buf); -+ int err = br_ssl_engine_last_error(&resp->sc->eng); -+ if (err != 0) { -+ // TODO: Bubble this up properly -+ fprintf(stderr, "SSL error %d\n", err); -+ goto ssl_error; -+ } -+ -+ if (l < 3 || strcmp(&buf[l-2], "\r\n") != 0) { -+ fprintf(stderr, "invalid line '%s'\n", buf); - res = GEMINI_ERR_PROTOCOL; - goto cleanup; - } -@@ -217,9 +215,9 @@ fprintf(stderr, "invalid status\n"); - res = GEMINI_ERR_PROTOCOL; - goto cleanup; - } -- resp->meta = calloc(r - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); -- strncpy(resp->meta, &endptr[1], r - 5); -- resp->meta[r - 5] = '\0'; -+ resp->meta = calloc(l - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); -+ strncpy(resp->meta, &endptr[1], l - 5); -+ resp->meta[l - 5] = '\0'; - - cleanup: - curl_url_cleanup(uri); -@@ -237,26 +235,18 @@ if (!resp) { - return; - } - -- if (resp->bio) { -- BIO_free_all(resp->bio); -- resp->bio = NULL; -+ if (resp->fd != -1) { -+ close(resp->fd); -+ resp->fd = -1; - } - -- if (resp->ssl) { -- SSL_free(resp->ssl); -- } -- if (resp->ssl_ctx) { -- SSL_CTX_free(resp->ssl_ctx); -- } - free(resp->meta); - -- if (resp->fd != -1) { -- close(resp->fd); -- resp->fd = -1; -+ if (resp->sc) { -+ br_sslio_close(&resp->body); - } - -- resp->ssl = NULL; -- resp->ssl_ctx = NULL; -+ resp->sc = NULL; - resp->meta = NULL; - } - -@@ -277,11 +267,11 @@ return gai_strerror(resp->status); - case GEMINI_ERR_CONNECT: - return strerror(errno); - case GEMINI_ERR_SSL: -- return ERR_error_string( -- SSL_get_error(resp->ssl, resp->status), -- NULL); -+ // TODO: more specific -+ return "SSL error"; - case GEMINI_ERR_SSL_VERIFY: -- return X509_verify_cert_error_string(resp->status); -+ // TODO: more specific -+ return "X.509 certificate not trusted"; - case GEMINI_ERR_IO: - return "I/O error"; - case GEMINI_ERR_PROTOCOL: -diff --git a/src/gmni.c b/src/gmni.c -index 49abb8524a012c27249006dc45f4688a846e63c3..a8321d06c367128706d19ac283a53e55d95d0a92 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -1,9 +1,8 @@ - #include <assert.h> -+#include <bearssl_ssl.h> - #include <errno.h> - #include <getopt.h> - #include <netdb.h> --#include <openssl/bio.h> --#include <openssl/err.h> - #include <stdbool.h> - #include <stdio.h> - #include <stdlib.h> -@@ -222,10 +221,7 @@ usage(argv[0]); - return 1; - } - -- SSL_load_error_strings(); -- ERR_load_crypto_strings(); -- opts.ssl_ctx = SSL_CTX_new(TLS_method()); -- gemini_tofu_init(&cfg.tofu, opts.ssl_ctx, &tofu_callback, &cfg); -+ gemini_tofu_init(&cfg.tofu, &tofu_callback, &cfg); - - bool exit = false; - struct Curl_URL *url = curl_url(); -@@ -242,7 +238,8 @@ char *buf; - curl_url_get(url, CURLUPART_URL, &buf, 0); - - struct gemini_response resp; -- enum gemini_result r = gemini_request(buf, &opts, &resp); -+ enum gemini_result r = gemini_request( -+ buf, &opts, &cfg.tofu, &resp); - - free(buf); - -@@ -340,11 +337,8 @@ - char last = 0; - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { -- n = BIO_read(resp.bio, buf, BUFSIZ); -- if (n == -1) { -- fprintf(stderr, "Error: read\n"); -- return 1; -- } else if (n != 0) { -+ n = br_sslio_read(&resp.body, buf, BUFSIZ); -+ if (n > 0) { - last = buf[n - 1]; - } - ssize_t w = 0; -@@ -370,7 +364,6 @@ next: - gemini_response_finish(&resp); - } - -- SSL_CTX_free(opts.ssl_ctx); - curl_url_cleanup(url); - gemini_tofu_finish(&cfg.tofu); - return ret; -diff --git a/src/gmnlm.c b/src/gmnlm.c -index f31222b3ee2c495ed42c614bbd3c5100b52ae767..0ea492bb85fe024bd250c304808dcb8f0a915f90 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1,22 +1,25 @@ - #include <assert.h> -+#include <bearssl_ssl.h> - #include <ctype.h> -+#include <errno.h> -+#include <fcntl.h> - #include <getopt.h> -+#include <gmni/gmni.h> -+#include <gmni/tofu.h> -+#include <gmni/url.h> - #include <libgen.h> - #include <limits.h> --#include <openssl/bio.h> --#include <openssl/err.h> - #include <regex.h> - #include <stdbool.h> - #include <stdio.h> -+#include <stdlib.h> - #include <string.h> - #include <sys/ioctl.h> - #include <sys/stat.h> -+#include <sys/types.h> - #include <sys/wait.h> - #include <termios.h> - #include <unistd.h> --#include <gmni/gmni.h> --#include <gmni/tofu.h> --#include <gmni/url.h> - #include "util.h" - - #define ANSI_COLOR_RED "\x1b[91m" -@@ -398,10 +401,13 @@ close(pfd[0]); - FILE *f = fdopen(pfd[1], "w"); - // XXX: may affect history, do we care? - for (int n = 1; n > 0;) { -- n = BIO_read(resp.bio, buf, BUFSIZ); -- if (n == -1) { -- fprintf(stderr, "Error: read\n"); -- return; -+ if (resp.sc) { -+ n = br_sslio_read(&resp.body, buf, BUFSIZ); -+ } else { -+ n = read(resp.fd, buf, BUFSIZ); -+ } -+ if (n < 0) { -+ n = 0; - } - ssize_t w = 0; - while (w < (ssize_t)n) { -@@ -445,23 +451,19 @@ resp->status = GEMINI_STATUS_BAD_REQUEST; - break; - } - -- FILE *fp = fopen(path, "r"); -- if (!fp) { -+ int fd = open(path, O_RDONLY); -+ if (fd < 0) { - resp->status = GEMINI_STATUS_NOT_FOUND; -- /* Make sure members of resp evaluate to false, so that -- gemini_response_finish does not try to free them. */ -- resp->bio = NULL; -- resp->ssl = NULL; -- resp->ssl_ctx = NULL; -+ // Make sure members of resp evaluate to false, -+ // so that gemini_response_finish does not try -+ // to free them. -+ resp->sc = NULL; - resp->meta = NULL; - resp->fd = -1; - free(path); - break; - } - -- BIO *file = BIO_new_fp(fp, BIO_CLOSE); -- resp->bio = BIO_new(BIO_f_buffer()); -- BIO_push(resp->bio, file); - if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) { - resp->meta = strdup("text/gemini"); - } else if (has_suffix(path, ".txt")) { -@@ -471,14 +473,14 @@ resp->meta = strdup("application/x-octet-stream"); - } - free(path); - resp->status = GEMINI_STATUS_SUCCESS; -- resp->fd = -1; -- resp->ssl = NULL; -- resp->ssl_ctx = NULL; -+ resp->fd = fd; -+ resp->sc = NULL; - return GEMINI_OK; - } - free(scheme); - -- res = gemini_request(browser->plain_url, &browser->opts, resp); -+ res = gemini_request(browser->plain_url, &browser->opts, -+ &browser->tofu, resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); - requesting = false; -@@ -835,12 +837,23 @@ } - return fprintf(f, "%s\n", s) - 1; - } - -+static int -+resp_read(void *state, void *buf, size_t nbyte) -+{ -+ struct gemini_response *resp = state; -+ if (resp->sc) { -+ return br_sslio_read(&resp->body, buf, nbyte); -+ } else { -+ return read(resp->fd, buf, nbyte); -+ } -+} -+ - static bool - display_gemini(struct browser *browser, struct gemini_response *resp) - { - int nlinks = 0; - struct gemini_parser p; -- gemini_parser_init(&p, resp->bio); -+ gemini_parser_init(&p, &resp_read, resp); - free(browser->page_title); - browser->page_title = NULL; - -@@ -1029,10 +1042,13 @@ ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { -- n = BIO_read(resp->bio, buf, BUFSIZ); -- if (n == -1) { -- fprintf(stderr, "Error: read\n"); -- return 1; -+ if (resp->sc) { -+ n = br_sslio_read(&resp->body, buf, BUFSIZ); -+ } else { -+ n = read(resp->fd, buf, BUFSIZ); -+ } -+ if (n < 0) { -+ n = 0; - } - ssize_t w = 0; - while (w < (ssize_t)n) { -@@ -1207,11 +1223,7 @@ } else { - open_bookmarks(&browser); - } - -- SSL_load_error_strings(); -- ERR_load_crypto_strings(); -- browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); -- gemini_tofu_init(&browser.tofu, browser.opts.ssl_ctx, -- &tofu_callback, &browser); -+ gemini_tofu_init(&browser.tofu, &tofu_callback, &browser); - - struct gemini_response resp; - browser.running = true; -@@ -1273,7 +1285,6 @@ while (hist && hist->prev) { - hist = hist->prev; - } - history_free(hist); -- SSL_CTX_free(browser.opts.ssl_ctx); - curl_url_cleanup(browser.url); - free(browser.page_title); - free(browser.plain_url); -diff --git a/src/parser.c b/src/parser.c -index ad2c0e6306872f8dde450063ba8815e2ee7e314a..6f12456ddbe5248b59ea146464a1d76365505c56 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -1,21 +1,23 @@ - #include <assert.h> - #include <ctype.h> --#include <openssl/bio.h> - #include <stdbool.h> - #include <stddef.h> -+#include <stdio.h> - #include <stdlib.h> - #include <string.h> - #include <gmni/gmni.h> - - void --gemini_parser_init(struct gemini_parser *p, BIO *f) -+gemini_parser_init(struct gemini_parser *p, -+ int (*read)(void *state, void *buf, size_t nbyte), -+ void *state) - { -- p->f = f; -+ p->read = read; -+ p->state = state; - p->bufln = 0; - p->bufsz = BUFSIZ; - p->buf = malloc(p->bufsz + 1); - p->buf[0] = 0; -- BIO_up_ref(p->f); - p->preformatted = false; - } - -@@ -25,7 +27,6 @@ { - if (!p) { - return; - } -- BIO_free(p->f); - free(p->buf); - } - -@@ -42,7 +43,7 @@ p->buf = realloc(p->buf, p->bufsz); - assert(p->buf); - } - -- ssize_t n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln - 1); -+ int n = p->read(p->state, &p->buf[p->bufln], p->bufsz - p->bufln - 1); - if (n == -1) { - return -1; - } else if (n == 0) { -diff --git a/src/tofu.c b/src/tofu.c -index 48395c08cdbf68b31c5defd15b360f1eb2897a3f..de2bc14ce6a7c269cd430c2beebd64d6b493913b 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -1,95 +1,97 @@ - #include <assert.h> -+#include <bearssl_hash.h> -+#include <bearssl_x509.h> - #include <errno.h> -+#include <gmni/gmni.h> -+#include <gmni/tofu.h> - #include <libgen.h> - #include <limits.h> --#include <openssl/asn1.h> --#include <openssl/evp.h> --#include <openssl/ssl.h> --#include <openssl/x509.h> --#include <openssl/x509v3.h> -+#include <stdint.h> - #include <stdio.h> -+#include <stdlib.h> - #include <string.h> --#include <time.h> --#include <gmni/gmni.h> --#include <gmni/tofu.h> - #include "util.h" - --static int --verify_callback(X509_STORE_CTX *ctx, void *data) -+static void -+xt_start_chain(const br_x509_class **ctx, const char *server_name) - { -- // Gemini clients handle TLS verification differently from the rest of -- // the internet. We use a TOFU system, so trust is based on two factors: -- // -- // - Is the certificate valid at the time of the request? -- // - Has the user trusted this certificate yet? -- // -- // If the answer to the latter is "no", then we give the user an -- // opportunity to explicitly agree to trust the certificate before -- // rejecting it. -- // -- // If you're reading this code with the intent to re-use it for -- // something unrelated to Gemini, think twice. -- struct gemini_tofu *tofu = (struct gemini_tofu *)data; -- X509 *cert = X509_STORE_CTX_get0_cert(ctx); -- struct known_host *host = NULL; -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ cc->server_name = server_name; -+ cc->err = 0; -+ cc->pkey = NULL; -+} - -- int rc; -- int day, sec; -- const ASN1_TIME *notBefore = X509_get0_notBefore(cert); -- const ASN1_TIME *notAfter = X509_get0_notAfter(cert); -- if (!ASN1_TIME_diff(&day, &sec, NULL, notBefore)) { -- rc = X509_V_ERR_UNSPECIFIED; -- goto invalid_cert; -+static void -+xt_start_cert(const br_x509_class **ctx, uint32_t length) -+{ -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ if (cc->err != 0) { -+ return; - } -- if (day > 0 || sec > 0) { -- rc = X509_V_ERR_CERT_NOT_YET_VALID; -- goto invalid_cert; -+ if (length == 0) { -+ cc->err = BR_ERR_X509_TRUNCATED; -+ return; - } -- if (!ASN1_TIME_diff(&day, &sec, NULL, notAfter)) { -- rc = X509_V_ERR_UNSPECIFIED; -- goto invalid_cert; -+ br_x509_decoder_init(&cc->decoder, NULL, NULL); -+ br_sha512_init(&cc->sha512); -+} -+ -+static void -+xt_append(const br_x509_class **ctx, const unsigned char *buf, size_t len) -+{ -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ if (cc->err != 0) { -+ return; - } -- if (day < 0 || sec < 0) { -- rc = X509_V_ERR_CERT_HAS_EXPIRED; -- goto invalid_cert; -+ br_x509_decoder_push(&cc->decoder, buf, len); -+ int err = br_x509_decoder_last_error(&cc->decoder); -+ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { -+ cc->err = err; - } -- -- unsigned char md[512 / 8]; -- const EVP_MD *sha512 = EVP_sha512(); -- unsigned int len = sizeof(md); -- rc = X509_digest(cert, sha512, md, &len); -- assert(rc == 1); -+ br_sha512_update(&cc->sha512, buf, len); -+} - -- char fingerprint[512 / 8 * 3]; -- for (size_t i = 0; i < sizeof(md); ++i) { -- snprintf(&fingerprint[i * 3], 4, "%02X%s", -- md[i], i + 1 == sizeof(md) ? "" : ":"); -+static void -+xt_end_cert(const br_x509_class **ctx) -+{ -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ if (cc->err != 0) { -+ return; -+ } -+ int err = br_x509_decoder_last_error(&cc->decoder); -+ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { -+ cc->err = err; -+ return; - } -- -- SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, -- SSL_get_ex_data_X509_STORE_CTX_idx()); -- const char *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); -- if (!servername) { -- rc = X509_V_ERR_HOSTNAME_MISMATCH; -- goto invalid_cert; -+ if (br_x509_decoder_isCA(&cc->decoder)) { -+ return; - } -+ cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); -+ br_sha512_out(&cc->sha512, &cc->hash); -+} - -- rc = X509_check_host(cert, servername, strlen(servername), 0, NULL); -- if (rc != 1) { -- rc = X509_V_ERR_HOSTNAME_MISMATCH; -- goto invalid_cert; -+static unsigned -+xt_end_chain(const br_x509_class **ctx) -+{ -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ if (cc->err != 0) { -+ return (unsigned)cc->err; -+ } -+ if (!cc->pkey) { -+ return BR_ERR_X509_EMPTY_CHAIN; - } - -- time_t now; -- time(&now); -+ char fingerprint[512 / 8 * 3]; -+ for (size_t i = 0; i < sizeof(cc->hash); ++i) { -+ snprintf(&fingerprint[i * 3], 4, "%02X%s", -+ cc->hash[i], i + 1 == sizeof(cc->hash) ? "" : ":"); -+ } - - enum tofu_error error = TOFU_UNTRUSTED_CERT; -- host = tofu->known_hosts; -+ (void)error; -+ struct known_host *host = cc->store->known_hosts; - while (host) { -- if (host->expires < now) { -- goto next; -- } -- if (strcmp(host->host, servername) != 0) { -+ if (strcmp(host->host, cc->server_name) != 0) { - goto next; - } - if (strcmp(host->fingerprint, fingerprint) == 0) { -@@ -102,66 +104,84 @@ next: - host = host->next; - } - -- rc = X509_V_ERR_CERT_UNTRUSTED; -- --callback: -- switch (tofu->callback(error, fingerprint, host, tofu->cb_data)) { -+ switch (cc->store->callback(error, fingerprint, -+ host, cc->store->cb_data)) { - case TOFU_ASK: - assert(0); // Invariant - case TOFU_FAIL: -- X509_STORE_CTX_set_error(ctx, rc); -- break; -+ return BR_ERR_X509_NOT_TRUSTED; - case TOFU_TRUST_ONCE: - // No further action necessary - return 0; - case TOFU_TRUST_ALWAYS:; -- FILE *f = fopen(tofu->known_hosts_path, "a"); -+ FILE *f = fopen(cc->store->known_hosts_path, "a"); - if (!f) { - fprintf(stderr, "Error opening %s for writing: %s\n", -- tofu->known_hosts_path, strerror(errno)); -+ cc->store->known_hosts_path, strerror(errno)); - break; - }; -- struct tm expires_tm; -- ASN1_TIME_to_tm(notAfter, &expires_tm); -- time_t expires = mktime(&expires_tm); -- fprintf(f, "%s %s %s %jd\n", servername, -- "SHA-512", fingerprint, (intmax_t)expires); -+ fprintf(f, "%s %s %s\n", cc->server_name, -+ "SHA-512", fingerprint); - fclose(f); - - host = calloc(1, sizeof(struct known_host)); -- host->host = strdup(servername); -+ host->host = strdup(cc->server_name); - host->fingerprint = strdup(fingerprint); -- host->expires = expires; -- host->lineno = ++tofu->lineno; -- host->next = tofu->known_hosts; -- tofu->known_hosts = host; -+ host->lineno = ++cc->store->lineno; -+ host->next = cc->store->known_hosts; -+ cc->store->known_hosts = host; - return 0; - } - -- X509_STORE_CTX_set_error(ctx, rc); -- return 0; -+ assert(0); // Unreachable -+} - --invalid_cert: -- error = TOFU_INVALID_CERT; -- goto callback; -+static const br_x509_pkey * -+xt_get_pkey(const br_x509_class *const *ctx, unsigned *usages) -+{ -+ struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -+ if (cc->err != 0) { -+ return NULL; -+ } -+ if (usages) { -+ // XXX: BearSSL doesn't pull the usages out of the X.509 for us -+ *usages = BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN; -+ } -+ return cc->pkey; - } - -+const br_x509_class xt_vtable = { -+ sizeof(struct x509_tofu_context), -+ xt_start_chain, -+ xt_start_cert, -+ xt_append, -+ xt_end_cert, -+ xt_end_chain, -+ xt_get_pkey, -+}; -+ -+static void -+x509_init_tofu(struct x509_tofu_context *ctx, struct gemini_tofu *store) -+{ -+ ctx->vtable = &xt_vtable; -+ ctx->store = store; -+} - - void --gemini_tofu_init(struct gemini_tofu *tofu, -- SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data) -+gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *cb_data) - { - const struct pathspec paths[] = { - {.var = "GMNIDATA", .path = "/%s"}, -- {.var = "XDG_DATA_HOME", .path = "/gemini/%s"}, -- {.var = "HOME", .path = "/.local/share/gemini/%s"} -+ {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, -+ {.var = "HOME", .path = "/.local/share/gmni/%s"} - }; - char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); - char dname[PATH_MAX+1]; - size_t n = 0; - -- n = snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -- path_fmt, "known_hosts"); -+ n = snprintf(tofu->known_hosts_path, -+ sizeof(tofu->known_hosts_path), -+ path_fmt, "known_hosts"); - assert(n < sizeof(tofu->known_hosts_path)); - - strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1); -@@ -179,10 +199,17 @@ free(path_fmt); - - tofu->callback = cb; - tofu->cb_data = cb_data; -- SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu); - - tofu->known_hosts = NULL; - -+ x509_init_tofu(&tofu->x509_ctx, tofu); -+ -+ br_x509_minimal_context _; // Discarded -+ br_ssl_client_init_full(&tofu->sc, &_, NULL, 0); -+ br_ssl_engine_set_x509(&tofu->sc.eng, &tofu->x509_ctx.vtable); -+ br_ssl_engine_set_buffer(&tofu->sc.eng, -+ &tofu->iobuf, sizeof(tofu->iobuf), 1); -+ - FILE *f = fopen(tofu->known_hosts_path, "r"); - if (!f) { - return; -@@ -191,6 +218,11 @@ n = 0; - int lineno = 1; - char *line = NULL; - while (getline(&line, &n, f) != -1) { -+ int ln = strlen(line); -+ if (line[ln-1] == '\n') { -+ line[ln-1] = 0; -+ } -+ - struct known_host *host = calloc(1, sizeof(struct known_host)); - char *tok = strtok(line, " "); - assert(tok); -@@ -207,10 +239,6 @@ - tok = strtok(NULL, " "); - assert(tok); - host->fingerprint = strdup(tok); -- -- tok = strtok(NULL, " "); -- assert(tok); -- host->expires = strtoul(tok, NULL, 10); - - host->lineno = lineno++; - -diff --git a/src/util.c b/src/util.c -index 2f62c29ce40ac012993e1d208d65491b56d204aa..780d0e8803ab9179683bd9a92cbead05624f2681 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -1,5 +1,7 @@ - #include <assert.h> -+#include <bearssl_ssl.h> - #include <errno.h> -+#include <gmni/gmni.h> - #include <libgen.h> - #include <limits.h> - #include <stdint.h> -@@ -7,7 +9,6 @@ #include <stdio.h> - #include <stdlib.h> - #include <string.h> - #include <sys/stat.h> --#include <gmni/gmni.h> - #include "util.h" - - static void -@@ -82,7 +83,7 @@ } - fprintf(out, "Downloading %s to %s\n", url, path); - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { -- n = BIO_read(resp.bio, buf, sizeof(buf)); -+ n = br_sslio_read(&resp.body, buf, sizeof(buf)); - if (n == -1) { - fprintf(stderr, "Error: read\n"); - return 1; diff --git a/sources/cgmnlm.git/commits/fc6d4a6f69e305627d06955da27b8e8c4c5af6e0.patch b/sources/cgmnlm.git/commits/fc6d4a6f69e305627d06955da27b8e8c4c5af6e0.patch @@ -1,96 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index d5cb2dc29f9ceb448622d5c36b7b73f0ca985bcb..5d7892b83f7914da46b08100014db6211e36173a 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -11,6 +11,7 @@ #include <stdio.h> - #include <string.h> - #include <sys/ioctl.h> - #include <sys/stat.h> -+#include <sys/wait.h> - #include <termios.h> - #include <unistd.h> - #include "gmni.h" -@@ -79,6 +80,7 @@ "m\tSave bookmark\n" - "M\tBrowse bookmarks\n" - "r\tReload the page\n" - "d <path>\tDownload page to <path>\n" -+ "|<prog>\tPipe page into program\n" - "\n" - "Other commands include:\n\n" - "<Enter>\tread more lines\n" -@@ -309,6 +311,55 @@ } - return strcmp(&str[strl - suffl], suff) == 0; - } - -+static void -+pipe_resp(FILE *out, struct gemini_response resp, char *cmd) { -+ char buf[BUFSIZ]; -+ int pfd[2]; -+ if (pipe(pfd) == -1) { -+ perror("pipe"); -+ return; -+ } -+ pid_t pid; -+ switch ((pid = fork())) { -+ case -1: -+ perror("fork"); -+ return; -+ case 0: -+ close(pfd[1]); -+ dup2(pfd[0], STDIN_FILENO); -+ close(pfd[0]); -+ execlp("sh", "sh", "-c", cmd); -+ perror("exec"); -+ _exit(1); -+ } -+ close(pfd[0]); -+ FILE *f = fdopen(pfd[1], "w"); -+ // XXX: may affect history, do we care? -+ for (int n = 1; n > 0;) { -+ n = BIO_read(resp.bio, buf, BUFSIZ); -+ if (n == -1) { -+ fprintf(stderr, "Error: read\n"); -+ return; -+ } -+ ssize_t w = 0; -+ while (w < (ssize_t)n) { -+ ssize_t x = fwrite(&buf[w], 1, n - w, f); -+ if (ferror(f)) { -+ fprintf(stderr, "Error: write: %s\n", -+ strerror(errno)); -+ return; -+ } -+ w += x; -+ } -+ } -+ fclose(f); -+ int status; -+ waitpid(pid, &status, 0); -+ if (status != 0) { -+ fprintf(out, "Command exited %d\n", status); -+ } -+} -+ - static enum gemini_result - do_requests(struct browser *browser, struct gemini_response *resp) - { -@@ -570,6 +621,19 @@ } - set_url(browser, url, NULL); - download_resp(browser->tty, resp, trim_ws(&in[1]), url); - gemini_response_finish(&resp); -+ result = PROMPT_AGAIN; -+ goto exit; -+ case '|': -+ strncpy(&url[0], browser->plain_url, sizeof(url)); -+ res = do_requests(browser, &resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", -+ gemini_strerr(res, &resp)); -+ goto exit; -+ } -+ pipe_resp(browser->tty, resp, &in[1]); -+ gemini_response_finish(&resp); -+ set_url(browser, url, NULL); - result = PROMPT_AGAIN; - goto exit; - case '?': diff --git a/sources/cgmnlm.git/commits/fd914b3aa40d63eb7fb2aafb1f6a30c1f78ee92f.patch b/sources/cgmnlm.git/commits/fd914b3aa40d63eb7fb2aafb1f6a30c1f78ee92f.patch @@ -1,40 +0,0 @@ -diff --git a/src/gmnic.c b/src/gmnic.c -index 4ddcb1d83bf03fba67fd2231c81c38c6e38b2136..3fcfa059fe344aa4dc96bd90c4f313a4b6e755cd 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -34,7 +34,7 @@ INPUT_SUPPRESS, - }; - enum input_mode input_mode = INPUT_READ; - FILE *input_source = stdin; -- bool linefeed = true; -+ bool follow_redirects = false, linefeed = true; - - int c; - while ((c = getopt(argc, argv, "46C:d:D:hlLiIN")) != -1) { -@@ -72,7 +72,7 @@ case 'l': - linefeed = false; - break; - case 'L': -- assert(0); // TODO: Follow redirects -+ follow_redirects = true; - break; - case 'i': - header_mode = SHOW_HEADERS; -@@ -142,7 +142,16 @@ free(url); - url = new_url; - goto next; - case 3: // REDIRECT -- assert(0); // TODO -+ free(url); -+ url = strdup(resp.meta); -+ if (!follow_redirects) { -+ if (header_mode == OMIT_HEADERS) { -+ fprintf(stderr, "REDIRECT: %d %s\n", -+ resp.status, resp.meta); -+ } -+ exit = true; -+ } -+ goto next; - case 6: // CLIENT CERTIFICATE REQUIRED - assert(0); // TODO - case 4: // TEMPORARY FAILURE diff --git a/sources/cgmnlm.git/commits/fed9c0561947126442fbb32e3437922dcc467ef1.patch b/sources/cgmnlm.git/commits/fed9c0561947126442fbb32e3437922dcc467ef1.patch @@ -1,39 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index e6910607646fdd4f4d5a04a052715eda35147c30..391d2b4a2a2dddd7e688a950bb852dfa0687cc32 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -154,8 +154,6 @@ fprintf(f, "\n"); - return 0; - } - for (int i = 0; s[i]; ++i) { -- // TODO: Other control sequences, and eat ANSI escapes before -- // they become a problem - switch (s[i]) { - case '\n': - assert(0); // Not supposed to happen -@@ -163,6 +161,9 @@ case '\t': - *col = *col + (8 - *col % 8); - break; - default: -+ if (iscntrl(s[i])) { -+ s[i] = '.'; -+ } - *col += 1; - break; - } -@@ -188,7 +189,6 @@ - static bool - display_gemini(struct browser *browser, struct gemini_response *resp) - { -- // TODO: Strip ANSI escape sequences - int nlinks = 0; - struct gemini_parser p; - gemini_parser_init(&p, resp->bio); -@@ -203,6 +203,7 @@ struct link **next = &browser->links; - while (text != NULL || gemini_parser_next(&p, &tok) == 0) { - switch (tok.token) { - case GEMINI_TEXT: -+ // TODO: Run other stuff through wrap() - if (text == NULL) { - text = tok.text; - } diff --git a/sources/cgmnlm.git/commits/ffc89b6cf01e8b68232b3de3f001d2e635936dfa.patch b/sources/cgmnlm.git/commits/ffc89b6cf01e8b68232b3de3f001d2e635936dfa.patch @@ -1,155 +0,0 @@ -diff --git a/configure b/configure -index 7b55a2580018cc69b2f52c9bc53832cc9faadc4f..6abccdd18d0b85e4b2d4b527d49727d360dec5c3 100755 ---- a/configure -+++ b/configure -@@ -15,6 +15,7 @@ } - - cgmnlm() { - genrules cgmnlm \ -+ src/certs.c \ - src/client.c \ - src/escape.c \ - src/gmnlm.c \ -@@ -26,6 +27,7 @@ } - - libgmni_a() { - genrules libgmni.a \ -+ src/certs.c \ - src/client.c \ - src/escape.c \ - src/tofu.c \ -diff --git a/include/gmni/certs.h b/include/gmni/certs.h -index 22e226d6b4252dddd8526970cec0a947f12242d1..53b8aad27fa50649ddfdacbd08724d5d0b033120 100644 ---- a/include/gmni/certs.h -+++ b/include/gmni/certs.h -@@ -20,7 +20,7 @@ }; - unsigned char data[]; - }; - --// Returns nonzero on failure and sets errno -+// Returns nonzero on failure and sets errno. Closes both files. - int gmni_ccert_load(struct gmni_client_certificate *cert, - FILE *certin, FILE *skin); - -diff --git a/src/gmnlm.c b/src/gmnlm.c -index aeb0c834d49c799fcdae54acfcbd3925dcedd392..12a6c971b7ae7d18bcdafc3b2b81f55539340095 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -4,6 +4,7 @@ #include <ctype.h> - #include <errno.h> - #include <fcntl.h> - #include <getopt.h> -+#include <gmni/certs.h> - #include <gmni/gmni.h> - #include <gmni/tofu.h> - #include <gmni/url.h> -@@ -434,13 +435,50 @@ { - int nredir = 0; - bool requesting = true; - enum gemini_result res; -- while (requesting) { -- char *scheme; -+ -+ char *scheme; -+ CURLUcode uc = curl_url_get(browser->url, -+ CURLUPART_SCHEME, &scheme, 0); -+ assert(uc == CURLUE_OK); // Invariant -+ -+ char *host = NULL; -+ struct gmni_client_certificate client_cert = {0}; -+ const struct pathspec paths[] = { -+ {.var = "GMNIDATA", .path = "/certs/%s.%s"}, -+ {.var = "XDG_DATA_HOME", .path = "/gmni/certs/%s.%s"}, -+ {.var = "HOME", .path = "/.local/share/gmni/certs/%s.%s"} -+ }; -+ char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); -+ char certpath[PATH_MAX+1], keypath[PATH_MAX+1]; -+ size_t n = 0; -+ -+ if (strcmp(scheme, "gemini") == 0) { - CURLUcode uc = curl_url_get(browser->url, -- CURLUPART_SCHEME, &scheme, 0); -- assert(uc == CURLUE_OK); // Invariant -+ CURLUPART_HOST, &host, 0); -+ assert(uc == CURLUE_OK); -+ -+ n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); -+ assert(n < sizeof(certpath)); -+ FILE *certin = fopen(certpath, "r"); -+ if (certin) { -+ n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); -+ assert(n < sizeof(keypath)); -+ -+ FILE *skin = fopen(keypath, "r"); -+ if (gmni_ccert_load(&client_cert, certin, skin)) { -+ browser->opts.client_cert = NULL; -+ fprintf(stderr, "Unable to load client certificate for host %s", host); -+ } else { -+ browser->opts.client_cert = &client_cert; -+ } -+ } else { -+ browser->opts.client_cert = NULL; -+ } -+ free(host); -+ } -+ -+ while (requesting) { - if (strcmp(scheme, "file") == 0) { -- free(scheme); - requesting = false; - - char *path; -@@ -475,9 +513,9 @@ free(path); - resp->status = GEMINI_STATUS_SUCCESS; - resp->fd = fd; - resp->sc = NULL; -- return GEMINI_OK; -+ res = GEMINI_OK; -+ goto out; - } -- free(scheme); - - res = gemini_request(browser->plain_url, &browser->opts, - &browser->tofu, resp); -@@ -519,7 +557,19 @@ } - set_url(browser, resp->meta, NULL); - break; - case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -- assert(0); // TODO -+ requesting = false; -+ assert(host); -+ n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); -+ assert(n < sizeof(certpath)); -+ n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); -+ assert(n < sizeof(keypath)); -+ fprintf(stderr, "The server requested a client certificate.\n" -+ "Presently, this process is not automated.\n" -+ "The following OpenSSL command will generate a certificate for this host:\n\n" -+ "openssl req -x509 -newkey rsa:4096 \\\n\t-keyout %s \\\n\t-out %s \\\n\t-days 36500 -nodes\n\n" -+ "Use the 'r' command to reload the page after creating this certificate.\n", -+ keypath, certpath); -+ break; - case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: - case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: - requesting = false; -@@ -529,7 +579,7 @@ "TEMPORARY FAILURE" : "PERMANENT FAILURE", - resp->status, resp->meta); - break; - case GEMINI_STATUS_CLASS_SUCCESS: -- return res; -+ goto out; - } - - if (requesting) { -@@ -537,6 +587,11 @@ gemini_response_finish(resp); - } - } - -+out: -+ if (client_cert.key) { -+ free(client_cert.key); -+ } -+ free(scheme); - return res; - } - diff --git a/sources/cgmnlm.git/commits/index.gmi b/sources/cgmnlm.git/commits/index.gmi @@ -1,1307 +0,0 @@ -# Commits - -## 3c63a64288f665a272974698d547bbca79769d5a -Author: René Wagner <rwa@clttr.info> -Date: Fri Jul 02 15:09:40 2021 +0200 -=> 3c63a64288f665a272974698d547bbca79769d5a.patch Patch -Message: fix typo in error message when removing a bookmark - -## 86b299819c86758f2b537c1de0475a2906f0a4d2 -Author: René Wagner <rwa@clttr.info> -Date: Thu Jul 01 20:22:01 2021 +0200 -=> 86b299819c86758f2b537c1de0475a2906f0a4d2.patch Patch -Message: add hints to history for easier navigation - -This commit adds simple hints in front of the URIs on the history -page to directly show what needs to be typed to jump to this page. - -## 18ead2644a8c525d1d3bbc729d9ccd9aa7e0d63c -Author: René Wagner <rwa@clttr.info> -Date: Mon Jun 28 22:34:26 2021 +0200 -=> 18ead2644a8c525d1d3bbc729d9ccd9aa7e0d63c.patch Patch -Message: add 'u' command to navigate one path element up - -## 4dd50ac07e82dfc1785f98a3535109e2d738029d -Author: k1nkreet <polyakovskiy.ilya@gmail.com> -Date: Tue Jun 15 19:37:56 2021 +0300 -=> 4dd50ac07e82dfc1785f98a3535109e2d738029d.patch Patch -Message: gmni: headers are not displayed for REDIRECT and INPUT responses in SHOW_HEADERS and ONLY_HEADERS modes - -I've noticed headers are not displayed in some cases with -i/-I -specified. For example: - -echo "printf" | gmni -i gemini://drewdevault.com/cgi-bin/man.sh -Output: empty - -echo "printf" | gmni -IL gemini://drewdevault.com/cgi-bin/man.sh - -Output: 10 Search for a POSIX man page -INPUT header is here, but no REDIRECT header appeared. - -The reason is headers processing is done after responses dispatch. So -some responses (redirect and input) are processed and dropped before. -Patch makes this logic a bit clearer imho: print response header before -any processing if mode is not OMIT_HEADERS and then process response body if mode -is not ONLY_HEADERS. It also deduplicates header printing as a bonus. - -## 7e4e43b05c298aa812027bf1921ce3f224e86bda -Author: Andrew <git@andrewzigerelli.com> -Date: Thu Jun 10 07:36:37 2021 -0400 -=> 7e4e43b05c298aa812027bf1921ce3f224e86bda.patch Patch -Message: gmnlm: host freed too early, causing UAF - -The host variable is freed too early. If a client certificate is not -found, the later error message in the -GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED case uses the freed host -variable to produce an incorrect openssl command. This fix just delays -the free to after the switch statement. - -Test case: -gmnlm gemini://feeds.drewdevault.com - -Prior: -The following OpenSSL command will generate a certificate for this host: - -openssl req -x509 -newkey rsa:4096 \ - -keyout /home/andrew/.local/share/gmni/certs/€Ú-=öU.key \ - -out /home/andrew/.local/share/gmni/certs/€Ú-=öU.crt \ - -days 36500 -nodes - -Now: -The following OpenSSL command will generate a certificate for this host: - -openssl req -x509 -newkey rsa:4096 \ --keyout /home/andrew/.local/share/gmni/certs/feeds.drewdevault.com.key \ --out /home/andrew/.local/share/gmni/certs/feeds.drewdevault.com.crt \ --days 36500 -nodes - -## 4274b06fe4b2702af297cd0cee3d7871741899ec -Author: René Wagner <rwa@clttr.info> -Date: Fri May 21 21:54:15 2021 +0200 -=> 4274b06fe4b2702af297cd0cee3d7871741899ec.patch Patch -Message: README cosmetics - -## d06c4cda5c5538ea71401bbfb9ff0bb6657d8413 -Author: René Wagner <rwagner@rw-net.de> -Date: Sat Apr 10 21:27:58 2021 +0200 -=> d06c4cda5c5538ea71401bbfb9ff0bb6657d8413.patch Patch -Message: introduce -A param for default to alt text - -## c3aa884144bb173073c6b973835a266bae27bf1e -Author: René Wagner <rwagner@rw-net.de> -Date: Fri Apr 09 21:38:45 2021 +0200 -=> c3aa884144bb173073c6b973835a266bae27bf1e.patch Patch -Message: minor improvements in rendering alt text - -## b54a100d7156ea279641a9e779b7658c42300fe9 -Author: Zach DeCook <zachdecook@librem.one> -Date: Fri Apr 02 09:31:00 2021 -0400 -=> b54a100d7156ea279641a9e779b7658c42300fe9.patch Patch -Message: gmnlm: Improve paging behavior on narrow terminals - -## 8ddc99fdc336957d7565cd50e329da9cbe9e4de8 -Author: Zach DeCook <zachdecook@librem.one> -Date: Fri Apr 02 09:31:01 2021 -0400 -=> 8ddc99fdc336957d7565cd50e329da9cbe9e4de8.patch Patch -Message: gmnlm: Include blank line to frame browser window when reading more - -## 801d9b8f13f6adef25fb14ec2e9acbc6dd4e92a9 -Author: René Wagner <rwagner@rw-net.de> -Date: Thu Mar 25 20:19:46 2021 +0100 -=> 801d9b8f13f6adef25fb14ec2e9acbc6dd4e92a9.patch Patch -Message: allow toggling between preformatted and alt text - -alt text is prefixed with A -preformatted text is prefixed with P - -closes #13 #10 - -## 78cfe1b669fad6b7a3638d371ed9825e2ee53243 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Tue Mar 16 00:07:27 2021 -0400 -=> 78cfe1b669fad6b7a3638d371ed9825e2ee53243.patch Patch -Message: all: use posix_dirname rather than dirname - -## dbc726616e6675ae82d9bb55be5693371255ed2f -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Mar 15 19:50:52 2021 +0100 -=> dbc726616e6675ae82d9bb55be5693371255ed2f.patch Patch -Message: Merge branch 'bearssl' - -## 479ea9e74f4b66645c0d7b51d99adf420d831f23 -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Mar 09 17:22:19 2021 +0100 -=> 479ea9e74f4b66645c0d7b51d99adf420d831f23.patch Patch -Message: add s command for searching in geminispace - -## d3afac098e2bf0a4ed9fad89f6a748f6288ae3bc -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Mar 01 20:27:05 2021 +0100 -=> d3afac098e2bf0a4ed9fad89f6a748f6288ae3bc.patch Patch -Message: add search engine to default bookmark - -## e7b2013160db902df5b833ff2e49eaff90807514 -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Mar 01 20:24:24 2021 +0100 -=> e7b2013160db902df5b833ff2e49eaff90807514.patch Patch -Message: keep 4 char border an right side - -## 100759a7d796f4e486a89b65b3ca491c1141056f -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Feb 23 07:56:32 2021 +0100 -=> 100759a7d796f4e486a89b65b3ca491c1141056f.patch Patch -Message: fix display of message on TOFU_FINGERPRINT_MISMATCH - -Previously the message was never displayed to users -leaving them with a simple "Error: certificate is untrusted". - -This also fixes the display of line numbers in the message. - -## 8cac260a4b7c0b4df4d1229a5e41e64c3a687173 -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Feb 07 11:17:45 2021 +0100 -=> 8cac260a4b7c0b4df4d1229a5e41e64c3a687173.patch Patch -Message: 'e' command without [N] sends current URL to external - -closes #6 - -## 26666e7838fd40ca7d6f20af7e0cb554ff8bb0f0 -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Feb 01 17:11:12 2021 +0100 -=> 26666e7838fd40ca7d6f20af7e0cb554ff8bb0f0.patch Patch -Message: fix wrong uri scheme - -## f8f6c5a12beeaa4ad614cc349a04e2984c06c83b -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Feb 01 17:11:12 2021 +0100 -=> f8f6c5a12beeaa4ad614cc349a04e2984c06c83b.patch Patch -Message: fix wrong uri scheme - -## 22a28fa92755b49254cac144894be1fdb917a6a3 -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Jan 31 17:41:11 2021 +0100 -=> 22a28fa92755b49254cac144894be1fdb917a6a3.patch Patch -Message: jump more than one entry back or forth in history - -by giving an optional number to b & f commands. -The default behaviour of b & f commands has not -been changed. - -## e299aec4a10ba3aaf01c50263cbcabe8d39a5214 -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Jan 24 21:00:18 2021 +0100 -=> e299aec4a10ba3aaf01c50263cbcabe8d39a5214.patch Patch -Message: revert kay binding modification - -we'd stay close to upstream if possible - -## 9a195d92566b5790b2b7d3ca848987a095bf3d9c -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Jan 19 15:21:31 2021 +0100 -=> 9a195d92566b5790b2b7d3ca848987a095bf3d9c.patch Patch -Message: fix indentation on help output - -## 1cfe0e794936cc51b9306327634a35f1c443643f -Author: Giuseppe Lumia <g.lumia@outlook.com> -Date: Mon Jan 18 21:26:48 2021 +0100 -=> 1cfe0e794936cc51b9306327634a35f1c443643f.patch Patch -Message: Fix incorrectly missing -g flag - -On systems using dsymutil the check for the "-g" flag was failing not -because the compiler didn't provide it but because of `/dev/null` -being used as output file. - -## dde8799e758b1e6c3985492a8205c189d4d47b9c -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Jan 10 16:54:10 2021 +0100 -=> dde8799e758b1e6c3985492a8205c189d4d47b9c.patch Patch -Message: implement s command to remove bookmark for current page - -It will remove all lines from the bookmark file that -match the URL of the current page viewed in the browser. - -## 9ef33fb102426d0bf56e93ceebcd81eb24171a9e -Author: René Wagner <rwagner@rw-net.de> -Date: Fri Jan 08 21:46:27 2021 +0100 -=> 9ef33fb102426d0bf56e93ceebcd81eb24171a9e.patch Patch -Message: minor rendering adjustments - -## 0ed7a4527c967ce3f14909923277cf62624f0900 -Author: Giuseppe Lumia <g.lumia@outlook.com> -Date: Thu Jan 07 09:15:33 2021 +0100 -=> 0ed7a4527c967ce3f14909923277cf62624f0900.patch Patch -Message: Fix OpenBSD compilation errors - -Those changes fix the following compilation errors on OpenBSD: - -src/tofu.c:128:28: error: format specifies type 'long' but the argument has type - 'time_t' (aka 'long long') [-Werror,-Wformat] - "SHA-512", fingerprint, expires); - -src/gmnlm.c:341:31: error: missing sentinel in function call - [-Werror,-Wsentinel] - execlp("sh", "sh", "-c", cmd); - ^ - , NULL - -## 6d2f78eeded101ccd755b1b2be16105fe5af881d -Author: René Wagner <rwagner@rw-net.de> -Date: Thu Jan 07 19:28:43 2021 +0100 -=> 6d2f78eeded101ccd755b1b2be16105fe5af881d.patch Patch -Message: keep source names for easier downstreaming patches - -## a6e0326291eee1e1f8ee723ac1e8467ed0561e86 -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Jan 06 20:50:43 2021 +0100 -=> a6e0326291eee1e1f8ee723ac1e8467ed0561e86.patch Patch -Message: different colors for local and remote gemini links - -## 1da4ff928a44f590e2c72cda1dcb4b097845cbc3 -Author: René Wagner <rwa@src.clttr.info> -Date: Wed Jan 06 19:50:24 2021 +0100 -=> 1da4ff928a44f590e2c72cda1dcb4b097845cbc3.patch Patch -Message: Update 'README.md' - -## c8041a15ac7d36ecc2e1c34dcaa14c51e62de788 -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Jan 06 19:36:39 2021 +0100 -=> c8041a15ac7d36ecc2e1c34dcaa14c51e62de788.patch Patch -Message: t[N] command - -t[N] downloads the content behind Nth link to -a tempfile in /tmp/ - -closes #2 - -## 4c0f931d6688d06df2e22d001182f6fa1b776fab -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Jan 05 20:17:06 2021 +0100 -=> 4c0f931d6688d06df2e22d001182f6fa1b776fab.patch Patch -Message: implement e[N] command - -this command sends the URI of Nth link to `xdg-open` to open -the assoziated default program and pass the URI. - -## b39e196040623a80bf9f1a0a05c3da8523e26ee3 -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Jan 05 19:16:06 2021 +0100 -=> b39e196040623a80bf9f1a0a05c3da8523e26ee3.patch Patch -Message: use light-gray for quotes - -## 3270a74590d4bfbbc9ae1fdc4c1d36eed943844f -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Jan 05 17:45:56 2021 +0100 -=> 3270a74590d4bfbbc9ae1fdc4c1d36eed943844f.patch Patch -Message: preserve all bytes except spaces when wrapping - -When wrapping the new line should not start with a space. -All other bytes must be preserved to avoid breaking unicode chars. - -fix for ~sircmpwn/gmni#21 - -## 320676ca5bc1980c96f5e4bc14240a741be8f3be -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Jan 04 20:33:37 2021 +0100 -=> 320676ca5bc1980c96f5e4bc14240a741be8f3be.patch Patch -Message: update .gitignore - -## cc3f9a5eea25ad350039d7e552bec944b3a121b0 -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Jan 04 19:07:12 2021 +0100 -=> cc3f9a5eea25ad350039d7e552bec944b3a121b0.patch Patch -Message: use different colors for gemini and other links - -## 3ce02e5183da68e017b572265d68f19fef59043c -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Jan 04 19:05:25 2021 +0100 -=> 3ce02e5183da68e017b572265d68f19fef59043c.patch Patch -Message: rename to cgmnlm - -## 0513b91be1173b1ed43a0f1d28cf502a81267185 -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Jan 04 13:08:11 2021 +0100 -=> 0513b91be1173b1ed43a0f1d28cf502a81267185.patch Patch -Message: fix premature line wraps - -caused by multibyte unicode characters. -During column count unicode continuation bytes -are not counted as columns. - -## 71ececc4f264eed36f022b4b52c9100b9e7b1b12 -Author: René Wagner <rwagner@rw-net.de> -Date: Fri Jan 01 12:41:02 2021 +0100 -=> 71ececc4f264eed36f022b4b52c9100b9e7b1b12.patch Patch -Message: add [N]| command to help - -## ae43b9190e1a18796222b94ec1e78b35f5826964 -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Dec 23 18:06:50 2020 +0100 -=> ae43b9190e1a18796222b94ec1e78b35f5826964.patch Patch -Message: add coloring of headings and links - -modify indenting of gemtext lines to always -indent 4 chars - -## c0c891f87b101db98c589da9a60e4a39bf048f0d -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Dec 23 17:04:46 2020 +0100 -=> c0c891f87b101db98c589da9a60e4a39bf048f0d.patch Patch -Message: add 2 empty lines on top for better readability - -## 84da4b3f2b95bead2c1609eb572a2369576eae77 -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Dec 23 16:27:06 2020 +0100 -=> 84da4b3f2b95bead2c1609eb572a2369576eae77.patch Patch -Message: modify keybindings - -## f5d540bc5d0112895376aebe6bf54adb32545d6e -Author: Eyal Sawady <ecs@d2evs.net> -Date: Mon Nov 30 19:44:59 2020 -0500 -=> f5d540bc5d0112895376aebe6bf54adb32545d6e.patch Patch -Message: Implement <n>| command - -## d8f0870446c471a42612d6a8e853ad9b723a6d39 -Author: Alexey Yerin <yerinalexey98fd@gmail.com> -Date: Sun Nov 22 20:52:48 2020 +0300 -=> d8f0870446c471a42612d6a8e853ad9b723a6d39.patch Patch -Message: Refactor gmni to use Curl_URL - -This also fixes relative redirects. - -Signed-off-by: Alexey Yerin <yerinalexey98fd@gmail.com> - -## 8a83030e5a390c2151c485b3c091ba28ddebcab7 -Author: William Casarin <jb55@jb55.com> -Date: Fri Nov 20 08:15:25 2020 -0800 -=> 8a83030e5a390c2151c485b3c091ba28ddebcab7.patch Patch -Message: Fix more strncpy bugs in gmnlm and tofu - ->From gcc 9.3.0: - -error: '__builtin_strncpy' specified bound 4097 equals destination size -[-Werror=stringop-truncation] - -Signed-off-by: William Casarin <jb55@jb55.com> - -## 8970adc23e0a1bcf29d211f353dbd5ebd68cfe66 -Author: Tommy Nguyen <remyabel@gmail.com> -Date: Wed Nov 18 04:44:06 2020 -0500 -=> 8970adc23e0a1bcf29d211f353dbd5ebd68cfe66.patch Patch -Message: Fix typo in error message. - -## cb63b8ddf093711607ff98e933d4bd04154a854b -Author: Giuseppe Lumia <g.lumia@outlook.com> -Date: Wed Nov 11 23:52:03 2020 +0100 -=> cb63b8ddf093711607ff98e933d4bd04154a854b.patch Patch -Message: Fix bug on mkdirs calls - -On some systems dirname uses a static string for its return value, so -we were calling mkdirs recursively on a string that was continuosly changing. - -A check was also added after the `snprintf` to make sure there's no -information loss since there is no limit to the length of the string -returned by `get_data_pathfmt`. - -Closes #48. - -## ec88f4558c48b3eece906143867dfba6d81e5e49 -Author: William Casarin <jb55@jb55.com> -Date: Sat Nov 07 14:58:32 2020 -0800 -=> ec88f4558c48b3eece906143867dfba6d81e5e49.patch Patch -Message: gmnlm: fix a few strncpy compile errors on gcc 9.3 - -In file included from .../include/string.h:495, - from src/gmnlm.c:11: -In function ‘strncpy’, - inlined from ‘do_prompts’ at src/gmnlm.c:627:3: -...glibc-2.31-dev/include/bits/string_fortified.h:106:10: -error: ‘__builtin_strncpy’ specified bound 1024 equals -destination size [-Werror=stringop-truncation] -return __builtin___strncpy_chk (__dest, __src, __len, __bos (__dest)); - ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In function ‘strncpy’, - inlined from ‘do_prompts’ at src/gmnlm.c:612:3: -...glibc-2.31-dev/include/bits/string_fortified.h:106:10: -error: ‘__builtin_strncpy’ specified bound 1024 equals destination size -[-Werror=stringop-truncation] -return __builtin___strncpy_chk (__dest, __src, __len, __bos (__dest)); - ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Signed-off-by: William Casarin <jb55@jb55.com> - -## ab66dd2be92931bef04cbccdb3aa008615bd8eba -Author: Giuseppe Lumia <g.lumia@outlook.com> -Date: Sat Nov 07 01:23:06 2020 +0100 -=> ab66dd2be92931bef04cbccdb3aa008615bd8eba.patch Patch -Message: Simplify posix_dirname logic - -dirname has two main problems: -1. It could change in place the string that is passed to it. -2. It uses a static string for its return value, so one should copy it - somewhere else as soon as possible to avoid subsequent calls to - dirname to corrupt his data (see #48). - -We avoid 1. passing a copy of `path` to dirname and 2. copying it's -return value into `dname`. - -## 4a6172f1bf9cb41eb1ce3a5f720f9ebe4febc62b -Author: Giuseppe Lumia <g.lumia@outlook.com> -Date: Wed Nov 04 09:08:45 2020 +0100 -=> 4a6172f1bf9cb41eb1ce3a5f720f9ebe4febc62b.patch Patch -Message: Remove -D option from install commands - -The '-D' option of `install` is not implemented on some systems that -don't use GNU install. Its uses were replaced with `mkdir -p` commands -to eventually provide the missing folders. - -## 1a747cb6c2765ee818506c886bad4ed36b2b9d51 -Author: Giuseppe Lumia <g.lumia@outlook.com> -Date: Wed Nov 04 09:08:43 2020 +0100 -=> 1a747cb6c2765ee818506c886bad4ed36b2b9d51.patch Patch -Message: Fix install in case of missing docs compilation - -Install on systems without `scdoc` was broken because `make install` was -attempting to install missing files. - -## 61af57e302efd90458e17fa9f0bfaf5b3828954f -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Nov 03 10:06:13 2020 +0100 -=> 61af57e302efd90458e17fa9f0bfaf5b3828954f.patch Patch -Message: fix typo in PERMANENT FAILURE response - -## 021d8f8fdfcb9be636a73d7c59d540d8255cc0df -Author: Jon Higgs <jon.higgs@redbubble.com> -Date: Tue Nov 03 08:37:21 2020 +1100 -=> 021d8f8fdfcb9be636a73d7c59d540d8255cc0df.patch Patch -Message: Improve the -j usage - -## 4134dc1b4a37b65f8176d799f03342583b49d932 -Author: Alexandre Oliveira <eu@aoalmeida.com> -Date: Mon Nov 02 19:37:26 2020 +0100 -=> 4134dc1b4a37b65f8176d799f03342583b49d932.patch Patch -Message: Check if pkg-config is valid before checking for libs - -## c036a43801d60b620262687e4bb6d98f97e23dbd -Author: Connor Kuehl <cipkuehl@gmail.com> -Date: Sat Oct 31 17:51:39 2020 -0500 -=> c036a43801d60b620262687e4bb6d98f97e23dbd.patch Patch -Message: Initialize result if res != GEMINI_OK - -My compiler barks about this unitialized variable: - -CC src/gmnlm.o -src/gmnlm.c:629:7: error: variable 'result' is used uninitialized whenever 'if' condition is true [-Werror,-Wsometimes-uninitialized] - if (res != GEMINI_OK) { - ^~~~~~~~~~~~~~~~ -src/gmnlm.c:673:9: note: uninitialized use occurs here - return result; - ^~~~~~ -src/gmnlm.c:629:3: note: remove the 'if' if its condition is always false - if (res != GEMINI_OK) { - ^~~~~~~~~~~~~~~~~~~~~~~ -src/gmnlm.c:482:2: note: variable 'result' is declared here - enum prompt_result result; - ^ -1 error generated. -make: *** [src/gmnlm.o] Error 1 - -## 514cb373019e94f743424b2813602722ca09b917 -Author: Joe Jenne <joe@jhjn.xyz> -Date: Thu Oct 29 22:54:27 2020 +0000 -=> 514cb373019e94f743424b2813602722ca09b917.patch Patch -Message: Fix const comparison Werror - -## 75087ce65f54c86d44ce13bb63e1041226a53f7b -Author: Cédric Hannotier <cedric.hannotier@ulb.be> -Date: Thu Oct 29 22:59:55 2020 +0100 -=> 75087ce65f54c86d44ce13bb63e1041226a53f7b.patch Patch -Message: {var//pattern/replacement} is not POSIX compliant - -## ac86b2f9fece0e39be57b81e02cb9946a10df570 -Author: Cédric Hannotier <cedric.hannotier@ulb.be> -Date: Thu Oct 29 22:57:54 2020 +0100 -=> ac86b2f9fece0e39be57b81e02cb9946a10df570.patch Patch -Message: Separate path and read buffers & use snprintf - -## b64d3d5ac9121bd3c5df1c48defe5fdc209e467d -Author: Luna Nieves <aicaya@posteo.net> -Date: Thu Oct 29 00:03:01 2020 -0400 -=> b64d3d5ac9121bd3c5df1c48defe5fdc209e467d.patch Patch -Message: Add uninstall target - -## ce1a524642e25da8a66a18797e9afc8f54f20903 -Author: Martijn Braam <martijn@brixit.nl> -Date: Sat Oct 24 23:37:45 2020 +0200 -=> ce1a524642e25da8a66a18797e9afc8f54f20903.patch Patch -Message: Added missing filename in install - -## 8d897e4a00be9986209f1ca394ed46befadf6088 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Sat Oct 24 14:04:50 2020 -0400 -=> 8d897e4a00be9986209f1ca394ed46befadf6088.patch Patch -Message: Remove -fPIC - -Breaks compilation under cproc and isn't necessary for static libraries. - -## f6643cf1b5ecbdd030420fb504ae2edcb9102410 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Sat Oct 24 13:55:33 2020 -0400 -=> f6643cf1b5ecbdd030420fb504ae2edcb9102410.patch Patch -Message: Re-add public headers - -## 122fb0a9fd5456e3b1fd9f084130df85c859394b -Author: Martijn Braam <martijn@brixit.nl> -Date: Sat Oct 24 19:40:39 2020 +0200 -=> 122fb0a9fd5456e3b1fd9f084130df85c859394b.patch Patch -Message: Add static library for gmni - -## 49c0c523c69842f8ebc33135947591cf6f7a7cab -Author: Drew DeVault <sir@cmpwn.com> -Date: Sat Oct 24 11:05:17 2020 -0400 -=> 49c0c523c69842f8ebc33135947591cf6f7a7cab.patch Patch -Message: Fix possibly uninitialized variable - -## afab58cb64f205ce9f469a328a7477b808b0c76c -Author: Eyal Sawady <ecs@d2evs.net> -Date: Wed Oct 21 10:15:07 2020 -0400 -=> afab58cb64f205ce9f469a328a7477b808b0c76c.patch Patch -Message: Fix plaintext display - -## fc6d4a6f69e305627d06955da27b8e8c4c5af6e0 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Tue Oct 20 12:55:40 2020 -0400 -=> fc6d4a6f69e305627d06955da27b8e8c4c5af6e0.patch Patch -Message: Add '|' to pipe page into an external program - -## fa78663748958eebb442f03d0406f42f6190522a -Author: Eyal Sawady <ecs@d2evs.net> -Date: Tue Oct 20 12:55:39 2020 -0400 -=> fa78663748958eebb442f03d0406f42f6190522a.patch Patch -Message: Add 'd' to download page - -## 9ddd5c16dae4b556c7aeac88c219568c479d87f2 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Tue Oct 20 12:55:38 2020 -0400 -=> 9ddd5c16dae4b556c7aeac88c219568c479d87f2.patch Patch -Message: gmnlm: separate do_requests and display_response - -## e1d4e9a07ec31664e61aa72ac9a6f7ab2efea6b9 -Author: Chris Vittal <chris@vittal.dev> -Date: Wed Oct 07 00:35:50 2020 -0400 -=> e1d4e9a07ec31664e61aa72ac9a6f7ab2efea6b9.patch Patch -Message: Remove gemlm with make clean - -## 40308b8b0bd7e15d0f6e2971b901a9c09a4bc681 -Author: Callum Brown <callum@calcuode.com> -Date: Tue Oct 06 17:53:56 2020 +0100 -=> 40308b8b0bd7e15d0f6e2971b901a9c09a4bc681.patch Patch -Message: gmnlm: Fix segfault when local file does not exist - -## 7619edcd116385414b55764a3401a0c66c04da79 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Mon Oct 05 14:42:46 2020 -0400 -=> 7619edcd116385414b55764a3401a0c66c04da79.patch Patch -Message: Display last line when it doesn't end in a newline - -## a5eae7ea6b35f7b2540fefdf4613a86916f0a0b0 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Tue Sep 29 01:45:40 2020 -0400 -=> a5eae7ea6b35f7b2540fefdf4613a86916f0a0b0.patch Patch -Message: gmnlm: open_bookmarks: call mkdirs - -## 4b7fba261a70bd37e160a7304d454c72c1f75b69 -Author: Callum Brown <callum@calcuode.com> -Date: Wed Sep 30 14:36:13 2020 +0100 -=> 4b7fba261a70bd37e160a7304d454c72c1f75b69.patch Patch -Message: Fix untrusted certificate message - -## 5d3ae7b7f52ba83428ba8d728712e8c1710b2ea0 -Author: Kevin Sangeelee <kevin@susa.net> -Date: Tue Sep 29 22:49:11 2020 +0100 -=> 5d3ae7b7f52ba83428ba8d728712e8c1710b2ea0.patch Patch -Message: Init known_hosts prior to fopen to avoid segfault. - -## d84ee77e249eae11c4b240294c6d37851f1ba11f -Author: Drew DeVault <sir@cmpwn.com> -Date: Tue Sep 29 13:19:18 2020 -0400 -=> d84ee77e249eae11c4b240294c6d37851f1ba11f.patch Patch -Message: gmnlm: add -W option - -## 90995e834f2e87427f2f4bddf26a93258b45aa31 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Mon Sep 28 23:42:57 2020 -0400 -=> 90995e834f2e87427f2f4bddf26a93258b45aa31.patch Patch -Message: gmnlm: show URL on untrusted cert - -## dcc0484a8c238acdbe988a898cf2deeac4f34ae5 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 27 18:06:51 2020 -0400 -=> dcc0484a8c238acdbe988a898cf2deeac4f34ae5.patch Patch -Message: Fix truncated hashes in known_hosts - -## 8c473eda5e4c6537058d0ff1815f2943e7b41498 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Fri Sep 25 12:28:21 2020 -0400 -=> 8c473eda5e4c6537058d0ff1815f2943e7b41498.patch Patch -Message: Add 'i' to show media type parameters - -Also don't show media type parameters in prompt - -## 0b5c37d2e65a46fe8e4a49c2f00cb6228fad59e3 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 27 13:32:55 2020 -0400 -=> 0b5c37d2e65a46fe8e4a49c2f00cb6228fad59e3.patch Patch -Message: Further improvement to quote display - -## 60496bae0cbda1162ae00bc6f6f4047ba9c7d86f -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 27 13:25:40 2020 -0400 -=> 60496bae0cbda1162ae00bc6f6f4047ba9c7d86f.patch Patch -Message: Improve display of preformatted & quotes - -## b4fc0c0993229b8fc8242e314e701f33a8102688 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 27 11:54:32 2020 -0400 -=> b4fc0c0993229b8fc8242e314e701f33a8102688.patch Patch -Message: tofu.c: clarify reuse warning - -## ce1ef1abde0d5519e0464f9326edea01b73a845f -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 27 11:40:49 2020 -0400 -=> ce1ef1abde0d5519e0464f9326edea01b73a845f.patch Patch -Message: TOFU: verify hostnames - -## 77de1bb2a84e0980d23b7fc2dda1480a1093ca21 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 27 10:36:09 2020 -0400 -=> 77de1bb2a84e0980d23b7fc2dda1480a1093ca21.patch Patch -Message: Revert "gmnlm: p: require whitespace before link number" - -This reverts commit 963700d8d6e31aecfc14e12184637f4c3360f6ed. - -Let's make the p command consistent with the others and tell users who -want to navigate to a relative URL unambiguosly to use ./page - -## 4e61e266076fbda20cbf268300e7f645669c7062 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 27 08:51:09 2020 -0400 -=> 4e61e266076fbda20cbf268300e7f645669c7062.patch Patch -Message: gmni: improve unknown trust message - -## b298fadb216bfbac8c84e05363e508b3b3a314a5 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sat Sep 26 16:59:06 2020 -0400 -=> b298fadb216bfbac8c84e05363e508b3b3a314a5.patch Patch -Message: Fix segfault on connection refused - -## 0eaf9cc109a99d6efb0d9c763291f6a5d9e74391 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sat Sep 26 13:13:41 2020 -0400 -=> 0eaf9cc109a99d6efb0d9c763291f6a5d9e74391.patch Patch -Message: TOFU: use ~/.local/share/gemini/known_hosts - -The rest of gmni's stuff will remain in ~/.local/share/gmni, but in -order to establish a common location and format for the TOFU file -between Gemini implementations, a more general path is required. - -## 8bb1d81f539f1e223e8fcd79e2d18f58e3c9d28f -Author: Eyal Sawady <ecs@d2evs.net> -Date: Thu Sep 24 15:32:16 2020 -0400 -=> 8bb1d81f539f1e223e8fcd79e2d18f58e3c9d28f.patch Patch -Message: gmnlm: handle CRLF line endings - -Bare CRs are still printed as '.'. - -Fixes https://todo.sr.ht/~sircmpwn/gmni/25 - -## 963700d8d6e31aecfc14e12184637f4c3360f6ed -Author: Eyal Sawady <ecs@d2evs.net> -Date: Thu Sep 24 14:56:46 2020 -0400 -=> 963700d8d6e31aecfc14e12184637f4c3360f6ed.patch Patch -Message: gmnlm: p: require whitespace before link number - -In order to allow users to navigate to relative URLs starting with 'p'. - -## 5ad3f0aaccbcf328756d0eaad0e98068587395d1 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Thu Sep 24 14:49:16 2020 -0400 -=> 5ad3f0aaccbcf328756d0eaad0e98068587395d1.patch Patch -Message: gmnlm: open bookmarks if started without URL - -## b050b9e467589561b1203f99e9f58c990c824b1a -Author: Eyal Sawady <ecs@d2evs.net> -Date: Thu Sep 24 14:28:24 2020 -0400 -=> b050b9e467589561b1203f99e9f58c990c824b1a.patch Patch -Message: gmnlm: return to previous page on empty input - -## 689fb8b470f19fb83ee1e32efe64b42d6961630c -Author: Charles E. Lehner <cel@celehner.com> -Date: Thu Sep 24 11:44:57 2020 -0400 -=> 689fb8b470f19fb83ee1e32efe64b42d6961630c.patch Patch -Message: Close fd after freeing SSL - -BIO_free_all may trigger write to close the SSL connection - -## 59d19b9894083cecafc4439f7df1031bd6cefb01 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Thu Sep 24 11:35:15 2020 -0400 -=> 59d19b9894083cecafc4439f7df1031bd6cefb01.patch Patch -Message: Fix bookmark save/open - -Fix a segfault on bookmark save and a memory leak on bookmark open. - -## f80e4037c21c87b11b7b4bfefba5d33a0fcc1ea6 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Thu Sep 24 02:09:39 2020 -0400 -=> f80e4037c21c87b11b7b4bfefba5d33a0fcc1ea6.patch Patch -Message: Add 'r' to reload - -## 5fd43e8d02ffea38b5e4a3531e366f2b9b510201 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Thu Sep 24 01:52:23 2020 -0400 -=> 5fd43e8d02ffea38b5e4a3531e366f2b9b510201.patch Patch -Message: gmni: use stdio to write body - -Fixes https://todo.sr.ht/~sircmpwn/gmni/22 - -## d754f34e7eb5f700b6de13c6c4692837d0a123f4 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Wed Sep 23 20:12:41 2020 -0400 -=> d754f34e7eb5f700b6de13c6c4692837d0a123f4.patch Patch -Message: config.sh: add args for install directories - -## 0a03e6dadf7c30cea1fb388a9e5386a00c853dbb -Author: Eyal Sawady <ecs@d2evs.net> -Date: Wed Sep 23 18:57:46 2020 -0400 -=> 0a03e6dadf7c30cea1fb388a9e5386a00c853dbb.patch Patch -Message: Fix a bug causing some text to be duplicated - -For example: - -$ gmnlm gemini://gemini.circumlunar.space -[...] -11) A lissper.strangled.net/mirrorlist/ A lisssources - t of mirrored services -[...] - -## 174fbd5d09bc13212fc1edc0cd1d3fa2400a8b7e -Author: Eyal Sawady <ecs@d2evs.net> -Date: Wed Sep 23 12:50:25 2020 -0400 -=> 174fbd5d09bc13212fc1edc0cd1d3fa2400a8b7e.patch Patch -Message: Fix memory leaks - -## 0976b0e44655163a34d1b53e62a348cbf4335940 -Author: Drew DeVault <sir@cmpwn.com> -Date: Wed Sep 23 12:55:12 2020 -0400 -=> 0976b0e44655163a34d1b53e62a348cbf4335940.patch Patch -Message: Fix search command - -## 9f98e013a6cd966cf4dc2d98187d6f0ba6f7fb5c -Author: Drew DeVault <sir@cmpwn.com> -Date: Wed Sep 23 10:20:32 2020 -0400 -=> 9f98e013a6cd966cf4dc2d98187d6f0ba6f7fb5c.patch Patch -Message: config.sh: remove project-specific code - -Or reduce, really. Just makes this script easier to re-use elsewhere. - -## 9551d0a3822312a0a4917ccbe80fdaeb49954d70 -Author: Arav K <nothien@uber.space> -Date: Tue Sep 22 14:10:56 2020 +0100 -=> 9551d0a3822312a0a4917ccbe80fdaeb49954d70.patch Patch -Message: Add 'p' to view link URL without visiting - -Answers https://todo.sr.ht/~sircmpwn/gmni/13 by adding a 'p' command -that, when given a (valid) link number, provides the URL that that link -leads to. - -## be0cf0dfd1e83a1ba4f6b27fb9464c4e95d10752 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Tue Sep 22 19:24:26 2020 -0400 -=> be0cf0dfd1e83a1ba4f6b27fb9464c4e95d10752.patch Patch -Message: Makefile: clean: remove objects - -## b25b4576e3fd889b3edadb51ff9a387ef0bae653 -Author: Leon Henrik Plickat <leonhenrik.plickat@stud.uni-goettingen.de> -Date: Tue Sep 22 13:39:03 2020 +0200 -=> b25b4576e3fd889b3edadb51ff9a387ef0bae653.patch Patch -Message: Correctly abort when launched with invalid URL - -The return value of set_url() was not checked, meaning that when it -failed, gmnlm continued anyway, causing an assertion to fail and -subsequentially resulting in a segfault. - -## 60cf41e7dd66897e6987704921b6cd57da1f084f -Author: Arav K <nothien@uber.space> -Date: Tue Sep 22 10:20:39 2020 +0100 -=> 60cf41e7dd66897e6987704921b6cd57da1f084f.patch Patch -Message: Add 'H' to view all page history - -This adds a command, 'H', to view all the URLs in the page history. It -first prints backward URLs, then the current URL (marked with an -asterisk), followed by forward URLs. - -## 39339c348f593bda9ca0a556affa38cd5e15138c -Author: Drew DeVault <sir@cmpwn.com> -Date: Mon Sep 21 22:22:18 2020 -0400 -=> 39339c348f593bda9ca0a556affa38cd5e15138c.patch Patch -Message: Fix issues with tofu.c - -## d6777ec2788f9ece56a6201fb091dba4a15f739a -Author: Eyal Sawady <ecs@d2evs.net> -Date: Mon Sep 21 22:21:45 2020 -0400 -=> d6777ec2788f9ece56a6201fb091dba4a15f739a.patch Patch -Message: gemini_response_finish: handle null ssl connection - -## e80d852a1ba62c8877dab40324ac90a14a0b7eea -Author: Eyal Sawady <ecs@d2evs.net> -Date: Mon Sep 21 19:17:01 2020 -0400 -=> e80d852a1ba62c8877dab40324ac90a14a0b7eea.patch Patch -Message: Makefile: silence gmni CCLD command - -## 02f6af661513683f0c6c1465c5ff1dd8f03a30c9 -Author: Drew DeVault <sir@cmpwn.com> -Date: Mon Sep 21 15:37:24 2020 -0400 -=> 02f6af661513683f0c6c1465c5ff1dd8f03a30c9.patch Patch -Message: Implement TOFU - -## 30660fc160a15504274d40d4a5ec1b31539f8c2f -Author: Charles E. Lehner <cel@celehner.com> -Date: Mon Sep 21 09:17:47 2020 -0400 -=> 30660fc160a15504274d40d4a5ec1b31539f8c2f.patch Patch -Message: Link with libraries before objects - -## c7592c6a5c2ff7b390af89f297d5c9e1c34c9414 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Mon Sep 21 00:09:47 2020 -0400 -=> c7592c6a5c2ff7b390af89f297d5c9e1c34c9414.patch Patch -Message: save_bookmark: fix bookmark file creation - -## eb2873b2ebe1f436d8fb4cd7c336889ad0ddddfd -Author: Drew DeVault <sir@cmpwn.com> -Date: Mon Sep 21 07:56:20 2020 -0400 -=> eb2873b2ebe1f436d8fb4cd7c336889ad0ddddfd.patch Patch -Message: gmnlm.c: add limits.h - -Supposedly fixes OpenBSD - -## 2e9d3c0bab8e7df635a8f0968f04fe9b1e2d979c -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 23:52:18 2020 -0400 -=> 2e9d3c0bab8e7df635a8f0968f04fe9b1e2d979c.patch Patch -Message: Update README.md - -## 601f9008863af571980c1cd39920483d59cfbfb4 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 23:50:50 2020 -0400 -=> 601f9008863af571980c1cd39920483d59cfbfb4.patch Patch -Message: Implement bookmarks - -## 852bc7198f9d1d838d76d74a006cc2a2e63e4f1c -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 23:33:32 2020 -0400 -=> 852bc7198f9d1d838d76d74a006cc2a2e63e4f1c.patch Patch -Message: Implement file:// URLs - -## d5936353392a17ae6bac3303d506a7e79855da2d -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 23:13:30 2020 -0400 -=> d5936353392a17ae6bac3303d506a7e79855da2d.patch Patch -Message: Swap quote character to > - -## 4c12342bcad95cdc44f9107161429524226a2d37 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 22:38:31 2020 -0400 -=> 4c12342bcad95cdc44f9107161429524226a2d37.patch Patch -Message: README.md: use newer asciinema link - -## 6f36d2a0fc5de0a9d25229c47c75481f47f32c87 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 22:37:44 2020 -0400 -=> 6f36d2a0fc5de0a9d25229c47c75481f47f32c87.patch Patch -Message: gmnlm: add install target & man page - -## 144693a3d001a436abaa37f11b1c1c2bdf88c813 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 22:34:27 2020 -0400 -=> 144693a3d001a436abaa37f11b1c1c2bdf88c813.patch Patch -Message: Fix segfault moving to next result outside search - -## 5799323f4c92181a3446a729366b230456e93c81 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 22:28:45 2020 -0400 -=> 5799323f4c92181a3446a729366b230456e93c81.patch Patch -Message: Implement preformatted text - -## 59d43726bb18a1e240a7188b3dd33af5876a126e -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 22:20:38 2020 -0400 -=> 59d43726bb18a1e240a7188b3dd33af5876a126e.patch Patch -Message: Fix segfault on name resolution errors - -## bb696e6e2823d38bf6ad2f5106f3808555c48b18 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 22:11:13 2020 -0400 -=> bb696e6e2823d38bf6ad2f5106f3808555c48b18.patch Patch -Message: Correct oversight which broke link following - -## f4a4be2513580809c01212a08a5284f9cf16ad5f -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 22:09:26 2020 -0400 -=> f4a4be2513580809c01212a08a5284f9cf16ad5f.patch Patch -Message: Implement regex-based page search - -## 28283bda98accf122b6424ac611fd4ff25dedbc9 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 21:53:08 2020 -0400 -=> 28283bda98accf122b6424ac611fd4ff25dedbc9.patch Patch -Message: Remove unnecessary TODO - -There is no line length limit and authors are strongly discouraged from -hard-wrapping by the specification. - -## 05cc8b85cdf731ea3a664b6099aad04f22bbca6c -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 21:42:27 2020 -0400 -=> 05cc8b85cdf731ea3a664b6099aad04f22bbca6c.patch Patch -Message: Add help message - -## d2fa1b4567aa841020f4d4bbdb1298b310534c98 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 21:35:05 2020 -0400 -=> d2fa1b4567aa841020f4d4bbdb1298b310534c98.patch Patch -Message: Allow user to navigate by typing in URL - -## 1c9a6e6a35448b76063f16b0f6aaaf8d43ebee9a -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 21:29:40 2020 -0400 -=> 1c9a6e6a35448b76063f16b0f6aaaf8d43ebee9a.patch Patch -Message: Show [b]ack and [f]orward prompts contextually - -## 46b5d74576ffce397c83ac53ebfacb25e1cdc851 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 21:22:40 2020 -0400 -=> 46b5d74576ffce397c83ac53ebfacb25e1cdc851.patch Patch -Message: Expand wrapping to all token types - -## fed9c0561947126442fbb32e3437922dcc467ef1 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 21:02:17 2020 -0400 -=> fed9c0561947126442fbb32e3437922dcc467ef1.patch Patch -Message: Whitelist control sequences in wrap() - -## 9bd1a7457ea58ddd568fdbe46a1155c28424e8be -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 20:57:26 2020 -0400 -=> 9bd1a7457ea58ddd568fdbe46a1155c28424e8be.patch Patch -Message: Remove obsolete TODO - -## a61a75f837239bed3aa74331699d301fb93d9da8 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 20:52:18 2020 -0400 -=> a61a75f837239bed3aa74331699d301fb93d9da8.patch Patch -Message: Implement basic word wrapping - -## e1d9773742b9832a87e912b44d4ad9e2e34364ef -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 20:42:46 2020 -0400 -=> e1d9773742b9832a87e912b44d4ad9e2e34364ef.patch Patch -Message: Indent # and ## to line up with text - -## 678bff58ed32e77c9af90a5d8fc7b1f3c38af86c -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 20:13:22 2020 -0400 -=> 678bff58ed32e77c9af90a5d8fc7b1f3c38af86c.patch Patch -Message: Improve links - -Removing the leading [ makes links 0-9 line up with the text body, which -is nice. - -## d0acd0f4d08a0d5e8f6c729f53de2f381b270202 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 20:11:48 2020 -0400 -=> d0acd0f4d08a0d5e8f6c729f53de2f381b270202.patch Patch -Message: Add reduced-unicode mode - -## 84df94447cdb081ec305a5ba9d2b0ef89dd34fc3 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 20:07:48 2020 -0400 -=> 84df94447cdb081ec305a5ba9d2b0ef89dd34fc3.patch Patch -Message: Scale down image - -## c414388c0f2c020c0e22d53f6df577e1fbeb32fc -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 20:05:44 2020 -0400 -=> c414388c0f2c020c0e22d53f6df577e1fbeb32fc.patch Patch -Message: README.md: add link to asciinema - -## 563922a7e2da77b3973dcf707854121932ca244e -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 20:01:31 2020 -0400 -=> 563922a7e2da77b3973dcf707854121932ca244e.patch Patch -Message: Merge normal mode & pagination prompts - -## b90888e71878eea7727cd507503198d134d91ab7 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 19:58:34 2020 -0400 -=> b90888e71878eea7727cd507503198d134d91ab7.patch Patch -Message: README.md: add gmnlm - -## 211b8c3dd36a950132efa5ba4e2f0172a42bb6bc -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 19:40:42 2020 -0400 -=> 211b8c3dd36a950132efa5ba4e2f0172a42bb6bc.patch Patch -Message: gmnlm: Implement input responses - -## 1808e6cd1880d3c08abb0ddfa19044afada925dd -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 19:40:27 2020 -0400 -=> 1808e6cd1880d3c08abb0ddfa19044afada925dd.patch Patch -Message: Don't + encode spaces in URLs - -## 4b2437b17e00d61da9356ac96bb38a8043c66fca -Author: Eyal Sawady <ecs@d2evs.net> -Date: Sun Sep 20 19:24:43 2020 -0400 -=> 4b2437b17e00d61da9356ac96bb38a8043c66fca.patch Patch -Message: .gitignore: update - -## 5a955c5f241b87018dfb0cda6872dc7ae2784222 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 19:33:43 2020 -0400 -=> 5a955c5f241b87018dfb0cda6872dc7ae2784222.patch Patch -Message: gmnlm: refactor - -## dcc2b34d434d8e0f695e43a1f775759846e60417 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 19:14:47 2020 -0400 -=> dcc2b34d434d8e0f695e43a1f775759846e60417.patch Patch -Message: gmnlm: implement redirects - -## ea595cdb2ba9d1e443c7ccdccf3842e49e7162a8 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 19:09:55 2020 -0400 -=> ea595cdb2ba9d1e443c7ccdccf3842e49e7162a8.patch Patch -Message: gmnlm: display plaintext files - -## 3547fd11d57da5c7aa610366d54eef3b47d0b1a4 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 19:01:14 2020 -0400 -=> 3547fd11d57da5c7aa610366d54eef3b47d0b1a4.patch Patch -Message: Implement history - -## 7a099135cd9dae483679cf51a4b630a5dd64c74e -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 18:41:30 2020 -0400 -=> 7a099135cd9dae483679cf51a4b630a5dd64c74e.patch Patch -Message: gmnlm: implement link following - -## 01567e578c9632960903e1f56dd2086547806da3 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 18:31:33 2020 -0400 -=> 01567e578c9632960903e1f56dd2086547806da3.patch Patch -Message: Initial work for line-mode browser - -## 48d0feed6d097c54662a7f231c7bc4704837f023 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 17:47:14 2020 -0400 -=> 48d0feed6d097c54662a7f231c7bc4704837f023.patch Patch -Message: Initial pass on text/gemini parser - -## 33495e8dd86139cafade2888227e37b1572d18ea -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 16:47:19 2020 -0400 -=> 33495e8dd86139cafade2888227e37b1572d18ea.patch Patch -Message: Detect attempts to use non-gemini URLs - -## 05d112b7d347b737bdac503ea05292db3347f2a8 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 16:44:43 2020 -0400 -=> 05d112b7d347b737bdac503ea05292db3347f2a8.patch Patch -Message: Prohibit >5 redirects - -## 7c453fb45f831ce9178799af9855ecb0bda518ea -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 16:41:05 2020 -0400 -=> 7c453fb45f831ce9178799af9855ecb0bda518ea.patch Patch -Message: Implement SENSITIVE INPUT response - -## 95518992983e6531106b48c82edeb0ce825bf351 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 16:32:01 2020 -0400 -=> 95518992983e6531106b48c82edeb0ce825bf351.patch Patch -Message: Add response status enum - -## 02c2be62daceb04e4891d415c997dd64db84b9d9 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 15:14:54 2020 -0400 -=> 02c2be62daceb04e4891d415c997dd64db84b9d9.patch Patch -Message: assert(url) after setting input URL - -## eb6a4e9740237bd1bd71c115476e187520a2fc8c -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 15:14:04 2020 -0400 -=> eb6a4e9740237bd1bd71c115476e187520a2fc8c.patch Patch -Message: Set SOCK_STREAM regardless of hints - -## 73f5a5bc2b97fb36ded8feb36828d598d6e9fed3 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 15:10:45 2020 -0400 -=> 73f5a5bc2b97fb36ded8feb36828d598d6e9fed3.patch Patch -Message: client.h: clarify some constraints - -## 37396a375a68868490342e16140e67287445be17 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:55:33 2020 -0400 -=> 37396a375a68868490342e16140e67287445be17.patch Patch -Message: Add make install target - -## 207a72012ef69de654a78e18d28182ecde1326e2 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:51:25 2020 -0400 -=> 207a72012ef69de654a78e18d28182ecde1326e2.patch Patch -Message: Rename binary from gmnic -> gmni - -## a3d5169d71f181efaa59a619e7362911a6c048b7 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:49:57 2020 -0400 -=> a3d5169d71f181efaa59a619e7362911a6c048b7.patch Patch -Message: Add README.md - -## b136eea98a33ffcaf2d965e907dd6799078d2110 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:44:30 2020 -0400 -=> b136eea98a33ffcaf2d965e907dd6799078d2110.patch Patch -Message: Add man page - -## c08e934c449c7a030fb1ebacd3166820b23faeb3 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:41:03 2020 -0400 -=> c08e934c449c7a030fb1ebacd3166820b23faeb3.patch Patch -Message: Change -C to -E for client certificates - -More consistent with curl - -## 262ccc2005617eb633f5b3ba434ee26f8000e0a8 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:37:43 2020 -0400 -=> 262ccc2005617eb633f5b3ba434ee26f8000e0a8.patch Patch -Message: Suppress trailing newline if stdout is not a TTY - -## 2e593cd48bd5e1f90fcbb54b83955752cd392466 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:25:42 2020 -0400 -=> 2e593cd48bd5e1f90fcbb54b83955752cd392466.patch Patch -Message: config.sh: drop unused LIBSSL variable - -## eb01fde6007ea86b61227f690f8ee7ff0047bfeb -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:21:03 2020 -0400 -=> eb01fde6007ea86b61227f690f8ee7ff0047bfeb.patch Patch -Message: Implement -4 and -6 - -To force IPv4 and IPv6 respectively. - -## fd914b3aa40d63eb7fb2aafb1f6a30c1f78ee92f -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:18:40 2020 -0400 -=> fd914b3aa40d63eb7fb2aafb1f6a30c1f78ee92f.patch Patch -Message: Implement -L flag (follow redirects) - -## ee6fe47e44bfb40cd92d6668e85fd893df3a12b8 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:18:26 2020 -0400 -=> ee6fe47e44bfb40cd92d6668e85fd893df3a12b8.patch Patch -Message: Correct trailing line feed detection - -## 4bc55aaa138171db365377db0624c3ce0d878257 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:13:18 2020 -0400 -=> 4bc55aaa138171db365377db0624c3ce0d878257.patch Patch -Message: Print line feed on text/* response if not included - -Adds -l flag to suppress this behavior - -## 78eb57cad45aa27e83d3a5e78be0bb5ce4d631f7 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 14:09:45 2020 -0400 -=> 78eb57cad45aa27e83d3a5e78be0bb5ce4d631f7.patch Patch -Message: Implement input - -## 9b1a618b4211a029c352c72f6d273e3085c8457d -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 13:30:28 2020 -0400 -=> 9b1a618b4211a029c352c72f6d273e3085c8457d.patch Patch -Message: Implement reading response body & meta - -## abcb9caf86020a7cdd9f502fe01eb5db3c70c685 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 13:06:34 2020 -0400 -=> abcb9caf86020a7cdd9f502fe01eb5db3c70c685.patch Patch -Message: Initial request riggings - -## ccec255833fa27789ffb75551b29528b10f8c62a -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Sep 20 10:17:39 2020 -0400 -=> ccec255833fa27789ffb75551b29528b10f8c62a.patch Patch -Message: Initial commit - -## 3dd06ab4813b6c8f4992e19fce9d4b094fd3a1a9 -Author: Eyal Sawady <ecs@d2evs.net> -Date: Tue Mar 09 05:21:59 2021 -0500 -=> 3dd06ab4813b6c8f4992e19fce9d4b094fd3a1a9.patch Patch -Message: gmnlm: create cert dir on 6x response - -So that the OpenSSL command doesn't fail when the cert dir hasn't -already been created. - -## d371589381e57835d37796f1d349638b806e43b4 -Author: Drew DeVault <sir@cmpwn.com> -Date: Sun Mar 07 12:21:15 2021 -0500 -=> d371589381e57835d37796f1d349638b806e43b4.patch Patch -Message: Always take the last cert, CA or not - -## ffc89b6cf01e8b68232b3de3f001d2e635936dfa -Author: Drew DeVault <sir@cmpwn.com> -Date: Fri Mar 05 08:50:50 2021 -0500 -=> ffc89b6cf01e8b68232b3de3f001d2e635936dfa.patch Patch -Message: Implement basic client certs for gmnlm - -## 955f7524b955e19bc89c6e9f76f3f3ecfb7bfb58 -Author: Drew DeVault <sir@cmpwn.com> -Date: Thu Mar 04 17:24:57 2021 -0500 -=> 955f7524b955e19bc89c6e9f76f3f3ecfb7bfb58.patch Patch -Message: Initial support for client side certificates - -This is only supported with gmni for now - gmnlm support will come -later. A limitation with BearSSL prevents us from doing automated -certificate generation for now, unfortunately. - -## 9b0006509931c9a3defb64c64f4b0071657f8e61 -Author: Drew DeVault <sir@cmpwn.com> -Date: Thu Mar 04 16:22:14 2021 -0500 -=> 9b0006509931c9a3defb64c64f4b0071657f8e61.patch Patch -Message: TOFU: more improvements to new cert handling logic - -## 49eea555e605e6e0155756ad9739a5729340db81 -Author: Drew DeVault <sir@cmpwn.com> -Date: Thu Mar 04 13:00:07 2021 -0500 -=> 49eea555e605e6e0155756ad9739a5729340db81.patch Patch -Message: Remove useless variable cast - -## 996bd24225e7a63fd160d1feb9af193225a065b3 -Author: Drew DeVault <sir@cmpwn.com> -Date: Thu Mar 04 12:58:50 2021 -0500 -=> 996bd24225e7a63fd160d1feb9af193225a065b3.patch Patch -Message: Discard CA certs unless there's no other - -## df2e4f8ab2f151eac61702b27f8c5cfe4145912e -Author: Drew DeVault <sir@cmpwn.com> -Date: Thu Mar 04 12:19:54 2021 -0500 -=> df2e4f8ab2f151eac61702b27f8c5cfe4145912e.patch Patch -Message: tofu: don't discard CA certs - -## fc0bf889a3e5ce270600811875ef9a50729c4135 -Author: Drew DeVault <sir@cmpwn.com> -Date: Thu Mar 04 10:59:19 2021 -0500 -=> fc0bf889a3e5ce270600811875ef9a50729c4135.patch Patch -Message: all: rewrite with BearSSL rather than OpenSSL - diff --git a/sources/cgmnlm.git/index.gmi b/sources/cgmnlm.git/index.gmi @@ -1,63 +0,0 @@ -```Command to clone this repository -git clone https://src.clttr.info/rwa/cgmnlm.git -``` - -=> commits/ -=> tree/ - -# cgmnlm - A colorful Gemini line mode client - -This is a Gemini client. Included are: - -=> https://gemini.circumlunar.space/ Gemini - -* A CLI utility (like curl): gmni -* A line-mode browser: cgmnlm - -## Features: - -* Page history -* Regex searches -* Bookmarks -* basic Client Certificate support (no autocreation of client certs currently) - -## Non-Features: - -* no inlining of any link type -* no caching of page content -* no persistent history across sessions - -### Modifications compared to upstream - -This project is of fork of https://git.sr.ht/~sircmpwn/gmni - -=> https://git.sr.ht/~sircmpwn/gmni https://git.sr.ht/~sircmpwn/gmni - -It includes the following modifications: - colored headings & links - default 4 char indenting - s command to directly search in geminispace (via geminispace.info) - k command to remove the bookmark for the current page - e[N] command to open a link in default external program (requires xdg-open) - t[N] command to download the content behind a link to a temporary file - a command to switch between display of preformatted blocks and alt text (if available) - -The actual colors used depend on your terminal palette: - heading 1: light red - heading 2: light yellow - heading 3: light green - gemini link on same capsule: light cyan - gemini link to another capsule: dark cyan - non-gemini link: light magenta - preformatted text: light gray - -Besides this rendering adjustments i'll try to keep track of upstream changes or send patches to upstream. - -## Usage - -See gmni(1), cgmnlm(1). - -# Installation - -* ArchLinux and derivates: https://aur.archlinux.org/packages/cgmnlm-git/ - -## Compiling - -``` -$ mkdir build && cd build -$ ../configure -$ make -# make install -``` - -### Dependencies: - -* A POSIX-like system and a C11 compiler -* BearSSL -* scdoc (optional) diff --git a/sources/cgmnlm.git/tree/.gitignore.txt b/sources/cgmnlm.git/tree/.gitignore.txt @@ -1,8 +0,0 @@ -.build -build -/gmni -cgmnlm -*.1 -*.o -*.a -*.pc diff --git a/sources/cgmnlm.git/tree/COPYING.txt b/sources/cgmnlm.git/tree/COPYING.txt @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - <program> Copyright (C) <year> <name of author> - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -<https://www.gnu.org/licenses/>. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/sources/cgmnlm.git/tree/Makefile.txt b/sources/cgmnlm.git/tree/Makefile.txt @@ -1,79 +0,0 @@ -.POSIX: -.SUFFIXES: -OUTDIR=.build -VERSION=0.0.0 -include $(OUTDIR)/config.mk -include $(OUTDIR)/cppcache - -gmni: $(gmni_objects) - @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) -o $@ $(gmni_objects) $(LIBS) - -cgmnlm: $(cgmnlm_objects) - @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) -o $@ $(cgmnlm_objects) $(LIBS) - -libgmni.a: $(libgmni.a_objects) - @printf 'AR\t$@\n' - @$(AR) -rcs $@ $(libgmni.a_objects) - -doc/gmni.1: doc/gmni.scd -doc/cgmnlm.1: doc/cgmnlm.scd - -libgmni.pc: - @printf 'GEN\t$@\n' - @printf 'prefix=%s\n' "$(PREFIX)" > $@ - @printf 'exec_prefix=$${prefix}\n' >> $@ - @printf 'includedir=$${prefix}/include\n' >> $@ - @printf 'libdir=$${prefix}/lib\n' >> $@ - @printf 'Name: libgmni\n' >> $@ - @printf 'Version: %s\n' "$(VERSION)" >> $@ - @printf 'Description: The gmni client library\n' >> $@ - @printf 'Requires: libssl libcrypto\n' >> $@ - @printf 'Cflags: -I$${includedir}/gmni\n' >> $@ - @printf 'Libs: -L$${libdir} -lgmni\n' >> $@ - -.SUFFIXES: .c .o .scd .1 - -.c.o: - @printf 'CC\t$@\n' - @touch $(OUTDIR)/cppcache - @grep $< $(OUTDIR)/cppcache >/dev/null || \ - $(CPP) $(CFLAGS) -MM -MT $@ $< >> $(OUTDIR)/cppcache - @$(CC) -c $(CFLAGS) -o $@ $< - -.scd.1: - @printf 'SCDOC\t$@\n' - @$(SCDOC) < $< > $@ - -docs: doc/gmni.1 doc/cgmnlm.1 - -clean: - @rm -f gmni cgmnlm libgmni.a libgmni.pc doc/gmni.1 doc/cgmnlm.1 $(cgmnlm_objects) $(gmni_objects) - -distclean: clean - @rm -rf "$(OUTDIR)" - -install: all install_docs - mkdir -p $(BINDIR) - mkdir -p $(LIBDIR) - mkdir -p $(INCLUDEDIR)/gmni - mkdir -p $(LIBDIR)/pkgconfig - install -m755 gmni $(BINDIR)/gmni - install -m755 cgmnlm $(BINDIR)/cgmnlm - install -m755 libgmni.a $(LIBDIR)/libgmni.a - install -m644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h - install -m644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h - install -m644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h - install -m644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc - -uninstall: - rm -f $(BINDIR)/gmni - rm -f $(BINDIR)/cgmnlm - rm -f $(LIBDIR)/libgmni.a - rm -rf $(INCLUDEDIR)/gmni - rm -f $(LIBDIR)/pkgconfig/libgmni.pc - rm -f $(MANDIR)/man1/gmni.1 - rm -f $(MANDIR)/man1/cgmnlm.1 - -.PHONY: clean distclean docs install diff --git a/sources/cgmnlm.git/tree/README.md.txt b/sources/cgmnlm.git/tree/README.md.txt @@ -1,66 +0,0 @@ -# cgmnlm - A colorful Gemini line mode client - -This is a [Gemini](https://gemini.circumlunar.space/) client. Included are: - -- A CLI utility (like curl): gmni -- A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): cgmnlm - -## Features: - -- Page history -- Regex searches -- Bookmarks -- basic Client Certificate support (no autocreation of client certs currently) - -## Non-Features: - -- no inlining of any link type -- no caching of page content -- no persistent history across sessions - -### Modifications compared to upstream - -This project is of fork of https://git.sr.ht/~sircmpwn/gmni - -It includes the following modifications: -- colored headings & links -- default 4 char indenting -- `s` command to directly search in geminispace (via geminispace.info) -- `k` command to remove the bookmark for the current page -- `e[N]` command to open a link in default external program (requires `xdg-open`) -- `t[N]` command to download the content behind a link to a temporary file -- `a` command to switch between display of preformatted blocks and alt text (if available) - -The actual colors used depend on your terminal palette: -- heading 1: light red -- heading 2: light yellow -- heading 3: light green -- gemini link on same capsule: light cyan -- gemini link to another capsule: dark cyan -- non-gemini link: light magenta -- preformatted text: light gray - -Besides this rendering adjustments i'll try to keep track of upstream changes or send patches to upstream. - -## Usage - -See `gmni(1)`, `cgmnlm(1)`. - -# Installation - -* ArchLinux and derivates: https://aur.archlinux.org/packages/cgmnlm-git/ - -## Compiling - -``` -$ mkdir build && cd build -$ ../configure -$ make -# make install -``` - -### Dependencies: - -- A POSIX-like system and a C11 compiler -- [BearSSL](https://www.bearssl.org/index.html) -- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) diff --git a/sources/cgmnlm.git/tree/config.sh.txt b/sources/cgmnlm.git/tree/config.sh.txt @@ -1,195 +0,0 @@ -outdir=${OUTDIR:-.build} -srcdir=${SRCDIR:-$(dirname "$0")} -AR=${AR:-ar} -AS=${AS:-as} -CC=${CC:-cc} -CFLAGS=${CFLAGS:-} -LD=${LD:-ld} -SCDOC=${SCDOC:-scdoc} - -for arg -do - case "$arg" in - --bindir=*) - BINDIR=${arg#*=} - ;; - --libdir=*) - LIBDIR=${arg#*=} - ;; - --mandir=*) - MANDIR=${arg#*=} - ;; - --prefix=*) - PREFIX=${arg#*=} - ;; - esac -done - -subdir() { - eval ". $srcdir/$1/configure" -} - -genrules() { - target="$1" - shift - printf '# Begin generated rules for %s\n' "$target" - for file in "$@" - do - ext="${file#*.}" - file="${file%.*}" - deps= - printf '%s.o: %s.%s%s\n' "$file" "$file" "$ext" "$deps" - done - printf '%s_objects=\\\n' "$target" - n=0 - for file in "$@" - do - file="${file%.*}" - n=$((n+1)) - if [ $n -eq $# ] - then - printf '\t%s.o\n' "$file" - else - printf '\t%s.o \\\n' "$file" - fi - done - printf '# End generated rules for %s\n' "$target" -} - -append_cflags() { - for flag - do - CFLAGS="$(printf '%s \\\n\t%s' "$CFLAGS" "$flag")" - done -} - -test_cflags() { - [ ! -e "$outdir"/check.c ] && cat <<-EOF > "$outdir"/check.c - int main(void) { return 0; } - EOF - werror="" - case "$CFLAGS" in - *-Werror*) - werror="-Werror" - ;; - esac - if $CC $werror "$@" -o "$outdir"/check "$outdir"/check.c >/dev/null 2>&1 - then - append_cflags "$@" - else - return 1 - fi -} - -find_library() { - name="$1" - pc="$2" - printf "Checking for %s... " "$name" - if ! command -v pkg-config >/dev/null - then - printf "ERROR: pkg-config not found\n" - return 1 - fi - if ! pkg-config "$pc" 2>/dev/null - then - printf "NOT FOUND\n" - printf "Tried pkg-config %s\n" "$pc" - return 1 - fi - printf "OK\n" - CFLAGS="$CFLAGS $(pkg-config --cflags "$pc")" - LIBS="$LIBS $(pkg-config --libs "$pc")" -} - -docs() { true; } - -run_configure() { - mkdir -p $outdir - - for flag in -g -std=c11 -D_XOPEN_SOURCE=700 -Wall -Wextra -Werror -pedantic - do - printf "Checking for %s... " "$flag" - if test_cflags "$flag" - then - echo yes - else - echo no - fi - done - - # XXX: Asked the maintainer to provide a .pc file - LIBS="$LIBS -lbearssl" - - printf "Checking for scdoc... " - if scdoc -v >/dev/null 2>&1 - then - echo yes - all="$all docs" - install_docs=" - mkdir -p \$(MANDIR)/man1 - install -m644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 - install -m644 doc/cgmnlm.1 \$(MANDIR)/man1/cgmnlm.1" - else - echo no - fi - - printf "Creating $outdir/config.mk... " - cat <<-EOF > "$outdir"/config.mk - CC=$CC - SCDOC=$SCDOC - LIBS=$LIBS - PREFIX=${PREFIX:-/usr/local} - OUTDIR=${outdir} - _INSTDIR=\$(DESTDIR)\$(PREFIX) - BINDIR?=${BINDIR:-\$(_INSTDIR)/bin} - LIBDIR?=${LIBDIR:-\$(_INSTDIR)/lib} - INCLUDEDIR?=${INCLUDEDIR:-\$(_INSTDIR)/include} - MANDIR?=${MANDIR:-\$(_INSTDIR)/share/man} - CACHE=\$(OUTDIR)/cache - CFLAGS=${CFLAGS} - CFLAGS+=-Iinclude -I\$(OUTDIR) - CFLAGS+=-DPREFIX='"\$(PREFIX)"' - CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' - - all: ${all} - install_docs: ${install_docs} - EOF - - for target in $(printf '%s\n' $all | tr '.' '_') - do - $target - done >>"$outdir"/config.mk - echo done - - touch $outdir/cppcache - - if [ "$srcdir" = "." ] - then - return - fi - - populate() ( - path="$1" - mkdir -p "${path#$srcdir/}" - fullpath() ( cd "$1" && pwd ) - for orig in "$path"/* - do - link="${orig#$srcdir/}" - if [ -d "$orig" ] - then - mkdir -p $link - populate "$orig" - elif [ -f "$orig" ] - then - ln -sf "$(fullpath "$path")"/"$(basename "$orig")" "$link" - fi - done - ) - - printf "Populating build dir... " - populate "$srcdir/include" - populate "$srcdir/src" - populate "$srcdir/doc" - ln -sf "$srcdir"/Makefile ./ - echo done -} diff --git a/sources/cgmnlm.git/tree/configure.txt b/sources/cgmnlm.git/tree/configure.txt @@ -1,46 +0,0 @@ -#!/bin/sh -e -srcdir=${SRCDIR:-$(dirname "$0")} -eval ". $srcdir/config.sh" - -gmni() { - genrules gmni \ - src/certs.c \ - src/client.c \ - src/escape.c \ - src/gmni.c \ - src/tofu.c \ - src/url.c \ - src/util.c -} - -cgmnlm() { - genrules cgmnlm \ - src/certs.c \ - src/client.c \ - src/escape.c \ - src/gmnlm.c \ - src/parser.c \ - src/tofu.c \ - src/url.c \ - src/util.c -} - -libgmni_a() { - genrules libgmni.a \ - src/certs.c \ - src/client.c \ - src/escape.c \ - src/tofu.c \ - src/url.c \ - src/util.c \ - src/parser.c -} - -libgmni_pc() { - : -} - -all="gmni cgmnlm libgmni.a libgmni.pc" - - -run_configure diff --git a/sources/cgmnlm.git/tree/doc/cgmnlm.scd.txt b/sources/cgmnlm.git/tree/doc/cgmnlm.scd.txt @@ -1,33 +0,0 @@ -gmnlm(1) - -# NAME - -cgmnlm - colored Gemini line-mode browser - -# SYNPOSIS - -*cgmnlm* [-PU] [-j _mode_] [-W _width_] _gemini://..._ - -# DESCRIPTION - -*cgmnlm* is an interactive line-mode Gemini browser. - -# OPTIONS - -*-j* _mode_ - Sets the TOFU (trust on first use) configuration, which controls if the - client shall trust new certificates. _mode_ can be one of *always*, - *once*, or *fail*. - -*-P* - Disable pagination. - -*-U* - Disable conservative use of Unicode symbols to render Gemini layout - features. - -*-W* _width_ - Sets the maximum width, in columns, of Gemtext pages. - -*-A* - Enables alternate text instead of preformatted text by default. diff --git a/sources/cgmnlm.git/tree/doc/gmni.scd.txt b/sources/cgmnlm.git/tree/doc/gmni.scd.txt @@ -1,72 +0,0 @@ -gmni(1) - -# NAME - -gmni - Gemini client - -# SYNPOSIS - -*gmni* [-46lLiIN] [-j _mode_] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ - -# DESCRIPTION - -*gmni* executes a gemini request and, if successful, prints the response body -to stdout. - -If an error is returned, information is printed to stderr and the process exits -with a non-zero exit status equal to the response status. If a response is -returned which is neither successful or an error, the response status and meta -text are printed to stderr. - -If the server requests user input, a prompt is shown and a second request is -performed with the user's input supplied to the server. - -# OPTIONS - -*-4* - Force the connection to use IPv4. - -*-6* - Force the connection to use IPv6. - -*-d* _input_ - If the server requests user input, a second request is performed with - the given input string as the user input. - -*-D* _path_ - If the server requests user input, _path_ is opened and read, and a - second request is performed with the contents of _path_ as the user - input. - -*-E* _path_:_key_ - Sets the path to the client certificate and private key file to use, - both PEM encoded. - -*-l* - For *text/\** responses, *gmni* normally adds a line feed if stdout is a - TTY and the response body does not include one. This flag suppresses - this behavior. - -*-L* - Follow redirects. - -*-j* _mode_ - Sets the TOFU (trust on first use) configuration, which controls if the - client shall trust new certificates. _mode_ can be one of *always*, - *once*, or *fail*. - -*-i* - Print the response status and meta text to stdout. - -*-I* - Print the response status and meta text to stdout, and suppress the - printing of the response body to stdout. - -*-N* - Suppress the input prompt if the server requests an input, and instead - print a diagnostic message and exit with a zero (successful) status - code. - -*-o* _path_ - Write output to _path_. If _path_ ends with a '/', the basename of the URL - will be appended. If _path_ is the empty string, "./" will be presumed. diff --git a/sources/cgmnlm.git/tree/doc/index.gmi b/sources/cgmnlm.git/tree/doc/index.gmi @@ -1,6 +0,0 @@ -# Tree - -Path: doc/ - -=> cgmnlm.scd.txt cgmnlm.scd -=> gmni.scd.txt gmni.scd diff --git a/sources/cgmnlm.git/tree/include/escape.h.txt b/sources/cgmnlm.git/tree/include/escape.h.txt @@ -1,175 +0,0 @@ -#ifndef ESCAPE_H -#define ESCAPE_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.haxx.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - ***************************************************************************/ - -/* from curl.h */ -typedef enum { - CURLE_OK = 0, - CURLE_UNSUPPORTED_PROTOCOL, /* 1 */ - CURLE_FAILED_INIT, /* 2 */ - CURLE_URL_MALFORMAT, /* 3 */ - CURLE_NOT_BUILT_IN, /* 4 - [was obsoleted in August 2007 for - 7.17.0, reused in April 2011 for 7.21.5] */ - CURLE_COULDNT_RESOLVE_PROXY, /* 5 */ - CURLE_COULDNT_RESOLVE_HOST, /* 6 */ - CURLE_COULDNT_CONNECT, /* 7 */ - CURLE_WEIRD_SERVER_REPLY, /* 8 */ - CURLE_REMOTE_ACCESS_DENIED, /* 9 a service was denied by the server - due to lack of access - when login fails - this is not returned. */ - CURLE_FTP_ACCEPT_FAILED, /* 10 - [was obsoleted in April 2006 for - 7.15.4, reused in Dec 2011 for 7.24.0]*/ - CURLE_FTP_WEIRD_PASS_REPLY, /* 11 */ - CURLE_FTP_ACCEPT_TIMEOUT, /* 12 - timeout occurred accepting server - [was obsoleted in August 2007 for 7.17.0, - reused in Dec 2011 for 7.24.0]*/ - CURLE_FTP_WEIRD_PASV_REPLY, /* 13 */ - CURLE_FTP_WEIRD_227_FORMAT, /* 14 */ - CURLE_FTP_CANT_GET_HOST, /* 15 */ - CURLE_HTTP2, /* 16 - A problem in the http2 framing layer. - [was obsoleted in August 2007 for 7.17.0, - reused in July 2014 for 7.38.0] */ - CURLE_FTP_COULDNT_SET_TYPE, /* 17 */ - CURLE_PARTIAL_FILE, /* 18 */ - CURLE_FTP_COULDNT_RETR_FILE, /* 19 */ - CURLE_OBSOLETE20, /* 20 - NOT USED */ - CURLE_QUOTE_ERROR, /* 21 - quote command failure */ - CURLE_HTTP_RETURNED_ERROR, /* 22 */ - CURLE_WRITE_ERROR, /* 23 */ - CURLE_OBSOLETE24, /* 24 - NOT USED */ - CURLE_UPLOAD_FAILED, /* 25 - failed upload "command" */ - CURLE_READ_ERROR, /* 26 - couldn't open/read from file */ - CURLE_OUT_OF_MEMORY, /* 27 */ - /* Note: CURLE_OUT_OF_MEMORY may sometimes indicate a conversion error - instead of a memory allocation error if CURL_DOES_CONVERSIONS - is defined - */ - CURLE_OPERATION_TIMEDOUT, /* 28 - the timeout time was reached */ - CURLE_OBSOLETE29, /* 29 - NOT USED */ - CURLE_FTP_PORT_FAILED, /* 30 - FTP PORT operation failed */ - CURLE_FTP_COULDNT_USE_REST, /* 31 - the REST command failed */ - CURLE_OBSOLETE32, /* 32 - NOT USED */ - CURLE_RANGE_ERROR, /* 33 - RANGE "command" didn't work */ - CURLE_HTTP_POST_ERROR, /* 34 */ - CURLE_SSL_CONNECT_ERROR, /* 35 - wrong when connecting with SSL */ - CURLE_BAD_DOWNLOAD_RESUME, /* 36 - couldn't resume download */ - CURLE_FILE_COULDNT_READ_FILE, /* 37 */ - CURLE_LDAP_CANNOT_BIND, /* 38 */ - CURLE_LDAP_SEARCH_FAILED, /* 39 */ - CURLE_OBSOLETE40, /* 40 - NOT USED */ - CURLE_FUNCTION_NOT_FOUND, /* 41 - NOT USED starting with 7.53.0 */ - CURLE_ABORTED_BY_CALLBACK, /* 42 */ - CURLE_BAD_FUNCTION_ARGUMENT, /* 43 */ - CURLE_OBSOLETE44, /* 44 - NOT USED */ - CURLE_INTERFACE_FAILED, /* 45 - CURLOPT_INTERFACE failed */ - CURLE_OBSOLETE46, /* 46 - NOT USED */ - CURLE_TOO_MANY_REDIRECTS, /* 47 - catch endless re-direct loops */ - CURLE_UNKNOWN_OPTION, /* 48 - User specified an unknown option */ - CURLE_TELNET_OPTION_SYNTAX, /* 49 - Malformed telnet option */ - CURLE_OBSOLETE50, /* 50 - NOT USED */ - CURLE_OBSOLETE51, /* 51 - NOT USED */ - CURLE_GOT_NOTHING, /* 52 - when this is a specific error */ - CURLE_SSL_ENGINE_NOTFOUND, /* 53 - SSL crypto engine not found */ - CURLE_SSL_ENGINE_SETFAILED, /* 54 - can not set SSL crypto engine as - default */ - CURLE_SEND_ERROR, /* 55 - failed sending network data */ - CURLE_RECV_ERROR, /* 56 - failure in receiving network data */ - CURLE_OBSOLETE57, /* 57 - NOT IN USE */ - CURLE_SSL_CERTPROBLEM, /* 58 - problem with the local certificate */ - CURLE_SSL_CIPHER, /* 59 - couldn't use specified cipher */ - CURLE_PEER_FAILED_VERIFICATION, /* 60 - peer's certificate or fingerprint - wasn't verified fine */ - CURLE_BAD_CONTENT_ENCODING, /* 61 - Unrecognized/bad encoding */ - CURLE_LDAP_INVALID_URL, /* 62 - Invalid LDAP URL */ - CURLE_FILESIZE_EXCEEDED, /* 63 - Maximum file size exceeded */ - CURLE_USE_SSL_FAILED, /* 64 - Requested FTP SSL level failed */ - CURLE_SEND_FAIL_REWIND, /* 65 - Sending the data requires a rewind - that failed */ - CURLE_SSL_ENGINE_INITFAILED, /* 66 - failed to initialise ENGINE */ - CURLE_LOGIN_DENIED, /* 67 - user, password or similar was not - accepted and we failed to login */ - CURLE_TFTP_NOTFOUND, /* 68 - file not found on server */ - CURLE_TFTP_PERM, /* 69 - permission problem on server */ - CURLE_REMOTE_DISK_FULL, /* 70 - out of disk space on server */ - CURLE_TFTP_ILLEGAL, /* 71 - Illegal TFTP operation */ - CURLE_TFTP_UNKNOWNID, /* 72 - Unknown transfer ID */ - CURLE_REMOTE_FILE_EXISTS, /* 73 - File already exists */ - CURLE_TFTP_NOSUCHUSER, /* 74 - No such user */ - CURLE_CONV_FAILED, /* 75 - conversion failed */ - CURLE_CONV_REQD, /* 76 - caller must register conversion - callbacks using curl_easy_setopt options - CURLOPT_CONV_FROM_NETWORK_FUNCTION, - CURLOPT_CONV_TO_NETWORK_FUNCTION, and - CURLOPT_CONV_FROM_UTF8_FUNCTION */ - CURLE_SSL_CACERT_BADFILE, /* 77 - could not load CACERT file, missing - or wrong format */ - CURLE_REMOTE_FILE_NOT_FOUND, /* 78 - remote file not found */ - CURLE_SSH, /* 79 - error from the SSH layer, somewhat - generic so the error message will be of - interest when this has happened */ - - CURLE_SSL_SHUTDOWN_FAILED, /* 80 - Failed to shut down the SSL - connection */ - CURLE_AGAIN, /* 81 - socket is not ready for send/recv, - wait till it's ready and try again (Added - in 7.18.2) */ - CURLE_SSL_CRL_BADFILE, /* 82 - could not load CRL file, missing or - wrong format (Added in 7.19.0) */ - CURLE_SSL_ISSUER_ERROR, /* 83 - Issuer check failed. (Added in - 7.19.0) */ - CURLE_FTP_PRET_FAILED, /* 84 - a PRET command failed */ - CURLE_RTSP_CSEQ_ERROR, /* 85 - mismatch of RTSP CSeq numbers */ - CURLE_RTSP_SESSION_ERROR, /* 86 - mismatch of RTSP Session Ids */ - CURLE_FTP_BAD_FILE_LIST, /* 87 - unable to parse FTP file list */ - CURLE_CHUNK_FAILED, /* 88 - chunk callback reported error */ - CURLE_NO_CONNECTION_AVAILABLE, /* 89 - No connection available, the - session will be queued */ - CURLE_SSL_PINNEDPUBKEYNOTMATCH, /* 90 - specified pinned public key did not - match */ - CURLE_SSL_INVALIDCERTSTATUS, /* 91 - invalid certificate status */ - CURLE_HTTP2_STREAM, /* 92 - stream error in HTTP/2 framing layer - */ - CURLE_RECURSIVE_API_CALL, /* 93 - an api function was called from - inside a callback */ - CURL_LAST /* never use! */ -} CURLcode; - -/* Escape and unescape URL encoding in strings. The functions return a new - * allocated string or NULL if an error occurred. */ - -bool Curl_isunreserved(unsigned char in); -CURLcode Curl_urldecode(const char *string, size_t length, - char **ostring, size_t *olen, - bool reject_crlf); - -char *curl_easy_escape(const char *string, int length); - -char *curl_escape(const char *string, int length); - -char *curl_easy_unescape(const char *string, - int length, int *outlength); - -char *curl_unescape(const char *string, int length); - - -#endif /* HEADER_CURL_ESCAPE_H */ diff --git a/sources/cgmnlm.git/tree/include/gmni/certs.h.txt b/sources/cgmnlm.git/tree/include/gmni/certs.h.txt @@ -1,27 +0,0 @@ -#ifndef GEMINI_CERTS_H -#define GEMINI_CERTS_H -#include <bearssl.h> -#include <stdio.h> - -struct gmni_options; - -struct gmni_client_certificate { - br_x509_certificate *chain; - size_t nchain; - struct gmni_private_key *key; -}; - -struct gmni_private_key { - int type; - union { - br_rsa_private_key rsa; - br_ec_private_key ec; - }; - unsigned char data[]; -}; - -// Returns nonzero on failure and sets errno. Closes both files. -int gmni_ccert_load(struct gmni_client_certificate *cert, - FILE *certin, FILE *skin); - -#endif diff --git a/sources/cgmnlm.git/tree/include/gmni/gmni.h.txt b/sources/cgmnlm.git/tree/include/gmni/gmni.h.txt @@ -1,174 +0,0 @@ -#ifndef GEMINI_CLIENT_H -#define GEMINI_CLIENT_H -#include <bearssl.h> -#include <netdb.h> -#include <stdbool.h> -#include <sys/socket.h> - -enum gemini_result { - GEMINI_OK, - GEMINI_ERR_OOM, - GEMINI_ERR_INVALID_URL, - GEMINI_ERR_NOT_GEMINI, - GEMINI_ERR_RESOLVE, - GEMINI_ERR_CONNECT, - GEMINI_ERR_SSL, - GEMINI_ERR_SSL_VERIFY, - GEMINI_ERR_IO, - GEMINI_ERR_PROTOCOL, -}; - -enum gemini_status { - GEMINI_STATUS_INPUT = 10, - GEMINI_STATUS_SENSITIVE_INPUT = 11, - GEMINI_STATUS_SUCCESS = 20, - GEMINI_STATUS_REDIRECT_TEMPORARY = 30, - GEMINI_STATUS_REDIRECT_PERMANENT = 31, - GEMINI_STATUS_TEMPORARY_FAILURE = 40, - GEMINI_STATUS_SERVER_UNAVAILABLE = 41, - GEMINI_STATUS_CGI_ERROR = 42, - GEMINI_STATUS_PROXY_ERROR = 43, - GEMINI_STATUS_SLOW_DOWN = 44, - GEMINI_STATUS_PERMANENT_FAILURE = 50, - GEMINI_STATUS_NOT_FOUND = 51, - GEMINI_STATUS_GONE = 52, - GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, - GEMINI_STATUS_BAD_REQUEST = 59, - GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, - GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, - GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, -}; - -enum gemini_status_class { - GEMINI_STATUS_CLASS_INPUT = 10, - GEMINI_STATUS_CLASS_SUCCESS = 20, - GEMINI_STATUS_CLASS_REDIRECT = 30, - GEMINI_STATUS_CLASS_TEMPORARY_FAILURE = 40, - GEMINI_STATUS_CLASS_PERMANENT_FAILURE = 50, - GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED = 60, -}; - -struct gemini_response { - enum gemini_status status; - char *meta; - - // TODO: Make these private - // Response body may be read from here if appropriate: - br_sslio_context body; - - // Connection state - br_ssl_client_context *sc; - int fd; -}; - -struct gmni_client_certificate; - -struct gemini_options { - // If ai_family != AF_UNSPEC (the default value on most systems), the - // client will connect to this address and skip name resolution. - struct addrinfo *addr; - - // If non-NULL, these hints are provided to getaddrinfo. Useful, for - // example, to force IPv4/IPv6. - struct addrinfo *hints; - - // If non-NULL, this will be used as the client certificate for the - // request. The other fields must be set as well. - struct gmni_client_certificate *client_cert; -}; - -struct gemini_tofu; - -// Requests the specified URL via the gemini protocol. If options is non-NULL, -// it may specify some additional configuration to adjust client behavior. -// -// Returns a value indicating the success of the request. -// -// Caller must call gemini_response_finish afterwards to clean up resources -// before exiting or re-using it for another request. -enum gemini_result gemini_request(const char *url, - struct gemini_options *options, - struct gemini_tofu *tofu, - struct gemini_response *resp); - -// Must be called after gemini_request in order to free up the resources -// allocated during the request. -void gemini_response_finish(struct gemini_response *resp); - -// Returns a user-friendly string describing an error. -const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); - -// Returns the given URL with the input response set to the specified value. -// The caller must free the string. -char *gemini_input_url(const char *url, const char *input); - -// Returns the general response class (i.e. with the second digit set to zero) -// of the given Gemini status code. -enum gemini_status_class gemini_response_class(enum gemini_status status); - -enum gemini_tok { - GEMINI_TEXT, - GEMINI_LINK, - GEMINI_PREFORMATTED_BEGIN, - GEMINI_PREFORMATTED_END, - GEMINI_PREFORMATTED_TEXT, - GEMINI_HEADING, - GEMINI_LIST_ITEM, - GEMINI_QUOTE, -}; - -struct gemini_token { - enum gemini_tok token; - - // The token field determines which of the union members is valid. - union { - char *text; - - struct { - char *text; - char *url; // May be NULL - } link; - - char *preformatted; - - struct { - char *title; - int level; // 1, 2, or 3 - } heading; - - char *list_item; - char *quote_text; - }; -}; - -struct gemini_parser { - int (*read)(void *state, void *buf, size_t nbyte); - void *state; - char *buf; - size_t bufsz; - size_t bufln; - bool preformatted; -}; - -// Initializes a text/gemini parser. The provided "read" function will be called -// with the provided "state" value in order to obtain more gemtext data. The -// read function should behave like read(3). -void gemini_parser_init(struct gemini_parser *p, - int (*read)(void *state, void *buf, size_t nbyte), - void *state); - -// Finishes this text/gemini parser and frees up its resources. -void gemini_parser_finish(struct gemini_parser *p); - -// Reads the next token from a text/gemini file. -// -// Returns 0 on success, 1 on EOF, and -1 on failure. -// -// Caller must call gemini_token_finish before exiting or re-using the token -// parameter. -int gemini_parser_next(struct gemini_parser *p, struct gemini_token *token); - -// Must be called after gemini_next to free up resources for the next token. -void gemini_token_finish(struct gemini_token *token); - -#endif diff --git a/sources/cgmnlm.git/tree/include/gmni/index.gmi b/sources/cgmnlm.git/tree/include/gmni/index.gmi @@ -1,8 +0,0 @@ -# Tree - -Path: include/gmni/ - -=> certs.h.txt certs.h -=> gmni.h.txt gmni.h -=> tofu.h.txt tofu.h -=> url.h.txt url.h diff --git a/sources/cgmnlm.git/tree/include/gmni/tofu.h.txt b/sources/cgmnlm.git/tree/include/gmni/tofu.h.txt @@ -1,61 +0,0 @@ -#ifndef GEMINI_TOFU_H -#define GEMINI_TOFU_H -#include <bearssl.h> -#include <limits.h> - -enum tofu_error { - TOFU_VALID, - // Expired, wrong CN, etc. - TOFU_INVALID_CERT, - // Cert is valid but we haven't seen it before - TOFU_UNTRUSTED_CERT, - // Cert is valid but we already trust another cert for this host - TOFU_FINGERPRINT_MISMATCH, -}; - -enum tofu_action { - TOFU_ASK, - TOFU_FAIL, - TOFU_TRUST_ONCE, - TOFU_TRUST_ALWAYS, -}; - -struct known_host { - char *host, *fingerprint; - int lineno; - struct known_host *next; -}; - -// Called when the user needs to be prompted to agree to trust an unknown -// certificate. Return true to trust this certificate. -typedef enum tofu_action (tofu_callback_t)(enum tofu_error error, - const char *fingerprint, struct known_host *host, void *data); - -struct gemini_tofu; - -struct x509_tofu_context { - const br_x509_class *vtable; - br_x509_decoder_context decoder; - br_x509_pkey *pkey; - br_sha512_context sha512; - unsigned char hash[64]; - struct gemini_tofu *store; - const char *server_name; - int err; -}; - -struct gemini_tofu { - struct x509_tofu_context x509_ctx; - br_ssl_client_context sc; - unsigned char iobuf[BR_SSL_BUFSIZE_BIDI]; - char known_hosts_path[PATH_MAX+1]; - struct known_host *known_hosts; - int lineno; - tofu_callback_t *callback; - void *cb_data; -}; - -void gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *data); -void gemini_tofu_finish(struct gemini_tofu *tofu); - -#endif diff --git a/sources/cgmnlm.git/tree/include/gmni/url.h.txt b/sources/cgmnlm.git/tree/include/gmni/url.h.txt @@ -1,103 +0,0 @@ -#ifndef URLAPI_H -#define URLAPI_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) 2018, Daniel Stenberg, <daniel@haxx.se>, et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.haxx.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - ***************************************************************************/ - -/* the error codes for the URL API */ -typedef enum { - CURLUE_OK, - CURLUE_BAD_HANDLE, /* 1 */ - CURLUE_BAD_PARTPOINTER, /* 2 */ - CURLUE_MALFORMED_INPUT, /* 3 */ - CURLUE_BAD_PORT_NUMBER, /* 4 */ - CURLUE_UNSUPPORTED_SCHEME, /* 5 */ - CURLUE_URLDECODE, /* 6 */ - CURLUE_OUT_OF_MEMORY, /* 7 */ - CURLUE_USER_NOT_ALLOWED, /* 8 */ - CURLUE_UNKNOWN_PART, /* 9 */ - CURLUE_NO_SCHEME, /* 10 */ - CURLUE_NO_USER, /* 11 */ - CURLUE_NO_PASSWORD, /* 12 */ - CURLUE_NO_OPTIONS, /* 13 */ - CURLUE_NO_HOST, /* 14 */ - CURLUE_NO_PORT, /* 15 */ - CURLUE_NO_QUERY, /* 16 */ - CURLUE_NO_FRAGMENT /* 17 */ -} CURLUcode; - -typedef enum { - CURLUPART_URL, - CURLUPART_SCHEME, - CURLUPART_USER, - CURLUPART_PASSWORD, - CURLUPART_OPTIONS, - CURLUPART_HOST, - CURLUPART_PORT, - CURLUPART_PATH, - CURLUPART_QUERY, - CURLUPART_FRAGMENT -} CURLUPart; - -#define CURLU_PATH_AS_IS (1<<4) /* leave dot sequences */ -#define CURLU_DISALLOW_USER (1<<5) /* no user+password allowed */ -#define CURLU_URLDECODE (1<<6) /* URL decode on get */ -#define CURLU_URLENCODE (1<<7) /* URL encode on set */ -#define CURLU_APPENDQUERY (1<<8) /* append a form style part */ - -typedef struct Curl_URL CURLU; - -/* - * curl_url() creates a new CURLU handle and returns a pointer to it. - * Must be freed with curl_url_cleanup(). - */ -struct Curl_URL *curl_url(void); - -/* - * curl_url_cleanup() frees the CURLU handle and related resources used for - * the URL parsing. It will not free strings previously returned with the URL - * API. - */ -void curl_url_cleanup(struct Curl_URL *handle); - -/* - * curl_url_dup() duplicates a CURLU handle and returns a new copy. The new - * handle must also be freed with curl_url_cleanup(). - */ -struct Curl_URL *curl_url_dup(struct Curl_URL *in); - -/* - * curl_url_get() extracts a specific part of the URL from a CURLU - * handle. Returns error code. The returned pointer MUST be freed with - * free() afterwards. - */ -CURLUcode curl_url_get(struct Curl_URL *handle, CURLUPart what, - char **part, unsigned int flags); - -/* - * curl_url_set() sets a specific part of the URL in a CURLU handle. Returns - * error code. The passed in string will be copied. Passing a NULL instead of - * a part string, clears that part. - */ -CURLUcode curl_url_set(struct Curl_URL *handle, CURLUPart what, - const char *part, unsigned int flags); - -#endif diff --git a/sources/cgmnlm.git/tree/include/index.gmi b/sources/cgmnlm.git/tree/include/index.gmi @@ -1,7 +0,0 @@ -# Tree - -Path: include/ - -=> escape.h.txt escape.h -=> gmni/ gmni/ -=> util.h.txt util.h diff --git a/sources/cgmnlm.git/tree/include/util.h.txt b/sources/cgmnlm.git/tree/include/util.h.txt @@ -1,17 +0,0 @@ -#ifndef GEMINI_UTIL_H -#define GEMINI_UTIL_H -#include <stdio.h> -#include <sys/types.h> - -struct pathspec { - const char *var; - const char *path; -}; - -char *getpath(const struct pathspec *paths, size_t npaths); -void posix_dirname(char *path, char *dname); -int mkdirs(char *path, mode_t mode); -int download_resp(FILE *out, struct gemini_response resp, const char *path, - char *url); - -#endif diff --git a/sources/cgmnlm.git/tree/index.gmi b/sources/cgmnlm.git/tree/index.gmi @@ -1,13 +0,0 @@ -# Tree - -Path: / - -=> .gitignore.txt .gitignore -=> COPYING.txt COPYING -=> Makefile.txt Makefile -=> README.md.txt README.md -=> config.sh.txt config.sh -=> configure.txt configure -=> doc/ doc/ -=> include/ include/ -=> src/ src/ diff --git a/sources/cgmnlm.git/tree/src/certs.c.txt b/sources/cgmnlm.git/tree/src/certs.c.txt @@ -1,156 +0,0 @@ -#include <assert.h> -#include <bearssl.h> -#include <errno.h> -#include <gmni/certs.h> -#include <gmni/gmni.h> -#include <stdio.h> -#include <stdlib.h> - -static void -crt_append(void *ctx, const void *src, size_t len) -{ - br_x509_certificate *crt = (br_x509_certificate *)ctx; - crt->data = realloc(crt->data, crt->data_len + len); - assert(crt->data); - memcpy(&crt->data[crt->data_len], src, len); - crt->data_len += len; -} - -static void -key_append(void *ctx, const void *src, size_t len) -{ - br_skey_decoder_context *skctx = (br_skey_decoder_context *)ctx; - br_skey_decoder_push(skctx, src, len); -} - -int -gmni_ccert_load(struct gmni_client_certificate *cert, FILE *certin, FILE *skin) -{ - // TODO: Better error propagation to caller - static unsigned char buf[BUFSIZ]; - - br_pem_decoder_context pemdec; - br_pem_decoder_init(&pemdec); - - cert->chain = NULL; - cert->nchain = 0; - - static const char *certname = "CERTIFICATE"; - while (!feof(certin)) { - size_t n = fread(&buf, 1, sizeof(buf), certin); - if (ferror(certin)) { - goto error; - } - size_t q = 0; - while (q < n) { - q += br_pem_decoder_push(&pemdec, &buf[q], n - q); - switch (br_pem_decoder_event(&pemdec)) { - case BR_PEM_BEGIN_OBJ: - if (strcmp(br_pem_decoder_name(&pemdec), certname) != 0) { - break; - } - cert->chain = realloc(cert->chain, - sizeof(br_x509_certificate) * (cert->nchain + 1)); - memset(&cert->chain[cert->nchain], 0, sizeof(*cert->chain)); - br_pem_decoder_setdest(&pemdec, &crt_append, - &cert->chain[cert->nchain]); - ++cert->nchain; - break; - case BR_PEM_END_OBJ: - break; - case BR_PEM_ERROR: - fprintf(stderr, "Error decoding PEM certificate\n"); - errno = EINVAL; - goto error; - } - } - } - - if (cert->nchain == 0) { - fprintf(stderr, "No certificates found in provided client certificate file\n"); - errno = EINVAL; - goto error; - } - - br_skey_decoder_context skdec = {0}; - br_skey_decoder_init(&skdec); - br_pem_decoder_init(&pemdec); - - // TODO: Better validation of PEM file - while (!feof(skin)) { - size_t n = fread(&buf, 1, sizeof(buf), skin); - if (ferror(skin)) { - goto error; - } - size_t q = 0; - while (q < n) { - q += br_pem_decoder_push(&pemdec, &buf[q], n - q); - switch (br_pem_decoder_event(&pemdec)) { - case BR_PEM_BEGIN_OBJ: - br_pem_decoder_setdest(&pemdec, &key_append, &skdec); - break; - case BR_PEM_END_OBJ: - // no-op - break; - case BR_PEM_ERROR: - fprintf(stderr, "Error decoding PEM private key\n"); - errno = EINVAL; - goto error; - } - } - } - - int err = br_skey_decoder_last_error(&skdec); - if (err != 0) { - fprintf(stderr, "Error loading private key: %d\n", err); - errno = EINVAL; - goto error; - } - switch (br_skey_decoder_key_type(&skdec)) { - struct gmni_private_key *k; - const br_ec_private_key *ec; - const br_rsa_private_key *rsa; - case BR_KEYTYPE_RSA: - rsa = br_skey_decoder_get_rsa(&skdec); - cert->key = k = malloc(sizeof(*k) - + rsa->plen + rsa->qlen - + rsa->dplen + rsa->dqlen - + rsa->iqlen); - assert(k); - k->type = BR_KEYTYPE_RSA; - k->rsa = *rsa; - k->rsa.p = k->data; - k->rsa.q = k->rsa.p + k->rsa.plen; - k->rsa.dp = k->rsa.q + k->rsa.qlen; - k->rsa.dq = k->rsa.dp + k->rsa.dplen; - k->rsa.iq = k->rsa.dq + k->rsa.dqlen; - memcpy(k->rsa.p, rsa->p, rsa->plen); - memcpy(k->rsa.q, rsa->q, rsa->qlen); - memcpy(k->rsa.dp, rsa->dp, rsa->dplen); - memcpy(k->rsa.dq, rsa->dq, rsa->dqlen); - memcpy(k->rsa.iq, rsa->iq, rsa->iqlen); - break; - case BR_KEYTYPE_EC: - ec = br_skey_decoder_get_ec(&skdec); - cert->key = k = malloc(sizeof(*k) + ec->xlen); - assert(k); - k->type = BR_KEYTYPE_EC; - k->ec.curve = ec->curve; - k->ec.x = k->data; - k->ec.xlen = ec->xlen; - memcpy(k->ec.x, ec->x, ec->xlen); - break; - default: - assert(0); - } - - fclose(certin); - fclose(skin); - return 0; - -error: - fclose(certin); - fclose(skin); - free(cert->chain); - return 1; -} diff --git a/sources/cgmnlm.git/tree/src/client.c.txt b/sources/cgmnlm.git/tree/src/client.c.txt @@ -1,330 +0,0 @@ -#include <assert.h> -#include <errno.h> -#include <netdb.h> -#include <bearssl.h> -#include <stdlib.h> -#include <stdio.h> -#include <string.h> -#include <sys/socket.h> -#include <sys/types.h> -#include <unistd.h> -#include <gmni/certs.h> -#include <gmni/gmni.h> -#include <gmni/tofu.h> -#include <gmni/url.h> - -static enum gemini_result -gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options, - struct gemini_response *resp, struct addrinfo **addr) -{ - int port = 1965; - char *uport; - if (curl_url_get(uri, CURLUPART_PORT, &uport, 0) == CURLUE_OK) { - port = (int)strtol(uport, NULL, 10); - free(uport); - } - - if (options && options->addr && options->addr->ai_family != AF_UNSPEC) { - *addr = options->addr; - } else { - struct addrinfo hints = {0}; - if (options && options->hints) { - hints = *options->hints; - } else { - hints.ai_family = AF_UNSPEC; - } - hints.ai_socktype = SOCK_STREAM; - - char pbuf[7]; - snprintf(pbuf, sizeof(pbuf), "%d", port); - - char *domain; - CURLUcode uc = curl_url_get(uri, CURLUPART_HOST, &domain, 0); - assert(uc == CURLUE_OK); - - int r = getaddrinfo(domain, pbuf, &hints, addr); - free(domain); - if (r != 0) { - resp->status = r; - return GEMINI_ERR_RESOLVE; - } - } - - return GEMINI_OK; -} - -static enum gemini_result -gemini_connect(struct Curl_URL *uri, struct gemini_options *options, - struct gemini_response *resp, int *sfd) -{ - struct addrinfo *addr; - enum gemini_result res = gemini_get_addrinfo(uri, options, resp, &addr); - if (res != GEMINI_OK) { - return res; - } - - struct addrinfo *rp; - for (rp = addr; rp != NULL; rp = rp->ai_next) { - *sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); - if (*sfd == -1) { - continue; - } - if (connect(*sfd, rp->ai_addr, rp->ai_addrlen) != -1) { - break; - } - close(*sfd); - } - if (rp == NULL) { - resp->status = errno; - res = GEMINI_ERR_CONNECT; - return res; - } - - if (!options || !options->addr) { - freeaddrinfo(addr); - } - return res; -} - -#define GEMINI_META_MAXLEN 1024 -#define GEMINI_STATUS_MAXLEN 2 - -static int -sock_read(void *ctx, unsigned char *buf, size_t len) -{ - for (;;) { - ssize_t rlen; - rlen = read(*(int *)ctx, buf, len); - if (rlen <= 0) { - if (rlen < 0 && errno == EINTR) { - continue; - } - return -1; - } - return (int)rlen; - } -} - -static int -sock_write(void *ctx, const unsigned char *buf, size_t len) -{ - for (;;) { - ssize_t wlen; - wlen = write(*(int *)ctx, buf, len); - if (wlen <= 0) { - if (wlen < 0 && errno == EINTR) { - continue; - } - return -1; - } - return (int)wlen; - } -} - -enum gemini_result -gemini_request(const char *url, struct gemini_options *options, - struct gemini_tofu *tofu, struct gemini_response *resp) -{ - assert(url); - assert(resp); - memset(resp, 0, sizeof(*resp)); - if (strlen(url) > 1024) { - return GEMINI_ERR_INVALID_URL; - } - - struct Curl_URL *uri = curl_url(); - if (!uri) { - return GEMINI_ERR_OOM; - } - - enum gemini_result res = GEMINI_OK; - if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { - res = GEMINI_ERR_INVALID_URL; - goto cleanup; - } - - char *scheme, *host; - if (curl_url_get(uri, CURLUPART_SCHEME, &scheme, 0) != CURLUE_OK) { - res = GEMINI_ERR_INVALID_URL; - goto cleanup; - } else { - if (strcmp(scheme, "gemini") != 0) { - res = GEMINI_ERR_NOT_GEMINI; - free(scheme); - goto cleanup; - } - free(scheme); - } - if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) { - res = GEMINI_ERR_INVALID_URL; - free(host); - goto cleanup; - } - - int r; - res = gemini_connect(uri, options, resp, &resp->fd); - if (res != GEMINI_OK) { - free(host); - goto cleanup; - } - - // TODO: session reuse - resp->sc = &tofu->sc; - if (options->client_cert) { - struct gmni_client_certificate *cert = options->client_cert; - struct gmni_private_key *key = cert->key; - switch (key->type) { - case BR_KEYTYPE_RSA: - br_ssl_client_set_single_rsa(resp->sc, - cert->chain, cert->nchain, &key->rsa, - br_rsa_pkcs1_sign_get_default()); - break; - case BR_KEYTYPE_EC: - br_ssl_client_set_single_ec(resp->sc, - cert->chain, cert->nchain, &key->ec, - BR_KEYTYPE_SIGN, 0, - br_ec_get_default(), - br_ecdsa_sign_asn1_get_default()); - break; - } - } - br_ssl_client_reset(resp->sc, host, 0); - - br_sslio_init(&resp->body, &resp->sc->eng, - sock_read, &resp->fd, sock_write, &resp->fd); - - char req[1024 + 3]; - r = snprintf(req, sizeof(req), "%s\r\n", url); - assert(r > 0); - - br_sslio_write_all(&resp->body, req, r); - br_sslio_flush(&resp->body); - - // The SSL engine maintains an internal buffer, so this shouldn't be as - // inefficient as it looks. It's necessary to do this one byte at a time - // to avoid consuming any of the response body buffer. - char buf[GEMINI_META_MAXLEN - + GEMINI_STATUS_MAXLEN - + 2 /* CRLF */ + 1 /* NUL */]; - memset(buf, 0, sizeof(buf)); - size_t l; - for (l = 0; l < 2 || memcmp(&buf[l-2], "\r\n", 2) != 0; ++l) { - r = br_sslio_read(&resp->body, &buf[l], 1); - if (r < 0) { - break; - } - } - - int err = br_ssl_engine_last_error(&resp->sc->eng); - if (err != 0) { - // TODO: Bubble this up properly - fprintf(stderr, "SSL error %d\n", err); - goto ssl_error; - } - - if (l < 3 || strcmp(&buf[l-2], "\r\n") != 0) { - fprintf(stderr, "invalid line '%s'\n", buf); - res = GEMINI_ERR_PROTOCOL; - goto cleanup; - } - - char *endptr; - resp->status = (enum gemini_status)strtol(buf, &endptr, 10); - if (*endptr != ' ' || resp->status < 10 || (int)resp->status >= 70) { - fprintf(stderr, "invalid status\n"); - res = GEMINI_ERR_PROTOCOL; - goto cleanup; - } - resp->meta = calloc(l - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); - strncpy(resp->meta, &endptr[1], l - 5); - resp->meta[l - 5] = '\0'; - -cleanup: - curl_url_cleanup(uri); - return res; -ssl_error: - res = GEMINI_ERR_SSL; - resp->status = r; - goto cleanup; -} - -void -gemini_response_finish(struct gemini_response *resp) -{ - if (!resp) { - return; - } - - if (resp->fd != -1) { - close(resp->fd); - resp->fd = -1; - } - - free(resp->meta); - - if (resp->sc) { - br_sslio_close(&resp->body); - } - - resp->sc = NULL; - resp->meta = NULL; -} - -const char * -gemini_strerr(enum gemini_result r, struct gemini_response *resp) -{ - switch (r) { - case GEMINI_OK: - return "OK"; - case GEMINI_ERR_OOM: - return "Out of memory"; - case GEMINI_ERR_INVALID_URL: - return "Invalid URL"; - case GEMINI_ERR_NOT_GEMINI: - return "Not a gemini URL"; - case GEMINI_ERR_RESOLVE: - return gai_strerror(resp->status); - case GEMINI_ERR_CONNECT: - return strerror(errno); - case GEMINI_ERR_SSL: - // TODO: more specific - return "SSL error"; - case GEMINI_ERR_SSL_VERIFY: - // TODO: more specific - return "X.509 certificate not trusted"; - case GEMINI_ERR_IO: - return "I/O error"; - case GEMINI_ERR_PROTOCOL: - return "Protocol error"; - } - assert(0); -} - -char * -gemini_input_url(const char *url, const char *input) -{ - char *new_url = NULL; - struct Curl_URL *uri = curl_url(); - if (!uri) { - return NULL; - } - if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { - goto cleanup; - } - if (curl_url_set(uri, CURLUPART_QUERY, input, CURLU_URLENCODE) != CURLUE_OK) { - goto cleanup; - } - if (curl_url_get(uri, CURLUPART_URL, &new_url, 0) != CURLUE_OK) { - new_url = NULL; - goto cleanup; - } -cleanup: - curl_url_cleanup(uri); - return new_url; -} - -enum gemini_status_class -gemini_response_class(enum gemini_status status) -{ - return status / 10 * 10; -} diff --git a/sources/cgmnlm.git/tree/src/escape.c.txt b/sources/cgmnlm.git/tree/src/escape.c.txt @@ -1,213 +0,0 @@ -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.haxx.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - ***************************************************************************/ - -/* Escape and unescape URL encoding in strings. The functions return a new - * allocated string or NULL if an error occurred. */ -#include <ctype.h> -#include <limits.h> -#include <stdbool.h> -#include <stddef.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include "escape.h" - -/* Portable character check (remember EBCDIC). Do not use isalnum() because - its behavior is altered by the current locale. - See https://tools.ietf.org/html/rfc3986#section-2.3 -*/ -bool Curl_isunreserved(unsigned char in) -{ - switch(in) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - case 'a': case 'b': case 'c': case 'd': case 'e': - case 'f': case 'g': case 'h': case 'i': case 'j': - case 'k': case 'l': case 'm': case 'n': case 'o': - case 'p': case 'q': case 'r': case 's': case 't': - case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': - case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': case 'G': case 'H': case 'I': case 'J': - case 'K': case 'L': case 'M': case 'N': case 'O': - case 'P': case 'Q': case 'R': case 'S': case 'T': - case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': - case '-': case '.': case '_': case '~': - return true; - default: - break; - } - return false; -} - -/* for ABI-compatibility with previous versions */ -char *curl_escape(const char *string, int inlength) -{ - return curl_easy_escape(string, inlength); -} - -/* for ABI-compatibility with previous versions */ -char *curl_unescape(const char *string, int length) -{ - return curl_easy_unescape(string, length, NULL); -} - -char *curl_easy_escape(const char *string, int inlength) -{ - size_t alloc; - char *ns; - char *testing_ptr = NULL; - size_t newlen; - size_t strindex = 0; - size_t length; - - if(inlength < 0) - return NULL; - - alloc = (inlength?(size_t)inlength:strlen(string)) + 1; - newlen = alloc; - - ns = malloc(alloc); - if(!ns) - return NULL; - - length = alloc-1; - while(length--) { - unsigned char in = *string; /* we need to treat the characters unsigned */ - - if(Curl_isunreserved(in)) - /* just copy this */ - ns[strindex++] = in; - else { - /* encode it */ - newlen += 2; /* the size grows with two, since this'll become a %XX */ - if(newlen > alloc) { - alloc *= 2; - testing_ptr = realloc(ns, alloc); - if(!testing_ptr) - return NULL; - ns = testing_ptr; - } - - snprintf(&ns[strindex], 4, "%%%02X", in); - - strindex += 3; - } - string++; - } - ns[strindex] = 0; /* terminate it */ - return ns; -} - -/* - * Curl_urldecode() URL decodes the given string. - * - * Optionally detects control characters (byte codes lower than 32) in the - * data and rejects such data. - * - * Returns a pointer to a malloced string in *ostring with length given in - * *olen. If length == 0, the length is assumed to be strlen(string). - */ -CURLcode Curl_urldecode(const char *string, size_t length, - char **ostring, size_t *olen, - bool reject_ctrl) -{ - size_t alloc = (length?length:strlen(string)) + 1; - char *ns = malloc(alloc); - size_t strindex = 0; - unsigned long hex; - - if(!ns) - return CURLE_OUT_OF_MEMORY; - - while(--alloc > 0) { - unsigned char in = *string; - if(('%' == in) && (alloc > 2) && - isxdigit(string[1]) && isxdigit(string[2])) { - /* this is two hexadecimal digits following a '%' */ - char hexstr[3]; - char *ptr; - hexstr[0] = string[1]; - hexstr[1] = string[2]; - hexstr[2] = 0; - - hex = strtoul(hexstr, &ptr, 16); - - in = (unsigned char)hex; /* this long is never bigger than 255 anyway */ - - string += 2; - alloc -= 2; - } - - if(reject_ctrl && (in < 0x20)) { - free(ns); - return CURLE_URL_MALFORMAT; - } - - ns[strindex++] = in; - string++; - } - ns[strindex] = 0; /* terminate it */ - - if(olen) - /* store output size */ - *olen = strindex; - - /* store output string */ - *ostring = ns; - - return CURLE_OK; -} - -/* - * Unescapes the given URL escaped string of given length. Returns a - * pointer to a malloced string with length given in *olen. - * If length == 0, the length is assumed to be strlen(string). - * If olen == NULL, no output length is stored. - */ -char *curl_easy_unescape(const char *string, int length, int *olen) -{ - char *str = NULL; - if(length >= 0) { - size_t inputlen = length; - size_t outputlen; - CURLcode res = Curl_urldecode(string, inputlen, &str, &outputlen, false); - if(res) - return NULL; - - if(olen) { - if(outputlen <= (size_t) INT_MAX) - *olen = (int)outputlen; - else - /* too large to return in an int, fail! */ - free(str); - } - } - return str; -} - -/* For operating systems/environments that use different malloc/free - systems for the app and for this library, we provide a free that uses - the library's memory system */ -void curl_free(void *p) -{ - free(p); -} diff --git a/sources/cgmnlm.git/tree/src/gmni.c.txt b/sources/cgmnlm.git/tree/src/gmni.c.txt @@ -1,407 +0,0 @@ -#include <assert.h> -#include <bearssl.h> -#include <errno.h> -#include <getopt.h> -#include <netdb.h> -#include <stdbool.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/socket.h> -#include <sys/types.h> -#include <termios.h> -#include <unistd.h> -#include <gmni/certs.h> -#include <gmni/gmni.h> -#include <gmni/tofu.h> -#include <gmni/url.h> -#include "util.h" - -static void -usage(const char *argv_0) -{ - fprintf(stderr, - "usage: %s [-46lLiIN] [-j mode] [-E cert] [-d input] [-D path] gemini://...\n", - argv_0); -} - -static char * -get_input(const struct gemini_response *resp, FILE *source) -{ - int r = 0; - struct termios attrs; - bool tty = fileno(source) != -1 && isatty(fileno(source)); - char *input = NULL; - if (tty) { - fprintf(stderr, "%s: ", resp->meta); - if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { - r = tcgetattr(fileno(source), &attrs); - struct termios new_attrs; - r = tcgetattr(fileno(source), &new_attrs); - if (r != -1) { - new_attrs.c_lflag &= ~ECHO; - tcsetattr(fileno(source), TCSANOW, &new_attrs); - } - } - } - size_t s = 0; - ssize_t n = getline(&input, &s, source); - if (n == -1) { - fprintf(stderr, "Error reading input: %s\n", - feof(source) ? "EOF" : - strerror(ferror(source))); - return NULL; - } - input[n - 1] = '\0'; // Drop LF - if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { - attrs.c_lflag &= ~ECHO; - tcsetattr(fileno(source), TCSANOW, &attrs); - } - return input; -} - -struct tofu_config { - struct gemini_tofu tofu; - enum tofu_action action; -}; - -static enum tofu_action -tofu_callback(enum tofu_error error, const char *fingerprint, - struct known_host *host, void *data) -{ - struct tofu_config *cfg = (struct tofu_config *)data; - enum tofu_action action = cfg->action; - switch (error) { - case TOFU_VALID: - assert(0); // Invariant - case TOFU_INVALID_CERT: - fprintf(stderr, - "The server presented an invalid certificate with fingerprint %s.\n", - fingerprint); - if (action == TOFU_TRUST_ALWAYS) { - action = TOFU_TRUST_ONCE; - } - break; - case TOFU_UNTRUSTED_CERT: - fprintf(stderr, - "The certificate offered by this server is of unknown trust. " - "Its fingerprint is: \n" - "%s\n\n" - "Use '-j once' to trust temporarily, or '-j always' to add to the trust store.\n", fingerprint); - break; - case TOFU_FINGERPRINT_MISMATCH: - fprintf(stderr, - "The certificate offered by this server DOES NOT MATCH the one we have on file.\n" - "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" - "The unknown certificate's fingerprint is:\n" - "%s\n\n" - "The expected fingerprint is:\n" - "%s\n\n" - "If you're certain that this is correct, edit %s:%d\n", - fingerprint, host->fingerprint, - cfg->tofu.known_hosts_path, host->lineno); - return TOFU_FAIL; - } - - if (action == TOFU_ASK) { - return TOFU_FAIL; - } - - return action; -} - -static struct gmni_client_certificate * -load_client_cert(char *argv_0, char *path) -{ - char *certpath = strtok(path, ":"); - if (!certpath) { - usage(argv_0); - exit(1); - } - - FILE *certf = fopen(certpath, "r"); - if (!certf) { - fprintf(stderr, "Failed to open certificate: %s\n", - strerror(errno)); - exit(1); - } - - char *keypath = strtok(NULL, ":"); - if (!keypath) { - usage(argv_0); - exit(1); - } - - FILE *keyf = fopen(keypath, "r"); - if (!keyf) { - fprintf(stderr, "Failed to open certificate: %s\n", - strerror(errno)); - exit(1); - } - - struct gmni_client_certificate *cert = - calloc(1, sizeof(struct gmni_client_certificate)); - if (gmni_ccert_load(cert, certf, keyf) != 0) { - fprintf(stderr, "Failed to load client certificate: %s\n", - strerror(errno)); - exit(1); - } - return cert; -} - -int -main(int argc, char *argv[]) -{ - enum header_mode { - OMIT_HEADERS, - SHOW_HEADERS, - ONLY_HEADERS, - }; - enum header_mode header_mode = OMIT_HEADERS; - - enum input_mode { - INPUT_READ, - INPUT_SUPPRESS, - }; - enum input_mode input_mode = INPUT_READ; - FILE *input_source = stdin; - - char *output_file = NULL; - - bool follow_redirects = false, linefeed = true; - int max_redirect = 5; - - struct addrinfo hints = {0}; - struct gemini_options opts = { - .hints = &hints, - }; - struct tofu_config cfg; - cfg.action = TOFU_ASK; - - int c; - while ((c = getopt(argc, argv, "46d:D:E:hj:lLiINR:o:")) != -1) { - switch (c) { - case '4': - hints.ai_family = AF_INET; - break; - case '6': - hints.ai_family = AF_INET6; - break; - case 'd': - input_mode = INPUT_READ; - input_source = fmemopen(optarg, strlen(optarg), "r"); - break; - case 'D': - input_mode = INPUT_READ; - if (strcmp(optarg, "-") == 0) { - input_source = stdin; - } else { - input_source = fopen(optarg, "r"); - if (!input_source) { - fprintf(stderr, "Error: open %s: %s", - optarg, strerror(errno)); - return 1; - } - } - break; - case 'E': - opts.client_cert = load_client_cert(argv[0], optarg); - break; - case 'h': - usage(argv[0]); - return 0; - case 'j': - if (strcmp(optarg, "fail") == 0) { - cfg.action = TOFU_FAIL; - } else if (strcmp(optarg, "once") == 0) { - cfg.action = TOFU_TRUST_ONCE; - } else if (strcmp(optarg, "always") == 0) { - cfg.action = TOFU_TRUST_ALWAYS; - } else { - usage(argv[0]); - return 1; - } - break; - case 'l': - linefeed = false; - break; - case 'L': - follow_redirects = true; - break; - case 'i': - header_mode = SHOW_HEADERS; - break; - case 'I': - header_mode = ONLY_HEADERS; - input_mode = INPUT_SUPPRESS; - break; - case 'N': - input_mode = INPUT_SUPPRESS; - break; - case 'R':; - char *endptr; - errno = 0; - max_redirect = strtoul(optarg, &endptr, 10); - if (*endptr || errno != 0) { - fprintf(stderr, "Error: -R expects numeric argument\n"); - return 1; - } - break; - case 'o': - output_file = optarg; - break; - default: - fprintf(stderr, "fatal: unknown flag %c\n", c); - return 1; - } - } - - if (optind != argc - 1) { - usage(argv[0]); - return 1; - } - - gemini_tofu_init(&cfg.tofu, &tofu_callback, &cfg); - - bool exit = false; - struct Curl_URL *url = curl_url(); - - if (curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { - // TODO: Better error - fprintf(stderr, "Error: invalid URL\n"); - return 1; - } - - int ret = 0, nredir = 0; - while (!exit) { - char *buf; - curl_url_get(url, CURLUPART_URL, &buf, 0); - - struct gemini_response resp; - enum gemini_result r = gemini_request(buf, - &opts, &cfg.tofu, &resp); - - free(buf); - - if (r != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); - ret = (int)r; - exit = true; - goto next; - } - - if (header_mode != OMIT_HEADERS) { - printf("%d %s\n", resp.status, resp.meta); - } - - switch (gemini_response_class(resp.status)) { - case GEMINI_STATUS_CLASS_INPUT: - if (input_mode == INPUT_SUPPRESS) { - exit = true; - break; - } - - char *input = get_input(&resp, input_source); - if (!input) { - r = 1; - exit = true; - break; - } - - char *buf; - curl_url_get(url, CURLUPART_URL, &buf, 0); - - char *new_url = gemini_input_url(buf, input); - assert(new_url); - - free(input); - free(buf); - - curl_url_set(url, CURLUPART_URL, new_url, 0); - goto next; - case GEMINI_STATUS_CLASS_REDIRECT: - if (++nredir >= max_redirect) { - fprintf(stderr, - "Error: maximum redirects (%d) exceeded", - max_redirect); - exit = true; - goto next; - } - - curl_url_set(url, CURLUPART_URL, resp.meta, 0); - - if (!follow_redirects) { - if (header_mode == OMIT_HEADERS) { - fprintf(stderr, "REDIRECT: %d %s\n", - resp.status, resp.meta); - } - exit = true; - } - goto next; - case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: - assert(0); // TODO - case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: - case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: - if (header_mode == OMIT_HEADERS) { - fprintf(stderr, "%s: %d %s\n", - resp.status / 10 == 4 ? - "TEMPORARY FAILURE" : "PERMANENT FAILURE", - resp.status, resp.meta); - } - exit = true; - break; - case GEMINI_STATUS_CLASS_SUCCESS: - exit = true; - break; - } - - if (header_mode != ONLY_HEADERS) { - if (gemini_response_class(resp.status) != - GEMINI_STATUS_CLASS_SUCCESS) { - break; - } - - if (output_file != NULL) { - char *buf; - curl_url_get(url, CURLUPART_URL, &buf, 0); - - ret = download_resp(stderr, resp, output_file, buf); - free(buf); - - break; - } - - char last = 0; - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { - n = br_sslio_read(&resp.body, buf, BUFSIZ); - if (n > 0) { - last = buf[n - 1]; - } - ssize_t w = 0; - while (w < (ssize_t)n) { - ssize_t x = fwrite(&buf[w], 1, n - w, stdout); - if (ferror(stdout)) { - fprintf(stderr, "Error: write: %s\n", - strerror(errno)); - return 1; - } - w += x; - } - } - if (strncmp(resp.meta, "text/", 5) == 0 - && linefeed && last != '\n' - && isatty(STDOUT_FILENO)) { - printf("\n"); - } - break; - } - -next: - gemini_response_finish(&resp); - } - - curl_url_cleanup(url); - gemini_tofu_finish(&cfg.tofu); - return ret; -} diff --git a/sources/cgmnlm.git/tree/src/gmnlm.c.txt b/sources/cgmnlm.git/tree/src/gmnlm.c.txt @@ -1,1402 +0,0 @@ -#include <assert.h> -#include <bearssl.h> -#include <ctype.h> -#include <errno.h> -#include <fcntl.h> -#include <getopt.h> -#include <gmni/certs.h> -#include <gmni/gmni.h> -#include <gmni/tofu.h> -#include <gmni/url.h> -#include <libgen.h> -#include <limits.h> -#include <regex.h> -#include <stdbool.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/ioctl.h> -#include <sys/stat.h> -#include <sys/types.h> -#include <sys/wait.h> -#include <termios.h> -#include <unistd.h> -#include "util.h" - -#define ANSI_COLOR_RED "\x1b[91m" -#define ANSI_COLOR_GREEN "\x1b[92m" -#define ANSI_COLOR_YELLOW "\x1b[93m" -#define ANSI_COLOR_BLUE "\x1b[94m" -#define ANSI_COLOR_MAGENTA "\x1b[35m" -#define ANSI_COLOR_CYAN "\x1b[36m" -#define ANSI_COLOR_LCYAN "\x1b[96m" -#define ANSI_COLOR_GRAY "\x1b[37m" -#define ANSI_COLOR_RESET "\x1b[0m" - -struct link { - char *url; - struct link *next; -}; - -struct history { - char *url; - struct history *prev, *next; -}; - -struct browser { - bool pagination, unicode, alttext; - int max_width; - struct gemini_options opts; - struct gemini_tofu tofu; - enum tofu_action tofu_mode; - - FILE *tty; - char *meta; - char *plain_url; - char *page_title; - struct Curl_URL *url; - struct link *links; - struct history *history; - bool running; - - bool searching; - regex_t regex; -}; - -enum prompt_result { - PROMPT_AGAIN, - PROMPT_MORE, - PROMPT_QUIT, - PROMPT_ANSWERED, - PROMPT_NEXT, -}; - -const char *default_bookmarks = - "# Welcome to cgmnlm\n\n" - "Links:\n\n" - "=> gemini://gmn.clttr.info/cgmnln.gmi The colorful gemini line mode client\n" - "=> gemini://gemini.circumlunar.space The gemini protocol\n" - "=> gemini://geminispace.info/search/ search in geminispace\n\n" - "This file can be found at %s and may be edited at your pleasure.\n\n" - "Bookmarks:\n" - ; - -const char *help_msg = - "The following commands are available:\n\n" - "<Enter>\t\tread more lines (if available)\n" - "<url>\t\tgo to url\n" - "[N]\t\tFollow Nth link (where N is a number)\n" - "p[N]\t\tPrint URL of Nth link (where N is a number)\n" - "e[N]\t\tSend URL of Nth link or current URL when N is ommited to external default program\n" - "t[N]\t\tDownload content of Nth link to a temporary file\n" - "b[N]\t\tJump back N entries in history, N is optional, default 1\n" - "f[N]\t\tJump forward N entries in history, N is optional, default 1\n" - "u\t\tone path element up\n" - "H\t\tView all page history\n" - "m\t\tSave bookmark\n" - "M\t\tBrowse bookmarks\n" - "k\t\tRemove bookmark for current page\n" - "r\t\tReload the page\n" - "s\t\tSearch via geminispace.info\n" - "/<text>\t\tsearch for text (POSIX regular expression)\n" - "n\t\tjump to next search match\n" - "d <path>\tDownload page to <path>\n" - "|<prog>\t\tPipe page into program\n" - "[N]|<prog>\tPipe content of Nth link into program\n" - "a\t\ttoggle usage of alt text instead of preformatted text\n" - "q\t\tQuit\n" - "\n" - ; - -static void -usage(const char *argv_0) -{ - fprintf(stderr, "usage: %s [gemini://...]\n", argv_0); -} - -static void -history_free(struct history *history) -{ - if (!history) { - return; - } - history_free(history->next); - free(history->url); - free(history); -} - -static bool -set_url(struct browser *browser, char *new_url, struct history **history) -{ - if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { - fprintf(stderr, "Error: invalid URL\n"); - return false; - } - if (browser->plain_url != NULL) { - free(browser->plain_url); - } - curl_url_get(browser->url, CURLUPART_URL, &browser->plain_url, 0); - if (history) { - struct history *next = calloc(1, sizeof(struct history)); - curl_url_get(browser->url, CURLUPART_URL, &next->url, 0); - next->prev = *history; - if (*history) { - if ((*history)->next) { - history_free((*history)->next); - } - (*history)->next = next; - } - *history = next; - } - return true; -} - -static char * -get_data_pathfmt() -{ - const struct pathspec paths[] = { - {.var = "GMNIDATA", .path = "/%s"}, - {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, - {.var = "HOME", .path = "/.local/share/gmni/%s"} - }; - return getpath(paths, sizeof(paths) / sizeof(paths[0])); -} - -static char * -trim_ws(char *in) -{ - while (*in && isspace(*in)) ++in; - return in; -} - -static void -save_bookmark(struct browser *browser) -{ - char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - static char dname[PATH_MAX+1]; - size_t n; - - n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - assert(n < sizeof(path)); - posix_dirname(path, dname); - if (mkdirs(dname, 0755) != 0) { - fprintf(stderr, "Error creating directory %s: %s\n", - dname, strerror(errno)); - return; - } - - FILE *f = fopen(path, "a"); - if (!f) { - fprintf(stderr, "Error opening %s for writing: %s\n", - path, strerror(errno)); - return; - } - - char *title = browser->page_title; - if (title) { - title = trim_ws(browser->page_title); - } - - fprintf(f, "=> %s%s%s\n", browser->plain_url, - title ? " " : "", title ? title : ""); - fclose(f); - - fprintf(browser->tty, "Bookmark saved: %s\n", - title ? title : browser->plain_url); -} - -static void -remove_bookmark(struct browser *browser) -{ - char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - - static char tempfile[PATH_MAX+2]; - snprintf(tempfile, sizeof(tempfile), "%s2", path); - FILE *fi = fopen(path, "r"); - FILE *fo = fopen(tempfile, "w"); - if(fi == NULL) { - fprintf(stderr, "Bookmark file not available!\n"); - return; - } - if(fo == NULL) { - fprintf(stderr, "tempfile not available!\n"); - return; - } - - char *line = NULL; - size_t len = 0; - size_t n = 0; - - static char url[1024]; - n = snprintf(url, sizeof(url), "=> %s ", browser->plain_url); - while(getline(&line, &len, fi) != -1) { - if (strncmp(line, url, n)==0) { - fprintf(browser->tty, "Bookmark removed!\n"); - } else { - fprintf(fo, line); - } - } - - fclose(fi); - fclose(fo); - free(line); - if ( rename(tempfile, path) != 0) { - fprintf(browser->tty, "Failed to update bookmarks: %s\n", strerror(errno)); - } -} - -static void -open_bookmarks(struct browser *browser) -{ - char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - static char dname[PATH_MAX+1]; - size_t n; - - n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - - assert(n < sizeof(path)); - posix_dirname(path, dname); - if (mkdirs(dname, 0755) != 0) { - fprintf(stderr, "Error creating directory %s: %s\n", - dname, strerror(errno)); - return; - } - - struct stat buf; - if (stat(path, &buf) == -1 && errno == ENOENT) { - // TOCTOU, but we almost certainly don't care - FILE *f = fopen(path, "a"); - if (f == NULL) { - fprintf(stderr, "Error opening %s for writing: %s\n", - path, strerror(errno)); - return; - } - fprintf(f, default_bookmarks, path); - fclose(f); - } - - static char url[PATH_MAX+1+7]; - snprintf(url, sizeof(url), "file://%s", path); - set_url(browser, url, &browser->history); -} - -static void -print_media_parameters(FILE *out, char *params) -{ - if (params == NULL) { - fprintf(out, "No media parameters\n"); - return; - } - for (char *param = strtok(params, ";"); param; - param = strtok(NULL, ";")) { - char *value = strchr(param, '='); - if (value == NULL) { - fprintf(out, "Invalid media type parameter '%s'\n", - trim_ws(param)); - continue; - } - *value = 0; - fprintf(out, "%s: ", trim_ws(param)); - *value++ = '='; - if (*value != '"') { - fprintf(out, "%s\n", value); - continue; - } - while (value++) { - switch (*value) { - case '\0': - if ((value = strtok(NULL, ";")) != NULL) { - fprintf(out, ";%c", *value); - } - break; - case '"': - value = NULL; - break; - case '\\': - if (value[1] == '\0') { - break; - } - value++; - /* fallthrough */ - default: - putc(*value, out); - } - } - putc('\n', out); - } -} - -static char * -get_input(const struct gemini_response *resp, FILE *source) -{ - int r = 0; - struct termios attrs; - bool tty = fileno(source) != -1 && isatty(fileno(source)); - char *input = NULL; - if (tty) { - fprintf(stderr, "%s: ", resp->meta); - if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { - r = tcgetattr(fileno(source), &attrs); - struct termios new_attrs; - r = tcgetattr(fileno(source), &new_attrs); - if (r != -1) { - new_attrs.c_lflag &= ~ECHO; - tcsetattr(fileno(source), TCSANOW, &new_attrs); - } - } - } - size_t s = 0; - ssize_t n = getline(&input, &s, source); - if (n == -1) { - fprintf(stderr, "Error reading input: %s\n", - feof(source) ? "EOF" : strerror(ferror(source))); - return NULL; - } - input[n - 1] = '\0'; // Drop LF - if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { - attrs.c_lflag &= ~ECHO; - tcsetattr(fileno(source), TCSANOW, &attrs); - } - return input; -} - -static bool -has_suffix(char *str, char *suff) -{ - size_t suffl = strlen(suff); - size_t strl = strlen(str); - if (strl < suffl) { - return false; - } - return strcmp(&str[strl - suffl], suff) == 0; -} - -static void -pipe_resp(FILE *out, struct gemini_response resp, char *cmd) { - char buf[BUFSIZ]; - int pfd[2]; - if (pipe(pfd) == -1) { - perror("pipe"); - return; - } - pid_t pid; - switch ((pid = fork())) { - case -1: - perror("fork"); - return; - case 0: - close(pfd[1]); - dup2(pfd[0], STDIN_FILENO); - close(pfd[0]); - execlp("sh", "sh", "-c", cmd, NULL); - perror("exec"); - _exit(1); - } - close(pfd[0]); - FILE *f = fdopen(pfd[1], "w"); - // XXX: may affect history, do we care? - for (int n = 1; n > 0;) { - if (resp.sc) { - n = br_sslio_read(&resp.body, buf, BUFSIZ); - } else { - n = read(resp.fd, buf, BUFSIZ); - } - if (n < 0) { - n = 0; - } - ssize_t w = 0; - while (w < (ssize_t)n) { - ssize_t x = fwrite(&buf[w], 1, n - w, f); - if (ferror(f)) { - fprintf(stderr, "Error: write: %s\n", - strerror(errno)); - return; - } - w += x; - } - } - fclose(f); - int status; - waitpid(pid, &status, 0); - if (status != 0) { - fprintf(out, "Command exited %d\n", status); - } -} - -static enum gemini_result -do_requests(struct browser *browser, struct gemini_response *resp) -{ - int nredir = 0; - bool requesting = true; - enum gemini_result res; - - char *scheme; - CURLUcode uc = curl_url_get(browser->url, - CURLUPART_SCHEME, &scheme, 0); - assert(uc == CURLUE_OK); // Invariant - - char *host = NULL; - struct gmni_client_certificate client_cert = {0}; - const struct pathspec paths[] = { - {.var = "GMNIDATA", .path = "/certs/%s.%s"}, - {.var = "XDG_DATA_HOME", .path = "/gmni/certs/%s.%s"}, - {.var = "HOME", .path = "/.local/share/gmni/certs/%s.%s"} - }; - char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); - char certpath[PATH_MAX+1], keypath[PATH_MAX+1]; - size_t n = 0; - - if (strcmp(scheme, "gemini") == 0) { - CURLUcode uc = curl_url_get(browser->url, - CURLUPART_HOST, &host, 0); - assert(uc == CURLUE_OK); - - n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); - assert(n < sizeof(certpath)); - FILE *certin = fopen(certpath, "r"); - if (certin) { - n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); - assert(n < sizeof(keypath)); - - FILE *skin = fopen(keypath, "r"); - if (gmni_ccert_load(&client_cert, certin, skin)) { - browser->opts.client_cert = NULL; - fprintf(stderr, "Unable to load client certificate for host %s", host); - } else { - browser->opts.client_cert = &client_cert; - } - } else { - browser->opts.client_cert = NULL; - } - } - - while (requesting) { - if (strcmp(scheme, "file") == 0) { - requesting = false; - - char *path; - uc = curl_url_get(browser->url, - CURLUPART_PATH, &path, 0); - if (uc != CURLUE_OK) { - resp->status = GEMINI_STATUS_BAD_REQUEST; - break; - } - - int fd = open(path, O_RDONLY); - if (fd < 0) { - resp->status = GEMINI_STATUS_NOT_FOUND; - // Make sure members of resp evaluate to false, - // so that gemini_response_finish does not try - // to free them. - resp->sc = NULL; - resp->meta = NULL; - resp->fd = -1; - free(path); - break; - } - - if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) { - resp->meta = strdup("text/gemini"); - } else if (has_suffix(path, ".txt")) { - resp->meta = strdup("text/plain"); - } else { - resp->meta = strdup("application/x-octet-stream"); - } - free(path); - resp->status = GEMINI_STATUS_SUCCESS; - resp->fd = fd; - resp->sc = NULL; - res = GEMINI_OK; - goto out; - } - - res = gemini_request(browser->plain_url, &browser->opts, - &browser->tofu, resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); - requesting = false; - resp->status = 70 + res; - break; - } - - char *input; - switch (gemini_response_class(resp->status)) { - case GEMINI_STATUS_CLASS_INPUT: - input = get_input(resp, browser->tty); - if (!input) { - requesting = false; - break; - } - if (input[0] == '\0' && browser->history->prev) { - free(input); - browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); - break; - } - - char *new_url = gemini_input_url( - browser->plain_url, input); - free(input); - assert(new_url); - set_url(browser, new_url, NULL); - free(new_url); - break; - case GEMINI_STATUS_CLASS_REDIRECT: - if (++nredir >= 5) { - requesting = false; - fprintf(stderr, "Error: maximum redirects (5) exceeded\n"); - break; - } - set_url(browser, resp->meta, NULL); - break; - case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: - requesting = false; - assert(host); - n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); - assert(n < sizeof(certpath)); - n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); - char dname[PATH_MAX + 1]; - posix_dirname(certpath, dname); - if (mkdirs(dname, 0755) != 0) { - fprintf(stderr, "Error creating directory %s: %s\n", - dname, strerror(errno)); - break; - } - assert(n < sizeof(keypath)); - fprintf(stderr, "The server requested a client certificate.\n" - "Presently, this process is not automated.\n" - "The following OpenSSL command will generate a certificate for this host:\n\n" - "openssl req -x509 -newkey rsa:4096 \\\n\t-keyout %s \\\n\t-out %s \\\n\t-days 36500 -nodes\n\n" - "Use the 'r' command to reload the page after creating this certificate.\n", - keypath, certpath); - break; - case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: - case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: - requesting = false; - fprintf(stderr, "Server returned %s %d %s\n", - resp->status / 10 == 4 ? - "TEMPORARY FAILURE" : "PERMANENT FAILURE", - resp->status, resp->meta); - break; - case GEMINI_STATUS_CLASS_SUCCESS: - goto out; - } - - if (requesting) { - gemini_response_finish(resp); - } - } - -out: - if (client_cert.key) { - free(client_cert.key); - } - free(scheme); - free(host); - return res; -} - -static enum prompt_result -do_prompts(const char *prompt, struct browser *browser) -{ - enum prompt_result result; - fprintf(browser->tty, "%s", prompt); - - struct link *link = browser->links; - char *endptr = NULL; - int linksel = 0; - int historyhops = 1; - - char *in = NULL; - size_t l = 0; - ssize_t n = getline(&in, &l, browser->tty); - if (n == -1 && feof(browser->tty)) { - result = PROMPT_QUIT; - goto exit; - } - in[n - 1] = 0; // Remove LF - - char url[1024] = {0}; - int r; - switch (in[0]) { - case '\0': - result = PROMPT_MORE; - goto exit; - case 'q': - if (in[1]) break; - result = PROMPT_QUIT; - goto exit; - case 's': - if (in[1]) break; - set_url(browser, "gemini://geminispace.info/search", &browser->history); - result = PROMPT_ANSWERED; - goto exit; - case 'a': - browser->alttext = !browser->alttext; - fprintf(browser->tty, "Alttext instead of preformatted block is now %s\n\n", browser->alttext ? "ENABLED" : "DISABLED"); - result = PROMPT_AGAIN; - goto exit; - case 'b': - if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); - while (historyhops > 0) { - if (browser->history->prev) { - browser->history = browser->history->prev; - } - historyhops--; - } - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; - case 'f': - if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); - while (historyhops > 0) { - if (browser->history->next) { - browser->history = browser->history->next; - } - historyhops--; - } - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; - case 'u':; - int keep = 0; - int len = strlen(browser->plain_url); - for (int i=0; i<len; i++) - { - // ignore trailing / on uri path - if (browser->plain_url[i] == '/' && i != len-1) { - keep = i; - } - } - if (keep > 9) { - strncpy(url , browser->plain_url, keep+1); - set_url(browser, url, &browser->history); - } - result = PROMPT_ANSWERED; - goto exit; - case 'H': - if (in[1]) break; - struct history *cur = browser->history; - int hist_count = 0; - while (cur->prev) { - cur = cur->prev; - hist_count++; - } - while (cur != browser->history) { - fprintf(browser->tty, "b%-3i %s\n", hist_count--, cur->url); - cur = cur->next; - } - fprintf(browser->tty, "* %s\n", cur->url); - cur = cur->next; - while (cur) { - fprintf(browser->tty, "f%-3i %s\n", ++hist_count, cur->url); - cur = cur->next; - } - result = PROMPT_AGAIN; - goto exit; - case 'm': - if (in[1]) break; - save_bookmark(browser); - result = PROMPT_AGAIN; - goto exit; - case 'k': - if (in[1]) break; - remove_bookmark(browser); - result = PROMPT_AGAIN; - goto exit; - case 'M': - if (in[1]) break; - open_bookmarks(browser); - result = PROMPT_ANSWERED; - goto exit; - case '/': - if (!in[1]) break; - if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) { - static char buf[1024]; - r = regerror(r, &browser->regex, buf, sizeof(buf)); - assert(r < (int)sizeof(buf)); - fprintf(stderr, "Error: %s\n", buf); - result = PROMPT_AGAIN; - } else { - browser->searching = true; - result = PROMPT_ANSWERED; - } - goto exit_re; - case 'n': - if (in[1]) break; - if (browser->searching) { - result = PROMPT_NEXT; - goto exit_re; - } else { - fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); - result = PROMPT_AGAIN; - goto exit; - } - case 'e': - case 'p': - case 't': - if (!in[1]) { - if (in[0] == 'e') { - snprintf(url, sizeof(url), "xdg-open %s", browser->plain_url); - if ( !system(url) ) fprintf(browser->tty, "Link send to xdg-open\n"); - goto exit; - } else { - break; - } - } - linksel = (int)strtol(in+1, &endptr, 10); - if (!endptr[0] && linksel >= 0) { - while (linksel > 0 && link) { - link = link->next; - --linksel; - } - - if (!link) { - fprintf(stderr, "Error: no such link.\n"); - } else { - fprintf(browser->tty, "=> %s\n", link->url); - if (in[0] == 'e') { - snprintf(url, sizeof(url), "xdg-open %s", link->url); - if ( !system(url) ) fprintf(browser->tty, "Link send to xdg-open\n"); - } - if (in[0] == 't') { - struct gemini_response resp; - strncpy(&url[0], browser->plain_url, sizeof(link->url)-1); - set_url(browser, link->url, &browser->history); - // XXX: may affect history, do we care? - enum gemini_result res = do_requests(browser, &resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", - gemini_strerr(res, &resp)); - } else { - char tempfile[] = "/tmp/cgmnlm_XXXXXX"; - mkstemp(tempfile); - download_resp(browser->tty, resp, tempfile, link->url); - } - gemini_response_finish(&resp); - set_url(browser, url, NULL); - } - fprintf(browser->tty, "\n"); - } - } else { - fprintf(stderr, "Error: invalid argument.\n"); - } - result = PROMPT_AGAIN; - goto exit; - case 'r': - if (in[1]) break; - result = PROMPT_ANSWERED; - goto exit; - case 'i': - if (in[1]) break; - print_media_parameters(browser->tty, browser->meta - ? strchr(browser->meta, ';') : NULL); - result = PROMPT_AGAIN; - goto exit; - case 'd': - if (in[1] != '\0' && !isspace(in[1])) break; - struct gemini_response resp; - strncpy(&url[0], browser->plain_url, sizeof(url)-1); - // XXX: may affect history, do we care? - enum gemini_result res = do_requests(browser, &resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", - gemini_strerr(res, &resp)); - result = PROMPT_AGAIN; - goto exit; - } - set_url(browser, url, NULL); - download_resp(browser->tty, resp, trim_ws(&in[1]), url); - gemini_response_finish(&resp); - result = PROMPT_AGAIN; - goto exit; - case '|': - strncpy(&url[0], browser->plain_url, sizeof(url)-1); - res = do_requests(browser, &resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", - gemini_strerr(res, &resp)); - result = PROMPT_AGAIN; - goto exit; - } - pipe_resp(browser->tty, resp, &in[1]); - gemini_response_finish(&resp); - set_url(browser, url, NULL); - result = PROMPT_AGAIN; - goto exit; - case '?': - if (in[1]) break; - fprintf(browser->tty, "%s", help_msg); - result = PROMPT_AGAIN; - goto exit; - } - - linksel = (int)strtol(in, &endptr, 10); - if ((endptr[0] == '\0' || endptr[0] == '|') && linksel >= 0) { - while (linksel > 0 && link) { - link = link->next; - --linksel; - } - - if (!link) { - fprintf(stderr, "Error: no such link.\n"); - } else if (endptr[0] == '|') { - struct gemini_response resp; - strncpy(url, browser->plain_url, sizeof(url) - 1); - set_url(browser, link->url, &browser->history); - enum gemini_result res = do_requests(browser, &resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", - gemini_strerr(res, &resp)); - set_url(browser, url, NULL); - result = PROMPT_AGAIN; - goto exit; - } - pipe_resp(browser->tty, resp, &endptr[1]); - gemini_response_finish(&resp); - set_url(browser, url, NULL); - result = PROMPT_AGAIN; - goto exit; - } else { - assert(endptr[0] == '\0'); - set_url(browser, link->url, &browser->history); - result = PROMPT_ANSWERED; - goto exit; - } - } - - set_url(browser, in, &browser->history); - result = PROMPT_ANSWERED; -exit: - if (browser->searching) { - browser->searching = false; - regfree(&browser->regex); - } -exit_re: - free(in); - return result; -} - -static int -wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col) -{ - if (!s[0]) { - fprintf(f, "\n"); - return 0; - } - for (int i = 0; s[i]; ++i) { - switch (s[i]) { - case '\n': - assert(0); // Not supposed to happen - case '\t': - *col = *col + (8 - *col % 8); - break; - case '\r': - if (!s[i+1]) break; - /* fallthrough */ - default: - // skip unicode continuation bytes - if ((s[i] & 0xc0) == 0x80) break; - - if (iscntrl(s[i])) s[i] = '.'; - *col += 1; - break; - } - - if (*col >= ws->ws_col - 4) { - int j = i--; - while (&s[i] != s && !isspace(s[i])) --i; - if (&s[i] == s) i = j; - char c = s[i]; - s[i] = 0; - int n = fprintf(f, "%s\n", s) - (isspace(c) ? 0 : 1); - s[i] = c; - *row += 1; - *col = 0; - return n; - } - } - return fprintf(f, "%s\n", s) - 1; -} - -static int -resp_read(void *state, void *buf, size_t nbyte) -{ - struct gemini_response *resp = state; - if (resp->sc) { - return br_sslio_read(&resp->body, buf, nbyte); - } else { - return read(resp->fd, buf, nbyte); - } -} - -static bool -display_gemini(struct browser *browser, struct gemini_response *resp) -{ - int nlinks = 0; - struct gemini_parser p; - gemini_parser_init(&p, &resp_read, resp); - free(browser->page_title); - browser->page_title = NULL; - - struct winsize ws; - ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - if (browser->max_width != 0 && ws.ws_col > browser->max_width) { - ws.ws_col = browser->max_width; - } - - FILE *out = browser->tty; - bool searching = browser->searching; - if (searching) { - out = fopen("/dev/null", "w+"); - } - - fprintf(out, "\n"); - char *text = NULL; - int row = 0, col = 0; - bool alttext_printed; - struct gemini_token tok; - struct link **next = &browser->links; - // When your screen is too narrow, more lines will be used for helptext and URL. - // 87 is the maximum width of the prompt. - int info_rows = (ws.ws_col >= 87) ? 4 : 6; - - while (text != NULL || gemini_parser_next(&p, &tok) == 0) { -repeat: - switch (tok.token) { - case GEMINI_TEXT: - col += fprintf(out, " "); - if (text == NULL) { - text = tok.text; - } - break; - case GEMINI_LINK: - if (text == NULL) { - col += fprintf(out, "%3d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_MAGENTA)); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); - next = &(*next)->next; - } else { - col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED_BEGIN: - alttext_printed = false; - if (text == NULL && browser->alttext && tok.preformatted != NULL) { - fprintf(out, " A %s", ANSI_COLOR_GRAY); - text = trim_ws(tok.preformatted); - alttext_printed = true; - } - break; - /* fallthrough */ - case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: - if (alttext_printed) continue; - if (text == NULL) { - fprintf(out, " P %s", ANSI_COLOR_GRAY); - text = tok.preformatted; - } - break; - case GEMINI_HEADING: - if (!browser->page_title) { - browser->page_title = strdup(tok.heading.title); - } - if (text == NULL) { - switch (tok.heading.level) { - case 1: - col += fprintf(out, " # %s", ANSI_COLOR_RED); - break; - case 2: - col += fprintf(out, " ## %s", ANSI_COLOR_YELLOW); - break; - case 3: - col += fprintf(out, " ### %s", ANSI_COLOR_GREEN); - break; - } - text = trim_ws(tok.heading.title); - } else { - col += fprintf(out, " "); - } - break; - case GEMINI_LIST_ITEM: - if (text == NULL) { - col += fprintf(out, " %s ", - browser->unicode ? "•" : "*"); - text = trim_ws(tok.list_item); - } else { - col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: - col += fprintf(out, " %s ", browser->unicode ? "┃" : ">"); - if (text == NULL) { - text = trim_ws(tok.quote_text); - } - break; - } - - if (text && searching) { - int r = regexec(&browser->regex, text, 0, NULL, 0); - if (r != 0) { - text = NULL; - continue; - } else { - fclose(out); - row = col = 0; - out = browser->tty; - text = NULL; - searching = false; - goto repeat; - } - } - - if (text) { - int w = wrap(out, text, &ws, &row, &col); - text += w; - if (text[0] && row < ws.ws_row - info_rows) { - continue; - } - - if (!text[0]) { - text = NULL; - } - } - if (text == NULL) { - gemini_token_finish(&tok); - } - - while (col >= ws.ws_col) { - col -= ws.ws_col; - ++row; - } - ++row; col = 0; - - fprintf(out, ANSI_COLOR_RESET); - if (browser->pagination && row >= ws.ws_row - info_rows) { - char prompt[4096]; - char *end = NULL; - if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) { - *end = 0; - } - snprintf(prompt, sizeof(prompt), "%s at %s\n" - "[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "(more) => ", resp->meta, browser->plain_url, - browser->searching ? "[n]ext result; " : "", - browser->history->prev ? "[b]ack; " : "", - browser->history->next ? "[f]orward; " : ""); - if (end != NULL) { - *end = ';'; - } - enum prompt_result result = PROMPT_AGAIN; - while (result == PROMPT_AGAIN) { - result = do_prompts(prompt, browser); - } - - switch (result) { - case PROMPT_AGAIN: - case PROMPT_MORE: - printf("\n"); - break; - case PROMPT_QUIT: - browser->running = false; - if (text != NULL) { - gemini_token_finish(&tok); - } - gemini_parser_finish(&p); - return true; - case PROMPT_ANSWERED: - if (text != NULL) { - gemini_token_finish(&tok); - } - gemini_parser_finish(&p); - return true; - case PROMPT_NEXT: - searching = true; - out = fopen("/dev/null", "w"); - break; - } - - row = col = 0; - } - } - - gemini_token_finish(&tok); - gemini_parser_finish(&p); - return false; -} - -static bool -display_plaintext(struct browser *browser, struct gemini_response *resp) -{ - // TODO: Strip ANSI escape sequences - struct winsize ws; - int row = 0, col = 0; - ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { - if (resp->sc) { - n = br_sslio_read(&resp->body, buf, BUFSIZ); - } else { - n = read(resp->fd, buf, BUFSIZ); - } - if (n < 0) { - n = 0; - } - ssize_t w = 0; - while (w < (ssize_t)n) { - ssize_t x = fwrite(&buf[w], 1, n - w, browser->tty); - if (ferror(browser->tty)) { - fprintf(stderr, "Error: write: %s\n", - strerror(errno)); - return 1; - } - w += x; - } - } - - (void)row; (void)col; // TODO: generalize pagination - return false; -} - -static bool -display_response(struct browser *browser, struct gemini_response *resp) -{ - if (gemini_response_class(resp->status) != GEMINI_STATUS_CLASS_SUCCESS) { - return false; - } - if (strcmp(resp->meta, "text/gemini") == 0 - || strncmp(resp->meta, "text/gemini;", 12) == 0) { - return display_gemini(browser, resp); - } - if (strncmp(resp->meta, "text/", 5) == 0) { - return display_plaintext(browser, resp); - } - fprintf(stderr, "Media type %s is unsupported, use \"d <path>\" to download this page\n", - resp->meta); - return false; -} - -static enum tofu_action -tofu_callback(enum tofu_error error, const char *fingerprint, - struct known_host *khost, void *data) -{ - struct browser *browser = data; - if (browser->tofu_mode != TOFU_ASK) { - return browser->tofu_mode; - } - - static char prompt[8192]; - switch (error) { - case TOFU_VALID: - assert(0); // Invariant - case TOFU_INVALID_CERT: - snprintf(prompt, sizeof(prompt), - "The server presented an invalid certificate. If you choose to proceed, " - "you should not disclose personal information or trust the contents of the page.\n" - "trust [o]nce; [a]bort\n" - "=> "); - break; - case TOFU_UNTRUSTED_CERT:; - char *host; - if (curl_url_get(browser->url, CURLUPART_HOST, &host, 0) != CURLUE_OK) { - fprintf(stderr, "Error: invalid URL %s\n", - browser->plain_url); - return TOFU_FAIL; - } - snprintf(prompt, sizeof(prompt), - "The certificate offered by %s is of unknown trust. " - "Its fingerprint is: \n" - "%s\n\n" - "If you knew the fingerprint to expect in advance, verify that this matches.\n" - "Otherwise, it should be safe to trust this certificate.\n\n" - "[t]rust always; trust [o]nce; [a]bort\n" - "=> ", host, fingerprint); - free(host); - break; - case TOFU_FINGERPRINT_MISMATCH: - fprintf(browser->tty, - "The certificate offered by this server DOES NOT MATCH the one we have on file.\n" - "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" - "The unknown certificate's fingerprint is:\n" - "%s\n\n" - "The expected fingerprint is:\n" - "%s\n\n" - "If you're certain that this is correct, edit %s:%d\n", - fingerprint, khost->fingerprint, - browser->tofu.known_hosts_path, khost->lineno); - return TOFU_FAIL; - } - - bool prompting = true; - while (prompting) { - fprintf(browser->tty, "%s", prompt); - - size_t sz = 0; - char *line = NULL; - if (getline(&line, &sz, browser->tty) == -1) { - free(line); - return TOFU_FAIL; - } - if (line[1] != '\n') { - free(line); - continue; - } - - char c = line[0]; - free(line); - - switch (c) { - case 't': - if (error == TOFU_INVALID_CERT) { - break; - } - return TOFU_TRUST_ALWAYS; - case 'o': - return TOFU_TRUST_ONCE; - case 'a': - return TOFU_FAIL; - } - } - - return TOFU_FAIL; -} - -int -main(int argc, char *argv[]) -{ - struct browser browser = { - .pagination = true, - .alttext = false, - .tofu_mode = TOFU_ASK, - .unicode = true, - .url = curl_url(), - .tty = fopen("/dev/tty", "w+"), - .meta = NULL, - }; - - int c; - while ((c = getopt(argc, argv, "hj:PAUW:")) != -1) { - switch (c) { - case 'h': - usage(argv[0]); - return 0; - case 'j': - if (strcmp(optarg, "fail") == 0) { - browser.tofu_mode = TOFU_FAIL; - } else if (strcmp(optarg, "once") == 0) { - browser.tofu_mode = TOFU_TRUST_ONCE; - } else if (strcmp(optarg, "always") == 0) { - browser.tofu_mode = TOFU_TRUST_ALWAYS; - } else { - usage(argv[0]); - return 1; - } - break; - case 'A': - browser.alttext = true; - break; - case 'P': - browser.pagination = false; - break; - case 'U': - browser.unicode = false; - break; - case 'W': - browser.max_width = strtoul(optarg, NULL, 10); - break; - default: - fprintf(stderr, "fatal: unknown flag %c\n", c); - curl_url_cleanup(browser.url); - return 1; - } - } - - if (optind == argc - 1) { - if (!set_url(&browser, argv[optind], &browser.history)) { - return 1; - } - } else { - open_bookmarks(&browser); - } - - gemini_tofu_init(&browser.tofu, &tofu_callback, &browser); - - struct gemini_response resp; - browser.running = true; - while (browser.running) { - static char prompt[4096]; - bool skip_prompt = do_requests(&browser, &resp) == GEMINI_OK - && display_response(&browser, &resp); - if (browser.meta) { - free(browser.meta); - } - browser.meta = resp.status == GEMINI_STATUS_SUCCESS - ? strdup(resp.meta) : NULL; - gemini_response_finish(&resp); - if (!skip_prompt) { - char *end = NULL; - if (browser.meta && (end = strchr(browser.meta, ';')) != NULL) { - *end = 0; - } - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" - "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "=> ", browser.meta ? browser.meta - : "[request failed]", browser.plain_url, - browser.history->prev ? "[b]ack; " : "", - browser.history->next ? "[f]orward; " : ""); - if (end != NULL) { - *end = ';'; - } - - enum prompt_result result = PROMPT_AGAIN; - while (result == PROMPT_AGAIN || result == PROMPT_MORE) { - result = do_prompts(prompt, &browser); - } - switch (result) { - case PROMPT_AGAIN: - case PROMPT_MORE: - assert(0); - case PROMPT_QUIT: - browser.running = false; - break; - case PROMPT_ANSWERED: - case PROMPT_NEXT: - break; - } - } - - struct link *link = browser.links; - while (link) { - struct link *next = link->next; - free(link->url); - free(link); - link = next; - } - browser.links = NULL; - } - - gemini_tofu_finish(&browser.tofu); - struct history *hist = browser.history; - while (hist && hist->prev) { - hist = hist->prev; - } - history_free(hist); - curl_url_cleanup(browser.url); - free(browser.page_title); - free(browser.plain_url); - if (browser.meta != NULL) { - free(browser.meta); - } - fclose(browser.tty); - return 0; -} diff --git a/sources/cgmnlm.git/tree/src/index.gmi b/sources/cgmnlm.git/tree/src/index.gmi @@ -1,13 +0,0 @@ -# Tree - -Path: src/ - -=> certs.c.txt certs.c -=> client.c.txt client.c -=> escape.c.txt escape.c -=> gmni.c.txt gmni.c -=> gmnlm.c.txt gmnlm.c -=> parser.c.txt parser.c -=> tofu.c.txt tofu.c -=> url.c.txt url.c -=> util.c.txt util.c diff --git a/sources/cgmnlm.git/tree/src/parser.c.txt b/sources/cgmnlm.git/tree/src/parser.c.txt @@ -1,160 +0,0 @@ -#include <assert.h> -#include <ctype.h> -#include <stdbool.h> -#include <stddef.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <gmni/gmni.h> - -void -gemini_parser_init(struct gemini_parser *p, - int (*read)(void *state, void *buf, size_t nbyte), - void *state) -{ - p->read = read; - p->state = state; - p->bufln = 0; - p->bufsz = BUFSIZ; - p->buf = malloc(p->bufsz + 1); - p->buf[0] = 0; - p->preformatted = false; -} - -void -gemini_parser_finish(struct gemini_parser *p) -{ - if (!p) { - return; - } - free(p->buf); -} - -int -gemini_parser_next(struct gemini_parser *p, struct gemini_token *tok) -{ - memset(tok, 0, sizeof(*tok)); - - int eof = 0; - while (!strchr(p->buf, '\n')) { - while (p->bufln >= p->bufsz - 1) { - p->bufsz *= 2; - p->buf = realloc(p->buf, p->bufsz); - assert(p->buf); - } - - int n = p->read(p->state, &p->buf[p->bufln], p->bufsz - p->bufln - 1); - if (n == -1) { - return -1; - } else if (n == 0) { - eof = p->bufln == 0; - break; - } - p->bufln += n; - p->buf[p->bufln] = 0; - } - - char *end; - if ((end = strchr(p->buf, '\n')) != NULL) { - *end = 0; - } - - if (p->preformatted) { - if (strncmp(p->buf, "```", 3) == 0) { - tok->token = GEMINI_PREFORMATTED_END; - p->preformatted = false; - } else { - tok->token = GEMINI_PREFORMATTED_TEXT; - tok->preformatted = strdup(p->buf); - } - } else if (strncmp(p->buf, "=>", 2) == 0) { - tok->token = GEMINI_LINK; - int i = 2; - while (p->buf[i] && isspace(p->buf[i])) ++i; - tok->link.url = &p->buf[i]; - - for (; p->buf[i]; ++i) { - if (isspace(p->buf[i])) { - p->buf[i++] = 0; - while (isspace(p->buf[i])) ++i; - if (p->buf[i]) { - tok->link.text = strdup(&p->buf[i]); - } - break; - } - } - - tok->link.url = strdup(tok->link.url); - } else if (strncmp(p->buf, "```", 3) == 0) { - tok->token = GEMINI_PREFORMATTED_BEGIN; - if (p->buf[3]) { - tok->preformatted = strdup(&p->buf[3]); - } - p->preformatted = true; - } else if (p->buf[0] == '#') { - tok->token = GEMINI_HEADING; - int level = 1; - while (p->buf[level] == '#' && level < 3) { - ++level; - } - tok->heading.level = level; - tok->heading.title = strdup(&p->buf[level]); - } else if (p->buf[0] == '*') { - tok->token = GEMINI_LIST_ITEM; - tok->list_item = strdup(&p->buf[1]); - } else if (p->buf[0] == '>') { - tok->token = GEMINI_QUOTE; - tok->quote_text = strdup(&p->buf[1]); - } else { - tok->token = GEMINI_TEXT; - tok->text = strdup(p->buf); - } - - if (end && end + 1 < p->buf + p->bufln) { - size_t len = end - p->buf + 1; - memmove(p->buf, end + 1, p->bufln - len); - p->bufln -= len; - p->buf[p->bufln] = 0; - } else { - p->buf[0] = 0; - p->bufln = 0; - } - - return eof; -} - -void -gemini_token_finish(struct gemini_token *tok) -{ - if (!tok) { - return; - } - - switch (tok->token) { - case GEMINI_TEXT: - free(tok->text); - break; - case GEMINI_LINK: - free(tok->link.text); - free(tok->link.url); - break; - case GEMINI_PREFORMATTED_BEGIN: - free(tok->preformatted); - break; - case GEMINI_PREFORMATTED_TEXT: - free(tok->preformatted); - break; - case GEMINI_PREFORMATTED_END: - // Nothing to free - break; - case GEMINI_HEADING: - free(tok->heading.title); - break; - case GEMINI_LIST_ITEM: - free(tok->list_item); - break; - case GEMINI_QUOTE: - free(tok->quote_text); - break; - } -} diff --git a/sources/cgmnlm.git/tree/src/tofu.c.txt b/sources/cgmnlm.git/tree/src/tofu.c.txt @@ -1,253 +0,0 @@ -#include <assert.h> -#include <bearssl.h> -#include <errno.h> -#include <gmni/gmni.h> -#include <gmni/tofu.h> -#include <libgen.h> -#include <limits.h> -#include <stdint.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include "util.h" - -static void -xt_start_chain(const br_x509_class **ctx, const char *server_name) -{ - struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; - cc->server_name = server_name; - cc->err = 0; - cc->pkey = NULL; -} - -static void -xt_start_cert(const br_x509_class **ctx, uint32_t length) -{ - struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; - if (cc->err != 0 || cc->pkey) { - return; - } - if (length == 0) { - cc->err = BR_ERR_X509_TRUNCATED; - return; - } - br_x509_decoder_init(&cc->decoder, NULL, NULL); - br_sha512_init(&cc->sha512); -} - -static void -xt_append(const br_x509_class **ctx, const unsigned char *buf, size_t len) -{ - struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; - if (cc->err != 0 || cc->pkey) { - return; - } - br_x509_decoder_push(&cc->decoder, buf, len); - int err = br_x509_decoder_last_error(&cc->decoder); - if (err != 0 && err != BR_ERR_X509_TRUNCATED) { - cc->err = err; - } - br_sha512_update(&cc->sha512, buf, len); -} - -static void -xt_end_cert(const br_x509_class **ctx) -{ - struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; - if (cc->err != 0) { - return; - } - int err = br_x509_decoder_last_error(&cc->decoder); - if (err != 0 && err != BR_ERR_X509_TRUNCATED) { - cc->err = err; - return; - } - cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); - br_sha512_out(&cc->sha512, &cc->hash); -} - -static unsigned -xt_end_chain(const br_x509_class **ctx) -{ - struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; - if (cc->err != 0) { - return (unsigned)cc->err; - } - if (!cc->pkey) { - return BR_ERR_X509_EMPTY_CHAIN; - } - - char fingerprint[512 / 8 * 3]; - for (size_t i = 0; i < sizeof(cc->hash); ++i) { - snprintf(&fingerprint[i * 3], 4, "%02X%s", - cc->hash[i], i + 1 == sizeof(cc->hash) ? "" : ":"); - } - - enum tofu_error error = TOFU_UNTRUSTED_CERT; - struct known_host *host = cc->store->known_hosts; - while (host) { - if (strcmp(host->host, cc->server_name) != 0) { - goto next; - } - if (strcmp(host->fingerprint, fingerprint) == 0) { - // Valid match in known hosts - return 0; - } - error = TOFU_FINGERPRINT_MISMATCH; - break; -next: - host = host->next; - } - - switch (cc->store->callback(error, fingerprint, - host, cc->store->cb_data)) { - case TOFU_ASK: - assert(0); // Invariant - case TOFU_FAIL: - return BR_ERR_X509_NOT_TRUSTED; - case TOFU_TRUST_ONCE: - // No further action necessary - return 0; - case TOFU_TRUST_ALWAYS:; - FILE *f = fopen(cc->store->known_hosts_path, "a"); - if (!f) { - fprintf(stderr, "Error opening %s for writing: %s\n", - cc->store->known_hosts_path, strerror(errno)); - break; - }; - fprintf(f, "%s %s %s\n", cc->server_name, - "SHA-512", fingerprint); - fclose(f); - - host = calloc(1, sizeof(struct known_host)); - host->host = strdup(cc->server_name); - host->fingerprint = strdup(fingerprint); - host->lineno = ++cc->store->lineno; - host->next = cc->store->known_hosts; - cc->store->known_hosts = host; - return 0; - } - - assert(0); // Unreachable -} - -static const br_x509_pkey * -xt_get_pkey(const br_x509_class *const *ctx, unsigned *usages) -{ - struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; - if (cc->err != 0) { - return NULL; - } - if (usages) { - // XXX: BearSSL doesn't pull the usages out of the X.509 for us - *usages = BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN; - } - return cc->pkey; -} - -const br_x509_class xt_vtable = { - sizeof(struct x509_tofu_context), - xt_start_chain, - xt_start_cert, - xt_append, - xt_end_cert, - xt_end_chain, - xt_get_pkey, -}; - -static void -x509_init_tofu(struct x509_tofu_context *ctx, struct gemini_tofu *store) -{ - ctx->vtable = &xt_vtable; - ctx->store = store; -} - -void -gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *cb_data) -{ - const struct pathspec paths[] = { - {.var = "GMNIDATA", .path = "/%s"}, - {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, - {.var = "HOME", .path = "/.local/share/gmni/%s"} - }; - char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); - char dname[PATH_MAX+1]; - size_t n = 0; - - n = snprintf(tofu->known_hosts_path, - sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); - free(path_fmt); - assert(n < sizeof(tofu->known_hosts_path)); - - posix_dirname(tofu->known_hosts_path, dname); - if (mkdirs(dname, 0755) != 0) { - fprintf(stderr, "Error creating directory %s: %s\n", dname, - strerror(errno)); - return; - } - - tofu->callback = cb; - tofu->cb_data = cb_data; - - tofu->known_hosts = NULL; - - x509_init_tofu(&tofu->x509_ctx, tofu); - - br_x509_minimal_context _; // Discarded - br_ssl_client_init_full(&tofu->sc, &_, NULL, 0); - br_ssl_engine_set_x509(&tofu->sc.eng, &tofu->x509_ctx.vtable); - br_ssl_engine_set_buffer(&tofu->sc.eng, - &tofu->iobuf, sizeof(tofu->iobuf), 1); - - FILE *f = fopen(tofu->known_hosts_path, "r"); - if (!f) { - return; - } - n = 0; - int lineno = 1; - char *line = NULL; - while (getline(&line, &n, f) != -1) { - int ln = strlen(line); - if (line[ln-1] == '\n') { - line[ln-1] = 0; - } - - struct known_host *host = calloc(1, sizeof(struct known_host)); - char *tok = strtok(line, " "); - assert(tok); - host->host = strdup(tok); - - tok = strtok(NULL, " "); - assert(tok); - if (strcmp(tok, "SHA-512") != 0) { - free(host->host); - free(host); - continue; - } - - tok = strtok(NULL, " "); - assert(tok); - host->fingerprint = strdup(tok); - - host->lineno = lineno++; - - host->next = tofu->known_hosts; - tofu->known_hosts = host; - } - free(line); - fclose(f); -} - -void -gemini_tofu_finish(struct gemini_tofu *tofu) -{ - struct known_host *host = tofu->known_hosts; - while (host) { - struct known_host *tmp = host; - host = host->next; - free(tmp->host); - free(tmp->fingerprint); - free(tmp); - } -} diff --git a/sources/cgmnlm.git/tree/src/url.c.txt b/sources/cgmnlm.git/tree/src/url.c.txt @@ -1,1435 +0,0 @@ -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.haxx.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - ***************************************************************************/ - -#define MAX_SCHEME_LEN 8 - -#include <assert.h> -#include <ctype.h> -#include <stdarg.h> -#include <stdbool.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <strings.h> -#include "escape.h" -#include <gmni/url.h> - -/* Provided by gmni */ -static char * -aprintf(const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - int n = vsnprintf(NULL, 0, fmt, ap); - va_end(ap); - - char *strp = calloc(n + 1, 1); - assert(strp); - - va_start(ap, fmt); - n = vsnprintf(strp, n + 1, fmt, ap); - va_end(ap); - return strp; -} - -/* via lib/dotdot.c */ -char *Curl_dedotdotify(const char *input) -{ - size_t inlen = strlen(input); - char *clone; - size_t clen = inlen; /* the length of the cloned input */ - char *out = malloc(inlen + 1); - char *outptr; - char *orgclone; - char *queryp; - if(!out) - return NULL; /* out of memory */ - - *out = 0; /* zero terminates, for inputs like "./" */ - - /* get a cloned copy of the input */ - clone = strdup(input); - if(!clone) { - free(out); - return NULL; - } - orgclone = clone; - outptr = out; - - if(!*clone) { - /* zero length string, return that */ - free(out); - return clone; - } - - /* - * To handle query-parts properly, we must find it and remove it during the - * dotdot-operation and then append it again at the end to the output - * string. - */ - queryp = strchr(clone, '?'); - if(queryp) - *queryp = 0; - - do { - - /* A. If the input buffer begins with a prefix of "../" or "./", then - remove that prefix from the input buffer; otherwise, */ - - if(!strncmp("./", clone, 2)) { - clone += 2; - clen -= 2; - } - else if(!strncmp("../", clone, 3)) { - clone += 3; - clen -= 3; - } - - /* B. if the input buffer begins with a prefix of "/./" or "/.", where - "." is a complete path segment, then replace that prefix with "/" in - the input buffer; otherwise, */ - else if(!strncmp("/./", clone, 3)) { - clone += 2; - clen -= 2; - } - else if(!strcmp("/.", clone)) { - clone[1]='/'; - clone++; - clen -= 1; - } - - /* C. if the input buffer begins with a prefix of "/../" or "/..", where - ".." is a complete path segment, then replace that prefix with "/" in - the input buffer and remove the last segment and its preceding "/" (if - any) from the output buffer; otherwise, */ - - else if(!strncmp("/../", clone, 4)) { - clone += 3; - clen -= 3; - /* remove the last segment from the output buffer */ - while(outptr > out) { - outptr--; - if(*outptr == '/') - break; - } - *outptr = 0; /* zero-terminate where it stops */ - } - else if(!strcmp("/..", clone)) { - clone[2]='/'; - clone += 2; - clen -= 2; - /* remove the last segment from the output buffer */ - while(outptr > out) { - outptr--; - if(*outptr == '/') - break; - } - *outptr = 0; /* zero-terminate where it stops */ - } - - /* D. if the input buffer consists only of "." or "..", then remove - that from the input buffer; otherwise, */ - - else if(!strcmp(".", clone) || !strcmp("..", clone)) { - *clone = 0; - *out = 0; - } - - else { - /* E. move the first path segment in the input buffer to the end of - the output buffer, including the initial "/" character (if any) and - any subsequent characters up to, but not including, the next "/" - character or the end of the input buffer. */ - - do { - *outptr++ = *clone++; - clen--; - } while(*clone && (*clone != '/')); - *outptr = 0; - } - - } while(*clone); - - if(queryp) { - size_t qlen; - /* There was a query part, append that to the output. The 'clone' string - may now have been altered so we copy from the original input string - from the correct index. */ - size_t oindex = queryp - orgclone; - qlen = strlen(&input[oindex]); - memcpy(outptr, &input[oindex], qlen + 1); /* include the end zero byte */ - } - - free(orgclone); - return out; -} - -/* via lib/url.c */ -CURLcode Curl_parse_login_details(const char *login, const size_t len, - char **userp, char **passwdp, - char **optionsp) -{ - CURLcode result = CURLE_OK; - char *ubuf = NULL; - char *pbuf = NULL; - char *obuf = NULL; - const char *psep = NULL; - const char *osep = NULL; - size_t ulen; - size_t plen; - size_t olen; - - /* Attempt to find the password separator */ - if(passwdp) { - psep = strchr(login, ':'); - - /* Within the constraint of the login string */ - if(psep >= login + len) - psep = NULL; - } - - /* Attempt to find the options separator */ - if(optionsp) { - osep = strchr(login, ';'); - - /* Within the constraint of the login string */ - if(osep >= login + len) - osep = NULL; - } - - /* Calculate the portion lengths */ - ulen = (psep ? - (size_t)(osep && psep > osep ? osep - login : psep - login) : - (osep ? (size_t)(osep - login) : len)); - plen = (psep ? - (osep && osep > psep ? (size_t)(osep - psep) : - (size_t)(login + len - psep)) - 1 : 0); - olen = (osep ? - (psep && psep > osep ? (size_t)(psep - osep) : - (size_t)(login + len - osep)) - 1 : 0); - - /* Allocate the user portion buffer */ - if(userp && ulen) { - ubuf = malloc(ulen + 1); - if(!ubuf) - result = CURLE_OUT_OF_MEMORY; - } - - /* Allocate the password portion buffer */ - if(!result && passwdp && plen) { - pbuf = malloc(plen + 1); - if(!pbuf) { - free(ubuf); - result = CURLE_OUT_OF_MEMORY; - } - } - - /* Allocate the options portion buffer */ - if(!result && optionsp && olen) { - obuf = malloc(olen + 1); - if(!obuf) { - free(pbuf); - free(ubuf); - result = CURLE_OUT_OF_MEMORY; - } - } - - if(!result) { - /* Store the user portion if necessary */ - if(ubuf) { - memcpy(ubuf, login, ulen); - ubuf[ulen] = '\0'; - free(*userp); - *userp = ubuf; - } - - /* Store the password portion if necessary */ - if(pbuf) { - memcpy(pbuf, psep + 1, plen); - pbuf[plen] = '\0'; - free(*passwdp); - *passwdp = pbuf; - } - - /* Store the options portion if necessary */ - if(obuf) { - memcpy(obuf, osep + 1, olen); - obuf[olen] = '\0'; - free(*optionsp); - *optionsp = obuf; - } - } - - return result; -} - -/* Internal representation of CURLU. Point to URL-encoded strings. */ -struct Curl_URL { - char *scheme; - char *user; - char *password; - char *options; /* IMAP only? */ - char *host; - char *port; - char *path; - char *query; - char *fragment; - - char *scratch; /* temporary scratch area */ - long portnum; /* the numerical version */ -}; - -#define DEFAULT_SCHEME "https" - -static void free_urlhandle(struct Curl_URL *u) -{ - free(u->scheme); - free(u->user); - free(u->password); - free(u->options); - free(u->host); - free(u->port); - free(u->path); - free(u->query); - free(u->fragment); - free(u->scratch); -} - -/* move the full contents of one handle onto another and - free the original */ -static void mv_urlhandle(struct Curl_URL *from, - struct Curl_URL *to) -{ - free_urlhandle(to); - *to = *from; - free(from); -} - -/* - * Find the separator at the end of the host name, or the '?' in cases like - * http://www.url.com?id=2380 - */ -static const char *find_host_sep(const char *url) -{ - const char *sep; - const char *query; - - /* Find the start of the hostname */ - sep = strstr(url, "//"); - if(!sep) - sep = url; - else - sep += 2; - - query = strchr(sep, '?'); - sep = strchr(sep, '/'); - - if(!sep) - sep = url + strlen(url); - - if(!query) - query = url + strlen(url); - - return sep < query ? sep : query; -} - -/* - * Decide in an encoding-independent manner whether a character in an - * URL must be escaped. The same criterion must be used in strlen_url() - * and strcpy_url(). - */ -static bool urlchar_needs_escaping(int c) -{ - return !(iscntrl(c) || isspace(c) || isgraph(c)); -} - -/* - * strlen_url() returns the length of the given URL if the spaces within the - * URL were properly URL encoded. - * URL encoding should be skipped for host names, otherwise IDN resolution - * will fail. - */ -size_t Curl_strlen_url(const char *url, bool relative) -{ - const unsigned char *ptr; - size_t newlen = 0; - bool left = true; /* left side of the ? */ - const unsigned char *host_sep = (const unsigned char *) url; - - if(!relative) - host_sep = (const unsigned char *) find_host_sep(url); - - for(ptr = (unsigned char *)url; *ptr; ptr++) { - - if(ptr < host_sep) { - ++newlen; - continue; - } - - switch(*ptr) { - case '?': - left = false; - /* FALLTHROUGH */ - default: - if(urlchar_needs_escaping(*ptr)) - newlen += 2; - newlen++; - break; - case ' ': - if(left) - newlen += 3; - else - newlen++; - break; - } - } - return newlen; -} - -/* strcpy_url() copies a url to a output buffer and URL-encodes the spaces in - * the source URL accordingly. - * URL encoding should be skipped for host names, otherwise IDN resolution - * will fail. - */ -void Curl_strcpy_url(char *output, const char *url, bool relative) -{ - /* we must add this with whitespace-replacing */ - bool left = true; - const unsigned char *iptr; - char *optr = output; - const unsigned char *host_sep = (const unsigned char *) url; - - if(!relative) - host_sep = (const unsigned char *) find_host_sep(url); - - for(iptr = (unsigned char *)url; /* read from here */ - *iptr; /* until zero byte */ - iptr++) { - - if(iptr < host_sep) { - *optr++ = *iptr; - continue; - } - - switch(*iptr) { - case '?': - left = false; - /* FALLTHROUGH */ - default: - if(urlchar_needs_escaping(*iptr)) { - snprintf(optr, 4, "%%%02x", *iptr); - optr += 3; - } - else - *optr++=*iptr; - break; - case ' ': - if(left) { - *optr++='%'; /* add a '%' */ - *optr++='2'; /* add a '2' */ - *optr++='0'; /* add a '0' */ - } - else - *optr++='+'; /* add a '+' here */ - break; - } - } - *optr = 0; /* zero terminate output buffer */ - -} - -/* - * Returns true if the given URL is absolute (as opposed to relative) within - * the buffer size. Returns the scheme in the buffer if true and 'buf' is - * non-NULL. - */ -bool Curl_is_absolute_url(const char *url, char *buf, size_t buflen) -{ - size_t i; - for(i = 0; i < buflen && url[i]; ++i) { - char s = url[i]; - if((s == ':') && (url[i + 1] == '/')) { - if(buf) - buf[i] = 0; - return true; - } - /* RFC 3986 3.1 explains: - scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) - */ - else if(isalnum(s) || (s == '+') || (s == '-') || (s == '.') ) { - if(buf) - buf[i] = (char)tolower(s); - } - else - break; - } - return false; -} - -/* - * Concatenate a relative URL to a base URL making it absolute. - * URL-encodes any spaces. - * The returned pointer must be freed by the caller unless NULL - * (returns NULL on out of memory). - */ -char *Curl_concat_url(const char *base, const char *relurl) -{ - /*** - TRY to append this new path to the old URL - to the right of the host part. Oh crap, this is doomed to cause - problems in the future... - */ - char *newest; - char *protsep; - char *pathsep; - size_t newlen; - bool host_changed = false; - - const char *useurl = relurl; - size_t urllen; - - /* we must make our own copy of the URL to play with, as it may - point to read-only data */ - char *url_clone = strdup(base); - - if(!url_clone) - return NULL; /* skip out of this NOW */ - - /* protsep points to the start of the host name */ - protsep = strstr(url_clone, "//"); - if(!protsep) - protsep = url_clone; - else - protsep += 2; /* pass the slashes */ - - if('/' != relurl[0]) { - int level = 0; - - /* First we need to find out if there's a ?-letter in the URL, - and cut it and the right-side of that off */ - pathsep = strchr(protsep, '?'); - if(pathsep) - *pathsep = 0; - - /* we have a relative path to append to the last slash if there's one - available, or if the new URL is just a query string (starts with a - '?') we append the new one at the end of the entire currently worked - out URL */ - if(useurl[0] != '?') { - pathsep = strrchr(protsep, '/'); - if(pathsep) - *pathsep = 0; - } - - /* Check if there's any slash after the host name, and if so, remember - that position instead */ - pathsep = strchr(protsep, '/'); - if(pathsep) - protsep = pathsep + 1; - else - protsep = NULL; - - /* now deal with one "./" or any amount of "../" in the newurl - and act accordingly */ - - if((useurl[0] == '.') && (useurl[1] == '/')) - useurl += 2; /* just skip the "./" */ - - while((useurl[0] == '.') && - (useurl[1] == '.') && - (useurl[2] == '/')) { - level++; - useurl += 3; /* pass the "../" */ - } - - if(protsep) { - while(level--) { - /* cut off one more level from the right of the original URL */ - pathsep = strrchr(protsep, '/'); - if(pathsep) - *pathsep = 0; - else { - *protsep = 0; - break; - } - } - } - } - else { - /* We got a new absolute path for this server */ - - if((relurl[0] == '/') && (relurl[1] == '/')) { - /* the new URL starts with //, just keep the protocol part from the - original one */ - *protsep = 0; - useurl = &relurl[2]; /* we keep the slashes from the original, so we - skip the new ones */ - host_changed = true; - } - else { - /* cut off the original URL from the first slash, or deal with URLs - without slash */ - pathsep = strchr(protsep, '/'); - if(pathsep) { - /* When people use badly formatted URLs, such as - "http://www.url.com?dir=/home/daniel" we must not use the first - slash, if there's a ?-letter before it! */ - char *sep = strchr(protsep, '?'); - if(sep && (sep < pathsep)) - pathsep = sep; - *pathsep = 0; - } - else { - /* There was no slash. Now, since we might be operating on a badly - formatted URL, such as "http://www.url.com?id=2380" which doesn't - use a slash separator as it is supposed to, we need to check for a - ?-letter as well! */ - pathsep = strchr(protsep, '?'); - if(pathsep) - *pathsep = 0; - } - } - } - - /* If the new part contains a space, this is a mighty stupid redirect - but we still make an effort to do "right". To the left of a '?' - letter we replace each space with %20 while it is replaced with '+' - on the right side of the '?' letter. - */ - newlen = Curl_strlen_url(useurl, !host_changed); - - urllen = strlen(url_clone); - - newest = malloc(urllen + 1 + /* possible slash */ - newlen + 1 /* zero byte */); - - if(!newest) { - free(url_clone); /* don't leak this */ - return NULL; - } - - /* copy over the root url part */ - memcpy(newest, url_clone, urllen); - - /* check if we need to append a slash */ - if(('/' == useurl[0]) || (protsep && !*protsep) || ('?' == useurl[0])) - ; - else - newest[urllen++]='/'; - - /* then append the new piece on the right side */ - Curl_strcpy_url(&newest[urllen], useurl, !host_changed); - - free(url_clone); - - return newest; -} - -/* - * parse_hostname_login() - * - * Parse the login details (user name, password and options) from the URL and - * strip them out of the host name - * - */ -static CURLUcode parse_hostname_login(struct Curl_URL *u, - char **hostname, - unsigned int flags) -{ - CURLUcode result = CURLUE_OK; - CURLcode ccode; - char *userp = NULL; - char *passwdp = NULL; - char *optionsp = NULL; - - /* At this point, we're hoping all the other special cases have - * been taken care of, so conn->host.name is at most - * [user[:password][;options]]@]hostname - * - * We need somewhere to put the embedded details, so do that first. - */ - - char *ptr = strchr(*hostname, '@'); - char *login = *hostname; - - if(!ptr) - goto out; - - /* We will now try to extract the - * possible login information in a string like: - * ftp://user:password@ftp.my.site:8021/README */ - *hostname = ++ptr; - - /* We could use the login information in the URL so extract it. Only parse - options if the handler says we should. Note that 'h' might be NULL! */ - ccode = Curl_parse_login_details(login, ptr - login - 1, - &userp, &passwdp, NULL); - if(ccode) { - result = CURLUE_MALFORMED_INPUT; - goto out; - } - - if(userp) { - if(flags & CURLU_DISALLOW_USER) { - /* Option DISALLOW_USER is set and url contains username. */ - result = CURLUE_USER_NOT_ALLOWED; - goto out; - } - - u->user = userp; - } - - if(passwdp) - u->password = passwdp; - - if(optionsp) - u->options = optionsp; - - return CURLUE_OK; - out: - - free(userp); - free(passwdp); - free(optionsp); - - return result; -} - -static CURLUcode parse_port(struct Curl_URL *u, char *hostname) -{ - char *portptr; - char endbracket; - int len; - - if((1 == sscanf(hostname, "[%*45[0123456789abcdefABCDEF:.%%]%c%n", - &endbracket, &len)) && - (']' == endbracket)) { - /* this is a RFC2732-style specified IP-address */ - portptr = &hostname[len]; - if(*portptr) { - if(*portptr != ':') - return CURLUE_MALFORMED_INPUT; - } - else - portptr = NULL; - } - else - portptr = strchr(hostname, ':'); - - if(portptr) { - char *rest; - long port; - char portbuf[7]; - - if(!isdigit(portptr[1])) - return CURLUE_BAD_PORT_NUMBER; - - port = strtol(portptr + 1, &rest, 10); /* Port number must be decimal */ - - if((port <= 0) || (port > 0xffff)) - /* Single unix standard says port numbers are 16 bits long, but we don't - treat port zero as OK. */ - return CURLUE_BAD_PORT_NUMBER; - - if(rest[0]) - return CURLUE_BAD_PORT_NUMBER; - - if(rest != &portptr[1]) { - *portptr++ = '\0'; /* cut off the name there */ - *rest = 0; - /* generate a new to get rid of leading zeroes etc */ - snprintf(portbuf, sizeof(portbuf), "%ld", port); - u->portnum = port; - u->port = strdup(portbuf); - if(!u->port) - return CURLUE_OUT_OF_MEMORY; - } - else { - /* Browser behavior adaptation. If there's a colon with no digits after, - just cut off the name there which makes us ignore the colon and just - use the default port. Firefox and Chrome both do that. */ - *portptr = '\0'; - } - } - - return CURLUE_OK; -} - -/* scan for byte values < 31 or 127 */ -static CURLUcode junkscan(char *part) -{ - char badbytes[]={ - /* */ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, - 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - 0x7f, - 0x00 /* zero terminate */ - }; - if(part) { - size_t n = strlen(part); - size_t nfine = strcspn(part, badbytes); - if(nfine != n) - /* since we don't know which part is scanned, return a generic error - code */ - return CURLUE_MALFORMED_INPUT; - } - return CURLUE_OK; -} - -static CURLUcode hostname_check(char *hostname, unsigned int flags) -{ - const char *l = NULL; /* accepted characters */ - size_t len; - size_t hlen = strlen(hostname); - (void)flags; - - if(hostname[0] == '[') { - hostname++; - l = "0123456789abcdefABCDEF::.%"; - hlen -= 2; - } - - if(l) { - /* only valid letters are ok */ - len = strspn(hostname, l); - if(hlen != len) - /* hostname with bad content */ - return CURLUE_MALFORMED_INPUT; - } - else { - /* letters from the second string is not ok */ - len = strcspn(hostname, " "); - if(hlen != len) - /* hostname with bad content */ - return CURLUE_MALFORMED_INPUT; - } - return CURLUE_OK; -} - -#define HOSTNAME_END(x) (((x) == '/') || ((x) == '?') || ((x) == '#')) - -static CURLUcode seturl(const char *url, struct Curl_URL *u, unsigned int flags) -{ - char *path; - bool path_alloced = false; - char *hostname; - char *query = NULL; - char *fragment = NULL; - CURLUcode result; - bool url_has_scheme = false; - char schemebuf[MAX_SCHEME_LEN]; - char *schemep = NULL; - size_t schemelen = 0; - size_t urllen; - - if(!url) - return CURLUE_MALFORMED_INPUT; - - /************************************************************* - * Parse the URL. - ************************************************************/ - /* allocate scratch area */ - urllen = strlen(url); - path = u->scratch = malloc(urllen * 2 + 2); - if(!path) - return CURLUE_OUT_OF_MEMORY; - - hostname = &path[urllen + 1]; - hostname[0] = 0; - - if(Curl_is_absolute_url(url, schemebuf, sizeof(schemebuf))) { - url_has_scheme = true; - schemelen = strlen(schemebuf); - } - - /* handle the file: scheme */ - if(url_has_scheme && strcasecmp(schemebuf, "file") == 0) { - /* path has been allocated large enough to hold this */ - strcpy(path, &url[5]); - - hostname = NULL; /* no host for file: URLs */ - u->scheme = strdup("file"); - if(!u->scheme) - return CURLUE_OUT_OF_MEMORY; - - /* Extra handling URLs with an authority component (i.e. that start with - * "file://") - * - * We allow omitted hostname (e.g. file:/<path>) -- valid according to - * RFC 8089, but not the (current) WHAT-WG URL spec. - */ - if(path[0] == '/' && path[1] == '/') { - /* swallow the two slashes */ - char *ptr = &path[2]; - path = ptr; - } - } - else { - /* clear path */ - const char *p; - const char *hostp; - size_t len; - path[0] = 0; - - if(url_has_scheme) { - int i = 0; - p = &url[schemelen + 1]; - while(p && (*p == '/') && (i < 4)) { - p++; - i++; - } - if((i < 1) || (i>3)) - /* less than one or more than three slashes */ - return CURLUE_MALFORMED_INPUT; - - schemep = schemebuf; - if(junkscan(schemep)) - return CURLUE_MALFORMED_INPUT; - } - else { - /* no scheme! */ - return CURLUE_MALFORMED_INPUT; - } - hostp = p; /* host name starts here */ - - while(*p && !HOSTNAME_END(*p)) /* find end of host name */ - p++; - - len = p - hostp; - if(!len) - return CURLUE_MALFORMED_INPUT; - - memcpy(hostname, hostp, len); - hostname[len] = 0; - - len = strlen(p); - memcpy(path, p, len); - path[len] = 0; - - u->scheme = strdup(schemep); - if(!u->scheme) - return CURLUE_OUT_OF_MEMORY; - } - - if(junkscan(path)) - return CURLUE_MALFORMED_INPUT; - - query = strchr(path, '?'); - if(query) - *query++ = 0; - - fragment = strchr(query?query:path, '#'); - if(fragment) - *fragment++ = 0; - - if(!path[0]) - /* if there's no path set, unset */ - path = NULL; - else if(!(flags & CURLU_PATH_AS_IS)) { - /* sanitise paths and remove ../ and ./ sequences according to RFC3986 */ - char *newp = Curl_dedotdotify(path); - if(!newp) - return CURLUE_OUT_OF_MEMORY; - - if(strcmp(newp, path)) { - /* if we got a new version */ - path = newp; - path_alloced = true; - } - else - free(newp); - } - if(path) { - u->path = path_alloced?path:strdup(path); - if(!u->path) - return CURLUE_OUT_OF_MEMORY; - } - - if(hostname) { - /* - * Parse the login details and strip them out of the host name. - */ - if(junkscan(hostname)) - return CURLUE_MALFORMED_INPUT; - - result = parse_hostname_login(u, &hostname, flags); - if(result) - return result; - - result = parse_port(u, hostname); - if(result) - return result; - - result = hostname_check(hostname, flags); - if(result) - return result; - - u->host = strdup(hostname); - if(!u->host) - return CURLUE_OUT_OF_MEMORY; - } - - if(query && query[0]) { - u->query = strdup(query); - if(!u->query) - return CURLUE_OUT_OF_MEMORY; - } - if(fragment && fragment[0]) { - u->fragment = strdup(fragment); - if(!u->fragment) - return CURLUE_OUT_OF_MEMORY; - } - - free(u->scratch); - u->scratch = NULL; - - return CURLUE_OK; -} - -/* - * Parse the URL and set the relevant members of the Curl_URL struct. - */ -static CURLUcode parseurl(const char *url, struct Curl_URL *u, unsigned int flags) -{ - CURLUcode result = seturl(url, u, flags); - if(result) { - free_urlhandle(u); - memset(u, 0, sizeof(struct Curl_URL)); - } - return result; -} - -/* - */ -struct Curl_URL *curl_url(void) -{ - return calloc(sizeof(struct Curl_URL), 1); -} - -void curl_url_cleanup(struct Curl_URL *u) -{ - if(u) { - free_urlhandle(u); - free(u); - } -} - -#define DUP(dest, src, name) \ - if(src->name) { \ - dest->name = strdup(src->name); \ - if(!dest->name) \ - goto fail; \ - } - -struct Curl_URL *curl_url_dup(struct Curl_URL *in) -{ - struct Curl_URL *u = calloc(sizeof(struct Curl_URL), 1); - if(u) { - DUP(u, in, scheme); - DUP(u, in, user); - DUP(u, in, password); - DUP(u, in, options); - DUP(u, in, host); - DUP(u, in, port); - DUP(u, in, path); - DUP(u, in, query); - DUP(u, in, fragment); - u->portnum = in->portnum; - } - return u; - fail: - curl_url_cleanup(u); - return NULL; -} - -CURLUcode curl_url_get(struct Curl_URL *u, CURLUPart what, - char **part, unsigned int flags) -{ - char *ptr; - CURLUcode ifmissing = CURLUE_UNKNOWN_PART; - bool urldecode = (flags & CURLU_URLDECODE)?1:0; - bool plusdecode = false; - (void)flags; - if(!u) - return CURLUE_BAD_HANDLE; - if(!part) - return CURLUE_BAD_PARTPOINTER; - *part = NULL; - - switch(what) { - case CURLUPART_SCHEME: - ptr = u->scheme; - ifmissing = CURLUE_NO_SCHEME; - urldecode = false; /* never for schemes */ - break; - case CURLUPART_USER: - ptr = u->user; - ifmissing = CURLUE_NO_USER; - break; - case CURLUPART_PASSWORD: - ptr = u->password; - ifmissing = CURLUE_NO_PASSWORD; - break; - case CURLUPART_OPTIONS: - ptr = u->options; - ifmissing = CURLUE_NO_OPTIONS; - break; - case CURLUPART_HOST: - ptr = u->host; - ifmissing = CURLUE_NO_HOST; - break; - case CURLUPART_PORT: - ptr = u->port; - ifmissing = CURLUE_NO_PORT; - urldecode = false; /* never for port */ - break; - case CURLUPART_PATH: - ptr = u->path; - if(!ptr) { - ptr = u->path = strdup("/"); - if(!u->path) - return CURLUE_OUT_OF_MEMORY; - } - break; - case CURLUPART_QUERY: - ptr = u->query; - ifmissing = CURLUE_NO_QUERY; - plusdecode = urldecode; - break; - case CURLUPART_FRAGMENT: - ptr = u->fragment; - ifmissing = CURLUE_NO_FRAGMENT; - break; - case CURLUPART_URL: { - char *url; - char *scheme; - char *options = u->options; - char *port = u->port; - if(u->scheme && strcasecmp("file", u->scheme) == 0) { - url = aprintf("file://%s%s%s", - u->path, - u->fragment? "#": "", - u->fragment? u->fragment : ""); - } - else if(!u->host) - return CURLUE_NO_HOST; - else { - if(u->scheme) - scheme = u->scheme; - else - return CURLUE_NO_SCHEME; - - options = NULL; - - url = aprintf("%s://%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", - scheme, - u->user ? u->user : "", - u->password ? ":": "", - u->password ? u->password : "", - options ? ";" : "", - options ? options : "", - (u->user || u->password || options) ? "@": "", - u->host, - port ? ":": "", - port ? port : "", - (u->path && (u->path[0] != '/')) ? "/": "", - u->path ? u->path : "/", - u->query? "?": "", - u->query? u->query : "", - u->fragment? "#": "", - u->fragment? u->fragment : ""); - } - if(!url) - return CURLUE_OUT_OF_MEMORY; - *part = url; - return CURLUE_OK; - break; - } - default: - ptr = NULL; - } - if(ptr) { - *part = strdup(ptr); - if(!*part) - return CURLUE_OUT_OF_MEMORY; - if(plusdecode) { - /* convert + to space */ - char *plus; - for(plus = *part; *plus; ++plus) { - if(*plus == '+') - *plus = ' '; - } - } - if(urldecode) { - char *decoded; - size_t dlen; - CURLcode res = Curl_urldecode(*part, 0, &decoded, &dlen, true); - free(*part); - if(res) { - *part = NULL; - return CURLUE_URLDECODE; - } - *part = decoded; - } - return CURLUE_OK; - } - else - return ifmissing; -} - -CURLUcode curl_url_set(struct Curl_URL *u, CURLUPart what, - const char *part, unsigned int flags) -{ - char **storep = NULL; - long port = 0; - bool urlencode = (flags & CURLU_URLENCODE)? 1 : 0; - bool plusencode = false; - bool urlskipslash = false; - bool appendquery = false; - bool equalsencode = false; - - if(!u) - return CURLUE_BAD_HANDLE; - if(!part) { - /* setting a part to NULL clears it */ - switch(what) { - case CURLUPART_URL: - break; - case CURLUPART_SCHEME: - storep = &u->scheme; - break; - case CURLUPART_USER: - storep = &u->user; - break; - case CURLUPART_PASSWORD: - storep = &u->password; - break; - case CURLUPART_OPTIONS: - storep = &u->options; - break; - case CURLUPART_HOST: - storep = &u->host; - break; - case CURLUPART_PORT: - storep = &u->port; - break; - case CURLUPART_PATH: - storep = &u->path; - break; - case CURLUPART_QUERY: - storep = &u->query; - break; - case CURLUPART_FRAGMENT: - storep = &u->fragment; - break; - default: - return CURLUE_UNKNOWN_PART; - } - if(storep && *storep) { - free(*storep); - *storep = NULL; - } - return CURLUE_OK; - } - - switch(what) { - case CURLUPART_SCHEME: - storep = &u->scheme; - urlencode = false; /* never */ - break; - case CURLUPART_USER: - storep = &u->user; - break; - case CURLUPART_PASSWORD: - storep = &u->password; - break; - case CURLUPART_OPTIONS: - storep = &u->options; - break; - case CURLUPART_HOST: - storep = &u->host; - break; - case CURLUPART_PORT: - urlencode = false; /* never */ - port = strtol(part, NULL, 10); /* Port number must be decimal */ - if((port <= 0) || (port > 0xffff)) - return CURLUE_BAD_PORT_NUMBER; - storep = &u->port; - break; - case CURLUPART_PATH: - urlskipslash = true; - storep = &u->path; - break; - case CURLUPART_QUERY: - plusencode = urlencode; - appendquery = (flags & CURLU_APPENDQUERY)?1:0; - equalsencode = appendquery; - storep = &u->query; - break; - case CURLUPART_FRAGMENT: - storep = &u->fragment; - break; - case CURLUPART_URL: { - /* - * Allow a new URL to replace the existing (if any) contents. - * - * If the existing contents is enough for a URL, allow a relative URL to - * replace it. - */ - CURLUcode result; - char *oldurl; - char *redired_url; - struct Curl_URL *handle2; - - if(Curl_is_absolute_url(part, NULL, MAX_SCHEME_LEN)) { - handle2 = curl_url(); - if(!handle2) - return CURLUE_OUT_OF_MEMORY; - result = parseurl(part, handle2, flags); - if(!result) - mv_urlhandle(handle2, u); - else - curl_url_cleanup(handle2); - return result; - } - /* extract the full "old" URL to do the redirect on */ - result = curl_url_get(u, CURLUPART_URL, &oldurl, flags); - if(result) { - /* couldn't get the old URL, just use the new! */ - handle2 = curl_url(); - if(!handle2) - return CURLUE_OUT_OF_MEMORY; - result = parseurl(part, handle2, flags); - if(!result) - mv_urlhandle(handle2, u); - else - curl_url_cleanup(handle2); - return result; - } - - /* apply the relative part to create a new URL */ - redired_url = Curl_concat_url(oldurl, part); - free(oldurl); - if(!redired_url) - return CURLUE_OUT_OF_MEMORY; - - /* now parse the new URL */ - handle2 = curl_url(); - if(!handle2) { - free(redired_url); - return CURLUE_OUT_OF_MEMORY; - } - result = parseurl(redired_url, handle2, flags); - free(redired_url); - if(!result) - mv_urlhandle(handle2, u); - else - curl_url_cleanup(handle2); - return result; - } - default: - return CURLUE_UNKNOWN_PART; - } - if(storep) { - const char *newp = part; - size_t nalloc = strlen(part); - - if(urlencode) { - const char *i; - char *o; - bool free_part = false; - char *enc = malloc(nalloc * 3 + 1); /* for worst case! */ - if(!enc) - return CURLUE_OUT_OF_MEMORY; - for(i = part, o = enc; *i; i++) { - if(Curl_isunreserved(*i) || - ((*i == '/') && urlskipslash) || - ((*i == '=') && equalsencode) || - ((*i == '+') && plusencode)) { - if((*i == '=') && equalsencode) - /* only skip the first equals sign */ - equalsencode = false; - *o = *i; - o++; - } - else { - snprintf(o, 4, "%%%02x", *i); - o += 3; - } - } - *o = 0; /* zero terminate */ - newp = enc; - if(free_part) - free((char *)part); - } - else { - char *p; - newp = strdup(part); - if(!newp) - return CURLUE_OUT_OF_MEMORY; - p = (char *)newp; - while(*p) { - /* make sure percent encoded are lower case */ - if((*p == '%') && isxdigit(p[1]) && isxdigit(p[2]) && - (isupper(p[1]) || isupper(p[2]))) { - p[1] = (char)tolower(p[1]); - p[2] = (char)tolower(p[2]); - p += 3; - } - else - p++; - } - } - - if(appendquery) { - /* Append the string onto the old query. Add a '&' separator if none is - present at the end of the exsting query already */ - size_t querylen = u->query ? strlen(u->query) : 0; - bool addamperand = querylen && (u->query[querylen -1] != '&'); - if(querylen) { - size_t newplen = strlen(newp); - char *p = malloc(querylen + addamperand + newplen + 1); - if(!p) { - free((char *)newp); - return CURLUE_OUT_OF_MEMORY; - } - strcpy(p, u->query); /* original query */ - if(addamperand) - p[querylen] = '&'; /* ampersand */ - strcpy(&p[querylen + addamperand], newp); /* new suffix */ - free((char *)newp); - free(*storep); - *storep = p; - return CURLUE_OK; - } - } - - free(*storep); - *storep = (char *)newp; - } - /* set after the string, to make it not assigned if the allocation above - fails */ - if(port) - u->portnum = port; - return CURLUE_OK; -} diff --git a/sources/cgmnlm.git/tree/src/util.c.txt b/sources/cgmnlm.git/tree/src/util.c.txt @@ -1,105 +0,0 @@ -#include <assert.h> -#include <bearssl.h> -#include <errno.h> -#include <gmni/gmni.h> -#include <libgen.h> -#include <limits.h> -#include <stdint.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/stat.h> -#include "util.h" - -void -posix_dirname(char *path, char *dname) -{ - char p[PATH_MAX+1]; - char *t; - - assert(strlen(path) <= PATH_MAX); - - strcpy(p, path); - t = dirname(p); - memmove(dname, t, strlen(t) + 1); -} - -/** Make directory and all of its parents */ -int -mkdirs(char *path, mode_t mode) -{ - char dname[PATH_MAX + 1]; - posix_dirname(path, dname); - if (strcmp(dname, "/") == 0) { - return 0; - } - if (mkdirs(dname, mode) != 0) { - return -1; - } - if (mkdir(path, mode) != 0 && errno != EEXIST) { - return -1; - } - errno = 0; - return 0; -} - -char * -getpath(const struct pathspec *paths, size_t npaths) { - for (size_t i = 0; i < npaths; i++) { - const char *var = ""; - if (paths[i].var) { - var = getenv(paths[i].var); - } - if (var) { - char *out = calloc(1, - strlen(var) + strlen(paths[i].path) + 1); - strcat(strcat(out, var), paths[i].path); - return out; - } - } - return NULL; -} - -int -download_resp(FILE *out, struct gemini_response resp, const char *path, - char *url) -{ - char path_buf[PATH_MAX]; - assert(path); - if (path[0] == '\0') { - path = "./"; - } - if (path[strlen(path)-1] == '/') { - int n = snprintf(path_buf, sizeof(path_buf), "%s%s", path, basename(url)); - assert((size_t)n < sizeof(path_buf)); - path = path_buf; - } - FILE *f = fopen(path, "w"); - if (f == NULL) { - fprintf(stderr, "Could not open %s for writing: %s\n", - path, strerror(errno)); - return 1; - } - fprintf(out, "Downloading %s to %s\n", url, path); - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { - n = br_sslio_read(&resp.body, buf, sizeof(buf)); - if (n == -1) { - fprintf(stderr, "Error: read\n"); - return 1; - } - ssize_t w = 0; - while (w < (ssize_t)n) { - ssize_t x = fwrite(&buf[w], 1, n - w, f); - if (ferror(f)) { - fprintf(stderr, "Error: write: %s\n", - strerror(errno)); - return 1; - } - w += x; - } - } - fprintf(out, "Finished download\n"); - fclose(f); - return 0; -} diff --git a/sources/gmnifaq.git/commits/0cfdd64ea47cf0d24b3280ba8b821dff01a849d9.patch b/sources/gmnifaq.git/commits/0cfdd64ea47cf0d24b3280ba8b821dff01a849d9.patch @@ -1,124 +0,0 @@ -diff --git a/TODO.md b/TODO.md -deleted file mode 100644 -index fe826d03ff3b2caceaa8caac5b3b54eec36df7cc..0000000000000000000000000000000000000000 ---- a/TODO.md -+++ /dev/null -@@ -1,7 +0,0 @@ --# initial todo --- avoid serving of data files?! --- search using FTS --- built admin interface --- consolidate things -- - header generation -- - footer generation -diff --git a/faqs.pl b/faqs.pl -index 28d7b8c027bf1ed6d37465b588880054821e52f6..3f934f981a8acca1d1508b6b359f1346f65cf95a 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/gmnifaq -+# https://src.clttr.info/rwa/gmnifaq - - use strict; - use DBI; -diff --git a/index.pl b/index.pl -index ff1197c9522318b44f7b0ad5f842b512a83d3d00..066934a04c2b00054c03b873adf3d9dd8b0e528f 100755 ---- a/index.pl -+++ b/index.pl -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/gmnifaq -+# https://src.clttr.info/rwa/gmnifaq - - use strict; - use DBI; -@@ -30,7 +30,7 @@ exit; - - sub body - { -- return ('## Meta', '', '=> search.pl Search', '=> tags.pl Tags', '=> faqs.pl View all', ''); -+ return ('## Meta', '', '=> ./search.pl search', '=> ./tags.pl browse tags', '=> ./faqs.pl view all', ''); - } - - sub header -diff --git a/lib/gmnifaq.pm b/lib/gmnifaq.pm -index bd6e3ed00fed594267ae15db257fb8d889ab09a4..6cb6c0e3c3e8d318bbdf52474459387a83719a6b 100755 ---- a/lib/gmnifaq.pm -+++ b/lib/gmnifaq.pm -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/gmnifaq -+# https://src.clttr.info/rwa/gmnifaq - - package gmnifaq; - use strict; -@@ -51,7 +51,7 @@ } - - sub footer - { -- return ('', '=> index.pl Home', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq'); -+ return ('', '=> ./index.pl Home', '=> https://src.clttr.info/rwa/gmnifaq powered by gmnifaq'); - } - - sub write_response -diff --git a/search.pl b/search.pl -index b39cd56f182859dae2e5d8965e062560f1f00822..f9a151f6333887869b0f51a5895df4d84ec23103 100755 ---- a/search.pl -+++ b/search.pl -@@ -1,7 +1,8 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/gmni-perl-cgi-demo -+# https://src.clttr.info/rwa/gmnifaq -+ - - use strict; - use DBI; -@@ -49,7 +50,7 @@ if ( scalar(@matchingtags)) { - push @result, '## matching tags'; - push @result, ''; - foreach (@matchingtags) { -- push @result, sprintf("=> faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); -+ push @result, sprintf("=> ./faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); - } - push @result, ''; - } -@@ -58,7 +59,7 @@ if ( scalar(@matchingfaqs)) { - push @result, '## matching FAQs'; - push @result, ''; - foreach (@matchingfaqs) { -- push @result, sprintf("=> faqs.pl?faq=%d %s", @$_[0], @$_[1]); -+ push @result, sprintf("=> ./faqs.pl?faq=%d %s", @$_[0], @$_[1]); - } - } - push @result, ''; -diff --git a/tags.pl b/tags.pl -index 96cde68f7c320473982ce75519441cf6283ccb67..5b1ddc47f2313f053b1a7e2e3a6541629f3196aa 100755 ---- a/tags.pl -+++ b/tags.pl -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/gmnifaq -+# https://src.clttr.info/rwa/gmnifaq - - use strict; - use DBI; -@@ -42,7 +42,7 @@ push @result, 'No tags found!'; - } - else { - foreach (@rows) { -- push @result, sprintf("=> faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); -+ push @result, sprintf("=> ./faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); - } - } - push @result, ''; diff --git a/sources/gmnifaq.git/commits/2d3b160b0cf8a1e87693e53645deb2690efaa3fc.patch b/sources/gmnifaq.git/commits/2d3b160b0cf8a1e87693e53645deb2690efaa3fc.patch @@ -1,12 +0,0 @@ -diff --git a/README.md b/README.md -index 16b494e4fdadde7ef33c08ba88e02b3094cdc1bd..cbb37a225ab91016b930626546b24e42f9b5621d 100644 ---- a/README.md -+++ b/README.md -@@ -30,5 +30,7 @@ - - gemini server with cgi enabled (like gmnisrv or stargazer) - - Perl >= 5.28 with modules - - URI::Escape -+ - DBD -+ - DBI::SQLite - - SQLite - diff --git a/sources/gmnifaq.git/commits/4102d2a37da9349f1842a15a1c161e69a4cb2d16.patch b/sources/gmnifaq.git/commits/4102d2a37da9349f1842a15a1c161e69a4cb2d16.patch @@ -1,189 +0,0 @@ -diff --git a/faqs.pl b/faqs.pl -index ba5d3a709798fb19e366f8b02ad59d2fe90e3eac..df5ddc88784c02a33f9c55236a8837d4c4e0eee9 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -1,18 +1,14 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/gmni-perl-cgi-demo -+# https://git.sr.ht/~rwa/gmnifaq - - use strict; - use DBI; - use lib 'lib/'; --use gmnifaq qw(write_response footer); -+use gmnifaq; - --our $sitename = 'gmnifaq'; --our $siteintro = 'Welcome to gmnifaq!'; --if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } -- --my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; -+config(); - - # enable UTF-8 mode for everything - use utf8; -@@ -23,10 +19,6 @@ if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); - } - --if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } -- --if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; -- - my @body = (); - push @body, header(); - push @body, faqs(); -@@ -53,7 +45,7 @@ } - - sub faqs - { -- my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr; -+ my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - - my @return; - my $stmt = $dbh->prepare(sql()); -@@ -78,5 +70,5 @@ } - - sub header - { -- return ( '# FAQs on '. $sitename, ''); -+ return ( '# FAQs on '. $CONF{'name'}, ''); - } -diff --git a/gmnifaq.conf.example b/gmnifaq.conf.example -index 2c0c2be11d38c80d85179ba3d324c1437390c9d5..74be561fe229e7f2f5052fb3d96ca5037e8f0dc1 100644 ---- a/gmnifaq.conf.example -+++ b/gmnifaq.conf.example -@@ -1,2 +1,2 @@ --$sitename = 'gemini faq'; --$siteintro = 'Welcome to gmnifaq, the simple cgi engine for your gemini capsule'; -+$CONF{'name'} = 'gemini faq'; -+$CONF{'intro'} = 'Welcome to gmnifaq, the simple cgi engine for your gemini capsule'; -diff --git a/index.pl b/index.pl -index de8be6c2176b2f7d82871ad89949ce66791b161c..d24d102d296328527890cec3ac8fc4900b57c7f2 100755 ---- a/index.pl -+++ b/index.pl -@@ -8,11 +8,7 @@ use DBI; - use lib 'lib/'; - use gmnifaq; - --our $sitename = 'gmnifaq'; --our $siteintro = 'Welcome to gmnifaq!'; --if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } -- --my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; -+config(); - - # enable UTF-8 mode for everything - use utf8; -@@ -23,12 +19,10 @@ if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); - } - --if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; -- - my @body = (); - push @body, header(); - push @body, body(); --push @body, gmnifaq::footer(); -+push @body, footer(); - - write_response('SUCCESS', 'text/gemini', @body); - -@@ -41,11 +35,11 @@ } - - sub header - { -- my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr; -+ my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - - my $tagcount = $dbh->selectrow_array("SELECT count(id) from tags"); - my $faqcount = $dbh->selectrow_array("SELECT count(id) from questions"); - $dbh->disconnect(); - -- return ('# Welcome to '. $sitename, '', $siteintro, '', sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount), ''); -+ return ('# Welcome to '. $CONF{'name'}, '', $CONF{'intro'}, '', sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount), ''); - } -diff --git a/lib/gmnifaq.pm b/lib/gmnifaq.pm -index 021c87e0eb210cb313428e0c56504f07afa1d89c..c1b807efc5a3e795583af0d50540e6428f57bb8c 100755 ---- a/lib/gmnifaq.pm -+++ b/lib/gmnifaq.pm -@@ -7,12 +7,14 @@ package gmnifaq; - use strict; - use Exporter; - our @ISA = qw(Exporter); --our @EXPORT = qw(footer write_response); # automatically exported subs -+our @EXPORT = qw(config footer write_response %CONF %RC); # automatically exported subs - - # enable UTF-8 mode for everything - use utf8; - binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; -+ -+our %CONF = (); - - # define return codes - our %RC = ( -@@ -35,6 +37,17 @@ 'CLIENT_CERT_REQUIRED', 60, - 'CERT_NOT_AUTHORISED', 61, - 'CERT_NOT_VALID', 62 - ); -+ -+sub config -+{ -+ $CONF{'name'} = 'gmnifaq'; -+ $CONF{'intro'} = 'Welcome to gmnifaq!'; -+ if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } -+ -+ if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; -+ -+ $CONF{'dsn'} = "DBI:SQLite:dbname=data/data.sqlite"; -+} - - sub footer - { -diff --git a/tags.pl b/tags.pl -index 1f1605adf46cd4818dc60574603b7aabaaa02a0c..60cd01025990abd3d72a6cee6469be7ee589a600 100755 ---- a/tags.pl -+++ b/tags.pl -@@ -8,11 +8,7 @@ use DBI; - use lib 'lib/'; - use gmnifaq; - --our $sitename = 'gmnifaq'; --our $siteintro = 'Welcome to gmnifaq!'; --if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } -- --my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; -+config(); - - # enable UTF-8 mode for everything - use utf8; -@@ -23,10 +19,6 @@ if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); - } - --if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } -- --if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; -- - my @body = (); - push @body, header(); - push @body, tags(); -@@ -38,7 +30,7 @@ exit; - - sub tags - { -- my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr; -+ my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - - my @tags; - my $stmt = $dbh->prepare('SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id'); -@@ -60,5 +52,5 @@ } - - sub header - { -- return ('# Welcome to '. $sitename, '', 'Select a tag to browse the questions associated with this tag.', '', '## Tags', ''); -+ return ('# Welcome to '. $CONF{'name'}, '', 'Select a tag to browse the questions associated with this tag.', '', '## Tags', ''); - } diff --git a/sources/gmnifaq.git/commits/4516bfc5a39f8ea8632cd1ed240d350454878c33.patch b/sources/gmnifaq.git/commits/4516bfc5a39f8ea8632cd1ed240d350454878c33.patch @@ -1,127 +0,0 @@ -diff --git a/data/data.sqlite.example b/data/data.sqlite.example -index 5f2a56d6a519463f35094e6313da5f70419d41f2..924a85fd9649d51a764099c4b68e05731365d751 100644 -Binary files a/data/data.sqlite.example and b/data/data.sqlite.example differ -diff --git a/faqs.pl b/faqs.pl -index f3ffefbd6308c67f27f7c9adf2b58eaef854d60f..cfccd2a99fabfe126c0702ed7564c5176d09aab6 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -38,11 +38,11 @@ return "SELECT * FROM questions q ORDER BY question;"; - } - - if ( $query =~ /tag=([0-9]+)/i ) { -- return "SELECT q.* FROM questions q JOIN tags_questions tq ON q.id = tq.q_id WHERE tq.t_id = $1"; -+ return "SELECT q.* FROM questions q LEFT JOIN tags_questions tq ON q.id = tq.q_id WHERE tq.t_id = $1"; - } - - if ( $query =~ /faq=([0-9]+)/i ) { -- return "SELECT q.* FROM questions q JOIN tags_questions tq ON q.id = tq.q_id WHERE q.id = $1"; -+ return "SELECT q.* FROM questions q LEFT JOIN tags_questions tq ON q.id = tq.q_id WHERE q.id = $1"; - } - write_response('INTERNAL_SERVER_ERROR', 'CGI execution error', undef); - } -@@ -53,17 +53,25 @@ my @return; - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - - my @rows = $dbh->selectall_array(sql()); -- $dbh->disconnect(); - - if ( !scalar @rows ) { - push @return, 'No faqs found!'; - } - else { - foreach (@rows) { -- push @return, sprintf("### %s", @$_[1]); -+ push @return, sprintf("## %s", @$_[1]); -+ my @tags = $dbh->selectall_array("SELECT id, name FROM tags t LEFT JOIN tags_questions tq ON tq.t_id = t.id WHERE tq.q_id = @$_[0];"); -+ -+ if ( scalar @tags ) { -+ push @return, ('', 'Tags:'); -+ foreach (@tags) { -+ push @return, sprintf("=> ./faqs.pl?tag=%d %s", @$_[0], @$_[1]); -+ } -+ } - push @return, ('', @$_[2], ''); - } - } -+ $dbh->disconnect(); - push @return, ''; - return @return; - } -diff --git a/scripts.sql b/scripts.sql -new file mode 100644 -index 0000000000000000000000000000000000000000..6adfe8dfa40bcd67acda96cbb409a12c368ee33d ---- /dev/null -+++ b/scripts.sql -@@ -0,0 +1,17 @@ -+select DISTINCT tags.*, count(tags_questions.q_id) as count from fts_data inner join tags on tags.id = fts_data.t_id -+left join tags_questions on tags_questions.t_id = tags.id -+where fts_data match '"text": "gemin"' -+ORDER BY rank -+ -+select DISTINCT q_id, questions.question from fts_data inner join questions on questions.id = q_id -+where fts_data match '"text": "linux"' -+ORDER BY rank; -+ -+-- fill fts_data -+INSERT INTO fts_data (q_id, text) SELECT id, question FROM questions; -+INSERT INTO fts_data (q_id, text) SELECT id, answer FROM questions; -+INSERT INTO fts_data (t_id, text) SELECT id, name FROM tags; -+ -+-- create fts_data table -+CREATE VIRTUAL TABLE fts_data -+USING FTS5(t_id, q_id, text, tokenize = "porter unicode61") -diff --git a/search.pl b/search.pl -index b506809600c3d4ef36c6c82d8afd4f078abb3a3a..2428f41ab3d990e60c136db035ba4ecf76a19847 100755 ---- a/search.pl -+++ b/search.pl -@@ -22,7 +22,7 @@ write_response('CGI_ERROR', 'CGI execution error', undef); - } - - my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); --if ($query eq '' || $query !~ /^[0-9a-z]*$/i) -+if ($query eq '' || $query !~ /^[0-9a-z\s]*$/i) - { - write_response('INPUT', 'Search faqs for the following terms (separate by blank)', undef); - } -@@ -42,15 +42,15 @@ my ($term) = @_; - my @result = (); - - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; -- my @matchingtags = $dbh->selectall_array("SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id WHERE name LIKE '%$term%' GROUP BY t_id"); -- my @matchingfaqs = $dbh->selectall_array("SELECT * FROM questions WHERE question LIKE '%$term%' OR answer LIKE '%$term%'"); -+ my @matchingtags = $dbh->selectall_array("SELECT DISTINCT t.*, count(tq.q_id) AS count FROM fts_data INNER JOIN tags t ON t.id = fts_data.t_id LEFT JOIN tags_questions tq ON tq.t_id = t.id WHERE fts_data MATCH '\"text\": \"$term\"' GROUP BY t.id ORDER BY rank"); -+ my @matchingfaqs = $dbh->selectall_array("SELECT DISTINCT q_id, q.question FROM fts_data INNER JOIN questions q ON q.id = q_id WHERE fts_data MATCH '\"text\": \"$term\"' ORDER BY rank;"); - $dbh->disconnect(); - -- if ( !scalar(@matchingtags) && !scalar(@matchingfaqs)) { -+ if ( !scalar @matchingtags && !scalar @matchingfaqs) { - push @result, ('Sorry, we can\'t find an entry that matches your search data.', ''); - } - -- if ( scalar(@matchingtags)) { -+ if ( scalar @matchingtags) { - push @result, ('## matching tags', ''); - foreach (@matchingtags) { - push @result, sprintf("=> ./faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); -@@ -58,7 +58,7 @@ } - push @result, ''; - } - -- if ( scalar(@matchingfaqs)) { -+ if ( scalar @matchingfaqs) { - push @result, ('## matching FAQs', ''); - foreach (@matchingfaqs) { - push @result, sprintf("=> ./faqs.pl?faq=%d %s", @$_[0], @$_[1]); -diff --git a/tags.pl b/tags.pl -index 5b1ddc47f2313f053b1a7e2e3a6541629f3196aa..81996bdf9c9adfd898f6308c91bc65e2239243db 100755 ---- a/tags.pl -+++ b/tags.pl -@@ -34,7 +34,7 @@ { - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - - my @result; -- my @rows = $dbh->selectall_array('SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id'); -+ my @rows = $dbh->selectall_array('SELECT id, name, count(t_id) FROM tags t LEFT JOIN tags_questions tq ON tq.t_id = t.id GROUP BY t_id'); - $dbh->disconnect(); - - if ( !scalar @rows ) { diff --git a/sources/gmnifaq.git/commits/46ca4c42e2fa4202c1574d014a18460316505f42.patch b/sources/gmnifaq.git/commits/46ca4c42e2fa4202c1574d014a18460316505f42.patch @@ -1,36 +0,0 @@ -diff --git a/faqs.pl b/faqs.pl -index cfccd2a99fabfe126c0702ed7564c5176d09aab6..67d706673da80a17b9ad8738ceca810baa8c73da 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -55,7 +55,7 @@ - my @rows = $dbh->selectall_array(sql()); - - if ( !scalar @rows ) { -- push @return, 'No faqs found!'; -+ push @return, ('No faqs found!', ''); - } - else { - foreach (@rows) { -@@ -72,7 +72,6 @@ push @return, ('', @$_[2], ''); - } - } - $dbh->disconnect(); -- push @return, ''; - return @return; - } - -diff --git a/index.pl b/index.pl -index 066934a04c2b00054c03b873adf3d9dd8b0e528f..f7ad582c886b952c312275f7b49de21ce7e393a2 100755 ---- a/index.pl -+++ b/index.pl -@@ -19,6 +19,10 @@ if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); - } - -+if ($ENV{'QUERY_STRING'} ne '' || $ENV{'PATH_INFO'} ne '') { -+ write_response('NOT_FOUND', 'File not found', undef); -+} -+ - my @body = (); - push @body, header(); - push @body, body(); diff --git a/sources/gmnifaq.git/commits/5b8503c2d37e7d32484bd7128f903a4f97bee9d8.patch b/sources/gmnifaq.git/commits/5b8503c2d37e7d32484bd7128f903a4f97bee9d8.patch @@ -1,35 +0,0 @@ -diff --git a/data/data.sqlite.example b/data/data.sqlite.example -index 924a85fd9649d51a764099c4b68e05731365d751..f12647834d98f1c7541c4372e58dbee009f74766 100644 -Binary files a/data/data.sqlite.example and b/data/data.sqlite.example differ -diff --git a/faqs.pl b/faqs.pl -index 719f3c0b1707fd16a96b4ff916059e6b276fbc37..3550f23de640644557c2137e974209f0fa56ca27 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -44,7 +44,7 @@ - if ( $query =~ /faq=([0-9]+)/i ) { - return "SELECT q.* FROM questions q LEFT JOIN tags_questions tq ON q.id = tq.q_id WHERE q.id = $1"; - } -- write_response('INTERNAL_SERVER_ERROR', 'CGI execution error', undef); -+ write_response('CGI_ERROR', 'invalid query string', undef); - } - - sub faqs -@@ -60,15 +60,16 @@ } - else { - foreach (@rows) { - push @return, sprintf("## %s", @$_[1]); -+ push @return, (@$_[2], ''); - my @tags = $dbh->selectall_array("SELECT id, name FROM tags t LEFT JOIN tags_questions tq ON tq.t_id = t.id WHERE tq.q_id = @$_[0];"); - - if ( scalar @tags ) { -- push @return, ('', '### Tags'); -+ push @return, ('### tags'); - foreach (@tags) { - push @return, sprintf("=> ./faqs.pl?tag=%d %s", @$_[0], @$_[1]); - } -+ push @return, ''; - } -- push @return, ('', '### Answer', @$_[2], ''); - } - } - $dbh->disconnect(); diff --git a/sources/gmnifaq.git/commits/6cc21c44f0e1ca40fcd3408ed4a3edae0c6c7b47.patch b/sources/gmnifaq.git/commits/6cc21c44f0e1ca40fcd3408ed4a3edae0c6c7b47.patch @@ -1,203 +0,0 @@ -diff --git a/TODO.md b/TODO.md -index 61b8a55447d832b09932d121bceac32c53552478..92d1ba407b76d776d4860603d0e142e7ce600226 100644 ---- a/TODO.md -+++ b/TODO.md -@@ -1,10 +1,7 @@ - # initial todo - - avoid serving of data files?! --- implement display of question -- - single question --- implement search -- - check soundslike search --- URI:encode -+- search using FTS -+- URI:encode/decode for params - - built admin interface - - consolidate things - - header generation -diff --git a/data/data.sqlite.example b/data/data.sqlite.example -index a1480e5b5f170772dbe223cdc1a859996ed4f9ad..8ec065e65fff590b317f61acd2b74a8f3dd25909 100644 -Binary files a/data/data.sqlite.example and b/data/data.sqlite.example differ -diff --git a/faqs.pl b/faqs.pl -index df5ddc88784c02a33f9c55236a8837d4c4e0eee9..0e6fde0690fb597cc5e4537205edfd17e9c4f24c 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -40,35 +40,35 @@ if ( $query =~ /tag=([0-9]+)/i ) { - return "SELECT q.* FROM questions q JOIN tags_questions tq ON q.id = tq.q_id WHERE tq.t_id = $1"; - } - -+ if ( $query =~ /faq=([0-9]+)/i ) { -+ return "SELECT q.* FROM questions q JOIN tags_questions tq ON q.id = tq.q_id WHERE q.id = $1"; -+ } - write_response('INTERNAL_SERVER_ERROR', 'CGI execution error', undef); - } - - sub faqs - { -+ my @return; - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - -- my @return; -- my $stmt = $dbh->prepare(sql()); -- $stmt->execute(); -- -- my $rows = $stmt->fetchall_arrayref; -+ my @rows = $dbh->selectall_array(sql()); - $dbh->disconnect(); - -- if ( !scalar @$rows ) { -+ if ( !scalar @rows ) { - push @return, 'No faqs found!'; - } - else { -- foreach (@$rows) { -+ foreach (@rows) { - push @return, sprintf("### %s", @$_[1]); - push @return, ''; - push @return, @$_[2]; -- push @return, ''; - } - } -+ push @return, ''; - return @return; - } - - sub header - { -- return ( '# FAQs on '. $CONF{'name'}, ''); -+ return ( '# '. $CONF{'name'}, ''); - } -diff --git a/index.pl b/index.pl -index d24d102d296328527890cec3ac8fc4900b57c7f2..9b27f07e619d9075b8f8947788a7f825b4d6b097 100755 ---- a/index.pl -+++ b/index.pl -@@ -30,7 +30,7 @@ exit; - - sub body - { -- return ('## Meta', '', 'Search', '=> tags.pl Tags', '=> faqs.pl View all'); -+ return ('## Meta', '', '=> search.pl Search', '=> tags.pl Tags', '=> faqs.pl View all', ''); - } - - sub header -@@ -41,5 +41,5 @@ my $tagcount = $dbh->selectrow_array("SELECT count(id) from tags"); - my $faqcount = $dbh->selectrow_array("SELECT count(id) from questions"); - $dbh->disconnect(); - -- return ('# Welcome to '. $CONF{'name'}, '', $CONF{'intro'}, '', sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount), ''); -+ return ('# '. $CONF{'name'}, '', $CONF{'intro'}, '', sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount), ''); - } -diff --git a/search.pl b/search.pl -new file mode 100755 -index 0000000000000000000000000000000000000000..4d157ecfa7227b4663ab821c3d0360a06326c5c0 ---- /dev/null -+++ b/search.pl -@@ -0,0 +1,69 @@ -+#!/usr/bin/perl -+# Copyright René Wagner 2020 -+# licenced under BSD 3-Clause licence -+# https://git.sr.ht/~rwa/gmni-perl-cgi-demo -+ -+use strict; -+use DBI; -+use lib 'lib/'; -+use gmnifaq; -+ -+config(); -+ -+# enable UTF-8 mode for everything -+use utf8; -+binmode STDOUT, ':utf8'; -+binmode STDERR, ':utf8'; -+ -+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { -+ write_response('CGI_ERROR', 'CGI execution error', undef); -+} -+ -+my $query = lc($ENV{'QUERY_STRING'}); -+if ($query eq '') -+{ -+ write_response('INPUT', 'Search faqs for the following terms (blank-space separated):', undef); -+} -+ -+my @body = (); -+push @body, header(); -+push @body, search($query); -+push @body, footer(); -+ -+write_response('SUCCESS', 'text/gemini', @body); -+ -+exit; -+ -+sub search -+{ -+ my ($term) = @_; -+ my @result = (); -+ -+ my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; -+ my @matchingtags = $dbh->selectall_array("SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id WHERE name LIKE '%$term%' GROUP BY t_id"); -+ my @matchingfaqs = $dbh->selectall_array("SELECT * FROM questions WHERE question LIKE '%$term%' OR answer LIKE '%$term%'"); -+ $dbh->disconnect(); -+ -+ if ( scalar(@matchingtags)) { -+ push @result, '## matching tags'; -+ push @result, ''; -+ foreach (@matchingtags) { -+ push @result, sprintf("=> faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); -+ } -+ } -+ -+ if ( scalar(@matchingfaqs)) { -+ push @result, '## matching tags'; -+ push @result, ''; -+ foreach (@matchingfaqs) { -+ push @result, sprintf("=> faqs.pl?faq=%d %s", @$_[0], @$_[1]); -+ } -+ } -+ push @result, ''; -+ return @result; -+} -+ -+sub header -+{ -+ return ('# '. $CONF{'name'}, '', "Matching results for your search term '$query':", ''); -+} -diff --git a/tags.pl b/tags.pl -index 60cd01025990abd3d72a6cee6469be7ee589a600..805b3235d2e0ca22d56c67162c35178a842f98c0 100755 ---- a/tags.pl -+++ b/tags.pl -@@ -32,25 +32,23 @@ sub tags - { - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - -- my @tags; -- my $stmt = $dbh->prepare('SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id'); -- $stmt->execute(); -- -- my $rows = $stmt->fetchall_arrayref; -+ my @result; -+ my @rows = $dbh->selectall_array('SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id'); - $dbh->disconnect(); - -- if ( !scalar @$rows ) { -- push @tags, 'No tags found!'; -+ if ( !scalar @rows ) { -+ push @result, 'No tags found!'; - } - else { -- foreach (@$rows) { -- push @tags, sprintf("=> faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); -+ foreach (@rows) { -+ push @result, sprintf("=> faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); - } - } -- return @tags; -+ push @result, ''; -+ return @result; - } - - sub header - { -- return ('# Welcome to '. $CONF{'name'}, '', 'Select a tag to browse the questions associated with this tag.', '', '## Tags', ''); -+ return ('# '. $CONF{'name'}, '', 'Select a tag to browse the questions associated with this tag.', '', '## Tags', ''); - } diff --git a/sources/gmnifaq.git/commits/7e2f5958e9055dfcf7ceedf8a583584bb8ca52c8.patch b/sources/gmnifaq.git/commits/7e2f5958e9055dfcf7ceedf8a583584bb8ca52c8.patch @@ -1,154 +0,0 @@ -diff --git a/faqs.pl b/faqs.pl -index 0c93c8ae1ba5c421cc1852929dd7358ebdc08672..28d7b8c027bf1ed6d37465b588880054821e52f6 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -9,15 +9,15 @@ use URI::Escape; - use lib 'lib/'; - use gmnifaq; - --config(); -- - # enable UTF-8 mode for everything - use utf8; - binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; - -+config(); -+ - if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { -- write_response('CGI_ERROR', 'CGI execution error', undef); -+ write_response('CGI_ERROR', 'CGI execution error', undef); - } - - my @body = (); -@@ -52,8 +52,8 @@ { - my @return; - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - -- my @rows = $dbh->selectall_array(sql()); -- $dbh->disconnect(); -+ my @rows = $dbh->selectall_array(sql()); -+ $dbh->disconnect(); - - if ( !scalar @rows ) { - push @return, 'No faqs found!'; -diff --git a/index.pl b/index.pl -index 9b27f07e619d9075b8f8947788a7f825b4d6b097..209120f4625388a52c083e3937b40c5d78a4a5e4 100755 ---- a/index.pl -+++ b/index.pl -@@ -8,15 +8,15 @@ use DBI; - use lib 'lib/'; - use gmnifaq; - --config(); -- - # enable UTF-8 mode for everything - use utf8; - binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; - -+config(); -+ - if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { -- write_response('CGI_ERROR', 'CGI execution error', undef); -+ write_response('CGI_ERROR', 'CGI execution error', undef); - } - - my @body = (); -@@ -37,9 +37,9 @@ sub header - { - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - -- my $tagcount = $dbh->selectrow_array("SELECT count(id) from tags"); -- my $faqcount = $dbh->selectrow_array("SELECT count(id) from questions"); -- $dbh->disconnect(); -+ my $tagcount = $dbh->selectrow_array("SELECT count(id) from tags"); -+ my $faqcount = $dbh->selectrow_array("SELECT count(id) from questions"); -+ $dbh->disconnect(); - - return ('# '. $CONF{'name'}, '', $CONF{'intro'}, '', sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount), ''); - } -diff --git a/lib/gmnifaq.pm b/lib/gmnifaq.pm -index c1b807efc5a3e795583af0d50540e6428f57bb8c..7311d29c7fe0a86f4e2ffcdc61d49739b69ded05 100755 ---- a/lib/gmnifaq.pm -+++ b/lib/gmnifaq.pm -@@ -56,16 +56,16 @@ } - - sub write_response - { -- my ($returncode, $meta, @content) = @_; -- -+ my ($returncode, $meta, @content) = @_; -+ - if (!defined($RC{$returncode})) { die "Unknown response code!"; } - -- printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -- foreach (@content) { -- print("$_\r\n"); -- } -+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -+ foreach (@content) { -+ print("$_\r\n"); -+ } - -- exit; -+ exit; - } - - 1; -diff --git a/search.pl b/search.pl -index b31921d6980b1388f053d2b91a792df1eb22e834..b39cd56f182859dae2e5d8965e062560f1f00822 100755 ---- a/search.pl -+++ b/search.pl -@@ -17,7 +17,7 @@ binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; - - if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { -- write_response('CGI_ERROR', 'CGI execution error', undef); -+ write_response('CGI_ERROR', 'CGI execution error', undef); - } - - my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); -@@ -41,8 +41,8 @@ my ($term) = @_; - my @result = (); - - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; -- my @matchingtags = $dbh->selectall_array("SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id WHERE name LIKE '%$term%' GROUP BY t_id"); -- my @matchingfaqs = $dbh->selectall_array("SELECT * FROM questions WHERE question LIKE '%$term%' OR answer LIKE '%$term%'"); -+ my @matchingtags = $dbh->selectall_array("SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id WHERE name LIKE '%$term%' GROUP BY t_id"); -+ my @matchingfaqs = $dbh->selectall_array("SELECT * FROM questions WHERE question LIKE '%$term%' OR answer LIKE '%$term%'"); - $dbh->disconnect(); - - if ( scalar(@matchingtags)) { -diff --git a/tags.pl b/tags.pl -index 805b3235d2e0ca22d56c67162c35178a842f98c0..8417146bc235d3c4f4783b9a624507d04e08ebaa 100755 ---- a/tags.pl -+++ b/tags.pl -@@ -5,6 +5,7 @@ # https://git.sr.ht/~rwa/gmni-perl-cgi-demo - - use strict; - use DBI; -+use URI::Escape; - use lib 'lib/'; - use gmnifaq; - -@@ -16,7 +17,7 @@ binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; - - if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { -- write_response('CGI_ERROR', 'CGI execution error', undef); -+ write_response('CGI_ERROR', 'CGI execution error', undef); - } - - my @body = (); -@@ -33,8 +34,8 @@ { - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - - my @result; -- my @rows = $dbh->selectall_array('SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id'); -- $dbh->disconnect(); -+ my @rows = $dbh->selectall_array('SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id'); -+ $dbh->disconnect(); - - if ( !scalar @rows ) { - push @result, 'No tags found!'; diff --git a/sources/gmnifaq.git/commits/98ab6ca1ab3770d925bed026084215e36cf65913.patch b/sources/gmnifaq.git/commits/98ab6ca1ab3770d925bed026084215e36cf65913.patch @@ -1,44 +0,0 @@ -diff --git a/faqs.pl b/faqs.pl -index 67d706673da80a17b9ad8738ceca810baa8c73da..719f3c0b1707fd16a96b4ff916059e6b276fbc37 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -63,12 +63,12 @@ push @return, sprintf("## %s", @$_[1]); - my @tags = $dbh->selectall_array("SELECT id, name FROM tags t LEFT JOIN tags_questions tq ON tq.t_id = t.id WHERE tq.q_id = @$_[0];"); - - if ( scalar @tags ) { -- push @return, ('', 'Tags:'); -+ push @return, ('', '### Tags'); - foreach (@tags) { - push @return, sprintf("=> ./faqs.pl?tag=%d %s", @$_[0], @$_[1]); - } - } -- push @return, ('', @$_[2], ''); -+ push @return, ('', '### Answer', @$_[2], ''); - } - } - $dbh->disconnect(); -diff --git a/search.pl b/search.pl -index 2428f41ab3d990e60c136db035ba4ecf76a19847..a343cc7c78083f0a9838b779a2377c35bc81d667 100755 ---- a/search.pl -+++ b/search.pl -@@ -24,7 +24,7 @@ - my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); - if ($query eq '' || $query !~ /^[0-9a-z\s]*$/i) - { -- write_response('INPUT', 'Search faqs for the following terms (separate by blank)', undef); -+ write_response('INPUT', 'Search faqs for the following terms (separate by blank, all terms must match)', undef); - } - - my @body = (); -@@ -42,8 +42,9 @@ my ($term) = @_; - my @result = (); - - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; -- my @matchingtags = $dbh->selectall_array("SELECT DISTINCT t.*, count(tq.q_id) AS count FROM fts_data INNER JOIN tags t ON t.id = fts_data.t_id LEFT JOIN tags_questions tq ON tq.t_id = t.id WHERE fts_data MATCH '\"text\": \"$term\"' GROUP BY t.id ORDER BY rank"); -- my @matchingfaqs = $dbh->selectall_array("SELECT DISTINCT q_id, q.question FROM fts_data INNER JOIN questions q ON q.id = q_id WHERE fts_data MATCH '\"text\": \"$term\"' ORDER BY rank;"); -+ my $fts_search = join(' AND ', split(/ /, $term)); -+ my @matchingtags = $dbh->selectall_array("SELECT DISTINCT t.*, count(tq.q_id) AS count FROM fts_data INNER JOIN tags t ON t.id = fts_data.t_id LEFT JOIN tags_questions tq ON tq.t_id = t.id WHERE fts_data MATCH '$fts_search' GROUP BY t.id ORDER BY rank"); -+ my @matchingfaqs = $dbh->selectall_array("SELECT DISTINCT q_id, q.question FROM fts_data INNER JOIN questions q ON q.id = q_id WHERE fts_data MATCH '$fts_search' ORDER BY rank;"); - $dbh->disconnect(); - - if ( !scalar @matchingtags && !scalar @matchingfaqs) { diff --git a/sources/gmnifaq.git/commits/9ae9dd44d5e06347d29f28250d32a92fb1ad7725.patch b/sources/gmnifaq.git/commits/9ae9dd44d5e06347d29f28250d32a92fb1ad7725.patch @@ -1,80 +0,0 @@ -diff --git a/README.md b/README.md -index f60e15b66132ff99d1805b54ed925ce882379f5b..16b494e4fdadde7ef33c08ba88e02b3094cdc1bd 100644 ---- a/README.md -+++ b/README.md -@@ -2,7 +2,7 @@ # gmnifaq - - gmnifaq is going to be a simple, self-hostable FAQ-engine for the [gemini protocol](gemini://gemini.circumlunar.space). - --Visit the [demo](gemini://gmnspc.clttr.info). -+Visit the [demo](gemini://gmndemo.clttr.info/faq/). - - ## already implemented - -@@ -18,7 +18,7 @@ - admin site with auth - - # installation - --- setup your geminiserver with cgi enable -+- setup your geminiserver with cgi enabled - - `git clone` the repo to the directory - - rename the files - - `gmnifaq.conf.example` to `gmnifaq.conf` -@@ -28,7 +28,7 @@ - ## requirements - - - gemini server with cgi enabled (like gmnisrv or stargazer) --- Perl >= 5.28 -- - URI::Encode -+- Perl >= 5.28 with modules -+ - URI::Escape - - SQLite - -diff --git a/index.pl b/index.pl -index 209120f4625388a52c083e3937b40c5d78a4a5e4..ff1197c9522318b44f7b0ad5f842b512a83d3d00 100755 ---- a/index.pl -+++ b/index.pl -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/gmni-perl-cgi-demo -+# https://git.sr.ht/~rwa/gmnifaq - - use strict; - use DBI; -diff --git a/lib/gmnifaq.pm b/lib/gmnifaq.pm -index 7311d29c7fe0a86f4e2ffcdc61d49739b69ded05..bd6e3ed00fed594267ae15db257fb8d889ab09a4 100755 ---- a/lib/gmnifaq.pm -+++ b/lib/gmnifaq.pm -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/gmni-perl-cgi-demo -+# https://git.sr.ht/~rwa/gmnifaq - - package gmnifaq; - use strict; -@@ -58,7 +58,7 @@ sub write_response - { - my ($returncode, $meta, @content) = @_; - -- if (!defined($RC{$returncode})) { die "Unknown response code!"; } -+ defined($RC{$returncode}) or die "Unknown response code!"; - - printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); - foreach (@content) { -diff --git a/tags.pl b/tags.pl -index 8417146bc235d3c4f4783b9a624507d04e08ebaa..96cde68f7c320473982ce75519441cf6283ccb67 100755 ---- a/tags.pl -+++ b/tags.pl -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/gmni-perl-cgi-demo -+# https://git.sr.ht/~rwa/gmnifaq - - use strict; - use DBI; diff --git a/sources/gmnifaq.git/commits/ac99b4092b057be8aeb3237c92a32329d931aa01.patch b/sources/gmnifaq.git/commits/ac99b4092b057be8aeb3237c92a32329d931aa01.patch @@ -1,75 +0,0 @@ -diff --git a/faqs.pl b/faqs.pl -index 3f934f981a8acca1d1508b6b359f1346f65cf95a..f3ffefbd6308c67f27f7c9adf2b58eaef854d60f 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -61,8 +61,7 @@ } - else { - foreach (@rows) { - push @return, sprintf("### %s", @$_[1]); -- push @return, ''; -- push @return, @$_[2]; -+ push @return, ('', @$_[2], ''); - } - } - push @return, ''; -diff --git a/lib/gmnifaq.pm b/lib/gmnifaq.pm -index 6cb6c0e3c3e8d318bbdf52474459387a83719a6b..5a65d4f7dcbd135c4b5b7a2f883591212ccaaee1 100755 ---- a/lib/gmnifaq.pm -+++ b/lib/gmnifaq.pm -@@ -51,7 +51,7 @@ } - - sub footer - { -- return ('', '=> ./index.pl Home', '=> https://src.clttr.info/rwa/gmnifaq powered by gmnifaq'); -+ return ('', '=> ./index.pl [home]', '=> https://src.clttr.info/rwa/gmnifaq powered by gmnifaq'); - } - - sub write_response -diff --git a/search.pl b/search.pl -index f9a151f6333887869b0f51a5895df4d84ec23103..b506809600c3d4ef36c6c82d8afd4f078abb3a3a 100755 ---- a/search.pl -+++ b/search.pl -@@ -22,9 +22,9 @@ write_response('CGI_ERROR', 'CGI execution error', undef); - } - - my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); --if ($query eq '') -+if ($query eq '' || $query !~ /^[0-9a-z]*$/i) - { -- write_response('INPUT', 'Search faqs for the following terms (blank-space separated):', undef); -+ write_response('INPUT', 'Search faqs for the following terms (separate by blank)', undef); - } - - my @body = (); -@@ -45,10 +45,13 @@ my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - my @matchingtags = $dbh->selectall_array("SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id WHERE name LIKE '%$term%' GROUP BY t_id"); - my @matchingfaqs = $dbh->selectall_array("SELECT * FROM questions WHERE question LIKE '%$term%' OR answer LIKE '%$term%'"); - $dbh->disconnect(); -+ -+ if ( !scalar(@matchingtags) && !scalar(@matchingfaqs)) { -+ push @result, ('Sorry, we can\'t find an entry that matches your search data.', ''); -+ } - - if ( scalar(@matchingtags)) { -- push @result, '## matching tags'; -- push @result, ''; -+ push @result, ('## matching tags', ''); - foreach (@matchingtags) { - push @result, sprintf("=> ./faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); - } -@@ -56,13 +59,12 @@ push @result, ''; - } - - if ( scalar(@matchingfaqs)) { -- push @result, '## matching FAQs'; -- push @result, ''; -+ push @result, ('## matching FAQs', ''); - foreach (@matchingfaqs) { - push @result, sprintf("=> ./faqs.pl?faq=%d %s", @$_[0], @$_[1]); - } - } -- push @result, ''; -+ push @result, ('', '=> ./search.pl search again'); - return @result; - } - diff --git a/sources/gmnifaq.git/commits/adbf69557321fece135394ffd331da789fc7379a.patch b/sources/gmnifaq.git/commits/adbf69557321fece135394ffd331da789fc7379a.patch @@ -1,301 +0,0 @@ -diff --git a/TODO.md b/TODO.md -index 8ad3088b909987c9a99a7aa465d68d05fff7dc53..61b8a55447d832b09932d121bceac32c53552478 100644 ---- a/TODO.md -+++ b/TODO.md -@@ -1,4 +1,5 @@ - # initial todo -+- avoid serving of data files?! - - implement display of question - - single question - - implement search -diff --git a/faqs.pl b/faqs.pl -index 5626eda5b201e7925d37480bc5d7140ebe660263..ba5d3a709798fb19e366f8b02ad59d2fe90e3eac 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -5,31 +5,12 @@ # https://git.sr.ht/~rwa/gmni-perl-cgi-demo - - use strict; - use DBI; --#use URI::Encode qw(uri_encode uri_decode); --# define return codes --our %RC = ( -- 'INPUT', 10, -- 'SENSITIVE_INPUT', 11, -- 'SUCCESS', 20, -- 'TEMPORARY_REDIRECT', 30, -- 'PERMANENT_REDIRECT', 31, -- 'TEMPORARY_FAILURE', 40, -- 'SERVER_UNAVAILABLE', 41, -- 'CGI_ERROR', 42, -- 'PROXY_ERROR', 43, -- 'SLOW_DOWN', 44, -- 'PERMANENT_FAILURE', 50, -- 'NOT_FOUND', 51, -- 'GONE', 52, -- 'PROXY_REQUEST_REFUSE', 53, -- 'BAD_REQUEST', 59, -- 'CLIENT_CERT_REQUIRED', 60, -- 'CERT_NOT_AUTHORISED', 61, -- 'CERT_NOT_VALID', 62 --); -+use lib 'lib/'; -+use gmnifaq qw(write_response footer); - --our $sitename; --our $siteintro; -+our $sitename = 'gmnifaq'; -+our $siteintro = 'Welcome to gmnifaq!'; -+if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } - - my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; - -@@ -99,22 +80,3 @@ sub header - { - return ( '# FAQs on '. $sitename, ''); - } -- --sub footer --{ -- return ('', '=> index.pl Home', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq'); --} -- --sub write_response --{ -- my ($returncode, $meta, @content) = @_; -- -- if (!defined($RC{$returncode})) { die "Unknown response code!"; } -- -- printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -- foreach (@content) { -- print("$_\r\n"); -- } -- -- exit; --} -diff --git a/index.pl b/index.pl -index 91136f409b5ddaa5a280715f4ae387992167d157..de8be6c2176b2f7d82871ad89949ce66791b161c 100755 ---- a/index.pl -+++ b/index.pl -@@ -5,31 +5,12 @@ # https://git.sr.ht/~rwa/gmni-perl-cgi-demo - - use strict; - use DBI; -- --# define return codes --our %RC = ( -- 'INPUT', 10, -- 'SENSITIVE_INPUT', 11, -- 'SUCCESS', 20, -- 'TEMPORARY_REDIRECT', 30, -- 'PERMANENT_REDIRECT', 31, -- 'TEMPORARY_FAILURE', 40, -- 'SERVER_UNAVAILABLE', 41, -- 'CGI_ERROR', 42, -- 'PROXY_ERROR', 43, -- 'SLOW_DOWN', 44, -- 'PERMANENT_FAILURE', 50, -- 'NOT_FOUND', 51, -- 'GONE', 52, -- 'PROXY_REQUEST_REFUSE', 53, -- 'BAD_REQUEST', 59, -- 'CLIENT_CERT_REQUIRED', 60, -- 'CERT_NOT_AUTHORISED', 61, -- 'CERT_NOT_VALID', 62 --); -+use lib 'lib/'; -+use gmnifaq; - - our $sitename = 'gmnifaq'; - our $siteintro = 'Welcome to gmnifaq!'; -+if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } - - my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; - -@@ -38,28 +19,26 @@ use utf8; - binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; - --if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') --{ -+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); - } -- --if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } - - if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; - - my @body = (); - push @body, header(); --push @body, '## Meta'; --push @body, ''; --push @body, 'Search'; --push @body, '=> tags.pl Tags'; --push @body, '=> faqs.pl View all'; --push @body, footer(); -+push @body, body(); -+push @body, gmnifaq::footer(); - - write_response('SUCCESS', 'text/gemini', @body); - - exit; - -+sub body -+{ -+ return ('## Meta', '', 'Search', '=> tags.pl Tags', '=> faqs.pl View all'); -+} -+ - sub header - { - my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr; -@@ -70,22 +49,3 @@ $dbh->disconnect(); - - return ('# Welcome to '. $sitename, '', $siteintro, '', sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount), ''); - } -- --sub footer --{ -- return ('', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq'); --} -- --sub write_response --{ -- my ($returncode, $meta, @content) = @_; -- -- if (!defined($RC{$returncode})) { die "Unknown response code!"; } -- -- printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -- foreach (@content) { -- print("$_\r\n"); -- } -- -- exit; --} -diff --git a/lib/gmnifaq.pm b/lib/gmnifaq.pm -new file mode 100755 -index 0000000000000000000000000000000000000000..021c87e0eb210cb313428e0c56504f07afa1d89c ---- /dev/null -+++ b/lib/gmnifaq.pm -@@ -0,0 +1,58 @@ -+#!/usr/bin/perl -+# Copyright René Wagner 2020 -+# licenced under BSD 3-Clause licence -+# https://git.sr.ht/~rwa/gmni-perl-cgi-demo -+ -+package gmnifaq; -+use strict; -+use Exporter; -+our @ISA = qw(Exporter); -+our @EXPORT = qw(footer write_response); # automatically exported subs -+ -+# enable UTF-8 mode for everything -+use utf8; -+binmode STDOUT, ':utf8'; -+binmode STDERR, ':utf8'; -+ -+# define return codes -+our %RC = ( -+ 'INPUT', 10, -+ 'SENSITIVE_INPUT', 11, -+ 'SUCCESS', 20, -+ 'TEMPORARY_REDIRECT', 30, -+ 'PERMANENT_REDIRECT', 31, -+ 'TEMPORARY_FAILURE', 40, -+ 'SERVER_UNAVAILABLE', 41, -+ 'CGI_ERROR', 42, -+ 'PROXY_ERROR', 43, -+ 'SLOW_DOWN', 44, -+ 'PERMANENT_FAILURE', 50, -+ 'NOT_FOUND', 51, -+ 'GONE', 52, -+ 'PROXY_REQUEST_REFUSE', 53, -+ 'BAD_REQUEST', 59, -+ 'CLIENT_CERT_REQUIRED', 60, -+ 'CERT_NOT_AUTHORISED', 61, -+ 'CERT_NOT_VALID', 62 -+); -+ -+sub footer -+{ -+ return ('', '=> index.pl Home', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq'); -+} -+ -+sub write_response -+{ -+ my ($returncode, $meta, @content) = @_; -+ -+ if (!defined($RC{$returncode})) { die "Unknown response code!"; } -+ -+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -+ foreach (@content) { -+ print("$_\r\n"); -+ } -+ -+ exit; -+} -+ -+1; -diff --git a/tags.pl b/tags.pl -index 8c82b415093755e44535367a475639fcfac91094..1f1605adf46cd4818dc60574603b7aabaaa02a0c 100755 ---- a/tags.pl -+++ b/tags.pl -@@ -5,31 +5,12 @@ # https://git.sr.ht/~rwa/gmni-perl-cgi-demo - - use strict; - use DBI; --#use URI::Encode qw(uri_encode uri_decode); --# define return codes --our %RC = ( -- 'INPUT', 10, -- 'SENSITIVE_INPUT', 11, -- 'SUCCESS', 20, -- 'TEMPORARY_REDIRECT', 30, -- 'PERMANENT_REDIRECT', 31, -- 'TEMPORARY_FAILURE', 40, -- 'SERVER_UNAVAILABLE', 41, -- 'CGI_ERROR', 42, -- 'PROXY_ERROR', 43, -- 'SLOW_DOWN', 44, -- 'PERMANENT_FAILURE', 50, -- 'NOT_FOUND', 51, -- 'GONE', 52, -- 'PROXY_REQUEST_REFUSE', 53, -- 'BAD_REQUEST', 59, -- 'CLIENT_CERT_REQUIRED', 60, -- 'CERT_NOT_AUTHORISED', 61, -- 'CERT_NOT_VALID', 62 --); -+use lib 'lib/'; -+use gmnifaq; - --our $sitename; --our $siteintro; -+our $sitename = 'gmnifaq'; -+our $siteintro = 'Welcome to gmnifaq!'; -+if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } - - my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; - -@@ -81,22 +62,3 @@ sub header - { - return ('# Welcome to '. $sitename, '', 'Select a tag to browse the questions associated with this tag.', '', '## Tags', ''); - } -- --sub footer --{ -- return ('', '=> index.pl Home', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq'); --} -- --sub write_response --{ -- my ($returncode, $meta, @content) = @_; -- -- if (!defined($RC{$returncode})) { die "Unknown response code!"; } -- -- printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -- foreach (@content) { -- print("$_\r\n"); -- } -- -- exit; --} diff --git a/sources/gmnifaq.git/commits/e3616bfde048241b5ea2c2e0266283ba725a0894.patch b/sources/gmnifaq.git/commits/e3616bfde048241b5ea2c2e0266283ba725a0894.patch @@ -1,292 +0,0 @@ -diff --git a/.gitignore b/.gitignore -new file mode 100644 -index 0000000000000000000000000000000000000000..7b724693cb1228ac01c4054b648888b95c32cace ---- /dev/null -+++ b/.gitignore -@@ -0,0 +1,2 @@ -+data/data.sqlite -+gmnifaq.conf -diff --git a/LICENSE b/LICENSE -new file mode 100755 -index 0000000000000000000000000000000000000000..027d71d0073f90db1ea80f7524f6fceccb001e93 ---- /dev/null -+++ b/LICENSE -@@ -0,0 +1,29 @@ -+BSD 3-Clause License -+ -+Copyright (c) 2018-2020, René Wagner -+All rights reserved. -+ -+Redistribution and use in source and binary forms, with or without -+modification, are permitted provided that the following conditions are met: -+ -+* Redistributions of source code must retain the above copyright notice, this -+ list of conditions and the following disclaimer. -+ -+* Redistributions in binary form must reproduce the above copyright notice, -+ this list of conditions and the following disclaimer in the documentation -+ and/or other materials provided with the distribution. -+ -+* Neither the name of the copyright holder nor the names of its -+ contributors may be used to endorse or promote products derived from -+ this software without specific prior written permission. -+ -+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -diff --git a/README.md b/README.md -new file mode 100644 -index 0000000000000000000000000000000000000000..a62cea7fb42d100a02f22b60bc623d0f2e965aca ---- /dev/null -+++ b/README.md -@@ -0,0 +1,17 @@ -+# gmnifaq -+ -+gmnifaq is going to be a simple, self-hostable FAQ-engine for the [gemini protocol](gemini://gemini.circumlunar.space) -+ -+## planned features -+ -+- support for tags -+- searching -+- view by tag -+- view all -+ -+## requirements -+ -+- gemini server with cgi enabled -+- Perl >= 5.28 -+- SQLite -+ -diff --git a/data/data.sqlite.example b/data/data.sqlite.example -new file mode 100644 -index 0000000000000000000000000000000000000000..d8c4059aa1104544e7399cafb62f38c9b210290e -Binary files /dev/null and b/data/data.sqlite.example differ -diff --git a/gmnifaq.conf.example b/gmnifaq.conf.example -new file mode 100644 -index 0000000000000000000000000000000000000000..2c0c2be11d38c80d85179ba3d324c1437390c9d5 ---- /dev/null -+++ b/gmnifaq.conf.example -@@ -0,0 +1,2 @@ -+$sitename = 'gemini faq'; -+$siteintro = 'Welcome to gmnifaq, the simple cgi engine for your gemini capsule'; -diff --git a/index.pl b/index.pl -new file mode 100755 -index 0000000000000000000000000000000000000000..d7865c082337d027e890cf73f9ebb8fbc7c91d58 ---- /dev/null -+++ b/index.pl -@@ -0,0 +1,97 @@ -+#!/usr/bin/perl -+# Copyright René Wagner 2020 -+# licenced under BSD 3-Clause licence -+# https://git.sr.ht/~rwa/gmni-perl-cgi-demo -+ -+use strict; -+use DBI; -+ -+# define return codes -+our %RC = ( -+ 'INPUT', 10, -+ 'SENSITIVE_INPUT', 11, -+ 'SUCCESS', 20, -+ 'TEMPORARY_REDIRECT', 30, -+ 'PERMANENT_REDIRECT', 31, -+ 'TEMPORARY_FAILURE', 40, -+ 'SERVER_UNAVAILABLE', 41, -+ 'CGI_ERROR', 42, -+ 'PROXY_ERROR', 43, -+ 'SLOW_DOWN', 44, -+ 'PERMANENT_FAILURE', 50, -+ 'NOT_FOUND', 51, -+ 'GONE', 52, -+ 'PROXY_REQUEST_REFUSE', 53, -+ 'BAD_REQUEST', 59, -+ 'CLIENT_CERT_REQUIRED', 60, -+ 'CERT_NOT_AUTHORISED', 61, -+ 'CERT_NOT_VALID', 62 -+); -+ -+my $sitename = 'gmnifaq'; -+my $siteintro = 'Welcome to gmnifaq!'; -+ -+my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; -+ -+# enable UTF-8 mode for everything -+use utf8; -+binmode STDOUT, ':utf8'; -+binmode STDERR, ':utf8'; -+ -+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') -+{ -+ write_response('CGI_ERROR', 'CGI execution error', undef); -+} -+ -+if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } -+ -+if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; -+ -+my @body = (); -+push @body, '# Welcome to '. $sitename; -+push @body, ''; -+push @body, $siteintro; -+push @body, ''; -+push @body, header(); -+push @body, ''; -+push @body, '## Meta'; -+push @body, ''; -+push @body, 'Search'; -+push @body, '=> tags.pl Tags'; -+push @body, 'View all'; -+push @body, footer(); -+ -+write_response('SUCCESS', 'text/gemini', @body); -+ -+exit; -+ -+sub header -+{ -+ my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr; -+ -+ my $tagcount = $dbh->selectrow_array("SELECT count(id) from tags"); -+ my $faqcount = $dbh->selectrow_array("SELECT count(id) from questions"); -+ $dbh->disconnect(); -+ -+ return sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount); -+} -+ -+sub footer -+{ -+ return ('', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq'); -+} -+ -+sub write_response -+{ -+ my ($returncode, $meta, @content) = @_; -+ -+ if (!defined($RC{$returncode})) { die "Unknown response code!"; } -+ -+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -+ foreach (@content) -+ { -+ print("$_\r\n"); -+ } -+ -+ exit; -+} -diff --git a/tags.pl b/tags.pl -new file mode 100755 -index 0000000000000000000000000000000000000000..87405788d7c7506edf9d023c2b199d06fbf64030 ---- /dev/null -+++ b/tags.pl -@@ -0,0 +1,105 @@ -+#!/usr/bin/perl -+# Copyright René Wagner 2020 -+# licenced under BSD 3-Clause licence -+# https://git.sr.ht/~rwa/gmni-perl-cgi-demo -+ -+use strict; -+use DBI; -+#use URI::Encode qw(uri_encode uri_decode); -+# define return codes -+our %RC = ( -+ 'INPUT', 10, -+ 'SENSITIVE_INPUT', 11, -+ 'SUCCESS', 20, -+ 'TEMPORARY_REDIRECT', 30, -+ 'PERMANENT_REDIRECT', 31, -+ 'TEMPORARY_FAILURE', 40, -+ 'SERVER_UNAVAILABLE', 41, -+ 'CGI_ERROR', 42, -+ 'PROXY_ERROR', 43, -+ 'SLOW_DOWN', 44, -+ 'PERMANENT_FAILURE', 50, -+ 'NOT_FOUND', 51, -+ 'GONE', 52, -+ 'PROXY_REQUEST_REFUSE', 53, -+ 'BAD_REQUEST', 59, -+ 'CLIENT_CERT_REQUIRED', 60, -+ 'CERT_NOT_AUTHORISED', 61, -+ 'CERT_NOT_VALID', 62 -+); -+ -+my $sitename; -+my $siteintro; -+ -+my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; -+ -+# enable UTF-8 mode for everything -+use utf8; -+binmode STDOUT, ':utf8'; -+binmode STDERR, ':utf8'; -+ -+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') -+{ -+ write_response('CGI_ERROR', 'CGI execution error', undef); -+} -+ -+if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } -+ -+if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; -+ -+my @body = (); -+push @body, '# Welcome to '. $sitename; -+push @body, ''; -+push @body, 'Select a tag to browse the questions associated with this tag.'; -+push @body, ''; -+push @body, '## Tags'; -+push @body, ''; -+push @body, tags(); -+push @body, footer(); -+ -+write_response('SUCCESS', 'text/gemini', @body); -+ -+exit; -+ -+sub tags -+{ -+ my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr; -+ -+ my @tags; -+ my $stmt = $dbh->prepare('SELECT name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id'); -+ $stmt->execute(); -+ -+ my $rows = $stmt->fetchall_arrayref; -+ $dbh->disconnect(); -+ -+ if ( !scalar @$rows ) { -+ push @body, 'No tags found!'; -+ } -+ else { -+ foreach (@$rows) -+ { -+ push @tags, sprintf("=> tags.pl?%s %s (%d entrys)", @$_[0], @$_[0], @$_[1]); -+ } -+ } -+ return @tags; -+} -+ -+sub footer -+{ -+ return ('', '=> index.pl [Home]', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq'); -+} -+ -+sub write_response -+{ -+ my ($returncode, $meta, @content) = @_; -+ -+ if (!defined($RC{$returncode})) { die "Unknown response code!"; } -+ -+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -+ foreach (@content) -+ { -+ print("$_\r\n"); -+ } -+ -+ exit; -+} diff --git a/sources/gmnifaq.git/commits/ed9009e79d88ebe5eeb1ce78cedd0d1bb4fe5dd8.patch b/sources/gmnifaq.git/commits/ed9009e79d88ebe5eeb1ce78cedd0d1bb4fe5dd8.patch @@ -1,330 +0,0 @@ -diff --git a/README.md b/README.md -index 9ad328058e50995621b5be586f6aa5d93bab2258..f60e15b66132ff99d1805b54ed925ce882379f5b 100644 ---- a/README.md -+++ b/README.md -@@ -1,19 +1,33 @@ - # gmnifaq - --gmnifaq is going to be a simple, self-hostable FAQ-engine for the [gemini protocol](gemini://gemini.circumlunar.space) -+gmnifaq is going to be a simple, self-hostable FAQ-engine for the [gemini protocol](gemini://gemini.circumlunar.space). - - Visit the [demo](gemini://gmnspc.clttr.info). - -+## already implemented -+ -+- tags -+- questions -+ - all -+ - by tag -+ - ## planned features - --- support for tags - - searching --- view by tag --- view all -+- admin site with auth -+ -+# installation -+ -+- setup your geminiserver with cgi enable -+- `git clone` the repo to the directory -+- rename the files -+ - `gmnifaq.conf.example` to `gmnifaq.conf` -+ - `data/data.sqlite.example` to `data/data.sqlite` -+- enter your FAQs into the db (with SQLiteStudio or similar) - only till admin site is finished - - ## requirements - --- gemini server with cgi enabled -+- gemini server with cgi enabled (like gmnisrv or stargazer) - - Perl >= 5.28 - - URI::Encode - - SQLite -diff --git a/TODO.md b/TODO.md -index 9e12f2eb042ad51af1b9ad837eec8fe699dee61b..8ad3088b909987c9a99a7aa465d68d05fff7dc53 100644 ---- a/TODO.md -+++ b/TODO.md -@@ -1,9 +1,11 @@ - # initial todo - - implement display of question -- - all -- - by tag - - single question - - implement search - - check soundslike search - - URI:encode - - built admin interface -+- consolidate things -+ - header generation -+ - footer generation -+ - config slurp -diff --git a/data/data.sqlite.example b/data/data.sqlite.example -index d8c4059aa1104544e7399cafb62f38c9b210290e..a1480e5b5f170772dbe223cdc1a859996ed4f9ad 100644 -Binary files a/data/data.sqlite.example and b/data/data.sqlite.example differ -diff --git a/faqs.pl b/faqs.pl -new file mode 100755 -index 0000000000000000000000000000000000000000..5626eda5b201e7925d37480bc5d7140ebe660263 ---- /dev/null -+++ b/faqs.pl -@@ -0,0 +1,120 @@ -+#!/usr/bin/perl -+# Copyright René Wagner 2020 -+# licenced under BSD 3-Clause licence -+# https://git.sr.ht/~rwa/gmni-perl-cgi-demo -+ -+use strict; -+use DBI; -+#use URI::Encode qw(uri_encode uri_decode); -+# define return codes -+our %RC = ( -+ 'INPUT', 10, -+ 'SENSITIVE_INPUT', 11, -+ 'SUCCESS', 20, -+ 'TEMPORARY_REDIRECT', 30, -+ 'PERMANENT_REDIRECT', 31, -+ 'TEMPORARY_FAILURE', 40, -+ 'SERVER_UNAVAILABLE', 41, -+ 'CGI_ERROR', 42, -+ 'PROXY_ERROR', 43, -+ 'SLOW_DOWN', 44, -+ 'PERMANENT_FAILURE', 50, -+ 'NOT_FOUND', 51, -+ 'GONE', 52, -+ 'PROXY_REQUEST_REFUSE', 53, -+ 'BAD_REQUEST', 59, -+ 'CLIENT_CERT_REQUIRED', 60, -+ 'CERT_NOT_AUTHORISED', 61, -+ 'CERT_NOT_VALID', 62 -+); -+ -+our $sitename; -+our $siteintro; -+ -+my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; -+ -+# enable UTF-8 mode for everything -+use utf8; -+binmode STDOUT, ':utf8'; -+binmode STDERR, ':utf8'; -+ -+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { -+ write_response('CGI_ERROR', 'CGI execution error', undef); -+} -+ -+if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } -+ -+if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; -+ -+my @body = (); -+push @body, header(); -+push @body, faqs(); -+push @body, footer(); -+ -+write_response('SUCCESS', 'text/gemini', @body); -+ -+exit; -+ -+sub sql -+{ -+ my $query = $ENV{'QUERY_STRING'}; -+ -+ if ( $query eq '' ) { -+ return "SELECT * FROM questions q ORDER BY question;"; -+ } -+ -+ if ( $query =~ /tag=([0-9]+)/i ) { -+ return "SELECT q.* FROM questions q JOIN tags_questions tq ON q.id = tq.q_id WHERE tq.t_id = $1"; -+ } -+ -+ write_response('INTERNAL_SERVER_ERROR', 'CGI execution error', undef); -+} -+ -+sub faqs -+{ -+ my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr; -+ -+ my @return; -+ my $stmt = $dbh->prepare(sql()); -+ $stmt->execute(); -+ -+ my $rows = $stmt->fetchall_arrayref; -+ $dbh->disconnect(); -+ -+ if ( !scalar @$rows ) { -+ push @return, 'No faqs found!'; -+ } -+ else { -+ foreach (@$rows) { -+ push @return, sprintf("### %s", @$_[1]); -+ push @return, ''; -+ push @return, @$_[2]; -+ push @return, ''; -+ } -+ } -+ return @return; -+} -+ -+sub header -+{ -+ return ( '# FAQs on '. $sitename, ''); -+} -+ -+sub footer -+{ -+ return ('', '=> index.pl Home', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq'); -+} -+ -+sub write_response -+{ -+ my ($returncode, $meta, @content) = @_; -+ -+ if (!defined($RC{$returncode})) { die "Unknown response code!"; } -+ -+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -+ foreach (@content) { -+ print("$_\r\n"); -+ } -+ -+ exit; -+} -diff --git a/index.pl b/index.pl -index d7865c082337d027e890cf73f9ebb8fbc7c91d58..91136f409b5ddaa5a280715f4ae387992167d157 100755 ---- a/index.pl -+++ b/index.pl -@@ -28,8 +28,8 @@ 'CERT_NOT_AUTHORISED', 61, - 'CERT_NOT_VALID', 62 - ); - --my $sitename = 'gmnifaq'; --my $siteintro = 'Welcome to gmnifaq!'; -+our $sitename = 'gmnifaq'; -+our $siteintro = 'Welcome to gmnifaq!'; - - my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; - -@@ -48,17 +48,12 @@ - if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; - - my @body = (); --push @body, '# Welcome to '. $sitename; --push @body, ''; --push @body, $siteintro; --push @body, ''; - push @body, header(); --push @body, ''; - push @body, '## Meta'; - push @body, ''; - push @body, 'Search'; - push @body, '=> tags.pl Tags'; --push @body, 'View all'; -+push @body, '=> faqs.pl View all'; - push @body, footer(); - - write_response('SUCCESS', 'text/gemini', @body); -@@ -73,7 +68,7 @@ my $tagcount = $dbh->selectrow_array("SELECT count(id) from tags"); - my $faqcount = $dbh->selectrow_array("SELECT count(id) from questions"); - $dbh->disconnect(); - -- return sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount); -+ return ('# Welcome to '. $sitename, '', $siteintro, '', sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount), ''); - } - - sub footer -@@ -88,8 +83,7 @@ - if (!defined($RC{$returncode})) { die "Unknown response code!"; } - - printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -- foreach (@content) -- { -+ foreach (@content) { - print("$_\r\n"); - } - -diff --git a/tags.pl b/tags.pl -index 87405788d7c7506edf9d023c2b199d06fbf64030..8c82b415093755e44535367a475639fcfac91094 100755 ---- a/tags.pl -+++ b/tags.pl -@@ -28,8 +28,8 @@ 'CERT_NOT_AUTHORISED', 61, - 'CERT_NOT_VALID', 62 - ); - --my $sitename; --my $siteintro; -+our $sitename; -+our $siteintro; - - my $dsn = "DBI:SQLite:dbname=data/data.sqlite"; - -@@ -38,8 +38,7 @@ use utf8; - binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; - --if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') --{ -+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); - } - -@@ -48,12 +47,7 @@ - if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; - - my @body = (); --push @body, '# Welcome to '. $sitename; --push @body, ''; --push @body, 'Select a tag to browse the questions associated with this tag.'; --push @body, ''; --push @body, '## Tags'; --push @body, ''; -+push @body, header(); - push @body, tags(); - push @body, footer(); - -@@ -66,27 +60,31 @@ { - my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr; - - my @tags; -- my $stmt = $dbh->prepare('SELECT name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id'); -+ my $stmt = $dbh->prepare('SELECT id, name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id'); - $stmt->execute(); - - my $rows = $stmt->fetchall_arrayref; - $dbh->disconnect(); - - if ( !scalar @$rows ) { -- push @body, 'No tags found!'; -+ push @tags, 'No tags found!'; - } - else { -- foreach (@$rows) -- { -- push @tags, sprintf("=> tags.pl?%s %s (%d entrys)", @$_[0], @$_[0], @$_[1]); -+ foreach (@$rows) { -+ push @tags, sprintf("=> faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); - } - } - return @tags; - } - -+sub header -+{ -+ return ('# Welcome to '. $sitename, '', 'Select a tag to browse the questions associated with this tag.', '', '## Tags', ''); -+} -+ - sub footer - { -- return ('', '=> index.pl [Home]', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq'); -+ return ('', '=> index.pl Home', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq'); - } - - sub write_response -@@ -96,8 +94,7 @@ - if (!defined($RC{$returncode})) { die "Unknown response code!"; } - - printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -- foreach (@content) -- { -+ foreach (@content) { - print("$_\r\n"); - } - diff --git a/sources/gmnifaq.git/commits/fae1610eb1c7a29f29244578926a97477471e5b7.patch b/sources/gmnifaq.git/commits/fae1610eb1c7a29f29244578926a97477471e5b7.patch @@ -1,72 +0,0 @@ -diff --git a/TODO.md b/TODO.md -index 92d1ba407b76d776d4860603d0e142e7ce600226..fe826d03ff3b2caceaa8caac5b3b54eec36df7cc 100644 ---- a/TODO.md -+++ b/TODO.md -@@ -1,9 +1,7 @@ - # initial todo - - avoid serving of data files?! - - search using FTS --- URI:encode/decode for params - - built admin interface - - consolidate things - - header generation - - footer generation -- - config slurp -diff --git a/data/data.sqlite.example b/data/data.sqlite.example -index 8ec065e65fff590b317f61acd2b74a8f3dd25909..5f2a56d6a519463f35094e6313da5f70419d41f2 100644 -Binary files a/data/data.sqlite.example and b/data/data.sqlite.example differ -diff --git a/faqs.pl b/faqs.pl -index 0e6fde0690fb597cc5e4537205edfd17e9c4f24c..0c93c8ae1ba5c421cc1852929dd7358ebdc08672 100755 ---- a/faqs.pl -+++ b/faqs.pl -@@ -5,6 +5,7 @@ # https://git.sr.ht/~rwa/gmnifaq - - use strict; - use DBI; -+use URI::Escape; - use lib 'lib/'; - use gmnifaq; - -@@ -30,7 +31,7 @@ exit; - - sub sql - { -- my $query = $ENV{'QUERY_STRING'}; -+ my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); - - if ( $query eq '' ) { - return "SELECT * FROM questions q ORDER BY question;"; -diff --git a/search.pl b/search.pl -index 4d157ecfa7227b4663ab821c3d0360a06326c5c0..b31921d6980b1388f053d2b91a792df1eb22e834 100755 ---- a/search.pl -+++ b/search.pl -@@ -5,6 +5,7 @@ # https://git.sr.ht/~rwa/gmni-perl-cgi-demo - - use strict; - use DBI; -+use URI::Escape; - use lib 'lib/'; - use gmnifaq; - -@@ -19,7 +20,7 @@ if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); - } - --my $query = lc($ENV{'QUERY_STRING'}); -+my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); - if ($query eq '') - { - write_response('INPUT', 'Search faqs for the following terms (blank-space separated):', undef); -@@ -50,10 +51,11 @@ push @result, ''; - foreach (@matchingtags) { - push @result, sprintf("=> faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); - } -+ push @result, ''; - } - - if ( scalar(@matchingfaqs)) { -- push @result, '## matching tags'; -+ push @result, '## matching FAQs'; - push @result, ''; - foreach (@matchingfaqs) { - push @result, sprintf("=> faqs.pl?faq=%d %s", @$_[0], @$_[1]); diff --git a/sources/gmnifaq.git/commits/fc9c20994275edb9dbc6d1b5e97d53785643c021.patch b/sources/gmnifaq.git/commits/fc9c20994275edb9dbc6d1b5e97d53785643c021.patch @@ -1,35 +0,0 @@ -diff --git a/README.md b/README.md -index a62cea7fb42d100a02f22b60bc623d0f2e965aca..9ad328058e50995621b5be586f6aa5d93bab2258 100644 ---- a/README.md -+++ b/README.md -@@ -2,6 +2,8 @@ # gmnifaq - - gmnifaq is going to be a simple, self-hostable FAQ-engine for the [gemini protocol](gemini://gemini.circumlunar.space) - -+Visit the [demo](gemini://gmnspc.clttr.info). -+ - ## planned features - - - support for tags -@@ -13,5 +15,6 @@ ## requirements - - - gemini server with cgi enabled - - Perl >= 5.28 -+ - URI::Encode - - SQLite - -diff --git a/TODO.md b/TODO.md -new file mode 100644 -index 0000000000000000000000000000000000000000..9e12f2eb042ad51af1b9ad837eec8fe699dee61b ---- /dev/null -+++ b/TODO.md -@@ -0,0 +1,9 @@ -+# initial todo -+- implement display of question -+ - all -+ - by tag -+ - single question -+- implement search -+ - check soundslike search -+- URI:encode -+- built admin interface diff --git a/sources/gmnifaq.git/commits/index.gmi b/sources/gmnifaq.git/commits/index.gmi @@ -1,100 +0,0 @@ -# Commits - -## 2d3b160b0cf8a1e87693e53645deb2690efaa3fc -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Apr 19 21:06:21 2021 +0200 -=> 2d3b160b0cf8a1e87693e53645deb2690efaa3fc.patch Patch -Message: add missing perl dependencies - -## 5b8503c2d37e7d32484bd7128f903a4f97bee9d8 -Author: René Wagner <rwagner@rw-net.de> -Date: Fri Feb 26 22:14:38 2021 +0100 -=> 5b8503c2d37e7d32484bd7128f903a4f97bee9d8.patch Patch -Message: improve error handling in faqs view - -## 98ab6ca1ab3770d925bed026084215e36cf65913 -Author: René Wagner <rwagner@rw-net.de> -Date: Fri Feb 26 18:02:22 2021 +0100 -=> 98ab6ca1ab3770d925bed026084215e36cf65913.patch Patch -Message: FTS using AND matching - -## 46ca4c42e2fa4202c1574d014a18460316505f42 -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Feb 08 21:52:37 2021 +0100 -=> 46ca4c42e2fa4202c1574d014a18460316505f42.patch Patch -Message: minor formatting and sanity check - -## 4516bfc5a39f8ea8632cd1ed240d350454878c33 -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Jan 19 21:35:23 2021 +0100 -=> 4516bfc5a39f8ea8632cd1ed240d350454878c33.patch Patch -Message: implement fts for searching - -## ac99b4092b057be8aeb3237c92a32329d931aa01 -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Jan 18 20:42:01 2021 +0100 -=> ac99b4092b057be8aeb3237c92a32329d931aa01.patch Patch -Message: use relative links - -small cosmetic improvements - -## 0cfdd64ea47cf0d24b3280ba8b821dff01a849d9 -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Jan 18 20:23:28 2021 +0100 -=> 0cfdd64ea47cf0d24b3280ba8b821dff01a849d9.patch Patch -Message: update links - -## 9ae9dd44d5e06347d29f28250d32a92fb1ad7725 -Author: René Wagner <rwagner@rw-net.de> -Date: Sat Nov 28 16:32:17 2020 +0100 -=> 9ae9dd44d5e06347d29f28250d32a92fb1ad7725.patch Patch -Message: fix links to repo - -## 7e2f5958e9055dfcf7ceedf8a583584bb8ca52c8 -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Nov 25 12:04:27 2020 +0100 -=> 7e2f5958e9055dfcf7ceedf8a583584bb8ca52c8.patch Patch -Message: tabify - -## fae1610eb1c7a29f29244578926a97477471e5b7 -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Nov 24 21:41:40 2020 +0100 -=> fae1610eb1c7a29f29244578926a97477471e5b7.patch Patch -Message: add URI-handling - -## 6cc21c44f0e1ca40fcd3408ed4a3edae0c6c7b47 -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Nov 24 20:36:12 2020 +0100 -=> 6cc21c44f0e1ca40fcd3408ed4a3edae0c6c7b47.patch Patch -Message: implement basic search - -## 4102d2a37da9349f1842a15a1c161e69a4cb2d16 -Author: René Wagner <rwagner@rw-net.de> -Date: Sat Nov 21 09:24:22 2020 +0100 -=> 4102d2a37da9349f1842a15a1c161e69a4cb2d16.patch Patch -Message: move config() to lib - -## adbf69557321fece135394ffd331da789fc7379a -Author: René Wagner <rwagner@rw-net.de> -Date: Fri Nov 20 22:26:54 2020 +0100 -=> adbf69557321fece135394ffd331da789fc7379a.patch Patch -Message: move footer() and write_response() to lib - -## ed9009e79d88ebe5eeb1ce78cedd0d1bb4fe5dd8 -Author: René Wagner <rwagner@rw-net.de> -Date: Fri Nov 20 21:35:16 2020 +0100 -=> ed9009e79d88ebe5eeb1ce78cedd0d1bb4fe5dd8.patch Patch -Message: implement initial faq display - -## fc9c20994275edb9dbc6d1b5e97d53785643c021 -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Nov 18 21:40:37 2020 +0100 -=> fc9c20994275edb9dbc6d1b5e97d53785643c021.patch Patch -Message: add todo - -## e3616bfde048241b5ea2c2e0266283ba725a0894 -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Nov 18 21:33:19 2020 +0100 -=> e3616bfde048241b5ea2c2e0266283ba725a0894.patch Patch -Message: initial commit of current wip - diff --git a/sources/gmnifaq.git/index.gmi b/sources/gmnifaq.git/index.gmi @@ -1,46 +0,0 @@ -```Command to clone this repository -git clone https://src.clttr.info/rwa/gmnifaq.git -``` - -=> commits/ -=> tree/ - -# gmnifaq - -gmnifaq is going to be a simple, self-hostable FAQ-engine for the gemini protocol. - -=> gemini://gemini.circumlunar.space gemini protocol - -Visit the demo. - -=> gemini://gmndemo.clttr.info/faq/ demo - -## already implemented - -* tags -* questions - * all - * by tag - -## planned features - -* searching -* admin site with auth - -# installation - -* setup your geminiserver with cgi enabled -* git clone the repo to the directory -* rename the files - * gmnifaq.conf.example to gmnifaq.conf - * data/data.sqlite.example to data/data.sqlite -* enter your FAQs into the db (with SQLiteStudio or similar) - only till admin site is finished - -## requirements - -* gemini server with cgi enabled (like gmnisrv or stargazer) -* Perl >= 5.28 with modules - * URI::Escape - * DBD - * DBI::SQLite -* SQLite diff --git a/sources/gmnifaq.git/tree/.gitignore.txt b/sources/gmnifaq.git/tree/.gitignore.txt @@ -1,2 +0,0 @@ -data/data.sqlite -gmnifaq.conf diff --git a/sources/gmnifaq.git/tree/LICENSE.txt b/sources/gmnifaq.git/tree/LICENSE.txt @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2018-2020, René Wagner -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/sources/gmnifaq.git/tree/README.md.txt b/sources/gmnifaq.git/tree/README.md.txt @@ -1,36 +0,0 @@ -# gmnifaq - -gmnifaq is going to be a simple, self-hostable FAQ-engine for the [gemini protocol](gemini://gemini.circumlunar.space). - -Visit the [demo](gemini://gmndemo.clttr.info/faq/). - -## already implemented - -- tags -- questions - - all - - by tag - -## planned features - -- searching -- admin site with auth - -# installation - -- setup your geminiserver with cgi enabled -- `git clone` the repo to the directory -- rename the files - - `gmnifaq.conf.example` to `gmnifaq.conf` - - `data/data.sqlite.example` to `data/data.sqlite` -- enter your FAQs into the db (with SQLiteStudio or similar) - only till admin site is finished - -## requirements - -- gemini server with cgi enabled (like gmnisrv or stargazer) -- Perl >= 5.28 with modules - - URI::Escape - - DBD - - DBI::SQLite -- SQLite - diff --git a/sources/gmnifaq.git/tree/data/data.sqlite.example b/sources/gmnifaq.git/tree/data/data.sqlite.example Binary files differ. diff --git a/sources/gmnifaq.git/tree/data/index.gmi b/sources/gmnifaq.git/tree/data/index.gmi @@ -1,5 +0,0 @@ -# Tree - -Path: data/ - -=> data.sqlite.example data.sqlite.example diff --git a/sources/gmnifaq.git/tree/faqs.pl.txt b/sources/gmnifaq.git/tree/faqs.pl.txt @@ -1,82 +0,0 @@ -#!/usr/bin/perl -# Copyright René Wagner 2020 -# licenced under BSD 3-Clause licence -# https://src.clttr.info/rwa/gmnifaq - -use strict; -use DBI; -use URI::Escape; -use lib 'lib/'; -use gmnifaq; - -# enable UTF-8 mode for everything -use utf8; -binmode STDOUT, ':utf8'; -binmode STDERR, ':utf8'; - -config(); - -if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); -} - -my @body = (); -push @body, header(); -push @body, faqs(); -push @body, footer(); - -write_response('SUCCESS', 'text/gemini', @body); - -exit; - -sub sql -{ - my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); - - if ( $query eq '' ) { - return "SELECT * FROM questions q ORDER BY question;"; - } - - if ( $query =~ /tag=([0-9]+)/i ) { - return "SELECT q.* FROM questions q LEFT JOIN tags_questions tq ON q.id = tq.q_id WHERE tq.t_id = $1"; - } - - if ( $query =~ /faq=([0-9]+)/i ) { - return "SELECT q.* FROM questions q LEFT JOIN tags_questions tq ON q.id = tq.q_id WHERE q.id = $1"; - } - write_response('CGI_ERROR', 'invalid query string', undef); -} - -sub faqs -{ - my @return; - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - - my @rows = $dbh->selectall_array(sql()); - - if ( !scalar @rows ) { - push @return, ('No faqs found!', ''); - } - else { - foreach (@rows) { - push @return, sprintf("## %s", @$_[1]); - push @return, (@$_[2], ''); - my @tags = $dbh->selectall_array("SELECT id, name FROM tags t LEFT JOIN tags_questions tq ON tq.t_id = t.id WHERE tq.q_id = @$_[0];"); - - if ( scalar @tags ) { - push @return, ('### tags'); - foreach (@tags) { - push @return, sprintf("=> ./faqs.pl?tag=%d %s", @$_[0], @$_[1]); - } - push @return, ''; - } - } - } - $dbh->disconnect(); - return @return; -} - -sub header -{ - return ( '# '. $CONF{'name'}, ''); -} diff --git a/sources/gmnifaq.git/tree/gmnifaq.conf.example.txt b/sources/gmnifaq.git/tree/gmnifaq.conf.example.txt @@ -1,2 +0,0 @@ -$CONF{'name'} = 'gemini faq'; -$CONF{'intro'} = 'Welcome to gmnifaq, the simple cgi engine for your gemini capsule'; diff --git a/sources/gmnifaq.git/tree/index.gmi b/sources/gmnifaq.git/tree/index.gmi @@ -1,15 +0,0 @@ -# Tree - -Path: / - -=> .gitignore.txt .gitignore -=> LICENSE.txt LICENSE -=> README.md.txt README.md -=> data/ data/ -=> faqs.pl.txt faqs.pl -=> gmnifaq.conf.example.txt gmnifaq.conf.example -=> index.pl.txt index.pl -=> lib/ lib/ -=> scripts.sql.txt scripts.sql -=> search.pl.txt search.pl -=> tags.pl.txt tags.pl diff --git a/sources/gmnifaq.git/tree/index.pl.txt b/sources/gmnifaq.git/tree/index.pl.txt @@ -1,49 +0,0 @@ -#!/usr/bin/perl -# Copyright René Wagner 2020 -# licenced under BSD 3-Clause licence -# https://src.clttr.info/rwa/gmnifaq - -use strict; -use DBI; -use lib 'lib/'; -use gmnifaq; - -# enable UTF-8 mode for everything -use utf8; -binmode STDOUT, ':utf8'; -binmode STDERR, ':utf8'; - -config(); - -if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); -} - -if ($ENV{'QUERY_STRING'} ne '' || $ENV{'PATH_INFO'} ne '') { - write_response('NOT_FOUND', 'File not found', undef); -} - -my @body = (); -push @body, header(); -push @body, body(); -push @body, footer(); - -write_response('SUCCESS', 'text/gemini', @body); - -exit; - -sub body -{ - return ('## Meta', '', '=> ./search.pl search', '=> ./tags.pl browse tags', '=> ./faqs.pl view all', ''); -} - -sub header -{ - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - - my $tagcount = $dbh->selectrow_array("SELECT count(id) from tags"); - my $faqcount = $dbh->selectrow_array("SELECT count(id) from questions"); - $dbh->disconnect(); - - return ('# '. $CONF{'name'}, '', $CONF{'intro'}, '', sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount), ''); -} diff --git a/sources/gmnifaq.git/tree/lib/gmnifaq.pm.txt b/sources/gmnifaq.git/tree/lib/gmnifaq.pm.txt @@ -1,71 +0,0 @@ -#!/usr/bin/perl -# Copyright René Wagner 2020 -# licenced under BSD 3-Clause licence -# https://src.clttr.info/rwa/gmnifaq - -package gmnifaq; -use strict; -use Exporter; -our @ISA = qw(Exporter); -our @EXPORT = qw(config footer write_response %CONF %RC); # automatically exported subs - -# enable UTF-8 mode for everything -use utf8; -binmode STDOUT, ':utf8'; -binmode STDERR, ':utf8'; - -our %CONF = (); - -# define return codes -our %RC = ( - 'INPUT', 10, - 'SENSITIVE_INPUT', 11, - 'SUCCESS', 20, - 'TEMPORARY_REDIRECT', 30, - 'PERMANENT_REDIRECT', 31, - 'TEMPORARY_FAILURE', 40, - 'SERVER_UNAVAILABLE', 41, - 'CGI_ERROR', 42, - 'PROXY_ERROR', 43, - 'SLOW_DOWN', 44, - 'PERMANENT_FAILURE', 50, - 'NOT_FOUND', 51, - 'GONE', 52, - 'PROXY_REQUEST_REFUSE', 53, - 'BAD_REQUEST', 59, - 'CLIENT_CERT_REQUIRED', 60, - 'CERT_NOT_AUTHORISED', 61, - 'CERT_NOT_VALID', 62 -); - -sub config -{ - $CONF{'name'} = 'gmnifaq'; - $CONF{'intro'} = 'Welcome to gmnifaq!'; - if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; } - - if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) }; - - $CONF{'dsn'} = "DBI:SQLite:dbname=data/data.sqlite"; -} - -sub footer -{ - return ('', '=> ./index.pl [home]', '=> https://src.clttr.info/rwa/gmnifaq powered by gmnifaq'); -} - -sub write_response -{ - my ($returncode, $meta, @content) = @_; - - defined($RC{$returncode}) or die "Unknown response code!"; - - printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); - foreach (@content) { - print("$_\r\n"); - } - - exit; -} - -1; diff --git a/sources/gmnifaq.git/tree/lib/index.gmi b/sources/gmnifaq.git/tree/lib/index.gmi @@ -1,5 +0,0 @@ -# Tree - -Path: lib/ - -=> gmnifaq.pm.txt gmnifaq.pm diff --git a/sources/gmnifaq.git/tree/scripts.sql.txt b/sources/gmnifaq.git/tree/scripts.sql.txt @@ -1,17 +0,0 @@ -select DISTINCT tags.*, count(tags_questions.q_id) as count from fts_data inner join tags on tags.id = fts_data.t_id -left join tags_questions on tags_questions.t_id = tags.id -where fts_data match '"text": "gemin"' -ORDER BY rank - -select DISTINCT q_id, questions.question from fts_data inner join questions on questions.id = q_id -where fts_data match '"text": "linux"' -ORDER BY rank; - --- fill fts_data -INSERT INTO fts_data (q_id, text) SELECT id, question FROM questions; -INSERT INTO fts_data (q_id, text) SELECT id, answer FROM questions; -INSERT INTO fts_data (t_id, text) SELECT id, name FROM tags; - --- create fts_data table -CREATE VIRTUAL TABLE fts_data -USING FTS5(t_id, q_id, text, tokenize = "porter unicode61") diff --git a/sources/gmnifaq.git/tree/search.pl.txt b/sources/gmnifaq.git/tree/search.pl.txt @@ -1,75 +0,0 @@ -#!/usr/bin/perl -# Copyright René Wagner 2020 -# licenced under BSD 3-Clause licence -# https://src.clttr.info/rwa/gmnifaq - - -use strict; -use DBI; -use URI::Escape; -use lib 'lib/'; -use gmnifaq; - -config(); - -# enable UTF-8 mode for everything -use utf8; -binmode STDOUT, ':utf8'; -binmode STDERR, ':utf8'; - -if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); -} - -my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); -if ($query eq '' || $query !~ /^[0-9a-z\s]*$/i) -{ - write_response('INPUT', 'Search faqs for the following terms (separate by blank, all terms must match)', undef); -} - -my @body = (); -push @body, header(); -push @body, search($query); -push @body, footer(); - -write_response('SUCCESS', 'text/gemini', @body); - -exit; - -sub search -{ - my ($term) = @_; - my @result = (); - - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - my $fts_search = join(' AND ', split(/ /, $term)); - my @matchingtags = $dbh->selectall_array("SELECT DISTINCT t.*, count(tq.q_id) AS count FROM fts_data INNER JOIN tags t ON t.id = fts_data.t_id LEFT JOIN tags_questions tq ON tq.t_id = t.id WHERE fts_data MATCH '$fts_search' GROUP BY t.id ORDER BY rank"); - my @matchingfaqs = $dbh->selectall_array("SELECT DISTINCT q_id, q.question FROM fts_data INNER JOIN questions q ON q.id = q_id WHERE fts_data MATCH '$fts_search' ORDER BY rank;"); - $dbh->disconnect(); - - if ( !scalar @matchingtags && !scalar @matchingfaqs) { - push @result, ('Sorry, we can\'t find an entry that matches your search data.', ''); - } - - if ( scalar @matchingtags) { - push @result, ('## matching tags', ''); - foreach (@matchingtags) { - push @result, sprintf("=> ./faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); - } - push @result, ''; - } - - if ( scalar @matchingfaqs) { - push @result, ('## matching FAQs', ''); - foreach (@matchingfaqs) { - push @result, sprintf("=> ./faqs.pl?faq=%d %s", @$_[0], @$_[1]); - } - } - push @result, ('', '=> ./search.pl search again'); - return @result; -} - -sub header -{ - return ('# '. $CONF{'name'}, '', "Matching results for your search term '$query':", ''); -} diff --git a/sources/gmnifaq.git/tree/tags.pl.txt b/sources/gmnifaq.git/tree/tags.pl.txt @@ -1,55 +0,0 @@ -#!/usr/bin/perl -# Copyright René Wagner 2020 -# licenced under BSD 3-Clause licence -# https://src.clttr.info/rwa/gmnifaq - -use strict; -use DBI; -use URI::Escape; -use lib 'lib/'; -use gmnifaq; - -config(); - -# enable UTF-8 mode for everything -use utf8; -binmode STDOUT, ':utf8'; -binmode STDERR, ':utf8'; - -if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); -} - -my @body = (); -push @body, header(); -push @body, tags(); -push @body, footer(); - -write_response('SUCCESS', 'text/gemini', @body); - -exit; - -sub tags -{ - my $dbh = DBI->connect($CONF{'dsn'}, '', '', { RaiseError => 1 }) or die $DBI::errstr; - - my @result; - my @rows = $dbh->selectall_array('SELECT id, name, count(t_id) FROM tags t LEFT JOIN tags_questions tq ON tq.t_id = t.id GROUP BY t_id'); - $dbh->disconnect(); - - if ( !scalar @rows ) { - push @result, 'No tags found!'; - } - else { - foreach (@rows) { - push @result, sprintf("=> ./faqs.pl?tag=%d %s (%d entrys)", @$_[0], @$_[1], @$_[2]); - } - } - push @result, ''; - return @result; -} - -sub header -{ - return ('# '. $CONF{'name'}, '', 'Select a tag to browse the questions associated with this tag.', '', '## Tags', ''); -} diff --git a/sources/orrg.git/commits/01370e679836f2a4a9a78b9e08ff08b5d9469329.patch b/sources/orrg.git/commits/01370e679836f2a4a9a78b9e08ff08b5d9469329.patch @@ -1,28 +0,0 @@ -diff --git a/README.md b/README.md -index 01c987e596a8b37c5f8adee405339658948bbdeb..5604153c98447581d038e8ebc530b67351c1a277 100644 ---- a/README.md -+++ b/README.md -@@ -8,9 +8,13 @@ - Visit the [demo](gemini://gmndemo.clttr.info/orrg/orrg.pl?https:%2F%2Fgit.sr.ht%2F~rwa%2Forrg%2Flog%2Frss.xml) ([through http proxy](https://portal.mozz.us/gemini/gmndemo.clttr.info/orrg.pl%3Fhttps:%252F%252Fgit.sr.ht%252F~rwa%252Forrg%252Flog%252Frss.xml)) that shows the commit log of this repo. - The demo might break regularly as i use it for integration testing during development. - -+### contact -+ -+Preferably over the [mailing list](https://lists.sr.ht/~rwa/gmni-perl-cgi). -+ - ## features - --- load an atom/rss feed from http(s) given by user input -+- load an atom/rss feed from https (http is not supported!) given by user input - - render feed (channel info & entrys) as a gemini site - - include links to originating site and every article - - strip html tags from item description -@@ -22,7 +26,7 @@ ## non-features - - *orrg* will never be a full-fletched feed aggregator with archiving and searching capabilities. - --Given this restrictions is not suitable for highly traffic feeds which are updated very often. But it should work quite well -+Given this restrictions is not suitable for highly traffic feeds which are updated very often. But it should work quite well for slow paced feeds of blogs and so on. - - # installation - diff --git a/sources/orrg.git/commits/01d0eb250880b32d366fbfa861c1e452094f6a0a.patch b/sources/orrg.git/commits/01d0eb250880b32d366fbfa861c1e452094f6a0a.patch @@ -1,30 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 98f881144d82da535611babc888eae9ed458c221..1be767a230394ae95b8eb91d07b61b192d2feb67 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -74,20 +74,22 @@ push @body, ('## recent feed items', ''); - - my $hs = HTML::Strip->new(emit_spaces => 0, auto_reset => 1); - foreach my $it ($feed->get_item()) { -- push @body, '### '. $it->title; -+ push @body, $it->description ne '' ? '### '. $it->title : $it->title; - if ($it->pubDate ne '') { - my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); - push @body, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); - } -- push @body, ''; - if ($it->description ne '') { -+ push @body, ''; - my $desc = $it->description; -+ chomp $desc; - $desc =~ s/\<li\>/* /ig; -+ $desc =~ s/\<br \/\>/\r\n/ig; - push @body, $hs->parse($desc); - } - $it->link eq '' or push @body, ('=> '.$it->link.' open entry in browser', ''); - } -- -+ - return @body; - } - diff --git a/sources/orrg.git/commits/02a05701a382e226b83d63bdad35b0a01209beec.patch b/sources/orrg.git/commits/02a05701a382e226b83d63bdad35b0a01209beec.patch @@ -1,185 +0,0 @@ -diff --git a/README.md b/README.md -index d56288ac5f5e3b0666701ae469af8a26d70a3a91..01c987e596a8b37c5f8adee405339658948bbdeb 100644 ---- a/README.md -+++ b/README.md -@@ -3,7 +3,9 @@ - *orrg* is a cgi script for [gemini](gemini://gemini.circumlunar.space) servers. - It delivers an easy way to consume atom or rss feeds shipped over http(s) within gemini. - --Visit the [demo](gemini://gmndemo.clttr.info/orrg.pl?https:%2F%2Fgit.sr.ht%2F~rwa%2Forrg%2Flog%2Frss.xml) ([through http proxy](https://portal.mozz.us/gemini/gmndemo.clttr.info/orrg.pl%3Fhttps:%252F%252Fgit.sr.ht%252F~rwa%252Forrg%252Flog%252Frss.xml)) that shows the commit log of this repo. -+Lists of popular and recently visited feeds help you discover new things. -+ -+Visit the [demo](gemini://gmndemo.clttr.info/orrg/orrg.pl?https:%2F%2Fgit.sr.ht%2F~rwa%2Forrg%2Flog%2Frss.xml) ([through http proxy](https://portal.mozz.us/gemini/gmndemo.clttr.info/orrg.pl%3Fhttps:%252F%252Fgit.sr.ht%252F~rwa%252Forrg%252Flog%252Frss.xml)) that shows the commit log of this repo. - The demo might break regularly as i use it for integration testing during development. - - ## features -@@ -12,12 +14,15 @@ - load an atom/rss feed from http(s) given by user input - - render feed (channel info & entrys) as a gemini site - - include links to originating site and every article - - strip html tags from item description -+- lists of popular and recently visited feeds - - Fetching feeds from gemini is currently not supported -> https://todo.sr.ht/~rwa/gmni-perl/4 - - ## non-features - --*orrg* is more of a PoC and will never be a full-fletched feed aggregator with archiving and searching capabilities. -+*orrg* will never be a full-fletched feed aggregator with archiving and searching capabilities. -+ -+Given this restrictions is not suitable for highly traffic feeds which are updated very often. But it should work quite well - - # installation - -diff --git a/index.pl b/index.pl -new file mode 100755 -index 0000000000000000000000000000000000000000..14fe354925780511523d735cbd8e16051053ea00 ---- /dev/null -+++ b/index.pl -@@ -0,0 +1,27 @@ -+#!/usr/bin/perl -+# Copyright René Wagner 2020 -+# licenced under BSD 3-Clause licence -+# https://git.sr.ht/~rwa/orrg -+ -+use strict; -+use DBI; -+use lib 'lib/'; -+use orrg; -+ -+# enable UTF-8 mode for everything -+use utf8; -+binmode STDOUT, ':utf8'; -+binmode STDERR, ':utf8'; -+ -+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { -+ write_response('CGI_ERROR', 'CGI execution error', undef); -+} -+ -+write_response('SUCCESS', 'text/gemini', body()); -+ -+exit; -+ -+sub body -+{ -+ return ('# Welcome to orrg', '', 'orrg is your online rss/atom feed reader for gemini.', '', '=> orrg.pl view a feed', '', '## explore', '', '=> recent.pl top 10 recent feeds', '=> popular.pl top 10 popular feeds', '', '', '=> https://git.sr.ht/~rwa/orrg powered by orrg'); -+} -diff --git a/lib/orrg.pm b/lib/orrg.pm -new file mode 100755 -index 0000000000000000000000000000000000000000..e4ca572f0b36c8b0eb3725c8f1f7f11dc0e3de4a ---- /dev/null -+++ b/lib/orrg.pm -@@ -0,0 +1,53 @@ -+#!/usr/bin/perl -+# Copyright René Wagner 2020 -+# licenced under BSD 3-Clause licence -+# https://git.sr.ht/~rwa/orrg -+ -+package orrg; -+use strict; -+use Exporter; -+our @ISA = qw(Exporter); -+our @EXPORT = qw(write_response %RC); # automatically exported subs -+ -+# enable UTF-8 mode for everything -+use utf8; -+binmode STDOUT, ':utf8'; -+binmode STDERR, ':utf8'; -+ -+# define return codes -+our %RC = ( -+ 'INPUT', 10, -+ 'SENSITIVE_INPUT', 11, -+ 'SUCCESS', 20, -+ 'TEMPORARY_REDIRECT', 30, -+ 'PERMANENT_REDIRECT', 31, -+ 'TEMPORARY_FAILURE', 40, -+ 'SERVER_UNAVAILABLE', 41, -+ 'CGI_ERROR', 42, -+ 'PROXY_ERROR', 43, -+ 'SLOW_DOWN', 44, -+ 'PERMANENT_FAILURE', 50, -+ 'NOT_FOUND', 51, -+ 'GONE', 52, -+ 'PROXY_REQUEST_REFUSE', 53, -+ 'BAD_REQUEST', 59, -+ 'CLIENT_CERT_REQUIRED', 60, -+ 'CERT_NOT_AUTHORISED', 61, -+ 'CERT_NOT_VALID', 62 -+); -+ -+sub write_response -+{ -+ my ($returncode, $meta, @content) = @_; -+ -+ if (!defined($RC{$returncode})) { die "Unknown response code!"; } -+ -+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -+ foreach (@content) { -+ print("$_\r\n"); -+ } -+ -+ exit; -+} -+ -+1; -diff --git a/orrg.pl b/orrg.pl -index 35458b2a68e7db614afe7cda48d6673840e23b57..5cf31e2c5c5aaa1270cbeac8e19af7f7a44c0544 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -11,33 +11,13 @@ use HTML::Strip; - use DateTime; - use DateTime::Format::ISO8601; - use POSIX qw(strftime); -+use lib 'lib/'; -+use orrg; - - # enable UTF-8 mode for everything - use utf8; - binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; -- --# define return codes --our %RC = ( -- 'INPUT', 10, -- 'SENSITIVE_INPUT', 11, -- 'SUCCESS', 20, -- 'TEMPORARY_REDIRECT', 30, -- 'PERMANENT_REDIRECT', 31, -- 'TEMPORARY_FAILURE', 40, -- 'SERVER_UNAVAILABLE', 41, -- 'CGI_ERROR', 42, -- 'PROXY_ERROR', 43, -- 'SLOW_DOWN', 44, -- 'PERMANENT_FAILURE', 50, -- 'NOT_FOUND', 51, -- 'GONE', 52, -- 'PROXY_REQUEST_REFUSE', 53, -- 'BAD_REQUEST', 59, -- 'CLIENT_CERT_REQUIRED', 60, -- 'CERT_NOT_AUTHORISED', 61, -- 'CERT_NOT_VALID', 62 -- ); - - if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') - { -@@ -90,19 +70,6 @@ } - $it->link eq '' or push @body, ('=> '.$it->link.' open entry in browser', ''); - } - -+ push @body, ('', '', '=> index.pl [home]'); - return @body; - } -- --sub write_response --{ -- my ($returncode, $meta, @content) = @_; -- -- if (!defined($RC{$returncode})) { die "Unknown response code!"; } -- -- printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -- foreach (@content) { -- print("$_\r\n"); -- } -- -- exit; --} diff --git a/sources/orrg.git/commits/08a67a7b51c33a348137b4e6b5dc2366e9f90e47.patch b/sources/orrg.git/commits/08a67a7b51c33a348137b4e6b5dc2366e9f90e47.patch @@ -1,15 +0,0 @@ -diff --git a/index.pl b/index.pl -index f3142c0154a16dabce397b1ed3b00efd30fb1a7e..69fcc276e3071f9f749f8a65d5a2eb4c8b3eaa08 100755 ---- a/index.pl -+++ b/index.pl -@@ -17,6 +17,10 @@ if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); - } - -+if ($ENV{'QUERY_STRING'} ne '' || $ENV{'PATH_INFO'} ne '') { -+ write_response('NOT_FOUND', 'File not found', undef); -+} -+ - write_response('SUCCESS', 'text/gemini', body()); - - exit; diff --git a/sources/orrg.git/commits/0a7ca079581bd01b63d6769cc34d2d6d40429b4c.patch b/sources/orrg.git/commits/0a7ca079581bd01b63d6769cc34d2d6d40429b4c.patch @@ -1,22 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 195999cd7d45aff0feedcc135c80336e65a7b4b6..88e42e7b37eb0985b70a27dd6d4dee731924485b 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -4,7 +4,6 @@ # licenced under BSD 3-Clause licence - # https://git.sr.ht/~rwa/orrg - - use strict; --no warnings 'experimental'; - use URI::Escape; - use XML::FeedPP; - use HTML::Strip; -@@ -47,7 +46,8 @@ } - - push @body, '# '. $feed->title; - push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); -- $feed->description eq '' or push @body, ('', $feed->description); -+ push @body, ''; -+ $feed->description eq '' or push @body, $feed->description; - $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; - $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); - diff --git a/sources/orrg.git/commits/17fb028c7177296e19bfc4c5ae18b04b5e82a27d.patch b/sources/orrg.git/commits/17fb028c7177296e19bfc4c5ae18b04b5e82a27d.patch @@ -1,20 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 71a558897c02ce80027afedcfd402460d3cf0041..7d4a9bf834d306ca23fa7c023542f5454a899efc 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -69,13 +69,13 @@ $title =~ s/^\s+//ig; - $title =~ s/\s+$//ig; - $link =~ s/^\s+//ig; - $link =~ s/\s+$//ig; -- push @item, ($it->description ne '' || $it->pubDate ne '')? '## '. $title : $title; -+ push @item, (($it->description ne '' && $it->description ne $it->title) || $it->pubDate ne '')? '## '. $title : $title; - if ($it->pubDate ne '') { - my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); - push @item, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); - push @item, ''; - } -- if ($it->description ne '') { -+ if ($it->description ne '' && $it->description ne $it->title) { - my $desc = $it->description; - $desc =~ s/\<li\>/* /igm; - $desc =~ s/\<h[1-2][^\>]+\>/### /igm; diff --git a/sources/orrg.git/commits/226986013afd88a8b39ef37d5b6f85b63107d1ee.patch b/sources/orrg.git/commits/226986013afd88a8b39ef37d5b6f85b63107d1ee.patch @@ -1,28 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 40099ebb8c8bc10dca3d71e19b2042b24597e923..d5484c5d9e43bef1ef6b4836a2f24421b24b3a0d 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -64,7 +64,13 @@ my ($it) = @_; - - my $hs = HTML::Strip->new(emit_spaces => 0, auto_reset => 1); - my @item = (); -- push @item, ($it->description ne '' || $it->pubDate ne '')? '### '. $it->title : $it->title; -+ my $title = $it->title; -+ my $link = $it->link; -+ $title =~ s/^\s+//ig; -+ $title =~ s/\s+$//ig; -+ $link =~ s/^\s+//ig; -+ $link =~ s/\s+$//ig; -+ push @item, ($it->description ne '' || $it->pubDate ne '')? '### '. $title : $title; - if ($it->pubDate ne '') { - my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); - push @item, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); -@@ -78,7 +84,7 @@ $desc =~ s/\<br \/\>/\r\n/igm; - $desc =~ s/^\s+\*/*/igm; - push @item, $hs->parse($desc); - } -- $it->link eq '' or push @item, ('=> '.$it->link.' open entry in browser', ''); -+ $link eq '' or push @item, ('=> '.$link.' open entry in browser', ''); - - return \@item; - } diff --git a/sources/orrg.git/commits/2531331a6c1f20a3585c146ea73bce18bdba596c.patch b/sources/orrg.git/commits/2531331a6c1f20a3585c146ea73bce18bdba596c.patch @@ -1,42 +0,0 @@ -diff --git a/README.md b/README.md -index 0cb8d9e9e84d9126815c779aed2fbbfef945c681..663c3131b819a57c5ca8550de1bd628217c0353c 100644 ---- a/README.md -+++ b/README.md -@@ -1,14 +1,17 @@ - # orrg (online rss feed reader for gemini) - - *orrg* is a cgi script for [gemini](gemini://gemini.circumlunar.space) servers. --It fetches an arbitrary rss feed given by the user and renders its content as a gemini site. -+It fetches an arbitrary rss/atom feed given by the user and renders its content as a gemini site. - - Visit the [demo](gemini://gmndemo.clttr.info/orrg.pl). - - ## features - --Load an arbitrary atom/rss feed. -- -+- load an atom/rss feed given by user input -+- render feed (channel & item info) as a gemini site -+ -+Fetching feeds from gemini is currently not supported -> https://todo.sr.ht/~rwa/gmni-perl/4 -+ - # installation - - - setup your geminiserver with cgi enabled -@@ -21,4 +24,3 @@ - gemini server with cgi enabled (like gmnisrv or stargazer) - - Perl >= 5.28 with modules - - URI::Escape - - XML::FeedPP -- - LWP::UserAgent -diff --git a/orrg.pl b/orrg.pl -index a8dc3b6536d8e25b40c53f0503b77fa8f1868d7f..b024597cb0c2a3f5af1ce50e5ac227d9010595aa 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -63,6 +63,7 @@ return @body; - } - - push @body, '# '. $feed->title; -+ push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); - push @body, ('', $feed->description); - push @body, ('=> '.$feed->link.' open website', ''); - push @body, ('## recent feed items', ''); diff --git a/sources/orrg.git/commits/33875dd1e6936d33e97dfaca4c2f82c92558034a.patch b/sources/orrg.git/commits/33875dd1e6936d33e97dfaca4c2f82c92558034a.patch @@ -1,36 +0,0 @@ -diff --git a/README.md b/README.md -index b279a59f40cf1e2111bc0add9c4e6af9a0f2cef6..d56288ac5f5e3b0666701ae469af8a26d70a3a91 100644 ---- a/README.md -+++ b/README.md -@@ -11,6 +11,7 @@ - - load an atom/rss feed from http(s) given by user input - - render feed (channel info & entrys) as a gemini site - - include links to originating site and every article -+ - strip html tags from item description - - Fetching feeds from gemini is currently not supported -> https://todo.sr.ht/~rwa/gmni-perl/4 - -diff --git a/orrg.pl b/orrg.pl -index d21f98206246374db29b773d525395ee92057928..98f881144d82da535611babc888eae9ed458c221 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -72,6 +72,7 @@ $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; - $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); - push @body, ('## recent feed items', ''); - -+ my $hs = HTML::Strip->new(emit_spaces => 0, auto_reset => 1); - foreach my $it ($feed->get_item()) { - push @body, '### '. $it->title; - if ($it->pubDate ne '') { -@@ -80,8 +81,9 @@ push @body, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); - } - push @body, ''; - if ($it->description ne '') { -- my $hs = HTML::Strip->new(); -- push @body, $hs->parse($it->description); -+ my $desc = $it->description; -+ $desc =~ s/\<li\>/* /ig; -+ push @body, $hs->parse($desc); - } - $it->link eq '' or push @body, ('=> '.$it->link.' open entry in browser', ''); - } diff --git a/sources/orrg.git/commits/373ae43bf2d6f2e6ca2a31beccda552122cd987f.patch b/sources/orrg.git/commits/373ae43bf2d6f2e6ca2a31beccda552122cd987f.patch @@ -1,149 +0,0 @@ -diff --git a/lib/orrg.pm b/lib/orrg.pm -index 88575dd44ddce8718626f6c0c78be335616d11e7..c741eccf9fc87961dcfcb3f8f68276f2ddcd8edd 100755 ---- a/lib/orrg.pm -+++ b/lib/orrg.pm -@@ -7,7 +7,7 @@ package orrg; - use strict; - use Exporter; - our @ISA = qw(Exporter); --our @EXPORT = qw(recent_get recent_add write_response %RC); # automatically exported subs -+our @EXPORT = qw(popular_get popular_add recent_get recent_add write_response %RC); # automatically exported subs - - # enable UTF-8 mode for everything - use utf8; -@@ -15,6 +15,7 @@ binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; - - my $recentfile = 'data/recent.txt'; -+my $popularfile = 'data/popular.txt'; - - # define return codes - our %RC = ( -@@ -44,12 +45,12 @@ (-f $recentfile) or return undef; - - my @recents = (); - open INFILE, $recentfile; -- flock OUTFILE, 1; -+ flock INFILE, 1; - while (<INFILE>) { - chomp($_); - push @recents, $_; - } -- flock OUTFILE, 8; -+ flock INFILE, 8; - close INFILE; - - return \@recents; -@@ -73,6 +74,45 @@ $c++; - } - ($c < 10) or last; - } -+ flock OUTFILE, 8; -+ close OUTFILE; -+} -+ -+sub popular_get -+{ -+ (-f $popularfile) or return undef; -+ -+ my @populars = (); -+ open INFILE, $popularfile; -+ flock INFILE, 1; -+ while (<INFILE>) { -+ chomp($_); -+ push @populars, $_; -+ } -+ flock INFILE, 8; -+ close INFILE; -+ -+ return \@populars; -+} -+ -+sub popular_add -+{ -+ my ( $uri, $name ) = @_; -+ my $populars = popular_get(); -+ -+ open OUTFILE, '>', $popularfile; -+ flock OUTFILE, 1; -+ -+ my $found = 0; -+ foreach (@$populars) { -+ my ($cnt, $popuri, $popname) = split / /, $_, 3; -+ if ($uri eq $popuri) { -+ $cnt++; -+ $found = 1; -+ } -+ print OUTFILE "$cnt $popuri $popname\n"; -+ } -+ $found or print OUTFILE "1 $uri $name"; - flock OUTFILE, 8; - close OUTFILE; - } -diff --git a/orrg.pl b/orrg.pl -index e9e258649242d929350b77364a977eded0563e1b..094ede1d23d98a0ade808511eeac111d4ed09095 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -47,6 +47,7 @@ return @body; - } - - recent_add($qs, $feed->title); -+ popular_add($qs, $feed->title); - push @body, '# '. $feed->title; - push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); - $feed->description eq '' or push @body, ('', $feed->description); -diff --git a/popular.pl b/popular.pl -new file mode 100755 -index 0000000000000000000000000000000000000000..a681cc7be286964e80a95af47406ab762231ae56 ---- /dev/null -+++ b/popular.pl -@@ -0,0 +1,49 @@ -+#!/usr/bin/perl -+# Copyright René Wagner 2020 -+# licenced under BSD 3-Clause licence -+# https://git.sr.ht/~rwa/orrg -+ -+use strict; -+no warnings 'experimental'; -+use URI::Escape; -+use lib 'lib/'; -+use orrg; -+ -+# enable UTF-8 mode for everything -+use utf8; -+binmode STDOUT, ':utf8'; -+binmode STDERR, ':utf8'; -+ -+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') -+{ -+ write_response('CGI_ERROR', '', undef); -+} -+ -+write_response('SUCCESS', 'text/gemini', create_response()); -+ -+exit; -+ -+sub create_response -+{ -+ my @body = (); -+ -+ push @body, ('# most popular feeds', ''); -+ -+ my $populars = popular_get(); -+ if ( defined($populars) ) { -+ my @sorted = sort { $a <=> $b } @$populars; -+ my $c = 0; -+ foreach (reverse @sorted) { -+ my ($cnt, $uri, $name) = split / /, $_, 3; -+ push @body, '=> orrg.pl?'. uri_escape($uri) .' '. $name; -+ -+ ++$c < 10 or last; -+ } -+ } else { -+ push @body, 'No feeds found'; -+ } -+ -+ -+ push @body, ('', '', '=> index.pl [home]'); -+ return @body; -+} diff --git a/sources/orrg.git/commits/3a8b1c9b9529a20915c21c16c2004e5dbde83893.patch b/sources/orrg.git/commits/3a8b1c9b9529a20915c21c16c2004e5dbde83893.patch @@ -1,52 +0,0 @@ -diff --git a/lib/orrg.pm b/lib/orrg.pm -index c741eccf9fc87961dcfcb3f8f68276f2ddcd8edd..07b0be1940e3dbc60540ce49eba956d7b1a4cfeb 100755 ---- a/lib/orrg.pm -+++ b/lib/orrg.pm -@@ -44,7 +44,7 @@ { - (-f $recentfile) or return undef; - - my @recents = (); -- open INFILE, $recentfile; -+ open INFILE, '< :encoding(UTF-8)', $recentfile; - flock INFILE, 1; - while (<INFILE>) { - chomp($_); -@@ -62,7 +62,7 @@ my ( $uri, $name ) = @_; - my $recent = recent_get(); - - my $newline = "$uri $name"; -- open OUTFILE, '>', $recentfile; -+ open OUTFILE, '> :encoding(UTF-8)', $recentfile; - flock OUTFILE, 1; - print OUTFILE "$newline\n"; - -@@ -83,7 +83,7 @@ { - (-f $popularfile) or return undef; - - my @populars = (); -- open INFILE, $popularfile; -+ open INFILE, '< :encoding(UTF-8)', $popularfile; - flock INFILE, 1; - while (<INFILE>) { - chomp($_); -@@ -100,7 +100,7 @@ { - my ( $uri, $name ) = @_; - my $populars = popular_get(); - -- open OUTFILE, '>', $popularfile; -+ open OUTFILE, '> :encoding(UTF-8)', $popularfile; - flock OUTFILE, 1; - - my $found = 0; -diff --git a/orrg.pl b/orrg.pl -index eb3842dcbcca748d184b3664608c8f58991e197b..7e26b51dc7c7ec270324b43d2517919ad3eb0ed1 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -85,7 +85,6 @@ $desc =~ s/^\r\n$//igm; - $desc =~ s/^\s+\*/*/igm; - $desc =~ s/\s+$//igm; - $desc =~ s/^\s+//igm; -- chomp $desc; - push @item, $desc; - } - $link eq '' or push @item, ('=> '.$link.' open entry in browser', ''); diff --git a/sources/orrg.git/commits/4454cc3f1b97506f166c883145dc8773de99b4be.patch b/sources/orrg.git/commits/4454cc3f1b97506f166c883145dc8773de99b4be.patch @@ -1,83 +0,0 @@ -diff --git a/index.pl b/index.pl -index 288ba6f083bc9081f67fc528e27c415f733ff6da..f3142c0154a16dabce397b1ed3b00efd30fb1a7e 100755 ---- a/index.pl -+++ b/index.pl -@@ -34,13 +34,13 @@ '# Welcome to orrg', - '', - 'orrg is your online rss/atom feed reader for gemini.', - '', -- '=> orrg.pl view a feed', -+ '=> ./orrg.pl view a feed', - '', - '## explore', - '', -- '=> orrg.pl?'. uri_escape($uri) .' random feed', -- '=> recent.pl top 10 recent feeds', -- '=> popular.pl top 10 popular feeds', -+ '=> ./orrg.pl?'. uri_escape($uri) .' random feed', -+ '=> ./recent.pl top 10 recent feeds', -+ '=> ./popular.pl top 10 popular feeds', - '', - '', - '=> https://src.clttr.info/rwa/orrg powered by orrg'); -diff --git a/orrg.pl b/orrg.pl -index 7be8abe2cfe7d1dec3465f6753131f81e565c605..de98956f87a350bc1cd6a85b1a46fa1c1585b325 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -53,7 +53,7 @@ $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); - - foreach my $it ($feed->get_item()) { push @body, @{item($it)}; } - -- push @body, ('', '', '=> index.pl [home]'); -+ push @body, ('', '', '=> ./index.pl [home]'); - return @body; - } - -diff --git a/popular.pl b/popular.pl -index 050b7a3f0dde95296cebc9d444f19713881c695e..28aff6fe2ba9c103b86c1cc544e2edcd9c743355 100755 ---- a/popular.pl -+++ b/popular.pl -@@ -35,7 +35,7 @@ my @sorted = sort { $a <=> $b } @$populars; - my $c = 0; - foreach (reverse @sorted) { - my ($cnt, $uri, $name) = split / /, $_, 3; -- push @body, '=> orrg.pl?'. uri_escape($uri) .' '. ($name eq '' ? $uri : $name); -+ push @body, '=> ./orrg.pl?'. uri_escape($uri) .' '. ($name eq '' ? $uri : $name); - - ++$c < 10 or last; - } -@@ -44,6 +44,6 @@ push @body, 'No feeds found'; - } - - -- push @body, ('', '', '=> index.pl [home]'); -+ push @body, ('', '', '=> ./index.pl [home]'); - return @body; - } -diff --git a/recent.pl b/recent.pl -index 6369944cfeedc499759381ed5d7ece53a517f023..4d92a3416247da27a694d2e8c8119c98783e114c 100755 ---- a/recent.pl -+++ b/recent.pl -@@ -33,12 +33,12 @@ my $recents = recent_get(); - if ( defined($recents) ) { - foreach (@$recents) { - my ($uri, $name) = split / /, $_, 2; -- push @body, '=> orrg.pl?'. uri_escape($uri) .' '. ($name eq '' ? $uri : $name); -+ push @body, '=> ./orrg.pl?'. uri_escape($uri) .' '. ($name eq '' ? $uri : $name); - } - } else { - push @body, 'No feeds found'; - } - -- push @body, ('', '', '=> index.pl [home]'); -+ push @body, ('', '', '=> ./index.pl [home]'); - return @body; - } -diff --git a/robots.txt b/robots.txt -index 46c4570b0be7c94113f577aeb128e415d90e7ddd..8f745ab9886af7dc66411c7215eb86076b996246 100644 ---- a/robots.txt -+++ b/robots.txt -@@ -1 +1,2 @@ --Disallow:orrg.pl -+User-agent: * -+Disallow: /orrg.pl diff --git a/sources/orrg.git/commits/44e27d26947e61b9f2731ec9620ce4852a0b683c.patch b/sources/orrg.git/commits/44e27d26947e61b9f2731ec9620ce4852a0b683c.patch @@ -1,11 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index ae60e760ef82a5a906b0e9aedf95b9bac71e0d7d..40099ebb8c8bc10dca3d71e19b2042b24597e923 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -98,5 +98,6 @@ } - - recent_add($query, $feed->title); - popular_add($query, $feed->title); -+ $feed->sort_item(); - return $feed; - } diff --git a/sources/orrg.git/commits/589d4a8d337c759535a8d7eee270f25667aaec18.patch b/sources/orrg.git/commits/589d4a8d337c759535a8d7eee270f25667aaec18.patch @@ -1,123 +0,0 @@ -diff --git a/README.md b/README.md -index c05170514e82f3774d2a681ff1aa145d965cdb60..dd022342309f2c5c1f78bdbac25ecc697efc4e2b 100644 ---- a/README.md -+++ b/README.md -@@ -1,16 +1,11 @@ - # orrg (online rss feed reader for gemini) - -+public instance: [gemini://orrg.clttr.info] -+ - *orrg* is a cgi script for [gemini](gemini://gemini.circumlunar.space) servers. - It delivers an easy way to consume atom or rss feeds shipped over http(s) within gemini. - --Lists of popular and recently visited feeds help you discover new things. -- --Visit the [demo](gemini://gmndemo.clttr.info/orrg/orrg.pl?https:%2F%2Fgit.sr.ht%2F~rwa%2Forrg%2Flog%2Frss.xml) ([through http proxy](https://portal.mozz.us/gemini/gmndemo.clttr.info/orrg.pl%3Fhttps:%252F%252Fgit.sr.ht%252F~rwa%252Forrg%252Flog%252Frss.xml)) that shows the commit log of this repo. --The demo might break regularly as i use it for integration testing during development. -- --### contact -- --Preferably over the [mailing list](https://lists.sr.ht/~rwa/gmni-perl-cgi). -+Lists of popular and recently visited feeds as well as viewing a random feed help you discover new things. - - ## features - -@@ -19,14 +14,15 @@ - render feed (channel info & entrys) as a gemini site - - include links to originating site and every article - - strip html tags from item description - - lists of popular and recently visited feeds -+- random feed selector - --gemini-support is currently implemented using [gcat](https://github.com/aaronjanse/gcat) till popular perl libs have catched up. :) -+gemini-protocol-support is currently implemented using [gcat](https://github.com/aaronjanse/gcat) till popular perl libs have catched up. :) - - ## non-features - - *orrg* will never be a full-fletched feed aggregator with archiving and searching capabilities. - --Given this restrictions is not suitable for highly traffic feeds which are updated very often. But it should work quite well for slow paced feeds of blogs and so on. -+Given this restrictions is not suitable for high traffic feeds which are updated very often. But it should work quite well for slow paced feeds of blogs and so on. - - # installation - -diff --git a/index.pl b/index.pl -index 9e5dd6905f2f528e1505923ab1b542275dd3a6b4..288ba6f083bc9081f67fc528e27c415f733ff6da 100755 ---- a/index.pl -+++ b/index.pl -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/orrg -+# https://src.clttr.info/rwa/orrg - - use strict; - use URI::Escape; -@@ -43,5 +43,5 @@ '=> recent.pl top 10 recent feeds', - '=> popular.pl top 10 popular feeds', - '', - '', -- '=> https://git.sr.ht/~rwa/orrg powered by orrg'); -+ '=> https://src.clttr.info/rwa/orrg powered by orrg'); - } -diff --git a/lib/orrg.pm b/lib/orrg.pm -index 07b0be1940e3dbc60540ce49eba956d7b1a4cfeb..314fa688d8804a5863a9e8c06a1b2b9637e430cc 100755 ---- a/lib/orrg.pm -+++ b/lib/orrg.pm -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/orrg -+# https://src.clttr.info/rwa/orrg - - package orrg; - use strict; -diff --git a/orrg.pl b/orrg.pl -index 6cc6843b64bc39ddcad803b98702e67ac271c489..7be8abe2cfe7d1dec3465f6753131f81e565c605 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/orrg -+# https://src.clttr.info/rwa/orrg - - use strict; - use URI::Escape; -@@ -50,7 +50,7 @@ push @body, ''; - $feed->description eq '' or push @body, trim_ws($feed->description); - $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; - $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); -- -+ - foreach my $it ($feed->get_item()) { push @body, @{item($it)}; } - - push @body, ('', '', '=> index.pl [home]'); -diff --git a/popular.pl b/popular.pl -index 3dcc60e9a004d6996ab87552ce7be532039e3aca..050b7a3f0dde95296cebc9d444f19713881c695e 100755 ---- a/popular.pl -+++ b/popular.pl -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/orrg -+# https://src.clttr.info/rwa/orrg - - use strict; - no warnings 'experimental'; -diff --git a/recent.pl b/recent.pl -index e9522b7862e2dab631acaff80115c70f30716aec..6369944cfeedc499759381ed5d7ece53a517f023 100755 ---- a/recent.pl -+++ b/recent.pl -@@ -1,7 +1,7 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/orrg -+# https://src.clttr.info/rwa/orrgg - - use strict; - no warnings 'experimental'; diff --git a/sources/orrg.git/commits/5a95a2973524305024fb2bd775b447737073d1d5.patch b/sources/orrg.git/commits/5a95a2973524305024fb2bd775b447737073d1d5.patch @@ -1,85 +0,0 @@ -diff --git a/README.md b/README.md -index dd022342309f2c5c1f78bdbac25ecc697efc4e2b..196022b8d4a7e9650818b4a67920c75feb357e7d 100644 ---- a/README.md -+++ b/README.md -@@ -40,3 +40,4 @@ - DateTime - - DateTime::Format::ISO8601 - - HTML::Strip - - Python 3 for `gcat` -+- wget -diff --git a/orrg.pl b/orrg.pl -index de98956f87a350bc1cd6a85b1a46fa1c1585b325..8da5f714c55956f5e00bbf5cd8275a873249d2ae 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -38,21 +38,28 @@ { - my ( $qs ) = @_; - my @body = (); - -- my $feed = feed_get($qs); -- if ( !defined($feed) ) { -+ my $content = feed_get($qs); -+ if ( !defined($content) || $content eq '' ) { - push @body, ('# orrg error', '', 'The requested feed could not be loaded. :(', '', '=> '. $qs .' open feed in browser'); -- return @body; -- } -- -- push @body, '# '. trim_ws($feed->title); -- push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); -- push @body, ''; -- $feed->description eq '' or push @body, trim_ws($feed->description); -- $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; -- $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); -+ } else { -+ my $feed = feed_parse($content); -+ -+ if (defined($feed)) { -+ recent_add($qs, trim_ws($feed->title)); -+ popular_add($qs, trim_ws($feed->title)); - -- foreach my $it ($feed->get_item()) { push @body, @{item($it)}; } -+ push @body, '# '. trim_ws($feed->title); -+ push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); -+ push @body, ''; -+ $feed->description eq '' or push @body, trim_ws($feed->description); -+ $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; -+ $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); - -+ foreach my $it ($feed->get_item()) { push @body, @{item($it)}; } -+ } else { -+ push @body, ('# orrg error', '', 'The requested feed could be loaded but not parsed. :(', '', '=> '. $qs .' open feed in native client'); -+ } -+ } - push @body, ('', '', '=> ./index.pl [home]'); - return @body; - } -@@ -101,17 +108,22 @@ sub feed_get - { - my ( $query ) = @_; - -- my $feed; -- if ( $query =~ /^https\:\/\// ) { $feed = XML::FeedPP->new($query, utf8_flag => 1); } -+ my $content; -+ if ( $query =~ /^https\:\/\// ) { $content = `wget -qO - $query`; } - if ( $query =~ /^gemini\:\/\// ) { -- my $content = `./gcat $query`; -+ $content = `./gcat $query`; - $content =~ /20\W/ or return undef; - $content =~ s/^[0-9]{0,2}\W.+\r\n//; -- $feed = XML::FeedPP->new($content, -type => 'string', utf8_flag => 1); - } -- -- recent_add($query, $feed->title); -- popular_add($query, $feed->title); -- $feed->sort_item(); -+ -+ return $content; -+} -+ -+sub feed_parse -+{ -+ my ($content) = @_; -+ -+ my $feed = XML::FeedPP->new($content, -type => 'string', utf8_flag => 1); -+ if (defined($feed)) { $feed->sort_item(); } - return $feed; - } diff --git a/sources/orrg.git/commits/637126e743aa72c05564095f71020e57bb44b22f.patch b/sources/orrg.git/commits/637126e743aa72c05564095f71020e57bb44b22f.patch @@ -1,13 +0,0 @@ -diff --git a/README.md b/README.md -index 663c3131b819a57c5ca8550de1bd628217c0353c..9a01b5f0b1e2602e5b03ff8f13a1ffc76f497cd6 100644 ---- a/README.md -+++ b/README.md -@@ -3,7 +3,7 @@ - *orrg* is a cgi script for [gemini](gemini://gemini.circumlunar.space) servers. - It fetches an arbitrary rss/atom feed given by the user and renders its content as a gemini site. - --Visit the [demo](gemini://gmndemo.clttr.info/orrg.pl). -+Visit the [demo](gemini://gmndemo.clttr.info/orrg.pl?https:%2F%2Fgit.sr.ht%2F~rwa%2Forrg%2Flog%2Frss.xml) ([through http proxy](ihttps://portal.mozz.us/gemini/gmndemo.clttr.info/orrg.pl%3Fhttps:%252F%252Fgit.sr.ht%252F~rwa%252Forrg%252Flog%252Frss.xml)) that shows the commit log of this repo. - - ## features - diff --git a/sources/orrg.git/commits/644fe70ba62746fa237c0de9e241951a9c9e6f6f.patch b/sources/orrg.git/commits/644fe70ba62746fa237c0de9e241951a9c9e6f6f.patch @@ -1,13 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 28547cf2e7b6b32609585b091f352d5417258338..ae60e760ef82a5a906b0e9aedf95b9bac71e0d7d 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -93,7 +93,7 @@ if ( $query =~ /^gemini\:\/\// ) { - my $content = `./gcat $query`; - $content =~ /20\W/ or return undef; - $content =~ s/^[0-9]{0,2}\W.+\r\n//; -- $feed = XML::FeedPP->new($content, -type => 'string'); -+ $feed = XML::FeedPP->new($content, -type => 'string', utf8_flag => 1); - } - - recent_add($query, $feed->title); diff --git a/sources/orrg.git/commits/70dfde6e6d1bc071379528ff85e97452ba341a7e.patch b/sources/orrg.git/commits/70dfde6e6d1bc071379528ff85e97452ba341a7e.patch @@ -1,47 +0,0 @@ -diff --git a/README.md b/README.md -index 3089c92428783521ec061f8e59657a8aa2e0f179..b279a59f40cf1e2111bc0add9c4e6af9a0f2cef6 100644 ---- a/README.md -+++ b/README.md -@@ -30,3 +30,6 @@ - gemini server with cgi enabled (like gmnisrv or stargazer) - - Perl >= 5.28 with modules - - URI::Escape - - XML::FeedPP -+ - DateTime -+ - DateTime::Format::ISO8601 -+ - HTML::Strip -diff --git a/orrg.pl b/orrg.pl -index 44d9b31e444a47bd2bc4ba9ba9b26b0c3c99c5e9..d21f98206246374db29b773d525395ee92057928 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -1,16 +1,17 @@ - #!/usr/bin/perl - # Copyright René Wagner 2020 - # licenced under BSD 3-Clause licence --# https://git.sr.ht/~rwa/willgemini.support -+# https://git.sr.ht/~rwa/orrg - - use strict; - no warnings 'experimental'; - use URI::Escape; - use XML::FeedPP; -+use HTML::Strip; - use DateTime; - use DateTime::Format::ISO8601; - use POSIX qw(strftime); --use v5.10; -+ - # enable UTF-8 mode for everything - use utf8; - binmode STDOUT, ':utf8'; -@@ -78,7 +79,10 @@ my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); - push @body, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); - } - push @body, ''; -- $it->description eq '' or push @body, $it->description; -+ if ($it->description ne '') { -+ my $hs = HTML::Strip->new(); -+ push @body, $hs->parse($it->description); -+ } - $it->link eq '' or push @body, ('=> '.$it->link.' open entry in browser', ''); - } - diff --git a/sources/orrg.git/commits/7b18a7528ee0938b19f35bd40ea2a110c80eadc5.patch b/sources/orrg.git/commits/7b18a7528ee0938b19f35bd40ea2a110c80eadc5.patch @@ -1,142 +0,0 @@ -diff --git a/lib/orrg.pm b/lib/orrg.pm -index e4ca572f0b36c8b0eb3725c8f1f7f11dc0e3de4a..88575dd44ddce8718626f6c0c78be335616d11e7 100755 ---- a/lib/orrg.pm -+++ b/lib/orrg.pm -@@ -7,12 +7,14 @@ package orrg; - use strict; - use Exporter; - our @ISA = qw(Exporter); --our @EXPORT = qw(write_response %RC); # automatically exported subs -+our @EXPORT = qw(recent_get recent_add write_response %RC); # automatically exported subs - - # enable UTF-8 mode for everything - use utf8; - binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; -+ -+my $recentfile = 'data/recent.txt'; - - # define return codes - our %RC = ( -@@ -36,11 +38,50 @@ 'CERT_NOT_AUTHORISED', 61, - 'CERT_NOT_VALID', 62 - ); - -+sub recent_get -+{ -+ (-f $recentfile) or return undef; -+ -+ my @recents = (); -+ open INFILE, $recentfile; -+ flock OUTFILE, 1; -+ while (<INFILE>) { -+ chomp($_); -+ push @recents, $_; -+ } -+ flock OUTFILE, 8; -+ close INFILE; -+ -+ return \@recents; -+} -+ -+sub recent_add -+{ -+ my ( $uri, $name ) = @_; -+ my $recent = recent_get(); -+ -+ my $newline = "$uri $name"; -+ open OUTFILE, '>', $recentfile; -+ flock OUTFILE, 1; -+ print OUTFILE "$newline\n"; -+ -+ my $c = 1; -+ foreach (@$recent) { -+ if ($newline ne $_) { -+ print OUTFILE "$_\n"; -+ $c++; -+ } -+ ($c < 10) or last; -+ } -+ flock OUTFILE, 8; -+ close OUTFILE; -+} -+ - sub write_response - { - my ($returncode, $meta, @content) = @_; - -- if (!defined($RC{$returncode})) { die "Unknown response code!"; } -+ defined($RC{$returncode}) or die "Unknown response code!"; - - printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); - foreach (@content) { -diff --git a/orrg.pl b/orrg.pl -index 5cf31e2c5c5aaa1270cbeac8e19af7f7a44c0544..e9e258649242d929350b77364a977eded0563e1b 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -29,6 +29,7 @@ - if ($query eq '' || $query !~ /^https\:\/\//) { - write_response('INPUT', 'Paste the URI of the rss feed you want to read:', undef); - } -+ - write_response('SUCCESS', 'text/gemini', create_response($query)); - - exit; -@@ -45,6 +46,7 @@ push @body, ('# orrg error', '', 'The requested feed could not be loaded. :(', '', '=> '. $qs .' open feed in browser'); - return @body; - } - -+ recent_add($qs, $feed->title); - push @body, '# '. $feed->title; - push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); - $feed->description eq '' or push @body, ('', $feed->description); -diff --git a/recent.pl b/recent.pl -new file mode 100755 -index 0000000000000000000000000000000000000000..bfc2c14271284eadf6dd7334b1982a69aceece11 ---- /dev/null -+++ b/recent.pl -@@ -0,0 +1,44 @@ -+#!/usr/bin/perl -+# Copyright René Wagner 2020 -+# licenced under BSD 3-Clause licence -+# https://git.sr.ht/~rwa/orrg -+ -+use strict; -+no warnings 'experimental'; -+use URI::Escape; -+use lib 'lib/'; -+use orrg; -+ -+# enable UTF-8 mode for everything -+use utf8; -+binmode STDOUT, ':utf8'; -+binmode STDERR, ':utf8'; -+ -+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') -+{ -+ write_response('CGI_ERROR', '', undef); -+} -+ -+write_response('SUCCESS', 'text/gemini', create_response()); -+ -+exit; -+ -+sub create_response -+{ -+ my @body = (); -+ -+ push @body, ('# recently visited feeds', ''); -+ -+ my $recents = recent_get(); -+ if ( defined($recents) ) { -+ foreach (@$recents) { -+ my ($uri, $name) = split / /, $_, 2; -+ push @body, '=> orrg.pl?'. uri_escape($uri) .' '. $name; -+ } -+ } else { -+ push @body, 'No feeds found'; -+ } -+ -+ push @body, ('', '', '=> index.pl [home]'); -+ return @body; -+} diff --git a/sources/orrg.git/commits/7eaf663655cd395a0c3b925142b7f07901a3cca4.patch b/sources/orrg.git/commits/7eaf663655cd395a0c3b925142b7f07901a3cca4.patch @@ -1,72 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 8735c34a93216ac785b347e3abe51dffce7f4d87..2df2ded24637be2b68cb621969d10661916b226e 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -38,13 +38,13 @@ ); - - if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') - { -- write_response('CGI_ERROR', '', undef); -+ write_response('CGI_ERROR', '', undef); - } - - my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); - - if ($query eq '' || $query !~ /^https\:\/\//) { -- write_response('INPUT', 'Paste the URI of the rss feed you want to read:', undef); -+ write_response('INPUT', 'Paste the URI of the rss feed you want to read:', undef); - } - write_response('SUCCESS', 'text/gemini', create_response($query)); - -@@ -52,7 +52,7 @@ exit; - - sub create_response - { -- my ( $qs ) = @_; -+ my ( $qs ) = @_; - my @body = (); - - my $feed = XML::FeedPP->new($qs, utf8_flag => 1); -@@ -62,7 +62,7 @@ push @body, ('# orrg error', '', 'The requested feed could not be loaded. :(', '', '=> '. $qs .' open feed in browser'); - return @body; - } - -- push @body, '# '. $feed->title; -+ push @body, '# '. $feed->title; - push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); - $feed->description eq '' or push @body, ('', $feed->description); - $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; -@@ -73,23 +73,23 @@ foreach my $it ($feed->get_item()) { - push @body, '### '. $it->title; - $it->pubDate eq '' or push @body, 'published '. $it->pubDate; - push @body, ''; -- $it->description eq '' or push @body, $it->description; -+ $it->description eq '' or push @body, $it->description; - $it->link eq '' or push @body, ('=> '.$it->link.' open entry in browser', ''); - } -- -- return @body; -+ -+ return @body; - } - - sub write_response - { -- my ($returncode, $meta, @content) = @_; -- -+ my ($returncode, $meta, @content) = @_; -+ - if (!defined($RC{$returncode})) { die "Unknown response code!"; } - -- printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -- foreach (@content) { -- print("$_\r\n"); -- } -+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -+ foreach (@content) { -+ print("$_\r\n"); -+ } - -- exit; -+ exit; - } diff --git a/sources/orrg.git/commits/89d2888f0c1880256b7e86745b75f7e282a7b54a.patch b/sources/orrg.git/commits/89d2888f0c1880256b7e86745b75f7e282a7b54a.patch @@ -1,40 +0,0 @@ -diff --git a/index.pl b/index.pl -index 14fe354925780511523d735cbd8e16051053ea00..9e5dd6905f2f528e1505923ab1b542275dd3a6b4 100755 ---- a/index.pl -+++ b/index.pl -@@ -4,7 +4,7 @@ # licenced under BSD 3-Clause licence - # https://git.sr.ht/~rwa/orrg - - use strict; --use DBI; -+use URI::Escape; - use lib 'lib/'; - use orrg; - -@@ -23,5 +23,25 @@ exit; - - sub body - { -- return ('# Welcome to orrg', '', 'orrg is your online rss/atom feed reader for gemini.', '', '=> orrg.pl view a feed', '', '## explore', '', '=> recent.pl top 10 recent feeds', '=> popular.pl top 10 popular feeds', '', '', '=> https://git.sr.ht/~rwa/orrg powered by orrg'); -+ my ($cnt, $uri, $name); -+ my $populars = popular_get(); -+ if ( defined($populars) ) { -+ ($cnt, $uri, $name) = split / /, @$populars[ rand @$populars ], 3; -+ } -+ -+ return ( -+ '# Welcome to orrg', -+ '', -+ 'orrg is your online rss/atom feed reader for gemini.', -+ '', -+ '=> orrg.pl view a feed', -+ '', -+ '## explore', -+ '', -+ '=> orrg.pl?'. uri_escape($uri) .' random feed', -+ '=> recent.pl top 10 recent feeds', -+ '=> popular.pl top 10 popular feeds', -+ '', -+ '', -+ '=> https://git.sr.ht/~rwa/orrg powered by orrg'); - } diff --git a/sources/orrg.git/commits/8c21cb40de18a1fc4f8cc0ee540a1c021d08d50a.patch b/sources/orrg.git/commits/8c21cb40de18a1fc4f8cc0ee540a1c021d08d50a.patch @@ -1,209 +0,0 @@ -diff --git a/.gitignore b/.gitignore -new file mode 100644 -index 0000000000000000000000000000000000000000..60baa9cb833f9e075739f327f4fe68477c84a093 ---- /dev/null -+++ b/.gitignore -@@ -0,0 +1 @@ -+data/* -diff --git a/README.md b/README.md -index 5604153c98447581d038e8ebc530b67351c1a277..c05170514e82f3774d2a681ff1aa145d965cdb60 100644 ---- a/README.md -+++ b/README.md -@@ -14,13 +14,13 @@ Preferably over the [mailing list](https://lists.sr.ht/~rwa/gmni-perl-cgi). - - ## features - --- load an atom/rss feed from https (http is not supported!) given by user input -+- load an atom/rss feed from gemini or https (http is deliberately not supported!) given by user input - - render feed (channel info & entrys) as a gemini site - - include links to originating site and every article - - strip html tags from item description - - lists of popular and recently visited feeds - --Fetching feeds from gemini is currently not supported -> https://todo.sr.ht/~rwa/gmni-perl/4 -+gemini-support is currently implemented using [gcat](https://github.com/aaronjanse/gcat) till popular perl libs have catched up. :) - - ## non-features - -@@ -43,3 +43,4 @@ - XML::FeedPP - - DateTime - - DateTime::Format::ISO8601 - - HTML::Strip -+- Python 3 for `gcat` -diff --git a/gcat b/gcat -new file mode 100755 -index 0000000000000000000000000000000000000000..702da2f3210dbfcc532e039b590e3fa68e40f4e9 ---- /dev/null -+++ b/gcat -@@ -0,0 +1,72 @@ -+#!/usr/bin/env python3 -+ -+import cgi -+import os -+import socket -+import ssl -+import sys -+import urllib.parse -+ -+def absolutise_url(base, relative): -+ # Absolutise relative links -+ if "://" not in relative: -+ # Python's URL tools somehow only work with known schemes? -+ base = base.replace("gemini://","http://") -+ relative = urllib.parse.urljoin(base, relative) -+ relative = relative.replace("http://", "gemini://") -+ return relative -+ -+if len(sys.argv) != 2: -+ print("Usage:") -+ print("gcat gemini://gemini.circumlunar.space") -+ sys.exit(1) -+ -+url = sys.argv[1] -+parsed_url = urllib.parse.urlparse(url) -+if parsed_url.scheme == "": -+ url = "gemini://"+url -+ parsed_url = urllib.parse.urlparse(url) -+ -+if parsed_url.scheme != "gemini": -+ print("Sorry, Gemini links only.") -+ sys.exit(1) -+if parsed_url.port is not None: -+ useport = parsed_url.port -+else: -+ useport = 1965 -+# Do the Gemini transaction -+while True: -+ s = socket.create_connection((parsed_url.hostname, useport)) -+ context = ssl.SSLContext() -+ context.check_hostname = False -+ context.verify_mode = ssl.CERT_NONE -+ s = context.wrap_socket(s, server_hostname = parsed_url.netloc) -+ s.sendall((url + '\r\n').encode("UTF-8")) -+ # Get header and check for redirects -+ fp = s.makefile("rb") -+ header = fp.readline() -+ print(header.decode("UTF-8"), end="") -+ header = header.decode("UTF-8").strip() -+ status, mime = header.split()[:2] -+ # Handle input requests -+ if status.startswith("1"): -+ # Prompt -+ query = input("INPUT" + mime + "> ") -+ url += "?" + urllib.parse.quote(query) # Bit lazy... -+ # Follow redirects -+ elif status.startswith("3"): -+ url = absolutise_url(url, mime) -+ parsed_url = urllib.parse.urlparse(url) -+ # Otherwise, we're done. -+ else: -+ break -+# Fail if transaction was not successful -+if status.startswith("2"): -+ if mime.startswith("text/"): -+ # Decode according to declared charset -+ mime, mime_opts = cgi.parse_header(mime) -+ body = fp.read() -+ body = body.decode(mime_opts.get("charset","UTF-8")) -+ print(body, end="") -+ else: -+ print(fp.read(), end="") -diff --git a/orrg.pl b/orrg.pl -index 094ede1d23d98a0ade808511eeac111d4ed09095..195999cd7d45aff0feedcc135c80336e65a7b4b6 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -26,7 +26,7 @@ } - - my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); - --if ($query eq '' || $query !~ /^https\:\/\//) { -+if ($query eq '' || $query !~ /^(https|gemini)\:\/\//) { - write_response('INPUT', 'Paste the URI of the rss feed you want to read:', undef); - } - -@@ -39,40 +39,63 @@ { - my ( $qs ) = @_; - my @body = (); - -- my $feed = XML::FeedPP->new($qs, utf8_flag => 1); -- -+ my $feed = feed_get($qs); - if ( !defined($feed) ) { - push @body, ('# orrg error', '', 'The requested feed could not be loaded. :(', '', '=> '. $qs .' open feed in browser'); - return @body; - } -- -- recent_add($qs, $feed->title); -- popular_add($qs, $feed->title); -+ - push @body, '# '. $feed->title; - push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); - $feed->description eq '' or push @body, ('', $feed->description); - $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; - $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); -+ - push @body, ('## recent feed items', ''); -+ foreach my $it ($feed->get_item()) { push @body, @{item($it)}; } -+ -+ push @body, ('', '', '=> index.pl [home]'); -+ return @body; -+} -+ -+sub item -+{ -+ my ($it) = @_; - - my $hs = HTML::Strip->new(emit_spaces => 0, auto_reset => 1); -- foreach my $it ($feed->get_item()) { -- push @body, ($it->description ne '' || $it->pubDate ne '')? '### '. $it->title : $it->title; -- if ($it->pubDate ne '') { -- my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); -- push @body, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); -- push @body, ''; -- } -- if ($it->description ne '') { -- my $desc = $it->description; -- chomp $desc; -- $desc =~ s/\<li\>/* /ig; -- $desc =~ s/\<br \/\>/\r\n/ig; -- push @body, $hs->parse($desc); -- } -- $it->link eq '' or push @body, ('=> '.$it->link.' open entry in browser', ''); -+ my @item = (); -+ push @item, ($it->description ne '' || $it->pubDate ne '')? '### '. $it->title : $it->title; -+ if ($it->pubDate ne '') { -+ my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); -+ push @item, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); -+ push @item, ''; -+ } -+ if ($it->description ne '') { -+ my $desc = $it->description; -+ chomp $desc; -+ $desc =~ s/\<li\>/* /ig; -+ $desc =~ s/\<br \/\>/\r\n/ig; -+ push @item, $hs->parse($desc); - } -+ $it->link eq '' or push @item, ('=> '.$it->link.' open entry in browser', ''); - -- push @body, ('', '', '=> index.pl [home]'); -- return @body; -+ return \@item; -+} -+ -+sub feed_get -+{ -+ my ( $query ) = @_; -+ -+ my $feed; -+ if ( $query =~ /^https\:\/\// ) { $feed = XML::FeedPP->new($query, utf8_flag => 1); } -+ if ( $query =~ /^gemini\:\/\// ) { -+ my $content = `./gcat $query`; -+ $content =~ /20\W/ or return undef; -+ $content =~ s/^[0-9]{0,2}\W.+\r\n//; -+ $feed = XML::FeedPP->new($content, -type => 'string'); -+ } -+ -+ recent_add($query, $feed->title); -+ popular_add($query, $feed->title); -+ return $feed; - } diff --git a/sources/orrg.git/commits/8d1d84dc5de30083cf64f405572ba62fe8d1da0a.patch b/sources/orrg.git/commits/8d1d84dc5de30083cf64f405572ba62fe8d1da0a.patch @@ -1,29 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index b024597cb0c2a3f5af1ce50e5ac227d9010595aa..8735c34a93216ac785b347e3abe51dffce7f4d87 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -64,18 +64,17 @@ } - - push @body, '# '. $feed->title; - push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); -- push @body, ('', $feed->description); -- push @body, ('=> '.$feed->link.' open website', ''); -+ $feed->description eq '' or push @body, ('', $feed->description); -+ $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; -+ $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); - push @body, ('## recent feed items', ''); - - foreach my $it ($feed->get_item()) { - push @body, '### '. $it->title; -- push @body, 'published '. $it->pubDate; -+ $it->pubDate eq '' or push @body, 'published '. $it->pubDate; - push @body, ''; -- if ( $it->get('description') ne '') { -- push @body, $it->description; -- } -- push @body, ('=> '.$it->link.' open entry in browser', ''); -+ $it->description eq '' or push @body, $it->description; -+ $it->link eq '' or push @body, ('=> '.$it->link.' open entry in browser', ''); - } - - return @body; diff --git a/sources/orrg.git/commits/8ebe00a117ed9d1528a0a8e268ad30b67884ba09.patch b/sources/orrg.git/commits/8ebe00a117ed9d1528a0a8e268ad30b67884ba09.patch @@ -1,16 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 88e42e7b37eb0985b70a27dd6d4dee731924485b..28547cf2e7b6b32609585b091f352d5417258338 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -73,8 +73,9 @@ } - if ($it->description ne '') { - my $desc = $it->description; - chomp $desc; -- $desc =~ s/\<li\>/* /ig; -- $desc =~ s/\<br \/\>/\r\n/ig; -+ $desc =~ s/\<li\>/* /igm; -+ $desc =~ s/\<br \/\>/\r\n/igm; -+ $desc =~ s/^\s+\*/*/igm; - push @item, $hs->parse($desc); - } - $it->link eq '' or push @item, ('=> '.$it->link.' open entry in browser', ''); diff --git a/sources/orrg.git/commits/9bdd8978ec1177627a2182317b202bce243f17ca.patch b/sources/orrg.git/commits/9bdd8978ec1177627a2182317b202bce243f17ca.patch @@ -1,36 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 0425f84b46641f18f49edcaaacc01ed103533545..a8dc3b6536d8e25b40c53f0503b77fa8f1868d7f 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -7,6 +7,7 @@ use strict; - no warnings 'experimental'; - use URI::Escape; - use XML::FeedPP; -+use POSIX qw(strftime); - use v5.10; - # enable UTF-8 mode for everything - use utf8; -@@ -63,19 +64,16 @@ } - - push @body, '# '. $feed->title; - push @body, ('', $feed->description); -- push @body, ('=> '.$feed->link.' link to the feed', ''); -+ push @body, ('=> '.$feed->link.' open website', ''); - push @body, ('## recent feed items', ''); - - foreach my $it ($feed->get_item()) { -+ push @body, '### '. $it->title; -+ push @body, 'published '. $it->pubDate; -+ push @body, ''; - if ( $it->get('description') ne '') { -- push @body, '## '. $it->title; -- push @body, ''; - push @body, $it->description; - } -- else { -- push @body, $it->title; -- } -- - push @body, ('=> '.$it->link.' open entry in browser', ''); - } - diff --git a/sources/orrg.git/commits/a09b98b4d50d664bce1dd9f206d4802754dd7abe.patch b/sources/orrg.git/commits/a09b98b4d50d664bce1dd9f206d4802754dd7abe.patch @@ -1,56 +0,0 @@ -diff --git a/README.md b/README.md -index 9a01b5f0b1e2602e5b03ff8f13a1ffc76f497cd6..3089c92428783521ec061f8e59657a8aa2e0f179 100644 ---- a/README.md -+++ b/README.md -@@ -1,16 +1,22 @@ - # orrg (online rss feed reader for gemini) - - *orrg* is a cgi script for [gemini](gemini://gemini.circumlunar.space) servers. --It fetches an arbitrary rss/atom feed given by the user and renders its content as a gemini site. -+It delivers an easy way to consume atom or rss feeds shipped over http(s) within gemini. - --Visit the [demo](gemini://gmndemo.clttr.info/orrg.pl?https:%2F%2Fgit.sr.ht%2F~rwa%2Forrg%2Flog%2Frss.xml) ([through http proxy](ihttps://portal.mozz.us/gemini/gmndemo.clttr.info/orrg.pl%3Fhttps:%252F%252Fgit.sr.ht%252F~rwa%252Forrg%252Flog%252Frss.xml)) that shows the commit log of this repo. -+Visit the [demo](gemini://gmndemo.clttr.info/orrg.pl?https:%2F%2Fgit.sr.ht%2F~rwa%2Forrg%2Flog%2Frss.xml) ([through http proxy](https://portal.mozz.us/gemini/gmndemo.clttr.info/orrg.pl%3Fhttps:%252F%252Fgit.sr.ht%252F~rwa%252Forrg%252Flog%252Frss.xml)) that shows the commit log of this repo. -+The demo might break regularly as i use it for integration testing during development. - - ## features - --- load an atom/rss feed given by user input --- render feed (channel & item info) as a gemini site -+- load an atom/rss feed from http(s) given by user input -+- render feed (channel info & entrys) as a gemini site -+ - include links to originating site and every article - - Fetching feeds from gemini is currently not supported -> https://todo.sr.ht/~rwa/gmni-perl/4 -+ -+## non-features -+ -+*orrg* is more of a PoC and will never be a full-fletched feed aggregator with archiving and searching capabilities. - - # installation - -diff --git a/orrg.pl b/orrg.pl -index 2df2ded24637be2b68cb621969d10661916b226e..44d9b31e444a47bd2bc4ba9ba9b26b0c3c99c5e9 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -7,6 +7,8 @@ use strict; - no warnings 'experimental'; - use URI::Escape; - use XML::FeedPP; -+use DateTime; -+use DateTime::Format::ISO8601; - use POSIX qw(strftime); - use v5.10; - # enable UTF-8 mode for everything -@@ -71,7 +73,10 @@ push @body, ('## recent feed items', ''); - - foreach my $it ($feed->get_item()) { - push @body, '### '. $it->title; -- $it->pubDate eq '' or push @body, 'published '. $it->pubDate; -+ if ($it->pubDate ne '') { -+ my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); -+ push @body, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); -+ } - push @body, ''; - $it->description eq '' or push @body, $it->description; - $it->link eq '' or push @body, ('=> '.$it->link.' open entry in browser', ''); diff --git a/sources/orrg.git/commits/a354cf94d629843e4a3f17fe9d48ad3ea4d89ba4.patch b/sources/orrg.git/commits/a354cf94d629843e4a3f17fe9d48ad3ea4d89ba4.patch @@ -1,29 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 7e26b51dc7c7ec270324b43d2517919ad3eb0ed1..71a558897c02ce80027afedcfd402460d3cf0041 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -51,7 +51,6 @@ $feed->description eq '' or push @body, $feed->description; - $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; - $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); - -- push @body, ('## recent feed items', ''); - foreach my $it ($feed->get_item()) { push @body, @{item($it)}; } - - push @body, ('', '', '=> index.pl [home]'); -@@ -70,7 +69,7 @@ $title =~ s/^\s+//ig; - $title =~ s/\s+$//ig; - $link =~ s/^\s+//ig; - $link =~ s/\s+$//ig; -- push @item, ($it->description ne '' || $it->pubDate ne '')? '### '. $title : $title; -+ push @item, ($it->description ne '' || $it->pubDate ne '')? '## '. $title : $title; - if ($it->pubDate ne '') { - my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); - push @item, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); -@@ -79,6 +78,7 @@ } - if ($it->description ne '') { - my $desc = $it->description; - $desc =~ s/\<li\>/* /igm; -+ $desc =~ s/\<h[1-2][^\>]+\>/### /igm; - $desc =~ s/\<br \/\>/\r\n/igm; - $desc = $hs->parse($desc); - $desc =~ s/^\r\n$//igm; diff --git a/sources/orrg.git/commits/a7927c64c3c34b17a034148ded8d9aacda7a56fa.patch b/sources/orrg.git/commits/a7927c64c3c34b17a034148ded8d9aacda7a56fa.patch @@ -1,7 +0,0 @@ -diff --git a/robots.txt b/robots.txt -new file mode 100644 -index 0000000000000000000000000000000000000000..46c4570b0be7c94113f577aeb128e415d90e7ddd ---- /dev/null -+++ b/robots.txt -@@ -0,0 +1 @@ -+Disallow:orrg.pl diff --git a/sources/orrg.git/commits/af7333093a747a6b9bf6e969664352bbe65348ae.patch b/sources/orrg.git/commits/af7333093a747a6b9bf6e969664352bbe65348ae.patch @@ -1,22 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index d5484c5d9e43bef1ef6b4836a2f24421b24b3a0d..eb3842dcbcca748d184b3664608c8f58991e197b 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -78,11 +78,15 @@ push @item, ''; - } - if ($it->description ne '') { - my $desc = $it->description; -- chomp $desc; - $desc =~ s/\<li\>/* /igm; - $desc =~ s/\<br \/\>/\r\n/igm; -+ $desc = $hs->parse($desc); -+ $desc =~ s/^\r\n$//igm; - $desc =~ s/^\s+\*/*/igm; -- push @item, $hs->parse($desc); -+ $desc =~ s/\s+$//igm; -+ $desc =~ s/^\s+//igm; -+ chomp $desc; -+ push @item, $desc; - } - $link eq '' or push @item, ('=> '.$link.' open entry in browser', ''); - diff --git a/sources/orrg.git/commits/b687e40056a5ab549f4cd0d2be68db4e1fc05e74.patch b/sources/orrg.git/commits/b687e40056a5ab549f4cd0d2be68db4e1fc05e74.patch @@ -1,168 +0,0 @@ -diff --git a/LICENSE b/LICENSE -new file mode 100755 -index 0000000000000000000000000000000000000000..027d71d0073f90db1ea80f7524f6fceccb001e93 ---- /dev/null -+++ b/LICENSE -@@ -0,0 +1,29 @@ -+BSD 3-Clause License -+ -+Copyright (c) 2018-2020, René Wagner -+All rights reserved. -+ -+Redistribution and use in source and binary forms, with or without -+modification, are permitted provided that the following conditions are met: -+ -+* Redistributions of source code must retain the above copyright notice, this -+ list of conditions and the following disclaimer. -+ -+* Redistributions in binary form must reproduce the above copyright notice, -+ this list of conditions and the following disclaimer in the documentation -+ and/or other materials provided with the distribution. -+ -+* Neither the name of the copyright holder nor the names of its -+ contributors may be used to endorse or promote products derived from -+ this software without specific prior written permission. -+ -+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -diff --git a/README.md b/README.md -new file mode 100644 -index 0000000000000000000000000000000000000000..0cb8d9e9e84d9126815c779aed2fbbfef945c681 ---- /dev/null -+++ b/README.md -@@ -0,0 +1,24 @@ -+# orrg (online rss feed reader for gemini) -+ -+*orrg* is a cgi script for [gemini](gemini://gemini.circumlunar.space) servers. -+It fetches an arbitrary rss feed given by the user and renders its content as a gemini site. -+ -+Visit the [demo](gemini://gmndemo.clttr.info/orrg.pl). -+ -+## features -+ -+Load an arbitrary atom/rss feed. -+ -+# installation -+ -+- setup your geminiserver with cgi enabled -+- `git clone` the repo to the directory -+- open the capsule in a gemini client -+ -+## requirements -+ -+- gemini server with cgi enabled (like gmnisrv or stargazer) -+- Perl >= 5.28 with modules -+ - URI::Escape -+ - XML::FeedPP -+ - LWP::UserAgent -diff --git a/orrg.pl b/orrg.pl -new file mode 100755 -index 0000000000000000000000000000000000000000..0425f84b46641f18f49edcaaacc01ed103533545 ---- /dev/null -+++ b/orrg.pl -@@ -0,0 +1,97 @@ -+#!/usr/bin/perl -+# Copyright René Wagner 2020 -+# licenced under BSD 3-Clause licence -+# https://git.sr.ht/~rwa/willgemini.support -+ -+use strict; -+no warnings 'experimental'; -+use URI::Escape; -+use XML::FeedPP; -+use v5.10; -+# enable UTF-8 mode for everything -+use utf8; -+binmode STDOUT, ':utf8'; -+binmode STDERR, ':utf8'; -+ -+# define return codes -+our %RC = ( -+ 'INPUT', 10, -+ 'SENSITIVE_INPUT', 11, -+ 'SUCCESS', 20, -+ 'TEMPORARY_REDIRECT', 30, -+ 'PERMANENT_REDIRECT', 31, -+ 'TEMPORARY_FAILURE', 40, -+ 'SERVER_UNAVAILABLE', 41, -+ 'CGI_ERROR', 42, -+ 'PROXY_ERROR', 43, -+ 'SLOW_DOWN', 44, -+ 'PERMANENT_FAILURE', 50, -+ 'NOT_FOUND', 51, -+ 'GONE', 52, -+ 'PROXY_REQUEST_REFUSE', 53, -+ 'BAD_REQUEST', 59, -+ 'CLIENT_CERT_REQUIRED', 60, -+ 'CERT_NOT_AUTHORISED', 61, -+ 'CERT_NOT_VALID', 62 -+ ); -+ -+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') -+{ -+ write_response('CGI_ERROR', '', undef); -+} -+ -+my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); -+ -+if ($query eq '' || $query !~ /^https\:\/\//) { -+ write_response('INPUT', 'Paste the URI of the rss feed you want to read:', undef); -+} -+write_response('SUCCESS', 'text/gemini', create_response($query)); -+ -+exit; -+ -+sub create_response -+{ -+ my ( $qs ) = @_; -+ my @body = (); -+ -+ my $feed = XML::FeedPP->new($qs, utf8_flag => 1); -+ -+ if ( !defined($feed) ) { -+ push @body, ('# orrg error', '', 'The requested feed could not be loaded. :(', '', '=> '. $qs .' open feed in browser'); -+ return @body; -+ } -+ -+ push @body, '# '. $feed->title; -+ push @body, ('', $feed->description); -+ push @body, ('=> '.$feed->link.' link to the feed', ''); -+ push @body, ('## recent feed items', ''); -+ -+ foreach my $it ($feed->get_item()) { -+ if ( $it->get('description') ne '') { -+ push @body, '## '. $it->title; -+ push @body, ''; -+ push @body, $it->description; -+ } -+ else { -+ push @body, $it->title; -+ } -+ -+ push @body, ('=> '.$it->link.' open entry in browser', ''); -+ } -+ -+ return @body; -+} -+ -+sub write_response -+{ -+ my ($returncode, $meta, @content) = @_; -+ -+ if (!defined($RC{$returncode})) { die "Unknown response code!"; } -+ -+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); -+ foreach (@content) { -+ print("$_\r\n"); -+ } -+ -+ exit; -+} diff --git a/sources/orrg.git/commits/c200079891e3277da48e952a1c7decd219c8aa6d.patch b/sources/orrg.git/commits/c200079891e3277da48e952a1c7decd219c8aa6d.patch @@ -1,20 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 1be767a230394ae95b8eb91d07b61b192d2feb67..35458b2a68e7db614afe7cda48d6673840e23b57 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -74,13 +74,13 @@ push @body, ('## recent feed items', ''); - - my $hs = HTML::Strip->new(emit_spaces => 0, auto_reset => 1); - foreach my $it ($feed->get_item()) { -- push @body, $it->description ne '' ? '### '. $it->title : $it->title; -+ push @body, ($it->description ne '' || $it->pubDate ne '')? '### '. $it->title : $it->title; - if ($it->pubDate ne '') { - my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); - push @body, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); -+ push @body, ''; - } - if ($it->description ne '') { -- push @body, ''; - my $desc = $it->description; - chomp $desc; - $desc =~ s/\<li\>/* /ig; diff --git a/sources/orrg.git/commits/ecf0d4d8ec1179291f21d20237d19026eb60c7d3.patch b/sources/orrg.git/commits/ecf0d4d8ec1179291f21d20237d19026eb60c7d3.patch @@ -1,26 +0,0 @@ -diff --git a/popular.pl b/popular.pl -index a681cc7be286964e80a95af47406ab762231ae56..3dcc60e9a004d6996ab87552ce7be532039e3aca 100755 ---- a/popular.pl -+++ b/popular.pl -@@ -35,7 +35,7 @@ my @sorted = sort { $a <=> $b } @$populars; - my $c = 0; - foreach (reverse @sorted) { - my ($cnt, $uri, $name) = split / /, $_, 3; -- push @body, '=> orrg.pl?'. uri_escape($uri) .' '. $name; -+ push @body, '=> orrg.pl?'. uri_escape($uri) .' '. ($name eq '' ? $uri : $name); - - ++$c < 10 or last; - } -diff --git a/recent.pl b/recent.pl -index bfc2c14271284eadf6dd7334b1982a69aceece11..e9522b7862e2dab631acaff80115c70f30716aec 100755 ---- a/recent.pl -+++ b/recent.pl -@@ -33,7 +33,7 @@ my $recents = recent_get(); - if ( defined($recents) ) { - foreach (@$recents) { - my ($uri, $name) = split / /, $_, 2; -- push @body, '=> orrg.pl?'. uri_escape($uri) .' '. $name; -+ push @body, '=> orrg.pl?'. uri_escape($uri) .' '. ($name eq '' ? $uri : $name); - } - } else { - push @body, 'No feeds found'; diff --git a/sources/orrg.git/commits/f1fa213eb470dbebf16dd42ce165726006a8e352.patch b/sources/orrg.git/commits/f1fa213eb470dbebf16dd42ce165726006a8e352.patch @@ -1,73 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 7d4a9bf834d306ca23fa7c023542f5454a899efc..6cc6843b64bc39ddcad803b98702e67ac271c489 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -44,10 +44,10 @@ push @body, ('# orrg error', '', 'The requested feed could not be loaded. :(', '', '=> '. $qs .' open feed in browser'); - return @body; - } - -- push @body, '# '. $feed->title; -+ push @body, '# '. trim_ws($feed->title); - push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); - push @body, ''; -- $feed->description eq '' or push @body, $feed->description; -+ $feed->description eq '' or push @body, trim_ws($feed->description); - $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; - $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); - -@@ -63,33 +63,38 @@ my ($it) = @_; - - my $hs = HTML::Strip->new(emit_spaces => 0, auto_reset => 1); - my @item = (); -- my $title = $it->title; -- my $link = $it->link; -- $title =~ s/^\s+//ig; -- $title =~ s/\s+$//ig; -- $link =~ s/^\s+//ig; -- $link =~ s/\s+$//ig; -- push @item, (($it->description ne '' && $it->description ne $it->title) || $it->pubDate ne '')? '## '. $title : $title; -+ my $title = trim_ws($it->title); -+ my $link = trim_ws($it->link); -+ my $desc = $it->description; -+ if ($desc ne '') { -+ $desc =~ s/\<li\>/* /igm; -+ $desc =~ s/\<h[1-2][^\>]+\>/### /igm; -+ $desc =~ s/\<br \/\>/\r\n/igm; -+ $desc = $hs->parse($desc); -+ $desc = trim_ws($desc); -+ } -+ push @item, (($desc ne '' && $desc ne $title) || $it->pubDate ne '')? '## '. $title : $title; - if ($it->pubDate ne '') { - my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); - push @item, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); - push @item, ''; - } -- if ($it->description ne '' && $it->description ne $it->title) { -- my $desc = $it->description; -- $desc =~ s/\<li\>/* /igm; -- $desc =~ s/\<h[1-2][^\>]+\>/### /igm; -- $desc =~ s/\<br \/\>/\r\n/igm; -- $desc = $hs->parse($desc); -- $desc =~ s/^\r\n$//igm; -- $desc =~ s/^\s+\*/*/igm; -- $desc =~ s/\s+$//igm; -- $desc =~ s/^\s+//igm; -+ if ($desc ne '' && $desc ne $title) { - push @item, $desc; - } - $link eq '' or push @item, ('=> '.$link.' open entry in browser', ''); - - return \@item; -+} -+ -+sub trim_ws -+{ -+ my ( $string ) = @_; -+ $string =~ s/^\s+\*/*/igm; -+ $string =~ s/\s+$//igm; -+ $string =~ s/^\s+//igm; -+ $string =~ s/^\r\n$//igm; -+ return $string; - } - - sub feed_get diff --git a/sources/orrg.git/commits/f847c41903390f842e3b4a1b1d7b4c6a99b30327.patch b/sources/orrg.git/commits/f847c41903390f842e3b4a1b1d7b4c6a99b30327.patch @@ -1,24 +0,0 @@ -diff --git a/orrg.pl b/orrg.pl -index 8da5f714c55956f5e00bbf5cd8275a873249d2ae..6ed425ae47375b14567217025b6d415f433a6837 100755 ---- a/orrg.pl -+++ b/orrg.pl -@@ -26,7 +26,7 @@ - my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); - - if ($query eq '' || $query !~ /^(https|gemini)\:\/\//) { -- write_response('INPUT', 'Paste the URI of the rss feed you want to read:', undef); -+ write_response('INPUT', 'Paste the URI of the rss feed you want to read', undef); - } - - write_response('SUCCESS', 'text/gemini', create_response($query)); -@@ -77,8 +77,9 @@ if ($desc ne '') { - $desc =~ s/\<li\>/* /igm; - $desc =~ s/\<h[1-2][^\>]+\>/### /igm; - $desc =~ s/\<br \/\>/\r\n/igm; -+ $desc =~ s/\<pre.*\>/\r\n```\r\n/igm; -+ $desc =~ s/\<\/pre\>/\r\n```\r\n/igm; - $desc = $hs->parse($desc); -- $desc = trim_ws($desc); - } - push @item, (($desc ne '' && $desc ne $title) || $it->pubDate ne '')? '## '. $title : $title; - if ($it->pubDate ne '') { diff --git a/sources/orrg.git/commits/index.gmi b/sources/orrg.git/commits/index.gmi @@ -1,254 +0,0 @@ -# Commits - -## f847c41903390f842e3b4a1b1d7b4c6a99b30327 -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Feb 22 20:34:19 2021 +0100 -=> f847c41903390f842e3b4a1b1d7b4c6a99b30327.patch Patch -Message: convert pre sections to preformatted gemini blocks - -preserve whitespaces in item description as they may -serve a purpose. -This will eventually be improved with a more -sophisticated algo that only preverses whitespaces -in non-pre-blocks - -closes #8 - -## 5a95a2973524305024fb2bd775b447737073d1d5 -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Feb 22 20:30:16 2021 +0100 -=> 5a95a2973524305024fb2bd775b447737073d1d5.patch Patch -Message: use wget instead of built-in Perl LWP - -improve handling of valid requests or parsing -attempts -closes #5 -closes #7 -closes #6 - -## 08a67a7b51c33a348137b4e6b5dc2366e9f90e47 -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Feb 08 16:23:26 2021 +0100 -=> 08a67a7b51c33a348137b4e6b5dc2366e9f90e47.patch Patch -Message: do not act if a path is set - -## 4454cc3f1b97506f166c883145dc8773de99b4be -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Jan 18 20:18:19 2021 +0100 -=> 4454cc3f1b97506f166c883145dc8773de99b4be.patch Patch -Message: use true relative links - -update robots.txt - -## a7927c64c3c34b17a034148ded8d9aacda7a56fa -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Jan 03 11:00:46 2021 +0100 -=> a7927c64c3c34b17a034148ded8d9aacda7a56fa.patch Patch -Message: add robots.txt - -## 589d4a8d337c759535a8d7eee270f25667aaec18 -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Jan 03 10:36:27 2021 +0100 -=> 589d4a8d337c759535a8d7eee270f25667aaec18.patch Patch -Message: update links - -## 89d2888f0c1880256b7e86745b75f7e282a7b54a -Author: René Wagner <rwagner@rw-net.de> -Date: Fri Dec 18 19:12:25 2020 +0100 -=> 89d2888f0c1880256b7e86745b75f7e282a7b54a.patch Patch -Message: add "random feed" function - -the random feed function helps you exploring the -likes of others - -## f1fa213eb470dbebf16dd42ce165726006a8e352 -Author: René Wagner <rwagner@rw-net.de> -Date: Thu Dec 17 19:28:50 2020 +0100 -=> f1fa213eb470dbebf16dd42ce165726006a8e352.patch Patch -Message: add whitespace removal for feed fields as well - -## 17fb028c7177296e19bfc4c5ae18b04b5e82a27d -Author: René Wagner <rwagner@rw-net.de> -Date: Thu Dec 17 17:05:49 2020 +0100 -=> 17fb028c7177296e19bfc4c5ae18b04b5e82a27d.patch Patch -Message: avoid title/desc duplicates - -## a354cf94d629843e4a3f17fe9d48ad3ea4d89ba4 -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Dec 15 19:07:54 2020 +0100 -=> a354cf94d629843e4a3f17fe9d48ad3ea4d89ba4.patch Patch -Message: convert <h1/2> in item content to third-level headers - -## 3a8b1c9b9529a20915c21c16c2004e5dbde83893 -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Dec 13 21:43:13 2020 +0100 -=> 3a8b1c9b9529a20915c21c16c2004e5dbde83893.patch Patch -Message: fix encoding of data files - -files for recent/popular must be utf-8 encoded as well. - -## af7333093a747a6b9bf6e969664352bbe65348ae -Author: René Wagner <rwagner@rw-net.de> -Date: Thu Dec 10 21:28:10 2020 +0100 -=> af7333093a747a6b9bf6e969664352bbe65348ae.patch Patch -Message: improve handling of trailing/leading whitespaces - -and newlines - -## ecf0d4d8ec1179291f21d20237d19026eb60c7d3 -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Dec 08 13:22:47 2020 +0100 -=> ecf0d4d8ec1179291f21d20237d19026eb60c7d3.patch Patch -Message: fix displaying of feeds without names - -## 226986013afd88a8b39ef37d5b6f85b63107d1ee -Author: René Wagner <rwagner@rw-net.de> -Date: Tue Dec 01 12:35:47 2020 +0100 -=> 226986013afd88a8b39ef37d5b6f85b63107d1ee.patch Patch -Message: remove trailing/leading whitespaces from title & link - -## 44e27d26947e61b9f2731ec9620ce4852a0b683c -Author: René Wagner <rwagner@rw-net.de> -Date: Mon Nov 30 07:41:08 2020 +0100 -=> 44e27d26947e61b9f2731ec9620ce4852a0b683c.patch Patch -Message: always sort feeditems by pubDate - -## 644fe70ba62746fa237c0de9e241951a9c9e6f6f -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Nov 29 21:06:38 2020 +0100 -=> 644fe70ba62746fa237c0de9e241951a9c9e6f6f.patch Patch -Message: fix utf8 handling on gemini feeds - -## 8ebe00a117ed9d1528a0a8e268ad30b67884ba09 -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Nov 29 20:27:05 2020 +0100 -=> 8ebe00a117ed9d1528a0a8e268ad30b67884ba09.patch Patch -Message: improve handling of list - -## 0a7ca079581bd01b63d6769cc34d2d6d40429b4c -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Nov 29 15:20:34 2020 +0100 -=> 0a7ca079581bd01b63d6769cc34d2d6d40429b4c.patch Patch -Message: modify header linebreaks - -## 8c21cb40de18a1fc4f8cc0ee540a1c021d08d50a -Author: René Wagner <rwagner@rw-net.de> -Date: Sun Nov 29 10:03:54 2020 +0100 -=> 8c21cb40de18a1fc4f8cc0ee540a1c021d08d50a.patch Patch -Message: integrate handling gemini:// uris using gcat - -improve error handling and restructure orrg.pl - -## 01370e679836f2a4a9a78b9e08ff08b5d9469329 -Author: René Wagner <rwagner@rw-net.de> -Date: Sat Nov 28 20:56:33 2020 +0100 -=> 01370e679836f2a4a9a78b9e08ff08b5d9469329.patch Patch -Message: add link to mailing list - -## 373ae43bf2d6f2e6ca2a31beccda552122cd987f -Author: René Wagner <rwagner@rw-net.de> -Date: Sat Nov 28 20:08:32 2020 +0100 -=> 373ae43bf2d6f2e6ca2a31beccda552122cd987f.patch Patch -Message: implement popular list - -state is saved in data/popular.txt and must be reset -manually if desired - -## 7b18a7528ee0938b19f35bd40ea2a110c80eadc5 -Author: René Wagner <rwagner@rw-net.de> -Date: Sat Nov 28 13:09:55 2020 +0100 -=> 7b18a7528ee0938b19f35bd40ea2a110c80eadc5.patch Patch -Message: add recent feeds list - -the 10 most recently visited feeds are stored and shown -on the recent page - -## 02a05701a382e226b83d63bdad35b0a01209beec -Author: René Wagner <rwagner@rw-net.de> -Date: Sat Nov 28 08:53:03 2020 +0100 -=> 02a05701a382e226b83d63bdad35b0a01209beec.patch Patch -Message: introduce lib/orrg.pm - -prepare for integration of feed lists - -## c200079891e3277da48e952a1c7decd219c8aa6d -Author: René Wagner <rwagner@rw-net.de> -Date: Fri Nov 27 20:33:16 2020 +0100 -=> c200079891e3277da48e952a1c7decd219c8aa6d.patch Patch -Message: more rendering adjustments - -## 01d0eb250880b32d366fbfa861c1e452094f6a0a -Author: René Wagner <rwagner@rw-net.de> -Date: Fri Nov 27 20:17:49 2020 +0100 -=> 01d0eb250880b32d366fbfa861c1e452094f6a0a.patch Patch -Message: improve formatting in some cases - -## 33875dd1e6936d33e97dfaca4c2f82c92558034a -Author: René Wagner <rwagner@rw-net.de> -Date: Thu Nov 26 21:16:52 2020 +0100 -=> 33875dd1e6936d33e97dfaca4c2f82c92558034a.patch Patch -Message: convert lists to gemtext - -convert li-tags to gemtext lists - -## 70dfde6e6d1bc071379528ff85e97452ba341a7e -Author: René Wagner <rwagner@rw-net.de> -Date: Thu Nov 26 16:42:45 2020 +0100 -=> 70dfde6e6d1bc071379528ff85e97452ba341a7e.patch Patch -Message: strip HTML tags from description - -this removes all tags, we might want to convert -<li> tags and links to gemini markup later - -all other fields despite description are left as they are - -## a09b98b4d50d664bce1dd9f206d4802754dd7abe -Author: René Wagner <rwagner@rw-net.de> -Date: Thu Nov 26 12:13:07 2020 +0100 -=> a09b98b4d50d664bce1dd9f206d4802754dd7abe.patch Patch -Message: use UTC timestamps everywhere to avoid misinterpretion - -update README - -## 7eaf663655cd395a0c3b925142b7f07901a3cca4 -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Nov 25 21:52:40 2020 +0100 -=> 7eaf663655cd395a0c3b925142b7f07901a3cca4.patch Patch -Message: tabify - -again... - -## 8d1d84dc5de30083cf64f405572ba62fe8d1da0a -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Nov 25 21:49:54 2020 +0100 -=> 8d1d84dc5de30083cf64f405572ba62fe8d1da0a.patch Patch -Message: skip empty fields for avoid empty line bloat - -channel image linked after description - -## 637126e743aa72c05564095f71020e57bb44b22f -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Nov 25 20:47:04 2020 +0100 -=> 637126e743aa72c05564095f71020e57bb44b22f.patch Patch -Message: update README - -link to the demo to the commit log - -## 2531331a6c1f20a3585c146ea73bce18bdba596c -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Nov 25 20:35:02 2020 +0100 -=> 2531331a6c1f20a3585c146ea73bce18bdba596c.patch Patch -Message: show date when feed was fetched - -## 9bdd8978ec1177627a2182317b202bce243f17ca -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Nov 25 20:28:15 2020 +0100 -=> 9bdd8978ec1177627a2182317b202bce243f17ca.patch Patch -Message: some cosmetics - -## b687e40056a5ab549f4cd0d2be68db4e1fc05e74 -Author: René Wagner <rwagner@rw-net.de> -Date: Wed Nov 25 20:02:13 2020 +0100 -=> b687e40056a5ab549f4cd0d2be68db4e1fc05e74.patch Patch -Message: implemented basic features - diff --git a/sources/orrg.git/index.gmi b/sources/orrg.git/index.gmi @@ -1,53 +0,0 @@ -```Command to clone this repository -git clone https://src.clttr.info/rwa/orrg.git -``` - -=> commits/ -=> tree/ - -# orrg (online rss feed reader for gemini) - -public instance: [gemini://orrg.clttr.info] - -orrg is a cgi script for gemini servers. It delivers an easy way to consume atom or rss feeds shipped over http(s) within gemini. - -=> gemini://gemini.circumlunar.space gemini - -Lists of popular and recently visited feeds as well as viewing a random feed help you discover new things. - -## features - -* load an atom/rss feed from gemini or https (http is deliberately not supported!) given by user input -* render feed (channel info & entrys) as a gemini site - * include links to originating site and every article - * strip html tags from item description -* lists of popular and recently visited feeds -* random feed selector - -gemini-protocol-support is currently implemented using gcat till popular perl libs have catched up. :) - -=> https://github.com/aaronjanse/gcat gcat - -## non-features - -orrg will never be a full-fletched feed aggregator with archiving and searching capabilities. - -Given this restrictions is not suitable for high traffic feeds which are updated very often. But it should work quite well for slow paced feeds of blogs and so on. - -# installation - -* setup your geminiserver with cgi enabled -* git clone the repo to the directory -* open the capsule in a gemini client - -## requirements - -* gemini server with cgi enabled (like gmnisrv or stargazer) -* Perl >= 5.28 with modules - * URI::Escape - * XML::FeedPP - * DateTime - * DateTime::Format::ISO8601 - * HTML::Strip -* Python 3 for gcat -* wget diff --git a/sources/orrg.git/tree/.gitignore.txt b/sources/orrg.git/tree/.gitignore.txt @@ -1 +0,0 @@ -data/* diff --git a/sources/orrg.git/tree/LICENSE.txt b/sources/orrg.git/tree/LICENSE.txt @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2018-2020, René Wagner -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/sources/orrg.git/tree/README.md.txt b/sources/orrg.git/tree/README.md.txt @@ -1,43 +0,0 @@ -# orrg (online rss feed reader for gemini) - -public instance: [gemini://orrg.clttr.info] - -*orrg* is a cgi script for [gemini](gemini://gemini.circumlunar.space) servers. -It delivers an easy way to consume atom or rss feeds shipped over http(s) within gemini. - -Lists of popular and recently visited feeds as well as viewing a random feed help you discover new things. - -## features - -- load an atom/rss feed from gemini or https (http is deliberately not supported!) given by user input -- render feed (channel info & entrys) as a gemini site - - include links to originating site and every article - - strip html tags from item description -- lists of popular and recently visited feeds -- random feed selector - -gemini-protocol-support is currently implemented using [gcat](https://github.com/aaronjanse/gcat) till popular perl libs have catched up. :) - -## non-features - -*orrg* will never be a full-fletched feed aggregator with archiving and searching capabilities. - -Given this restrictions is not suitable for high traffic feeds which are updated very often. But it should work quite well for slow paced feeds of blogs and so on. - -# installation - -- setup your geminiserver with cgi enabled -- `git clone` the repo to the directory -- open the capsule in a gemini client - -## requirements - -- gemini server with cgi enabled (like gmnisrv or stargazer) -- Perl >= 5.28 with modules - - URI::Escape - - XML::FeedPP - - DateTime - - DateTime::Format::ISO8601 - - HTML::Strip -- Python 3 for `gcat` -- wget diff --git a/sources/orrg.git/tree/gcat.txt b/sources/orrg.git/tree/gcat.txt @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 - -import cgi -import os -import socket -import ssl -import sys -import urllib.parse - -def absolutise_url(base, relative): - # Absolutise relative links - if "://" not in relative: - # Python's URL tools somehow only work with known schemes? - base = base.replace("gemini://","http://") - relative = urllib.parse.urljoin(base, relative) - relative = relative.replace("http://", "gemini://") - return relative - -if len(sys.argv) != 2: - print("Usage:") - print("gcat gemini://gemini.circumlunar.space") - sys.exit(1) - -url = sys.argv[1] -parsed_url = urllib.parse.urlparse(url) -if parsed_url.scheme == "": - url = "gemini://"+url - parsed_url = urllib.parse.urlparse(url) - -if parsed_url.scheme != "gemini": - print("Sorry, Gemini links only.") - sys.exit(1) -if parsed_url.port is not None: - useport = parsed_url.port -else: - useport = 1965 -# Do the Gemini transaction -while True: - s = socket.create_connection((parsed_url.hostname, useport)) - context = ssl.SSLContext() - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE - s = context.wrap_socket(s, server_hostname = parsed_url.netloc) - s.sendall((url + '\r\n').encode("UTF-8")) - # Get header and check for redirects - fp = s.makefile("rb") - header = fp.readline() - print(header.decode("UTF-8"), end="") - header = header.decode("UTF-8").strip() - status, mime = header.split()[:2] - # Handle input requests - if status.startswith("1"): - # Prompt - query = input("INPUT" + mime + "> ") - url += "?" + urllib.parse.quote(query) # Bit lazy... - # Follow redirects - elif status.startswith("3"): - url = absolutise_url(url, mime) - parsed_url = urllib.parse.urlparse(url) - # Otherwise, we're done. - else: - break -# Fail if transaction was not successful -if status.startswith("2"): - if mime.startswith("text/"): - # Decode according to declared charset - mime, mime_opts = cgi.parse_header(mime) - body = fp.read() - body = body.decode(mime_opts.get("charset","UTF-8")) - print(body, end="") - else: - print(fp.read(), end="") diff --git a/sources/orrg.git/tree/index.gmi b/sources/orrg.git/tree/index.gmi @@ -1,14 +0,0 @@ -# Tree - -Path: / - -=> .gitignore.txt .gitignore -=> LICENSE.txt LICENSE -=> README.md.txt README.md -=> gcat.txt gcat -=> index.pl.txt index.pl -=> lib/ lib/ -=> orrg.pl.txt orrg.pl -=> popular.pl.txt popular.pl -=> recent.pl.txt recent.pl -=> robots.txt.txt robots.txt diff --git a/sources/orrg.git/tree/index.pl.txt b/sources/orrg.git/tree/index.pl.txt @@ -1,51 +0,0 @@ -#!/usr/bin/perl -# Copyright René Wagner 2020 -# licenced under BSD 3-Clause licence -# https://src.clttr.info/rwa/orrg - -use strict; -use URI::Escape; -use lib 'lib/'; -use orrg; - -# enable UTF-8 mode for everything -use utf8; -binmode STDOUT, ':utf8'; -binmode STDERR, ':utf8'; - -if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') { - write_response('CGI_ERROR', 'CGI execution error', undef); -} - -if ($ENV{'QUERY_STRING'} ne '' || $ENV{'PATH_INFO'} ne '') { - write_response('NOT_FOUND', 'File not found', undef); -} - -write_response('SUCCESS', 'text/gemini', body()); - -exit; - -sub body -{ - my ($cnt, $uri, $name); - my $populars = popular_get(); - if ( defined($populars) ) { - ($cnt, $uri, $name) = split / /, @$populars[ rand @$populars ], 3; - } - - return ( - '# Welcome to orrg', - '', - 'orrg is your online rss/atom feed reader for gemini.', - '', - '=> ./orrg.pl view a feed', - '', - '## explore', - '', - '=> ./orrg.pl?'. uri_escape($uri) .' random feed', - '=> ./recent.pl top 10 recent feeds', - '=> ./popular.pl top 10 popular feeds', - '', - '', - '=> https://src.clttr.info/rwa/orrg powered by orrg'); -} diff --git a/sources/orrg.git/tree/lib/index.gmi b/sources/orrg.git/tree/lib/index.gmi @@ -1,5 +0,0 @@ -# Tree - -Path: lib/ - -=> orrg.pm.txt orrg.pm diff --git a/sources/orrg.git/tree/lib/orrg.pm.txt b/sources/orrg.git/tree/lib/orrg.pm.txt @@ -1,134 +0,0 @@ -#!/usr/bin/perl -# Copyright René Wagner 2020 -# licenced under BSD 3-Clause licence -# https://src.clttr.info/rwa/orrg - -package orrg; -use strict; -use Exporter; -our @ISA = qw(Exporter); -our @EXPORT = qw(popular_get popular_add recent_get recent_add write_response %RC); # automatically exported subs - -# enable UTF-8 mode for everything -use utf8; -binmode STDOUT, ':utf8'; -binmode STDERR, ':utf8'; - -my $recentfile = 'data/recent.txt'; -my $popularfile = 'data/popular.txt'; - -# define return codes -our %RC = ( - 'INPUT', 10, - 'SENSITIVE_INPUT', 11, - 'SUCCESS', 20, - 'TEMPORARY_REDIRECT', 30, - 'PERMANENT_REDIRECT', 31, - 'TEMPORARY_FAILURE', 40, - 'SERVER_UNAVAILABLE', 41, - 'CGI_ERROR', 42, - 'PROXY_ERROR', 43, - 'SLOW_DOWN', 44, - 'PERMANENT_FAILURE', 50, - 'NOT_FOUND', 51, - 'GONE', 52, - 'PROXY_REQUEST_REFUSE', 53, - 'BAD_REQUEST', 59, - 'CLIENT_CERT_REQUIRED', 60, - 'CERT_NOT_AUTHORISED', 61, - 'CERT_NOT_VALID', 62 -); - -sub recent_get -{ - (-f $recentfile) or return undef; - - my @recents = (); - open INFILE, '< :encoding(UTF-8)', $recentfile; - flock INFILE, 1; - while (<INFILE>) { - chomp($_); - push @recents, $_; - } - flock INFILE, 8; - close INFILE; - - return \@recents; -} - -sub recent_add -{ - my ( $uri, $name ) = @_; - my $recent = recent_get(); - - my $newline = "$uri $name"; - open OUTFILE, '> :encoding(UTF-8)', $recentfile; - flock OUTFILE, 1; - print OUTFILE "$newline\n"; - - my $c = 1; - foreach (@$recent) { - if ($newline ne $_) { - print OUTFILE "$_\n"; - $c++; - } - ($c < 10) or last; - } - flock OUTFILE, 8; - close OUTFILE; -} - -sub popular_get -{ - (-f $popularfile) or return undef; - - my @populars = (); - open INFILE, '< :encoding(UTF-8)', $popularfile; - flock INFILE, 1; - while (<INFILE>) { - chomp($_); - push @populars, $_; - } - flock INFILE, 8; - close INFILE; - - return \@populars; -} - -sub popular_add -{ - my ( $uri, $name ) = @_; - my $populars = popular_get(); - - open OUTFILE, '> :encoding(UTF-8)', $popularfile; - flock OUTFILE, 1; - - my $found = 0; - foreach (@$populars) { - my ($cnt, $popuri, $popname) = split / /, $_, 3; - if ($uri eq $popuri) { - $cnt++; - $found = 1; - } - print OUTFILE "$cnt $popuri $popname\n"; - } - $found or print OUTFILE "1 $uri $name"; - flock OUTFILE, 8; - close OUTFILE; -} - -sub write_response -{ - my ($returncode, $meta, @content) = @_; - - defined($RC{$returncode}) or die "Unknown response code!"; - - printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta); - foreach (@content) { - print("$_\r\n"); - } - - exit; -} - -1; diff --git a/sources/orrg.git/tree/orrg.pl.txt b/sources/orrg.git/tree/orrg.pl.txt @@ -1,130 +0,0 @@ -#!/usr/bin/perl -# Copyright René Wagner 2020 -# licenced under BSD 3-Clause licence -# https://src.clttr.info/rwa/orrg - -use strict; -use URI::Escape; -use XML::FeedPP; -use HTML::Strip; -use DateTime; -use DateTime::Format::ISO8601; -use POSIX qw(strftime); -use lib 'lib/'; -use orrg; - -# enable UTF-8 mode for everything -use utf8; -binmode STDOUT, ':utf8'; -binmode STDERR, ':utf8'; - -if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') -{ - write_response('CGI_ERROR', '', undef); -} - -my $query = lc(uri_unescape($ENV{'QUERY_STRING'})); - -if ($query eq '' || $query !~ /^(https|gemini)\:\/\//) { - write_response('INPUT', 'Paste the URI of the rss feed you want to read', undef); -} - -write_response('SUCCESS', 'text/gemini', create_response($query)); - -exit; - -sub create_response -{ - my ( $qs ) = @_; - my @body = (); - - my $content = feed_get($qs); - if ( !defined($content) || $content eq '' ) { - push @body, ('# orrg error', '', 'The requested feed could not be loaded. :(', '', '=> '. $qs .' open feed in browser'); - } else { - my $feed = feed_parse($content); - - if (defined($feed)) { - recent_add($qs, trim_ws($feed->title)); - popular_add($qs, trim_ws($feed->title)); - - push @body, '# '. trim_ws($feed->title); - push @body, 'fetched '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); - push @body, ''; - $feed->description eq '' or push @body, trim_ws($feed->description); - $feed->image eq '' or push @body, '=> '. $feed->image .' feed image'; - $feed->link eq '' or push @body, ('=> '.$feed->link.' open website', ''); - - foreach my $it ($feed->get_item()) { push @body, @{item($it)}; } - } else { - push @body, ('# orrg error', '', 'The requested feed could be loaded but not parsed. :(', '', '=> '. $qs .' open feed in native client'); - } - } - push @body, ('', '', '=> ./index.pl [home]'); - return @body; -} - -sub item -{ - my ($it) = @_; - - my $hs = HTML::Strip->new(emit_spaces => 0, auto_reset => 1); - my @item = (); - my $title = trim_ws($it->title); - my $link = trim_ws($it->link); - my $desc = $it->description; - if ($desc ne '') { - $desc =~ s/\<li\>/* /igm; - $desc =~ s/\<h[1-2][^\>]+\>/### /igm; - $desc =~ s/\<br \/\>/\r\n/igm; - $desc =~ s/\<pre.*\>/\r\n```\r\n/igm; - $desc =~ s/\<\/pre\>/\r\n```\r\n/igm; - $desc = $hs->parse($desc); - } - push @item, (($desc ne '' && $desc ne $title) || $it->pubDate ne '')? '## '. $title : $title; - if ($it->pubDate ne '') { - my $dt = DateTime::Format::ISO8601->parse_datetime($it->pubDate); - push @item, 'published '. strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt->epoch)); - push @item, ''; - } - if ($desc ne '' && $desc ne $title) { - push @item, $desc; - } - $link eq '' or push @item, ('=> '.$link.' open entry in browser', ''); - - return \@item; -} - -sub trim_ws -{ - my ( $string ) = @_; - $string =~ s/^\s+\*/*/igm; - $string =~ s/\s+$//igm; - $string =~ s/^\s+//igm; - $string =~ s/^\r\n$//igm; - return $string; -} - -sub feed_get -{ - my ( $query ) = @_; - - my $content; - if ( $query =~ /^https\:\/\// ) { $content = `wget -qO - $query`; } - if ( $query =~ /^gemini\:\/\// ) { - $content = `./gcat $query`; - $content =~ /20\W/ or return undef; - $content =~ s/^[0-9]{0,2}\W.+\r\n//; - } - - return $content; -} - -sub feed_parse -{ - my ($content) = @_; - - my $feed = XML::FeedPP->new($content, -type => 'string', utf8_flag => 1); - if (defined($feed)) { $feed->sort_item(); } - return $feed; -} diff --git a/sources/orrg.git/tree/popular.pl.txt b/sources/orrg.git/tree/popular.pl.txt @@ -1,49 +0,0 @@ -#!/usr/bin/perl -# Copyright René Wagner 2020 -# licenced under BSD 3-Clause licence -# https://src.clttr.info/rwa/orrg - -use strict; -no warnings 'experimental'; -use URI::Escape; -use lib 'lib/'; -use orrg; - -# enable UTF-8 mode for everything -use utf8; -binmode STDOUT, ':utf8'; -binmode STDERR, ':utf8'; - -if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') -{ - write_response('CGI_ERROR', '', undef); -} - -write_response('SUCCESS', 'text/gemini', create_response()); - -exit; - -sub create_response -{ - my @body = (); - - push @body, ('# most popular feeds', ''); - - my $populars = popular_get(); - if ( defined($populars) ) { - my @sorted = sort { $a <=> $b } @$populars; - my $c = 0; - foreach (reverse @sorted) { - my ($cnt, $uri, $name) = split / /, $_, 3; - push @body, '=> ./orrg.pl?'. uri_escape($uri) .' '. ($name eq '' ? $uri : $name); - - ++$c < 10 or last; - } - } else { - push @body, 'No feeds found'; - } - - - push @body, ('', '', '=> ./index.pl [home]'); - return @body; -} diff --git a/sources/orrg.git/tree/recent.pl.txt b/sources/orrg.git/tree/recent.pl.txt @@ -1,44 +0,0 @@ -#!/usr/bin/perl -# Copyright René Wagner 2020 -# licenced under BSD 3-Clause licence -# https://src.clttr.info/rwa/orrgg - -use strict; -no warnings 'experimental'; -use URI::Escape; -use lib 'lib/'; -use orrg; - -# enable UTF-8 mode for everything -use utf8; -binmode STDOUT, ':utf8'; -binmode STDERR, ':utf8'; - -if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI') -{ - write_response('CGI_ERROR', '', undef); -} - -write_response('SUCCESS', 'text/gemini', create_response()); - -exit; - -sub create_response -{ - my @body = (); - - push @body, ('# recently visited feeds', ''); - - my $recents = recent_get(); - if ( defined($recents) ) { - foreach (@$recents) { - my ($uri, $name) = split / /, $_, 2; - push @body, '=> ./orrg.pl?'. uri_escape($uri) .' '. ($name eq '' ? $uri : $name); - } - } else { - push @body, 'No feeds found'; - } - - push @body, ('', '', '=> ./index.pl [home]'); - return @body; -} diff --git a/sources/orrg.git/tree/robots.txt.txt b/sources/orrg.git/tree/robots.txt.txt @@ -1,2 +0,0 @@ -User-agent: * -Disallow: /orrg.pl