Diff -- /var/www/vhosts/webstls.com/httpdocs/sessions.cgi
Diff

/var/www/vhosts/webstls.com/httpdocs/sessions.cgi

added on WebSTLs (webstls.com) at 2026-07-01 22:26:46

Added
+188
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to b24c5ceb01be
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# WebSTLs -- active session manager.
4#
5# GET /sessions.cgi -> list every active session
6# POST /sessions.cgi action=revoke sid=... -> revoke a specific session
7# POST /sessions.cgi action=revoke_others -> revoke every session
8# except the current one
9#======================================================================
10use strict;
11use warnings;
12
13use lib '/var/www/vhosts/webstls.com/httpdocs';
14use CGI;
15use MODS::Template;
16use MODS::DBConnect;
17use MODS::Login;
18use MODS::WebSTLs::Config;
19use MODS::WebSTLs::Wrapper;
20
21$|=1;
22
23my $q = CGI->new;
24my $form = $q->Vars;
25my $auth = MODS::Login->new;
26my $tfile = MODS::Template->new;
27my $db = MODS::DBConnect->new;
28my $cfg = MODS::WebSTLs::Config->new;
29my $wrap = MODS::WebSTLs::Wrapper->new;
30my $DB = $cfg->settings('database_name');
31
32my $userinfo = $auth->login_verify();
33unless ($userinfo) {
34 print "Status: 302 Found\nLocation: /login.cgi\n\n";
35 exit;
36}
37my $uid = $userinfo->{user_id};
38$uid =~ s/[^0-9]//g;
39my $current_sid = $userinfo->{session_id} || '';
40$current_sid =~ s/[^a-zA-Z0-9\-]//g;
41
42# ---------- POST: revoke session(s) ----------------------------------
43if (($ENV{REQUEST_METHOD} || '') eq 'POST') {
44 my $act = $form->{action} || '';
45 my $dbh = $db->db_connect();
46
47 if ($act eq 'revoke') {
48 my $sid = $form->{sid} || '';
49 $sid =~ s/[^a-zA-Z0-9\-]//g;
50 # Ownership check + revoke. Cannot revoke a session we don't own.
51 if (length $sid) {
52 $db->db_readwrite($dbh, qq~
53 UPDATE `${DB}`.user_sessions
54 SET revoked_at=NOW()
55 WHERE id='$sid' AND user_id='$uid' AND revoked_at IS NULL
56 ~, $ENV{SCRIPT_NAME}, __LINE__);
57 }
58 # If they revoked the CURRENT session, bounce to login.
59 if ($sid eq $current_sid) {
60 $db->db_disconnect($dbh);
61 print "Status: 302 Found\nLocation: /login.cgi?revoked=1\n\n";
62 exit;
63 }
64 }
65 elsif ($act eq 'revoke_others') {
66 $db->db_readwrite($dbh, qq~
67 UPDATE `${DB}`.user_sessions
68 SET revoked_at=NOW()
69 WHERE user_id='$uid'
70 AND revoked_at IS NULL
71 AND id<>'$current_sid'
72 ~, $ENV{SCRIPT_NAME}, __LINE__);
73 }
74
75 $db->db_disconnect($dbh);
76 print "Status: 302 Found\nLocation: /sessions.cgi?ok=1\n\n";
77 exit;
78}
79
80# ---------- GET: list sessions ---------------------------------------
81# Older MariaDB on the prod box doesn't ship INET6_NTOA, so we pull
82# ip_address as HEX() and convert client-side. Either way the column
83# is a VARBINARY(16) holding the packed IP from Socket::inet_pton.
84my $dbh = $db->db_connect();
85my @rows = $db->db_readwrite_multiple($dbh, qq~
86 SELECT id, user_agent,
87 HEX(ip_address) AS ip_hex,
88 DATE_FORMAT(issued_at, '%Y-%m-%d %H:%i') AS issued,
89 DATE_FORMAT(expires_at, '%Y-%m-%d') AS expires,
90 TIMESTAMPDIFF(MINUTE, issued_at, NOW()) AS age_min
91 FROM `${DB}`.user_sessions
92 WHERE user_id='$uid'
93 AND revoked_at IS NULL
94 AND expires_at > NOW()
95 ORDER BY issued_at DESC
96~, $ENV{SCRIPT_NAME}, __LINE__);
97$db->db_disconnect($dbh);
98
99my @session_rows;
100foreach my $r (@rows) {
101 my $ua = $r->{user_agent} || '';
102 # Best-effort UA summary -- no library, just substring sniffing.
103 my $browser =
104 $ua =~ /Edg\// ? 'Edge' :
105 $ua =~ /Chrome\// ? 'Chrome' :
106 $ua =~ /Firefox\// ? 'Firefox' :
107 $ua =~ /Safari\// ? 'Safari' :
108 'Browser';
109 my $os =
110 $ua =~ /Windows NT/ ? 'Windows' :
111 $ua =~ /Mac OS X/ ? 'macOS' :
112 $ua =~ /Android/ ? 'Android' :
113 $ua =~ /iPhone|iPad/ ? 'iOS' :
114 $ua =~ /Linux/ ? 'Linux' :
115 'Unknown';
116
117 my $age_label;
118 my $a = $r->{age_min} || 0;
119 if ($a < 1) { $age_label = 'Just now'; }
120 elsif ($a < 60) { $age_label = $a . ' min ago'; }
121 elsif ($a < 60 * 24) { $age_label = int($a / 60) . ' hr ago'; }
122 else { $age_label = int($a / 1440) . ' days ago'; }
123
124 push @session_rows, {
125 sid => $r->{id},
126 is_current => ($r->{id} eq $current_sid) ? 1 : 0,
127 browser => $browser,
128 os => $os,
129 ip => _h(_hex_to_ip($r->{ip_hex})),
130 issued => _h($r->{issued} || ''),
131 age_label => $age_label,
132 expires => _h($r->{expires} || ''),
133 ua_short => _h(substr($ua, 0, 80)),
134 };
135}
136
137my $tvars = {
138 sessions => \@session_rows,
139 has_sessions => scalar(@session_rows) ? 1 : 0,
140 session_count => scalar(@session_rows),
141 has_others => (scalar(@session_rows) > 1) ? 1 : 0,
142 ok_flash => ($form->{ok} && $form->{ok} eq '1') ? 1 : 0,
143};
144
145print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
146my $body = join('', $tfile->template('webstls_sessions.html', $tvars, $userinfo));
147$wrap->render({
148 userinfo => $userinfo,
149 page_key => 'my_profile',
150 title => 'Active sessions',
151 body => $body,
152});
153exit;
154
155sub _h {
156 my $s = shift // '';
157 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
158 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
159 return $s;
160}
161
162# Convert the hex string from HEX(ip_address) back to a printable
163# IPv4 / IPv6 string. 8 hex chars = 4 bytes = IPv4. 32 hex chars =
164# 16 bytes = IPv6. Anything else = unknown, render as a dash.
165sub _hex_to_ip {
166 my ($hex) = @_;
167 return '-' unless defined $hex && length $hex;
168 $hex = uc $hex;
169 if (length($hex) == 8) {
170 my @oct = map { hex(substr($hex, $_ * 2, 2)) } 0..3;
171 return join('.', @oct);
172 }
173 if (length($hex) == 32) {
174 my @grp;
175 for (my $i = 0; $i < 32; $i += 4) {
176 my $g = substr($hex, $i, 4);
177 $g =~ s/^0+//;
178 $g = '0' if !length $g;
179 push @grp, lc $g;
180 }
181 # Collapse the longest run of zeros to :: (one pass only --
182 # this is for display, not strict RFC normalization).
183 my $s = join(':', @grp);
184 $s =~ s/(?:^|:)(?:0:){2,}/::/;
185 return $s;
186 }
187 return '-';
188}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help