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.
Files changed (54) hide show
  1. offpunk-2.4/.build.yml +21 -0
  2. {offpunk-2.3 → offpunk-2.4}/CHANGELOG +15 -0
  3. {offpunk-2.3 → offpunk-2.4}/PKG-INFO +6 -5
  4. {offpunk-2.3 → offpunk-2.4}/README.md +4 -3
  5. {offpunk-2.3 → offpunk-2.4}/ansicat.py +22 -17
  6. {offpunk-2.3 → offpunk-2.4}/man/ansicat.1 +1 -1
  7. {offpunk-2.3 → offpunk-2.4}/man/netcache.1 +1 -1
  8. {offpunk-2.3 → offpunk-2.4}/man/offpunk.1 +1 -1
  9. {offpunk-2.3 → offpunk-2.4}/man/opnk.1 +1 -1
  10. {offpunk-2.3 → offpunk-2.4}/netcache.py +137 -25
  11. {offpunk-2.3 → offpunk-2.4}/offpunk.py +73 -6
  12. {offpunk-2.3 → offpunk-2.4}/opnk.py +3 -2
  13. {offpunk-2.3 → offpunk-2.4}/pyproject.toml +1 -1
  14. offpunk-2.4/screenshots/decvt220.jpg +0 -0
  15. offpunk-2.4/tutorial/bookmarks.gmi +37 -0
  16. offpunk-2.4/tutorial/firststeps.gmi +55 -0
  17. offpunk-2.4/tutorial/frozen.gmi +29 -0
  18. offpunk-2.4/tutorial/index.gmi +46 -0
  19. offpunk-2.4/tutorial/install.gmi +38 -0
  20. offpunk-2.4/tutorial/lists.gmi +55 -0
  21. offpunk-2.4/tutorial/make_website.py +168 -0
  22. offpunk-2.4/tutorial/myfirstlink.gmi +14 -0
  23. offpunk-2.4/tutorial/offline.gmi +47 -0
  24. offpunk-2.4/tutorial/open.gmi +25 -0
  25. offpunk-2.4/tutorial/page_template.html +66 -0
  26. offpunk-2.4/tutorial/subscriptions.gmi +65 -0
  27. offpunk-2.4/tutorial/tour.gmi +29 -0
  28. offpunk-2.4/tutorial/tour1.gmi +10 -0
  29. offpunk-2.4/tutorial/tour2.gmi +12 -0
  30. offpunk-2.4/tutorial/tour3.gmi +13 -0
  31. offpunk-2.4/tutorial/view.gmi +32 -0
  32. offpunk-2.4/tutorial/whatisoffpunk.gmi +28 -0
  33. offpunk-2.4/tutorial/workflow_ploum.gmi +178 -0
  34. {offpunk-2.3 → offpunk-2.4}/.gitignore +0 -0
  35. {offpunk-2.3 → offpunk-2.4}/CONTRIBUTORS +0 -0
  36. {offpunk-2.3 → offpunk-2.4}/LICENSE +0 -0
  37. {offpunk-2.3 → offpunk-2.4}/cert_migration.py +0 -0
  38. {offpunk-2.3 → offpunk-2.4}/debug.sh +0 -0
  39. {offpunk-2.3 → offpunk-2.4}/doc/config.gmi +0 -0
  40. {offpunk-2.3 → offpunk-2.4}/doc/dev.gmi +0 -0
  41. {offpunk-2.3 → offpunk-2.4}/doc/index.gmi +0 -0
  42. {offpunk-2.3 → offpunk-2.4}/doc/install.gmi +0 -0
  43. {offpunk-2.3 → offpunk-2.4}/doc/lists.gmi +0 -0
  44. {offpunk-2.3 → offpunk-2.4}/doc/offline.gmi +0 -0
  45. {offpunk-2.3 → offpunk-2.4}/doc/shell.gmi +0 -0
  46. {offpunk-2.3 → offpunk-2.4}/doc/tutorial.gmi +0 -0
  47. {offpunk-2.3 → offpunk-2.4}/netcache_migration.py +0 -0
  48. {offpunk-2.3 → offpunk-2.4}/offblocklist.py +0 -0
  49. {offpunk-2.3 → offpunk-2.4}/offthemes.py +0 -0
  50. {offpunk-2.3 → offpunk-2.4}/offutils.py +0 -0
  51. {offpunk-2.3 → offpunk-2.4}/requirements.txt +0 -0
  52. /offpunk-2.3/screenshot_offpunk1.png → /offpunk-2.4/screenshots/1.png +0 -0
  53. /offpunk-2.3/screenshot_offpunk2.png → /offpunk-2.4/screenshots/2.png +0 -0
  54. {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
3
+ Version: 2.4
4
4
  Dynamic: Summary
5
- Project-URL: Homepage, https://sr.ht/~lioploum/offpunk/
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 project page (repository/mailing lists) : https://sr.ht/~lioploum/offpunk/
705
+ Official page : https://offpunk.net
706
+ Development (repository/mailing lists) : https://sr.ht/~lioploum/offpunk/
706
707
 
707
- ![Screenshot HTML page with picture](screenshot_offpunk1.png)
708
- ![Screenshot Gemini page](screenshot_offpunk2.png)
708
+ ![Screenshot HTML page with picture](screenshots/1.png)
709
+ ![Screenshot Gemini page](screenshots/2.png)
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 project page (repository/mailing lists) : https://sr.ht/~lioploum/offpunk/
7
+ Official page : https://offpunk.net
8
+ Development (repository/mailing lists) : https://sr.ht/~lioploum/offpunk/
8
9
 
9
- ![Screenshot HTML page with picture](screenshot_offpunk1.png)
10
- ![Screenshot Gemini page](screenshot_offpunk2.png)
10
+ ![Screenshot HTML page with picture](screenshots/1.png)
11
+ ![Screenshot Gemini page](screenshots/2.png)
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
- itemtype = name[0]
788
- name = name[1:]
789
- if port == "70":
790
- port = ""
791
- else:
792
- port = ":%s"%port
793
- if itemtype == "h" and path.startswith("URL:"):
794
- url = path[4:]
795
- else:
796
- url = "gopher://%s%s/%s%s" %(host,port,itemtype,path)
797
- url = url.replace(" ","%20")
798
- linkline = url + " " + name
799
- links.append(linkline)
800
- number = len(links) + startlinks
801
- towrap = "[%s] "%str(number)+ name
802
- r.add_text(towrap)
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 @@ original URL of the content.
68
68
  .Xr netcache 1 ,
69
69
  .Xr offpunk 1 ,
70
70
  .Xr opnk 1 ,
71
- .Lk https://sr.ht/~lioploum/offpunk/
71
+ .Lk https://offpunk.net/
72
72
  .
73
73
  .Sh AUTHORS
74
74
  .An Lionel Dricot (Ploum) Aq Mt offpunk2 at ploum.eu
@@ -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://sr.ht/~lioploum/offpunk/
71
+ .Lk https://offpunk.net/
72
72
  .
73
73
  .Sh AUTHORS
74
74
  .An Lionel Dricot (Ploum) Aq Mt offpunk2 at ploum.eu
@@ -81,7 +81,7 @@ display available features and dependancies then quit
81
81
  .Xr migrate-offpunk-cache 1 ,
82
82
  .Xr netcache 1 ,
83
83
  .Xr opnk 1 ,
84
- .Lk https://sr.ht/~lioploum/offpunk/
84
+ .Lk https://offpunk.net/
85
85
  .
86
86
  .Sh HISTORY
87
87
  .Nm
@@ -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://sr.ht/~lioploum/offpunk/
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
- port = parsed.port or standard_ports.get(scheme, 0)
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.utcnow()
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.not_valid_after - now
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
- new_netloc = host
771
+ new_host = host
669
772
  #Handle IPV6 hostname
670
- if ":" in new_netloc:
671
- new_netloc = "[" + new_netloc + "]"
773
+ if ":" in new_host:
774
+ new_host = "[" + new_host + "]"
672
775
  if port != standard_ports["gemini"]:
673
- new_netloc += ":" + str(port)
674
- url = urllib.parse.urlunparse(url._replace(netloc=new_netloc))
675
- s.sendall((url + CRLF).encode("UTF-8"))
676
- f= s.makefile(mode = "rb")
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 = "Handling certificates for status 6X are not supported by offpunk\n"
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,newurl
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
- #Firt, we look if we have a valid cache, even if offline
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(url,**kwargs)
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(url,**kwargs)
917
+ path=_fetch_gopher(newurl,**kwargs)
811
918
  elif scheme == "finger":
812
- path=_fetch_finger(url,**kwargs)
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(url, err)
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 not existing. A validity duration, in seconds, can also be given so that \
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.3"
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
- Uses "open url" to open current URL in a browser.
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
- u, m = unmode_url(self.current_url)
1043
- if args[0] == "url":
1044
- run("xdg-open %s", parameter=u, direct_output=True)
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
- self.opencache.opnk(u,terminal=False)
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
- if w.isdigit():
33
- less_version = int(w)
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://sr.ht/~lioploum/offpunk/"
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