offpunk 2.3__tar.gz → 2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- offpunk-2.4/.build.yml +21 -0
- {offpunk-2.3 → offpunk-2.4}/CHANGELOG +15 -0
- {offpunk-2.3 → offpunk-2.4}/PKG-INFO +6 -5
- {offpunk-2.3 → offpunk-2.4}/README.md +4 -3
- {offpunk-2.3 → offpunk-2.4}/ansicat.py +22 -17
- {offpunk-2.3 → offpunk-2.4}/man/ansicat.1 +1 -1
- {offpunk-2.3 → offpunk-2.4}/man/netcache.1 +1 -1
- {offpunk-2.3 → offpunk-2.4}/man/offpunk.1 +1 -1
- {offpunk-2.3 → offpunk-2.4}/man/opnk.1 +1 -1
- {offpunk-2.3 → offpunk-2.4}/netcache.py +137 -25
- {offpunk-2.3 → offpunk-2.4}/offpunk.py +73 -6
- {offpunk-2.3 → offpunk-2.4}/opnk.py +3 -2
- {offpunk-2.3 → offpunk-2.4}/pyproject.toml +1 -1
- offpunk-2.4/screenshots/decvt220.jpg +0 -0
- offpunk-2.4/tutorial/bookmarks.gmi +37 -0
- offpunk-2.4/tutorial/firststeps.gmi +55 -0
- offpunk-2.4/tutorial/frozen.gmi +29 -0
- offpunk-2.4/tutorial/index.gmi +46 -0
- offpunk-2.4/tutorial/install.gmi +38 -0
- offpunk-2.4/tutorial/lists.gmi +55 -0
- offpunk-2.4/tutorial/make_website.py +168 -0
- offpunk-2.4/tutorial/myfirstlink.gmi +14 -0
- offpunk-2.4/tutorial/offline.gmi +47 -0
- offpunk-2.4/tutorial/open.gmi +25 -0
- offpunk-2.4/tutorial/page_template.html +66 -0
- offpunk-2.4/tutorial/subscriptions.gmi +65 -0
- offpunk-2.4/tutorial/tour.gmi +29 -0
- offpunk-2.4/tutorial/tour1.gmi +10 -0
- offpunk-2.4/tutorial/tour2.gmi +12 -0
- offpunk-2.4/tutorial/tour3.gmi +13 -0
- offpunk-2.4/tutorial/view.gmi +32 -0
- offpunk-2.4/tutorial/whatisoffpunk.gmi +28 -0
- offpunk-2.4/tutorial/workflow_ploum.gmi +178 -0
- {offpunk-2.3 → offpunk-2.4}/.gitignore +0 -0
- {offpunk-2.3 → offpunk-2.4}/CONTRIBUTORS +0 -0
- {offpunk-2.3 → offpunk-2.4}/LICENSE +0 -0
- {offpunk-2.3 → offpunk-2.4}/cert_migration.py +0 -0
- {offpunk-2.3 → offpunk-2.4}/debug.sh +0 -0
- {offpunk-2.3 → offpunk-2.4}/doc/config.gmi +0 -0
- {offpunk-2.3 → offpunk-2.4}/doc/dev.gmi +0 -0
- {offpunk-2.3 → offpunk-2.4}/doc/index.gmi +0 -0
- {offpunk-2.3 → offpunk-2.4}/doc/install.gmi +0 -0
- {offpunk-2.3 → offpunk-2.4}/doc/lists.gmi +0 -0
- {offpunk-2.3 → offpunk-2.4}/doc/offline.gmi +0 -0
- {offpunk-2.3 → offpunk-2.4}/doc/shell.gmi +0 -0
- {offpunk-2.3 → offpunk-2.4}/doc/tutorial.gmi +0 -0
- {offpunk-2.3 → offpunk-2.4}/netcache_migration.py +0 -0
- {offpunk-2.3 → offpunk-2.4}/offblocklist.py +0 -0
- {offpunk-2.3 → offpunk-2.4}/offthemes.py +0 -0
- {offpunk-2.3 → offpunk-2.4}/offutils.py +0 -0
- {offpunk-2.3 → offpunk-2.4}/requirements.txt +0 -0
- /offpunk-2.3/screenshot_offpunk1.png → /offpunk-2.4/screenshots/1.png +0 -0
- /offpunk-2.3/screenshot_offpunk2.png → /offpunk-2.4/screenshots/2.png +0 -0
- {offpunk-2.3 → offpunk-2.4}/ubuntu_dependencies.txt +0 -0
offpunk-2.4/.build.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
image: alpine/3.19
|
|
2
|
+
oauth: pages.sr.ht/PAGES:RW
|
|
3
|
+
packages:
|
|
4
|
+
- hut
|
|
5
|
+
environment:
|
|
6
|
+
site1: offpunk.net
|
|
7
|
+
tasks:
|
|
8
|
+
- package-gemini: |
|
|
9
|
+
cd offpunk/tutorial
|
|
10
|
+
tar -cvzh . > ../../capsule.tar.gz
|
|
11
|
+
- deploy-gemini: |
|
|
12
|
+
hut pages publish capsule.tar.gz -p GEMINI -d $site1
|
|
13
|
+
- package-html: |
|
|
14
|
+
mkdir public_html
|
|
15
|
+
cd offpunk/tutorial
|
|
16
|
+
python make_website.py
|
|
17
|
+
cd ../../public_html
|
|
18
|
+
ln -s ../offpunk/screenshots .
|
|
19
|
+
tar -cvzh . > ../site.tar.gz
|
|
20
|
+
- deploy-html: |
|
|
21
|
+
hut pages publish site.tar.gz -d $site1
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Offpunk History
|
|
2
2
|
|
|
3
|
+
## 2.4 - November 21st 2024
|
|
4
|
+
NEW WEBSITE: Official homepage is now https://offpunk.net (or gemini://offpunk.net). Sources are in the /tutorial/ folder and contributions are welcome.
|
|
5
|
+
NEW FEATURE: This release includes work by Bert Livens to add gemini client-side certificates (see "help certs"). This means you can browse gemini capsule while being identified (such as astrobotany)
|
|
6
|
+
- Deprecation warning if using Chafa < 1.10
|
|
7
|
+
- introducing the "tutorial" command (which is only a link to offpunk.net for now)
|
|
8
|
+
- netcache: use client-certificate when going to a url like gemini://username@site.net (by Bert Livens)
|
|
9
|
+
- offpunk/netcache: added the "cert" command to list and create client certificates (Bert Livens)
|
|
10
|
+
- "open" now accept integer as parameters to open links (suggested by Matthieu Rakotojaona)
|
|
11
|
+
- fix cache not being properly accessed when server redirect to same host with standard port (gemini.ucant.org)
|
|
12
|
+
- fix crash when expired certificate due to not_valid_after deprecation
|
|
13
|
+
- fix crash in netcache when a port cannot be parsed in the URL
|
|
14
|
+
- fix parameter "interactive=False" not being sent to gemini redirections
|
|
15
|
+
- fix problem with non-integer version of less (Patch by Peter Cock)
|
|
16
|
+
- Gopher: hide lines starting with TAB (drkhsh.at), reported by Dylan D’Silva
|
|
17
|
+
|
|
3
18
|
## 2.3 - June 29th 2024
|
|
4
19
|
- Wayland clipboard support through wl-clipboard (new suggested dependency)
|
|
5
20
|
- Xclip clipboard support (in case xsel is missing)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: offpunk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4
|
|
4
4
|
Dynamic: Summary
|
|
5
|
-
Project-URL: Homepage, https://
|
|
5
|
+
Project-URL: Homepage, https://offpunk.net/
|
|
6
6
|
Project-URL: Source, https://git.sr.ht/~lioploum/offpunk
|
|
7
7
|
Project-URL: Bug Tracker, https://todo.sr.ht/~lioploum/offpunk
|
|
8
8
|
Author-email: Solderpunk <solderpunk@sdf.org>, "Lionel Dricot (Ploum)" <offpunk2@ploum.eu>
|
|
@@ -702,10 +702,11 @@ A command-line and offline-first smolnet browser/feed reader for Gemini, Gopher,
|
|
|
702
702
|
|
|
703
703
|
The goal of Offpunk is to be able to synchronise your content once (a day, a week, a month) and then browse/organise it while staying disconnected.
|
|
704
704
|
|
|
705
|
-
Official
|
|
705
|
+
Official page : https://offpunk.net
|
|
706
|
+
Development (repository/mailing lists) : https://sr.ht/~lioploum/offpunk/
|
|
706
707
|
|
|
707
|
-

|
|
709
|
+

|
|
709
710
|
|
|
710
711
|
Offpunk is a fork of the original [AV-98](https://tildegit.org/solderpunk/AV-98) by Solderpunk and was originally called AV-98-offline as an experimental branch.
|
|
711
712
|
|
|
@@ -4,10 +4,11 @@ A command-line and offline-first smolnet browser/feed reader for Gemini, Gopher,
|
|
|
4
4
|
|
|
5
5
|
The goal of Offpunk is to be able to synchronise your content once (a day, a week, a month) and then browse/organise it while staying disconnected.
|
|
6
6
|
|
|
7
|
-
Official
|
|
7
|
+
Official page : https://offpunk.net
|
|
8
|
+
Development (repository/mailing lists) : https://sr.ht/~lioploum/offpunk/
|
|
8
9
|
|
|
9
|
-

|
|
11
|
+

|
|
11
12
|
|
|
12
13
|
Offpunk is a fork of the original [AV-98](https://tildegit.org/solderpunk/AV-98) by Solderpunk and was originally called AV-98-offline as an experimental branch.
|
|
13
14
|
|
|
@@ -118,6 +118,8 @@ def inline_image(img_file,width):
|
|
|
118
118
|
if _HAS_CHAFA:
|
|
119
119
|
if _HAS_PIL and not _NEW_CHAFA:
|
|
120
120
|
# this code is a hack to remove frames from animated gif
|
|
121
|
+
print("WARNING: support for chafa < 1.10 will soon be removed")
|
|
122
|
+
print("If you can’t upgrade chafa or timg, please contact offpunk developers")
|
|
121
123
|
img_obj = Image.open(img_file)
|
|
122
124
|
if hasattr(img_obj,"n_frames") and img_obj.n_frames > 1:
|
|
123
125
|
# we remove all frames but the first one
|
|
@@ -784,22 +786,25 @@ class GopherRenderer(AbstractRenderer):
|
|
|
784
786
|
parts = parts[:-1]
|
|
785
787
|
if len(parts) == 4:
|
|
786
788
|
name,path,host,port = parts
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
port
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
789
|
+
#If line starts with TAB, there’s no name.
|
|
790
|
+
#We thus hide this line
|
|
791
|
+
if name:
|
|
792
|
+
itemtype = name[0]
|
|
793
|
+
name = name[1:]
|
|
794
|
+
if port == "70":
|
|
795
|
+
port = ""
|
|
796
|
+
else:
|
|
797
|
+
port = ":%s"%port
|
|
798
|
+
if itemtype == "h" and path.startswith("URL:"):
|
|
799
|
+
url = path[4:]
|
|
800
|
+
else:
|
|
801
|
+
url = "gopher://%s%s/%s%s" %(host,port,itemtype,path)
|
|
802
|
+
url = url.replace(" ","%20")
|
|
803
|
+
linkline = url + " " + name
|
|
804
|
+
links.append(linkline)
|
|
805
|
+
number = len(links) + startlinks
|
|
806
|
+
towrap = "[%s] "%str(number)+ name
|
|
807
|
+
r.add_text(towrap)
|
|
803
808
|
else:
|
|
804
809
|
r.add_text(line)
|
|
805
810
|
return r.get_final(),links
|
|
@@ -1359,7 +1364,7 @@ def get_mime(path,url=None):
|
|
|
1359
1364
|
mime = "mailto"
|
|
1360
1365
|
elif os.path.isdir(path):
|
|
1361
1366
|
mime = "Local Folder"
|
|
1362
|
-
elif path.endswith(".gmi"):
|
|
1367
|
+
elif path.endswith(".gmi") or path.endswith(".gemini"):
|
|
1363
1368
|
mime = "text/gemini"
|
|
1364
1369
|
elif path.endswith("gophermap"):
|
|
1365
1370
|
mime = "text/gopher"
|
|
@@ -68,7 +68,7 @@ Maximum age (in second) of the cached version before redownloading a new version
|
|
|
68
68
|
.Xr migrate-offpunk-cache 1 ,
|
|
69
69
|
.Xr offpunk 1 ,
|
|
70
70
|
.Xr opnk 1 ,
|
|
71
|
-
.Lk https://
|
|
71
|
+
.Lk https://offpunk.net/
|
|
72
72
|
.
|
|
73
73
|
.Sh AUTHORS
|
|
74
74
|
.An Lionel Dricot (Ploum) Aq Mt offpunk2 at ploum.eu
|
|
@@ -51,7 +51,7 @@ Maximum age (in second) of the cached version before redownloading a new version
|
|
|
51
51
|
.Xr migrate-offpunk-cache 1 ,
|
|
52
52
|
.Xr netcache 1 ,
|
|
53
53
|
.Xr offpunk 1 ,
|
|
54
|
-
.Lk https://
|
|
54
|
+
.Lk https://offpunk.net/
|
|
55
55
|
.
|
|
56
56
|
.Sh AUTHORS
|
|
57
57
|
.An Lionel Dricot (Ploum) Aq Mt offpunk2 at ploum.eu
|
|
@@ -24,6 +24,9 @@ except ModuleNotFoundError:
|
|
|
24
24
|
try:
|
|
25
25
|
from cryptography import x509
|
|
26
26
|
from cryptography.hazmat.backends import default_backend
|
|
27
|
+
from cryptography.hazmat.primitives import hashes
|
|
28
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
29
|
+
from cryptography.hazmat.primitives import serialization
|
|
27
30
|
_HAS_CRYPTOGRAPHY = True
|
|
28
31
|
_BACKEND = default_backend()
|
|
29
32
|
except(ModuleNotFoundError,ImportError):
|
|
@@ -158,7 +161,10 @@ def get_cache_path(url,add_index=True):
|
|
|
158
161
|
local = False
|
|
159
162
|
# Convert unicode hostname to punycode using idna RFC3490
|
|
160
163
|
host = parsed.netloc #.encode("idna").decode()
|
|
161
|
-
|
|
164
|
+
try:
|
|
165
|
+
port = parsed.port or standard_ports.get(scheme, 0)
|
|
166
|
+
except ValueError:
|
|
167
|
+
port = standard_ports.get(scheme,0)
|
|
162
168
|
# special gopher selector case
|
|
163
169
|
if scheme == "gopher":
|
|
164
170
|
if len(parsed.path) >= 2:
|
|
@@ -462,7 +468,7 @@ def _validate_cert(address, host, cert,accept_bad_ssl=False,automatic_choice=Non
|
|
|
462
468
|
previously encountered a different certificate for this IP address and
|
|
463
469
|
hostname.
|
|
464
470
|
"""
|
|
465
|
-
now = datetime.datetime.
|
|
471
|
+
now = datetime.datetime.now(datetime.UTC)
|
|
466
472
|
if _HAS_CRYPTOGRAPHY:
|
|
467
473
|
# Using the cryptography module we can get detailed access
|
|
468
474
|
# to the properties of even self-signed certs, unlike in
|
|
@@ -544,7 +550,7 @@ def _validate_cert(address, host, cert,accept_bad_ssl=False,automatic_choice=Non
|
|
|
544
550
|
# Load the most frequently seen certificate to see if it has
|
|
545
551
|
# expired
|
|
546
552
|
previous_cert = x509.load_der_x509_certificate(previous_cert, _BACKEND)
|
|
547
|
-
previous_ttl = previous_cert.
|
|
553
|
+
previous_ttl = previous_cert.not_valid_after_utc - now
|
|
548
554
|
print(previous_ttl)
|
|
549
555
|
|
|
550
556
|
print("****************************************")
|
|
@@ -589,12 +595,101 @@ def _validate_cert(address, host, cert,accept_bad_ssl=False,automatic_choice=Non
|
|
|
589
595
|
with open(os.path.join(certcache, fingerprint+".crt"), "wb") as fp:
|
|
590
596
|
fp.write(cert)
|
|
591
597
|
|
|
598
|
+
def _get_client_certkey(site_id: str, host: str):
|
|
599
|
+
# returns {cert: str, key: str}
|
|
600
|
+
certdir = os.path.join(xdg("data"), "certs", host)
|
|
601
|
+
certf = os.path.join(certdir, "%s.cert" % site_id)
|
|
602
|
+
keyf = os.path.join(certdir, "%s.key" % site_id)
|
|
603
|
+
if not os.path.exists(certf) or not os.path.exists(keyf):
|
|
604
|
+
if host != "":
|
|
605
|
+
split = host.split(".")
|
|
606
|
+
#if len(split) > 2: # Why not allow a global identity? Maybe I want
|
|
607
|
+
# to login to all sites with the same
|
|
608
|
+
# certificate.
|
|
609
|
+
return _get_client_certkey(site_id, ".".join(split[1:]))
|
|
610
|
+
return None
|
|
611
|
+
certkey = dict(cert=certf, key=keyf)
|
|
612
|
+
return certkey
|
|
613
|
+
|
|
614
|
+
def _get_site_ids(url: str):
|
|
615
|
+
newurl = normalize_url(url)
|
|
616
|
+
u = urllib.parse.urlparse(newurl)
|
|
617
|
+
if u.scheme == "gemini" and u.username == None:
|
|
618
|
+
certdir = os.path.join(xdg("data"), "certs")
|
|
619
|
+
netloc_parts = u.netloc.split(".")
|
|
620
|
+
site_ids = []
|
|
621
|
+
|
|
622
|
+
for i in range(len(netloc_parts), 0, -1):
|
|
623
|
+
lasti = ".".join(netloc_parts[-i:])
|
|
624
|
+
direc = os.path.join(certdir, lasti)
|
|
625
|
+
|
|
626
|
+
for certfile in glob.glob(os.path.join(direc, "*.cert")):
|
|
627
|
+
site_id = certfile.split('/')[-1].split(".")[-2]
|
|
628
|
+
site_ids.append(site_id)
|
|
629
|
+
return site_ids
|
|
630
|
+
else:
|
|
631
|
+
return []
|
|
632
|
+
|
|
633
|
+
def create_certificate(name: str, days: int, hostname: str):
|
|
634
|
+
key = rsa.generate_private_key(
|
|
635
|
+
public_exponent = 65537,
|
|
636
|
+
key_size = 2048)
|
|
637
|
+
sitecertdir = os.path.join(xdg("data"), "certs", hostname)
|
|
638
|
+
keyfile = os.path.join(sitecertdir, name+".key")
|
|
639
|
+
# create the directory of it doesn't exist
|
|
640
|
+
os.makedirs(sitecertdir, exist_ok=True)
|
|
641
|
+
with open(keyfile, "wb") as f:
|
|
642
|
+
f.write(key.private_bytes(
|
|
643
|
+
encoding=serialization.Encoding.PEM,
|
|
644
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
645
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
646
|
+
))
|
|
647
|
+
xname = x509.Name([
|
|
648
|
+
x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, name),
|
|
649
|
+
])
|
|
650
|
+
# generate the cert, valid a week ago (timekeeping is hard, let's give it a
|
|
651
|
+
# little margin). issuer and subject are your name
|
|
652
|
+
cert = (x509.CertificateBuilder()
|
|
653
|
+
.subject_name(xname)
|
|
654
|
+
.issuer_name(xname)
|
|
655
|
+
.public_key(key.public_key())
|
|
656
|
+
.serial_number(x509.random_serial_number())
|
|
657
|
+
.not_valid_before(datetime.datetime.utcnow() -
|
|
658
|
+
datetime.timedelta(days=7))
|
|
659
|
+
.not_valid_after(datetime.datetime.utcnow() +
|
|
660
|
+
datetime.timedelta(days=days))
|
|
661
|
+
.sign(key, hashes.SHA256())
|
|
662
|
+
)
|
|
663
|
+
certfile = os.path.join(sitecertdir, name + ".cert")
|
|
664
|
+
with open(certfile, "wb") as f:
|
|
665
|
+
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
|
666
|
+
|
|
667
|
+
def get_certs(url: str):
|
|
668
|
+
u = urllib.parse.urlparse(normalize_url(url))
|
|
669
|
+
if u.scheme == "gemini":
|
|
670
|
+
certdir = os.path.join(xdg("data"), "certs")
|
|
671
|
+
netloc_parts = u.netloc.split(".")
|
|
672
|
+
site_ids = []
|
|
673
|
+
if '@' in netloc_parts[0]:
|
|
674
|
+
netloc_parts[0] = netloc_parts[0].split('@')[1]
|
|
675
|
+
|
|
676
|
+
for i in range(len(netloc_parts), 0, -1):
|
|
677
|
+
lasti = ".".join(netloc_parts[-i:])
|
|
678
|
+
direc = os.path.join(certdir, lasti)
|
|
679
|
+
for certfile in glob.glob(os.path.join(direc, "*.cert")):
|
|
680
|
+
site_id = certfile.split('/')[-1].split(".")[-2]
|
|
681
|
+
site_ids.append(site_id)
|
|
682
|
+
return site_ids
|
|
683
|
+
else:
|
|
684
|
+
return []
|
|
685
|
+
|
|
592
686
|
def _fetch_gemini(url,timeout=DEFAULT_TIMEOUT,interactive=True,accept_bad_ssl_certificates=False,\
|
|
593
687
|
**kwargs):
|
|
594
688
|
cache = None
|
|
595
689
|
newurl = url
|
|
596
690
|
url_parts = urllib.parse.urlparse(url)
|
|
597
691
|
host = url_parts.hostname
|
|
692
|
+
site_id = url_parts.username
|
|
598
693
|
port = url_parts.port or standard_ports["gemini"]
|
|
599
694
|
path = url_parts.path or "/"
|
|
600
695
|
query = url_parts.query
|
|
@@ -622,8 +717,16 @@ def _fetch_gemini(url,timeout=DEFAULT_TIMEOUT,interactive=True,accept_bad_ssl_ce
|
|
|
622
717
|
# Prepare TLS context
|
|
623
718
|
protocol = ssl.PROTOCOL_TLS_CLIENT if sys.version_info.minor >=6 else ssl.PROTOCOL_TLSv1_2
|
|
624
719
|
context = ssl.SSLContext(protocol)
|
|
625
|
-
context.check_hostname=False
|
|
720
|
+
context.check_hostname = False
|
|
626
721
|
context.verify_mode = ssl.CERT_NONE
|
|
722
|
+
|
|
723
|
+
# When using an identity, use the certificate and key
|
|
724
|
+
if site_id:
|
|
725
|
+
certkey = _get_client_certkey(site_id, host)
|
|
726
|
+
if certkey:
|
|
727
|
+
context.load_cert_chain(certkey["cert"], certkey["key"])
|
|
728
|
+
else:
|
|
729
|
+
print("This identity doesn't exist for this site (or is disabled).")
|
|
627
730
|
# Impose minimum TLS version
|
|
628
731
|
## In 3.7 and above, this is easy...
|
|
629
732
|
if sys.version_info.minor >= 7:
|
|
@@ -665,15 +768,21 @@ def _fetch_gemini(url,timeout=DEFAULT_TIMEOUT,interactive=True,accept_bad_ssl_ce
|
|
|
665
768
|
_validate_cert(address[4][0], host, cert,automatic_choice="y")
|
|
666
769
|
# Send request and wrap response in a file descriptor
|
|
667
770
|
url = urllib.parse.urlparse(url)
|
|
668
|
-
|
|
771
|
+
new_host = host
|
|
669
772
|
#Handle IPV6 hostname
|
|
670
|
-
if ":" in
|
|
671
|
-
|
|
773
|
+
if ":" in new_host:
|
|
774
|
+
new_host = "[" + new_host + "]"
|
|
672
775
|
if port != standard_ports["gemini"]:
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
776
|
+
new_host += ":" + str(port)
|
|
777
|
+
url_no_username = urllib.parse.urlunparse(url._replace(netloc=new_host))
|
|
778
|
+
|
|
779
|
+
if site_id:
|
|
780
|
+
url = urllib.parse.urlunparse(url._replace(netloc=site_id+"@"+new_host))
|
|
781
|
+
else:
|
|
782
|
+
url = url_no_username
|
|
783
|
+
|
|
784
|
+
s.sendall((url_no_username + CRLF).encode("UTF-8"))
|
|
785
|
+
f = s.makefile(mode = "rb")
|
|
677
786
|
## end of send_request in AV98
|
|
678
787
|
# Spec dictates <META> should not exceed 1024 bytes,
|
|
679
788
|
# so maximum valid header length is 1027 bytes.
|
|
@@ -702,7 +811,6 @@ def _fetch_gemini(url,timeout=DEFAULT_TIMEOUT,interactive=True,accept_bad_ssl_ce
|
|
|
702
811
|
if status == "11":
|
|
703
812
|
user_input = getpass.getpass("> ")
|
|
704
813
|
else:
|
|
705
|
-
#TODO:FIXME we should not ask for user input while non-interactive
|
|
706
814
|
user_input = input("> ")
|
|
707
815
|
newurl = url.split("?")[0]
|
|
708
816
|
return _fetch_gemini(newurl+"?"+user_input)
|
|
@@ -738,14 +846,13 @@ def _fetch_gemini(url,timeout=DEFAULT_TIMEOUT,interactive=True,accept_bad_ssl_ce
|
|
|
738
846
|
# if status == "31":
|
|
739
847
|
# # Permanent redirect
|
|
740
848
|
# self.permanent_redirects[gi.url] = new_gi.url
|
|
741
|
-
return _fetch_gemini(newurl)
|
|
849
|
+
return _fetch_gemini(newurl,interactive=interactive)
|
|
742
850
|
# Errors
|
|
743
851
|
elif status.startswith("4") or status.startswith("5"):
|
|
744
852
|
raise RuntimeError(meta)
|
|
745
853
|
# Client cert
|
|
746
854
|
elif status.startswith("6"):
|
|
747
|
-
error = "
|
|
748
|
-
error += "See bug #31 for discussion about the problem"
|
|
855
|
+
error = "You need to provide a client-certificate to access this page."
|
|
749
856
|
raise RuntimeError(error)
|
|
750
857
|
# Invalid status
|
|
751
858
|
elif not status.startswith("2"):
|
|
@@ -777,7 +884,7 @@ def _fetch_gemini(url,timeout=DEFAULT_TIMEOUT,interactive=True,accept_bad_ssl_ce
|
|
|
777
884
|
else:
|
|
778
885
|
body = fbody
|
|
779
886
|
cache = write_body(url,body,mime)
|
|
780
|
-
return cache,
|
|
887
|
+
return cache,url
|
|
781
888
|
|
|
782
889
|
|
|
783
890
|
def fetch(url,offline=False,download_image_first=True,images_mode="readable",validity=0,**kwargs):
|
|
@@ -785,7 +892,7 @@ def fetch(url,offline=False,download_image_first=True,images_mode="readable",val
|
|
|
785
892
|
newurl = url
|
|
786
893
|
path=None
|
|
787
894
|
print_error = "print_error" in kwargs.keys() and kwargs["print_error"]
|
|
788
|
-
#
|
|
895
|
+
#First, we look if we have a valid cache, even if offline
|
|
789
896
|
#If we are offline, any cache is better than nothing
|
|
790
897
|
if is_cache_valid(url,validity=validity) or (offline and is_cache_valid(url,validity=0)):
|
|
791
898
|
path = get_cache_path(url)
|
|
@@ -803,13 +910,13 @@ def fetch(url,offline=False,download_image_first=True,images_mode="readable",val
|
|
|
803
910
|
path = None
|
|
804
911
|
elif scheme in ("http","https"):
|
|
805
912
|
if _DO_HTTP:
|
|
806
|
-
path=_fetch_http(
|
|
913
|
+
path=_fetch_http(newurl,**kwargs)
|
|
807
914
|
else:
|
|
808
915
|
print("HTTP requires python-requests")
|
|
809
916
|
elif scheme == "gopher":
|
|
810
|
-
path=_fetch_gopher(
|
|
917
|
+
path=_fetch_gopher(newurl,**kwargs)
|
|
811
918
|
elif scheme == "finger":
|
|
812
|
-
path=_fetch_finger(
|
|
919
|
+
path=_fetch_finger(newurl,**kwargs)
|
|
813
920
|
elif scheme == "gemini":
|
|
814
921
|
path,newurl=_fetch_gemini(url,**kwargs)
|
|
815
922
|
elif scheme == "spartan":
|
|
@@ -819,7 +926,7 @@ def fetch(url,offline=False,download_image_first=True,images_mode="readable",val
|
|
|
819
926
|
except UserAbortException:
|
|
820
927
|
return None, newurl
|
|
821
928
|
except Exception as err:
|
|
822
|
-
cache = set_error(
|
|
929
|
+
cache = set_error(newurl, err)
|
|
823
930
|
# Print an error message
|
|
824
931
|
# we fail silently when sync_only
|
|
825
932
|
if isinstance(err, socket.gaierror):
|
|
@@ -881,12 +988,14 @@ def main():
|
|
|
881
988
|
|
|
882
989
|
descri="Netcache is a command-line tool to retrieve, cache and access networked content.\n\
|
|
883
990
|
By default, netcache will returns a cached version of a given URL, downloading it \
|
|
884
|
-
only if
|
|
885
|
-
netcache downloads the content only if the existing cache is older than the validity."
|
|
991
|
+
only if a cache version doesn't exist. A validity duration, in seconds, can also \
|
|
992
|
+
be given so netcache downloads the content only if the existing cache is older than the validity."
|
|
886
993
|
# Parse arguments
|
|
887
994
|
parser = argparse.ArgumentParser(prog="netcache",description=descri)
|
|
888
995
|
parser.add_argument("--path", action="store_true",
|
|
889
996
|
help="return path to the cache instead of the content of the cache")
|
|
997
|
+
parser.add_argument("--ids", action="store_true",
|
|
998
|
+
help="return a list of id's for the gemini-site instead of the content of the cache")
|
|
890
999
|
parser.add_argument("--offline", action="store_true",
|
|
891
1000
|
help="Do not attempt to download, return cached version or error")
|
|
892
1001
|
parser.add_argument("--max-size", type=int,
|
|
@@ -902,17 +1011,20 @@ def main():
|
|
|
902
1011
|
# --validity : returns the date of the cached version, Null if no version
|
|
903
1012
|
# --force-download : download and replace cache, even if valid
|
|
904
1013
|
args = parser.parse_args()
|
|
905
|
-
|
|
906
1014
|
param = {}
|
|
907
|
-
|
|
1015
|
+
|
|
908
1016
|
for u in args.url:
|
|
909
1017
|
if args.offline:
|
|
910
1018
|
path = get_cache_path(u)
|
|
1019
|
+
elif args.ids:
|
|
1020
|
+
ids = _get_site_ids(u)
|
|
911
1021
|
else:
|
|
912
1022
|
path,url = fetch(u,max_size=args.max_size,timeout=args.timeout,\
|
|
913
1023
|
validity=args.cache_validity)
|
|
914
1024
|
if args.path:
|
|
915
1025
|
print(path)
|
|
1026
|
+
elif args.ids:
|
|
1027
|
+
print(ids)
|
|
916
1028
|
else:
|
|
917
1029
|
with open(path,"r") as f:
|
|
918
1030
|
print(f.read())
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Offline-First Gemini/Web/Gopher/RSS reader and browser
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "2.
|
|
7
|
+
__version__ = "2.4"
|
|
8
8
|
|
|
9
9
|
## Initial imports and conditional imports {{{
|
|
10
10
|
import argparse
|
|
@@ -89,6 +89,7 @@ _ABBREVS = {
|
|
|
89
89
|
"bb": "blackbox",
|
|
90
90
|
"bm": "bookmarks",
|
|
91
91
|
"book": "bookmarks",
|
|
92
|
+
"cert": "certs",
|
|
92
93
|
"cp": "copy",
|
|
93
94
|
"f": "forward",
|
|
94
95
|
"g": "go",
|
|
@@ -635,6 +636,7 @@ Use with "link" to copy a link in the gemtext format to that page with the title
|
|
|
635
636
|
else:
|
|
636
637
|
print("No content to copy, visit a page first")
|
|
637
638
|
|
|
639
|
+
|
|
638
640
|
### Stuff for getting around
|
|
639
641
|
def do_go(self, line):
|
|
640
642
|
"""Go to a gemini URL or marked item."""
|
|
@@ -815,6 +817,42 @@ Current tour can be listed with `tour ls` and scrubbed with `tour clear`."""
|
|
|
815
817
|
except IndexError:
|
|
816
818
|
print("Invalid index %d, skipping." % n)
|
|
817
819
|
|
|
820
|
+
@needs_gi
|
|
821
|
+
def do_certs(self, line) -> None:
|
|
822
|
+
"""Manage your client certificates (identities) for a site.
|
|
823
|
+
`certs` will display all valid certificates for the current site
|
|
824
|
+
`certs new <name> <days-valid> <url[optional]>` will create a new certificate, if no url is specified, the current open site will be used.
|
|
825
|
+
"""
|
|
826
|
+
line = line.strip()
|
|
827
|
+
if not line:
|
|
828
|
+
certs = netcache.get_certs(self.current_url)
|
|
829
|
+
if len(certs) == 0:
|
|
830
|
+
print("There are no certificates available for this site.")
|
|
831
|
+
else:
|
|
832
|
+
if len(certs) == 1:
|
|
833
|
+
print("The one available certificate for this site is:")
|
|
834
|
+
else:
|
|
835
|
+
print("The", len(certs) ,"available certificates for this site are:")
|
|
836
|
+
|
|
837
|
+
print(*certs)
|
|
838
|
+
print("Use the 'id@site.net' notation to activate a certificate.")
|
|
839
|
+
else:
|
|
840
|
+
lineparts = line.split(' ')
|
|
841
|
+
if lineparts[0] == 'new':
|
|
842
|
+
if len(lineparts) == 4:
|
|
843
|
+
name = lineparts[1]
|
|
844
|
+
days = lineparts[2]
|
|
845
|
+
site = lineparts[3]
|
|
846
|
+
netcache.create_certificate(name, int(days), site)
|
|
847
|
+
elif len(lineparts) == 3:
|
|
848
|
+
name = lineparts[1]
|
|
849
|
+
days = lineparts[2]
|
|
850
|
+
site = urllib.parse.urlparse(self.current_url)
|
|
851
|
+
netcache.create_certificate(name, int(days), site.hostname)
|
|
852
|
+
|
|
853
|
+
else:
|
|
854
|
+
print("usage")
|
|
855
|
+
|
|
818
856
|
@needs_gi
|
|
819
857
|
def do_mark(self, line):
|
|
820
858
|
"""Mark the current item with a single letter. This letter can then
|
|
@@ -1037,13 +1075,38 @@ Use "view XX" where XX is a number to view information about link XX.
|
|
|
1037
1075
|
@needs_gi
|
|
1038
1076
|
def do_open(self, *args):
|
|
1039
1077
|
"""Open current item with the configured handler or xdg-open.
|
|
1040
|
-
|
|
1078
|
+
Use "open url" to open current URL in a browser.
|
|
1079
|
+
Use "open 2 4" to open links 2 and 4
|
|
1080
|
+
You can combine with "open url 2 4" to open URL of links
|
|
1041
1081
|
see "handler" command to set your handler."""
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1082
|
+
#do we open the URL (true) or the cached file (false)
|
|
1083
|
+
url_list = []
|
|
1084
|
+
urlmode = False
|
|
1085
|
+
arglist = args[0].split()
|
|
1086
|
+
if len(arglist) > 0 and arglist[0] == "url":
|
|
1087
|
+
arglist.pop(0)
|
|
1088
|
+
urlmode = True
|
|
1089
|
+
if len(arglist) > 0:
|
|
1090
|
+
#we try to match each argument with a link
|
|
1091
|
+
for a in arglist:
|
|
1092
|
+
try:
|
|
1093
|
+
n = int(a)
|
|
1094
|
+
u = self.get_renderer().get_link(n)
|
|
1095
|
+
url_list.append(u)
|
|
1096
|
+
except ValueError:
|
|
1097
|
+
print("Non-numeric index %s, skipping." % a)
|
|
1098
|
+
except IndexError:
|
|
1099
|
+
print("Invalid index %d, skipping." % n)
|
|
1100
|
+
|
|
1045
1101
|
else:
|
|
1046
|
-
|
|
1102
|
+
#if no argument, we use current url
|
|
1103
|
+
u, m = unmode_url(self.current_url)
|
|
1104
|
+
url_list.append(u)
|
|
1105
|
+
for u in url_list:
|
|
1106
|
+
if urlmode:
|
|
1107
|
+
run("xdg-open %s", parameter=u, direct_output=True)
|
|
1108
|
+
else:
|
|
1109
|
+
self.opencache.opnk(u,terminal=False)
|
|
1047
1110
|
|
|
1048
1111
|
@needs_gi
|
|
1049
1112
|
def do_shell(self, line):
|
|
@@ -1643,6 +1706,10 @@ The following lists cannot be removed or frozen but can be edited with "list edi
|
|
|
1643
1706
|
else:
|
|
1644
1707
|
cmd.Cmd.do_help(self, arg)
|
|
1645
1708
|
|
|
1709
|
+
def do_tutorial(self,arg):
|
|
1710
|
+
"""Access the offpunk.net tutorial (online)"""
|
|
1711
|
+
self._go_to_url("gemini://offpunk.net/firststeps.gmi")
|
|
1712
|
+
|
|
1646
1713
|
def do_sync(self, line):
|
|
1647
1714
|
"""Synchronize all bookmarks lists and URLs from the to_fetch list.
|
|
1648
1715
|
- New elements in pages in subscribed lists will be added to tour
|
|
@@ -29,8 +29,9 @@ output = run("less --version")
|
|
|
29
29
|
words = output.split("\n")[0].split()
|
|
30
30
|
less_version = 0
|
|
31
31
|
for w in words:
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
# On macOS the version can be something like 581.2 not just an int:
|
|
33
|
+
if all(_.isdigit() for _ in w.split(".")):
|
|
34
|
+
less_version = int(w.split(".", 1)[0])
|
|
34
35
|
# restoring position only works for version of less > 572
|
|
35
36
|
if less_version >= 572:
|
|
36
37
|
_LESS_RESTORE_POSITION = True
|
|
@@ -37,7 +37,7 @@ process-title = ["setproctitle"]
|
|
|
37
37
|
rss = ["feedparser"]
|
|
38
38
|
|
|
39
39
|
[project.urls]
|
|
40
|
-
Homepage = "https://
|
|
40
|
+
Homepage = "https://offpunk.net/"
|
|
41
41
|
Source = "https://git.sr.ht/~lioploum/offpunk"
|
|
42
42
|
"Bug Tracker" = "https://todo.sr.ht/~lioploum/offpunk"
|
|
43
43
|
|
|
Binary file
|