* [REJECT 0/3] work-in-progress gemini:// support
@ 2025-03-14 9:22 Eric Wong
2025-03-14 9:22 ` [PATCH 1/3] daemon: define %TLS_ONLY hash Eric Wong
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Eric Wong @ 2025-03-14 9:22 UTC (permalink / raw)
To: meta
1 and 2 aren't harmful to accept, so they'll probably be
applied. 3/3: the test suite is already slow and incomplete
and more code to test won't make it any better.
Having more code to test and maintain just doesn't seem worth
the effort since everybody has an HTTP(S) client already; and
the people who care about resource usage would already be using
a lightweight browser.
At least ActivityPub might bring some new users (barring
cultural differences); but everybody on Gemini can already
access the web via some lightweight browser.
Eric Wong (3):
daemon: define %TLS_ONLY hash
www_static: path_info_raw: support non-HTTP(S) schemes
gemini work-in-progress...
MANIFEST | 4 +
lib/PublicInbox/Daemon.pm | 19 ++--
lib/PublicInbox/GMI.pm | 113 ++++++++++++++++++++++
lib/PublicInbox/Gemini.pm | 181 +++++++++++++++++++++++++++++++++++
lib/PublicInbox/GeminiD.pm | 72 ++++++++++++++
lib/PublicInbox/WwwStatic.pm | 2 +-
t/gemini.t | 56 +++++++++++
7 files changed, 438 insertions(+), 9 deletions(-)
create mode 100644 lib/PublicInbox/GMI.pm
create mode 100644 lib/PublicInbox/Gemini.pm
create mode 100644 lib/PublicInbox/GeminiD.pm
create mode 100644 t/gemini.t
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH 1/3] daemon: define %TLS_ONLY hash
2025-03-14 9:22 [REJECT 0/3] work-in-progress gemini:// support Eric Wong
@ 2025-03-14 9:22 ` Eric Wong
2025-03-14 9:22 ` [PATCH 2/3] www_static: path_info_raw: support non-HTTP(S) schemes Eric Wong
2025-03-14 9:22 ` [REJECT 3/3] gemini work-in-progress Eric Wong
2 siblings, 0 replies; 4+ messages in thread
From: Eric Wong @ 2025-03-14 9:22 UTC (permalink / raw)
To: meta
Defining TLS-only protocols only once makes it easier to support
new protocols in the future since we can rely on only updating
this new hash instead of having to update regexps in other
places.
---
lib/PublicInbox/Daemon.pm | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/lib/PublicInbox/Daemon.pm b/lib/PublicInbox/Daemon.pm
index 8fe93acd..17abf01d 100644
--- a/lib/PublicInbox/Daemon.pm
+++ b/lib/PublicInbox/Daemon.pm
@@ -37,7 +37,8 @@ my ($uid, $gid);
my ($default_cert, $default_key);
my %KNOWN_TLS = (443 => 'https', 563 => 'nntps', 993 => 'imaps', 995 =>'pop3s');
my %KNOWN_STARTTLS = (110 => 'pop3', 119 => 'nntp', 143 => 'imap');
-my %SCHEME2PORT = map { $KNOWN_TLS{$_} => $_ + 0 } keys %KNOWN_TLS;
+my %TLS_ONLY = map { $KNOWN_TLS{$_} => $_ + 0 } keys %KNOWN_TLS;
+my %SCHEME2PORT = %TLS_ONLY;
for (keys %KNOWN_STARTTLS) { $SCHEME2PORT{$KNOWN_STARTTLS{$_}} = $_ + 0 }
$SCHEME2PORT{http} = 80;
@@ -233,7 +234,7 @@ EOF
$tls_opt{"$scheme://$l"} = accept_tls_opt($opt);
} elsif (defined($default_cert)) {
$tls_opt{"$scheme://$l"} = accept_tls_opt('');
- } elsif ($scheme =~ /\A(?:https|imaps|nntps|pop3s)\z/) {
+ } elsif (defined($TLS_ONLY{$scheme})) {
die "$orig specified w/o cert=\n";
}
if ($listener_names->{$l}) { # already inherited
@@ -689,7 +690,7 @@ sub daemon_loop () {
my ($scheme, $l) = split(m!://!, $k, 2);
my $xn = $XNETD{$l} // die "BUG: no xnetd for $k";
$xn->{tlsd}->{ssl_ctx_opt} //= $ctx_opt;
- $scheme =~ m!\A(?:https|imaps|nntps|pop3s)! and
+ defined($TLS_ONLY{$scheme}) and
$POST_ACCEPT{$l} = tls_cb(@$xn{qw(post_accept tlsd)});
}
undef %tls_opt;
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH 2/3] www_static: path_info_raw: support non-HTTP(S) schemes
2025-03-14 9:22 [REJECT 0/3] work-in-progress gemini:// support Eric Wong
2025-03-14 9:22 ` [PATCH 1/3] daemon: define %TLS_ONLY hash Eric Wong
@ 2025-03-14 9:22 ` Eric Wong
2025-03-14 9:22 ` [REJECT 3/3] gemini work-in-progress Eric Wong
2 siblings, 0 replies; 4+ messages in thread
From: Eric Wong @ 2025-03-14 9:22 UTC (permalink / raw)
To: meta
We may add support for gemini:// which supports CGI-like
protocols like PSGI, so relaxing the HTTP(S) URL scheme
requirement seems to make sense.
---
lib/PublicInbox/WwwStatic.pm | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/PublicInbox/WwwStatic.pm b/lib/PublicInbox/WwwStatic.pm
index af4eb960..7136f3cd 100644
--- a/lib/PublicInbox/WwwStatic.pm
+++ b/lib/PublicInbox/WwwStatic.pm
@@ -220,7 +220,7 @@ sub path_info_raw ($) {
my $re = $path_re_cache{$sn} //= do {
$sn = '/'.$sn unless index($sn, '/') == 0;
$sn =~ s!/\z!!;
- qr!\A(?:https?://[^/]+)?\Q$sn\E(/[^\?\#]+)!;
+ qr!\A(?:[^:]+://[^/]+)?\Q$sn\E(/[^\?\#]+)!;
};
$env->{REQUEST_URI} =~ $re ? $1 : $env->{PATH_INFO};
}
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [REJECT 3/3] gemini work-in-progress...
2025-03-14 9:22 [REJECT 0/3] work-in-progress gemini:// support Eric Wong
2025-03-14 9:22 ` [PATCH 1/3] daemon: define %TLS_ONLY hash Eric Wong
2025-03-14 9:22 ` [PATCH 2/3] www_static: path_info_raw: support non-HTTP(S) schemes Eric Wong
@ 2025-03-14 9:22 ` Eric Wong
2 siblings, 0 replies; 4+ messages in thread
From: Eric Wong @ 2025-03-14 9:22 UTC (permalink / raw)
To: meta
/raw, /t.mbox.gz, and /t.atom endpoints are supported.
Adding text/gemini endpoints seem tedious, but I'm thinking
this experiment has run its course and probably not worth the
time to develop further:
Gemini isn't likely to bring new users to public-inbox.
High latencies, lack of compression and persistent connections
will continue to be a problem forever with long distance
connectivity (especially if space colonization happens).
However, extra code, tests, and maintenence costs will divert
our limited resources and slow down development in other areas.
---
MANIFEST | 4 +
lib/PublicInbox/Daemon.pm | 12 ++-
lib/PublicInbox/GMI.pm | 113 +++++++++++++++++++++++
lib/PublicInbox/Gemini.pm | 181 +++++++++++++++++++++++++++++++++++++
lib/PublicInbox/GeminiD.pm | 72 +++++++++++++++
t/gemini.t | 56 ++++++++++++
6 files changed, 433 insertions(+), 5 deletions(-)
create mode 100644 lib/PublicInbox/GMI.pm
create mode 100644 lib/PublicInbox/Gemini.pm
create mode 100644 lib/PublicInbox/GeminiD.pm
create mode 100644 t/gemini.t
diff --git a/MANIFEST b/MANIFEST
index 321c652d..760d08d1 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -203,7 +203,10 @@ lib/PublicInbox/Filter/Mirror.pm
lib/PublicInbox/Filter/RubyLang.pm
lib/PublicInbox/Filter/SubjectTag.pm
lib/PublicInbox/Filter/Vger.pm
+lib/PublicInbox/GMI.pm
lib/PublicInbox/Gcf2Client.pm
+lib/PublicInbox/Gemini.pm
+lib/PublicInbox/GeminiD.pm
lib/PublicInbox/GetlineResponse.pm
lib/PublicInbox/Git.pm
lib/PublicInbox/GitAsyncCat.pm
@@ -482,6 +485,7 @@ t/filter_subjecttag.t
t/filter_vger.t
t/gcf2.t
t/gcf2_client.t
+t/gemini.t
t/git-http-backend.psgi
t/git.fast-import-data
t/git.t
diff --git a/lib/PublicInbox/Daemon.pm b/lib/PublicInbox/Daemon.pm
index 17abf01d..edc0204d 100644
--- a/lib/PublicInbox/Daemon.pm
+++ b/lib/PublicInbox/Daemon.pm
@@ -35,7 +35,8 @@ my %tls_opt; # scheme://sockname => args for IO::Socket::SSL::SSL_Context->new
my $reexec_pid;
my ($uid, $gid);
my ($default_cert, $default_key);
-my %KNOWN_TLS = (443 => 'https', 563 => 'nntps', 993 => 'imaps', 995 =>'pop3s');
+my %KNOWN_TLS = (443 => 'https', 563 => 'nntps', 993 => 'imaps', 995 =>'pop3s',
+ 1965 => 'gemini');
my %KNOWN_STARTTLS = (110 => 'pop3', 119 => 'nntp', 143 => 'imap');
my %TLS_ONLY = map { $KNOWN_TLS{$_} => $_ + 0 } keys %KNOWN_TLS;
my %SCHEME2PORT = %TLS_ONLY;
@@ -113,10 +114,11 @@ sub open_log_path ($$) { # my ($fh, $path) = @_; # $_[0] is modified
sub load_mod ($;$$) {
my ($scheme, $opt, $addr) = @_;
- my $modc = "PublicInbox::\U$scheme";
+ my $modc = 'PublicInbox::';
+ $modc .= $scheme eq 'gemini' ? 'Gemini' : uc $scheme;
$modc =~ s/S\z//;
my $mod = $modc.'D';
- eval "require $mod"; # IMAPD|HTTPD|NNTPD|POP3D
+ eval "require $mod"; # IMAPD|HTTPD|NNTPD|POP3D|GeminiD
die $@ if $@;
my %xn;
my $tlsd = $xn{tlsd} = $mod->new;
@@ -716,8 +718,8 @@ sub worker_loop {
my $tls_cb = $POST_ACCEPT{$l};
my $xn = $XNETD{$l} // die "BUG: no xnetd for $l";
- # NNTPS, HTTPS, HTTP, IMAPS and POP3S are client-first traffic
- # IMAP, NNTP and POP3 are server-first
+ # Gemini, NNTPS, HTTPS, HTTP, IMAPS and POP3S are client-first
+ # traffic. IMAP, NNTP and POP3 are server-first
defer_accept($_, $tls_cb ? 'dataready' : $xn->{af_default});
# this calls epoll_create:
diff --git a/lib/PublicInbox/GMI.pm b/lib/PublicInbox/GMI.pm
new file mode 100644
index 00000000..8f550401
--- /dev/null
+++ b/lib/PublicInbox/GMI.pm
@@ -0,0 +1,113 @@
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+#
+# main gemini interface for public-inbox archives (PSGI compatible),
+# like PublicInbox::WWW
+package PublicInbox::GMI;
+use v5.12;
+use URI::Escape qw(uri_unescape);
+use PublicInbox::Config;
+use PublicInbox::WwwStatic qw(path_info_raw);
+my %STATUS = ( # code => text body response
+ 10 => 'user input required',
+ # 11 => 'sensitive input',
+ 20 => 'OK',
+ 30 => 'temporary redirect',
+ 31 => 'permanent redirect',
+ 40 => 'unspecified error',
+ 41 => 'server unavailable', # 503
+ 42 => 'CGI error',
+ #43 => 'proxy error',
+ 44 => 'slow down',
+ 50 => 'permanent failure',
+ 51 => 'not found',
+ # 52 => 'gone',
+ # 53 => 'proxy request refused',
+ # 59 => 'bad request',
+);
+
+sub new {
+ my ($cls, $pi_cfg) = @_;
+ bless { pi_cfg => $pi_cfg // PublicInbox::Config->new }, $cls;
+}
+
+sub g ($;$) {
+ my $code = $_[0];
+ [ $code, 'text/plain', $_[1] // $STATUS{$code} ];
+}
+
+sub set_ibx ($$) {
+ my ($ctx, $name) = @_;
+ my $ibx = $ctx->{www}->{pi_cfg}->lookup_name($name) //
+ $ctx->{www}->{pi_cfg}->lookup_ei($name);
+ defined($ibx) ? ($ctx->{ibx} = $ibx) : undef;
+}
+
+sub gmi_need ($$) {
+ my ($ctx, $extra) = @_;
+ [ 40, 'text/gemini',
+ [ "$extra is not available for this public-inbox" ] ]
+}
+
+sub gmi_msg_file ($$$$) {
+ my ($ctx, $ibx_name, $mid_ue, $type) = @_;
+ set_ibx $ctx, $ibx_name // return g 51;
+ $ctx->{mid} = uri_unescape($mid_ue);
+ if ($type eq 't.atom') {
+ require PublicInbox::Feed;
+ PublicInbox::Feed::generate_thread_atom($ctx);
+ } elsif ($type eq 'raw') {
+ require PublicInbox::Mbox;
+ PublicInbox::Mbox::emit_raw($ctx) || g 40;
+ } elsif ($type eq 't.mbox.gz') {
+ my $over = $ctx->{ibx}->over or
+ return gmi_need $ctx, 'Overview';
+ require PublicInbox::Mbox;
+ PublicInbox::Mbox::thread_mbox($ctx, $over);
+ } else {
+ g 40;
+ }
+}
+
+# PSGI entry point
+sub call {
+ my ($self, $env) = @_;
+ my $ctx = { env => $env, www => $self }; # `www' for PSGI code compat
+
+ # we don't care about multi-value
+ # '0' isn't a QUERY_STRING we care about
+ if (my $qs = $env->{QUERY_STRING}) { # FIXME duplicated from WWW
+ utf8::decode($qs);
+ $qs =~ tr/+/ /;
+ %{$ctx->{qp}} = map {
+ # we only use single-char query param keys
+ if (s/\A([A-Za-z])=//) {
+ $1 => uri_unescape($_)
+ } elsif (/\A[a-z]\z/) { # some boolean options
+ $_ => ''
+ } else {
+ () # ignored
+ }
+ } split(/[&;]+/, $qs);
+ }
+ my $path = path_info_raw($env);
+ # n.b. trying different regexp to support '/' in inbox name
+ if ($path =~ m!\A/(.+?)/([^/]+)/([stT])/\z!) {
+ # gmi_msg_multi $ctx $1, $2, $3;
+ } elsif ($path =~ m!\A/(.+?)/([^/]+)/(raw|t\.atom|t\.mbox\.gz)\z!) {
+ gmi_msg_file $ctx, $1, $2, $3;
+ } elsif ($path =~ m!\A/(.+?)/([^/]+)/\z!) {
+ # gmi_msg_permalink $ctx, $1, $2;
+ } elsif ($path =~ m!\A/(.+?)/\z!) {
+ # gmi_ibx $ctx, $1;
+ } elsif ($path =~ m!\A/(.+?)/manifest\.js\.gz\z!) {
+ # gmi_ibx_manifest $ctx, $1;
+ } elsif ($path eq '/manifest.js.gz') {
+ # gmi_all_manifest $ctx;
+ } elsif ($path eq '/' || $path eq '') {
+ # gmi_all_listing $ctx;
+ } else {
+ }
+}
+
+1;
diff --git a/lib/PublicInbox/Gemini.pm b/lib/PublicInbox/Gemini.pm
new file mode 100644
index 00000000..954f5a92
--- /dev/null
+++ b/lib/PublicInbox/Gemini.pm
@@ -0,0 +1,181 @@
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# each instance represents a gemini:// client
+# FIXME duplication from PublicInbox::HTTP
+package PublicInbox::Gemini;
+use v5.12;
+use parent qw(PublicInbox::DS);
+use PublicInbox::Syscall qw(EPOLLIN EPOLLONESHOT);
+use Errno qw(EAGAIN);
+my %HTTP2GMNI = (
+ 200 => 20,
+ 404 => 51,
+ 301 => 31,
+ 302 => 30,
+ 503 => 41,
+);
+
+sub new {
+ my ($cls, $sock, $addr, $srv_env) = @_;
+ my $self = bless { srv_env => $srv_env }, $cls;
+ my $ev = EPOLLIN;
+ if ($sock->can('accept_SSL') && !$sock->accept_SSL) {
+ return $sock->close if $! != EAGAIN;
+ $ev = PublicInbox::TLS::epollbit() or return $sock->close;
+ $self->{wbuf} = [ \&PublicInbox::DS::accept_tls_step ];
+ }
+ $self->{remote_addr} = $addr;
+ $self->SUPER::new($sock, $ev | EPOLLONESHOT);
+}
+
+sub close {
+ my $self = $_[0];
+ if (my $forward = delete $self->{forward}) {
+ eval { $forward->close };
+ warn "W: forward ->close error: $@" if $@;
+ }
+ $self->SUPER::close; # PublicInbox::DS::close
+}
+
+sub gemini_done ($) {
+ my ($self) = @_;
+ if (my $forward = delete $self->{forward}) { # avoid recursion
+ eval { $forward->close };
+ if ($@) {
+ warn "W: gemini_done forward->close error: $@";
+ return $self->close; # idempotent
+ }
+ }
+ $self->write(\&close);
+}
+
+sub gemini_pull ($) {
+ my ($self) = @_;
+ my $fwd = $self->{forward};
+
+ # limit our own running time for fairness with other
+ # clients and to avoid buffering too much:
+ my $buf = $fwd ? eval { local $/ = \65536; $fwd->getline } : undef;
+ if (defined $buf) {
+ # may close in PublicInbox::DS::write
+ $self->write(\$buf) if $buf ne '';
+
+ if ($self->{sock}) {
+ # autovivify wbuf
+ my $new_size = push @{$self->{wbuf}}, \&gemini_pull;
+
+ # wbuf may be populated by ->write()
+ # above, no need to rearm if so:
+ $self->requeue if $new_size == 1;
+ return; # likely
+ }
+ } elsif ($@) {
+ warn "W: gemini ->getline error: $@";
+ return $self->close;
+ }
+ gemini_done $self;
+}
+
+sub gemini_emit_response ($$$) {
+ my ($self, $env, $res) = @_;
+ my ($code, $type, $body) = @$res;
+ if ($code >= 100) { # translate HTTP response to gemini
+ $res->[0] = $HTTP2GMNI{$code} // do {
+ warn "W: cannot map HTTP code `$code' to gemini\n";
+ 40;
+ };
+ $code = $res->[0];
+ for (my $i = 0; $i < @$type; $i += 2) {
+ # our PSGI code always Capitalizes Headers
+ if ($type->[$i] eq 'Content-Type') {
+ $type = $type->[$i + 1];
+ last;
+ }
+ }
+ die <<EOM if ref $type;
+W: no Content-Type in HTTP response for gemini
+EOM
+ }
+ my $h = "$code $type\r\n";
+ if (defined $body) {
+ if (ref $body eq 'ARRAY') {
+ $self->writev($h, @$body);
+ gemini_done $self;
+ } else {
+ $self->msg_more($h);
+ $self->{forward} = $body;
+ gemini_pull $self; # kick-off!
+ }
+ } else {
+ $self->msg_more($h);
+ # returned to the calling PSGI application:
+ bless [ $self ], 'PublicInbox::Gemini::Identity';
+ }
+}
+
+# for graceful shutdown in PublicInbox::Daemon:
+sub busy { exists($_[0]->{env}) || defined($_[0]->{wbuf}) }
+
+# runs $cb on the next iteration of the event loop at earliest
+sub next_step {
+ my ($self, $cb) = @_;
+ return unless exists $self->{sock};
+ $self->requeue if 1 == push(@{$self->{wbuf}}, $cb);
+}
+
+sub event_step { # called by PublicInbox::DS
+ my ($self) = @_;
+ local $SIG{__WARN__} = $self->{srv_env}->{'pi-httpd.warn_cb'};
+ return unless $self->flush_write && $self->{sock} && !$self->{forward};
+
+ # n.b. titan:// supports input, gemini doesn't
+ my $rbuf = \(my $x = '');
+ do {
+ $self->do_read($rbuf, 1026 - length($x), length($x))
+ or return;
+ } until ($x =~ m!\A(gemini://([^/]+)(.*))\r\n\z! ||
+ # 1024 is max URL + CRLF
+ (length($x) >= 1026 and return $self->close));
+ my $env;
+ %$env = %{$self->{srv_env}}; # full hash copy
+ $env->{REQUEST_URI} = $1;
+ $env->{HTTP_HOST} = $2; # compat for Plack::App::URLMap
+ my $path = $3;
+ $env->{QUERY_STRING} = ($path =~ s/\?(.*)\z//) ? $1 : '';
+ $env->{PATH_INFO} = URI::Escape::uri_unescape($path);
+ ($env->{REMOTE_ADDR}, $env->{REMOTE_PORT}) =
+ split /:/, $self->{remote_addr};
+ $env->{'psgix.io'} = $self; # for ->close or async_pass
+ $self->{env} = undef; # for busy
+ my $res = eval { $env->{'pi-httpd.app'}->($env) };
+ if ($@) {
+ $env->{'psgi.errors'}->print($@);
+ $res = [ 42 ]; # 42 - CGI (or similar) error
+ }
+ eval {
+ if (ref($res) eq 'CODE') {
+ $res->(sub { gemini_emit_response $self, $env, $_[0] });
+ } else {
+ gemini_emit_response $self, $env, $res;
+ }
+ };
+ if ($@) {
+ warn "gemini_emit_response error: $@";
+ $self->close;
+ }
+}
+
+# exposed to the PSGI app when it returns a CODE ref for "push"-based responses
+package PublicInbox::Gemini::Identity;
+use bytes ();
+
+sub write {
+ # ([PublicInbox::Gemini obj], $buf) = @_;
+ $_[0]->[0]->write($_[1]); # PublicInbox::DS::write
+ $_[0]->[0]->{sock} ? bytes::length($_[1]) : undef;
+}
+
+sub close { $_[0]->[0]->gemini_done }
+
+1;
diff --git a/lib/PublicInbox/GeminiD.pm b/lib/PublicInbox/GeminiD.pm
new file mode 100644
index 00000000..363bef26
--- /dev/null
+++ b/lib/PublicInbox/GeminiD.pm
@@ -0,0 +1,72 @@
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# wraps a listen socket for gemini and links it to PSGI app(s)
+package PublicInbox::GeminiD;
+use v5.12;
+use Plack::Util ();
+use Plack::Builder;
+use PublicInbox::Gemini;
+
+# we have a different env for ever listener socket for
+# SERVER_NAME, SERVER_PORT and psgi.url_scheme
+# envs: listener FD => PSGI env
+sub new { bless { envs => {}, err => \*STDERR }, __PACKAGE__ }
+
+# this becomes {srv_env} in PublicInbox::Gemini
+sub env_for ($$) {
+ my ($self, $srv) = @_;
+ my $n = getsockname($srv) or die "E: not a socket: $srv $!\n";
+ my ($host, $port) = PublicInbox::Daemon::host_with_port($n);
+ {
+ SERVER_NAME => $host,
+ SERVER_PORT => $port,
+ SCRIPT_NAME => '',
+ 'psgi.version' => [ 1, 1 ],
+ 'psgi.errors' => $self->{err},
+ 'psgi.url_scheme' => 'gemini',
+ 'psgi.nonblocking' => Plack::Util::TRUE,
+ 'psgi.streaming' => Plack::Util::TRUE,
+ 'psgi.run_once' => Plack::Util::FALSE,
+ 'psgi.multithread' => Plack::Util::FALSE,
+ 'psgi.multiprocess' => Plack::Util::TRUE,
+
+ # maybe for titan...
+ # 'psgix.input.buffered' => Plack::Util::TRUE,
+
+ 'pi-httpd.async' => 1,
+ 'pi-httpd.app' => $self->{app},
+ 'pi-httpd.warn_cb' => $self->{warn_cb},
+ 'pi-httpd.ckhup' => $port ? \&PublicInbox::Daemon::tcp_hup :
+ \&PublicInbox::Daemon::stream_hup,
+ }
+}
+
+sub refresh_groups {
+ my ($self) = @_;
+ my $app;
+ if ($self->{psgi}) {
+ eval { $app = Plack::Util::load_psgi($self->{psgi}) };
+ die $@, <<EOM if $@;
+$0 runs in /, command-line paths must be absolute
+EOM
+ } else {
+ require PublicInbox::GMI;
+ my $gmi = PublicInbox::GMI->new;
+ $app = builder { sub { $gmi->call(@_) }; };
+ }
+ $_->{'pi-httpd.app'} = $app for values %{$self->{envs}};
+ $self->{app} = $app;
+}
+
+sub post_accept_cb { # for Listener->{post_accept}
+ my ($self) = @_;
+ sub {
+ my ($client, $addr, $srv) = @_;
+ PublicInbox::Gemini->new($client, $addr,
+ $self->{envs}->{fileno($srv)} //=
+ env_for $self, $srv);
+ }
+}
+
+1;
diff --git a/t/gemini.t b/t/gemini.t
new file mode 100644
index 00000000..401fd2b8
--- /dev/null
+++ b/t/gemini.t
@@ -0,0 +1,56 @@
+#!perl -w
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+use v5.12;
+use PublicInbox::TestCommon;
+use PublicInbox::IO;
+use IO::Uncompress::Gunzip qw(gunzip);
+require_mods qw(-httpd IO::Socket::SSL);
+require_git v2.6;
+my $cert = 'certs/server-cert.pem';
+my $key = 'certs/server-key.pem';
+unless (-r $key && -r $cert) {
+ plan skip_all =>
+ "certs/ missing for $0, run $^X ./create-certs.perl in certs/";
+}
+my $tmpdir = tmpdir;
+my ($out, $err) = ("$tmpdir/stdout.log", "$tmpdir/stderr.log");
+my $srv = tcp_server;
+my ($ro_home, $cfg_path) = setup_public_inboxes;
+local $ENV{HOME} = $ro_home;
+my $env = { PI_CONFIG => $cfg_path };
+my ($rhost, $rport) = tcp_host_port($srv);
+my $gmi_get = sub {
+ my ($path_qstr) = @_;
+ my $c = tcp_connect($srv);
+ $c = IO::Socket::SSL->start_SSL($c, SSL_hostname => $rhost,
+ SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE()) or
+ die 'TLS error '.(IO::Socket::SSL->errstr // '');
+ print $c "gemini://$rhost:$rport$path_qstr\r\n" or die "print: $!";
+ my $hdr = <$c>;
+ $hdr =~ s/\r\n// or xbail [ "hdr=$hdr missing CRLF" ];
+ my ($code, $type) = split / /, $hdr, 2;
+ ($code, $type, scalar(do { local $/; <$c> }));
+};
+
+my $cmd = [ '-netd', '-W0', "-lgemini://$rhost:$rport/?cert=$cert,key=$key",
+ "--stdout=$out", "--stderr=$err" ];
+my $td = start_script($cmd, $env, { 3 => $srv });
+my ($code, $type, $bdy) = $gmi_get->('/t2/testmessage@example.com/t.mbox.gz');
+is $code, 20, 'successful t.mbox.gz response';
+is $type, 'application/gzip', 'got gzip response';
+gunzip(\$bdy => \(my $tmp));
+my $raw = PublicInbox::IO::try_cat 't/utf8.eml';
+like $tmp, qr/\Q$raw\E/, 'mbox thread retrieved';
+($code, $type, $bdy) = $gmi_get->('/t2/testmessage@example.com/raw');
+is $code, 20, 'successful /raw response';
+like $type, qr!\Atext/plain\b!, 'raw message is text/plain';
+like $tmp, qr/\Q$bdy\E/, 'raw message retrieved';
+
+($code, $type, $bdy) = $gmi_get->('/t2/testmessage@example.com/t.atom');
+is $code, 20, 'successful /t.atom response';
+like $type, qr!\bapplication/atom\+xml\b!, 'got atom response';
+like $bdy, qr!</feed>\z!s, 'Atom XML terminated';
+
+$td->join('TERM');
+done_testing;
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2025-03-14 9:22 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-03-14 9:22 [REJECT 0/3] work-in-progress gemini:// support Eric Wong
2025-03-14 9:22 ` [PATCH 1/3] daemon: define %TLS_ONLY hash Eric Wong
2025-03-14 9:22 ` [PATCH 2/3] www_static: path_info_raw: support non-HTTP(S) schemes Eric Wong
2025-03-14 9:22 ` [REJECT 3/3] gemini work-in-progress Eric Wong
Code repositories for project(s) associated with this public inbox
https://80x24.org/public-inbox.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).