Compare commits

...

17 Commits

Author SHA1 Message Date
Tilde Black Admin 54463b03ee added iching oracle to gemini 2020-07-14 12:38:03 +00:00
Tilde Black Admin 5d86101cb6 iching added to cgi 2020-07-14 12:34:03 +00:00
Tilde Black Admin 4b4b948e70 updated 404 with better code clarity 2020-07-09 09:02:21 +00:00
Tilde Black Admin ff04d01208 404 page styled 2020-07-09 08:44:19 +00:00
Tilde Black Admin e067ac259d better description name for samizdat 2020-07-08 17:28:28 +00:00
Tilde Black Admin 30dfb6a3bd adding samizdat and snowflake to tilde black homepage 2020-07-08 17:25:34 +00:00
Tilde Black Admin 5dd64dd831 gemini over tor 2020-07-07 10:01:51 +00:00
Tilde Black Admin efd1f07b8c styles updates 2020-05-02 12:59:49 +00:00
Tilde Black Admin 9bc2dd082e updated template files 2020-03-04 10:33:18 +00:00
Tilde Black Admin 923bd4d077 changed names of gemini root files to indexgmi 2019-09-05 14:50:59 +00:00
James Tomasino 0c6f26e32b smooth scroll on anchors 2019-08-27 21:16:48 +00:00
James Tomasino aad237d153 small fade-in animation, sets stage for more 2019-08-27 20:55:42 +00:00
James Tomasino 7494f4623c explicitly sets font display 2019-08-26 22:14:34 +00:00
James Tomasino 175efa3360 adds aria text content for screen readers to menu button 2019-08-26 22:09:34 +00:00
James Tomasino ef8b0f78dc optimizes syntax and adds note for testing 2019-08-26 22:00:44 +00:00
James Tomasino 3bf377bd3c fixes some minor hover animations from js removal pr 2019-08-26 21:51:37 +00:00
southerntofu 1d425842e0 Free the navigation menu from Javascript!! (#1) 2019-08-26 17:35:33 -04:00
37 changed files with 7146 additions and 89 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
**/.*.sw*

View File

@ -3,13 +3,14 @@
all_users=$(grep /home < /etc/passwd | awk -F: '{print $1}')
current_dir=$(dirname "$(readlink -f "$0")")
template_dir="${current_dir}/templates/gemini"
output="${template_dir}/users.gemini"
printf "[Users]\\n" > "$output"
output="${template_dir}/users.gmi"
printf "## Users\\n" > "$output"
printf "%s" "$all_users" | while read -r n; do
if [ -f "/var/gemini/users/${n}/.gemini" ]; then
printf "=> /users/%s ~%s\\n" "$n" "$n" >> "$output"
if [ -f "/var/gemini/users/${n}/index.gmi" ]; then
printf "=> gemini://tilde.black:1965/users/%s/ ~%s\\n" "$n" "$n" >> "$output"
fi
done
cat "${template_dir}/head.gemini" "${output}" > /var/gemini/.gemini
cat "$output" > /var/gemini/users/.gemini
cat "${template_dir}/head.gmi" "${output}" > /var/gemini/index.gmi
cat "$output" > /var/gemini/users/index.gmi
cp -R "${template_dir}/cgi-bin" /var/gemini/

163
templates/gemini/cgi-bin/iching Executable file
View File

@ -0,0 +1,163 @@
#!/bin/sh
version="v2020.07.13"
stalks=0
hexagram=""
hex1=0
hex2=0
RANDOM=$$
gethex () {
# treat hexagram as bitarray, the following is the diagram order from 0-63
bin2gram="2,24,7,19,15,36,46,11,16,51,40,54,62,55,32,34,8,3,29,60,39,63,48,5,45,17,47,58,31,49,28,43,23,27,4,41,52,22,18,26,35,21,64,38,56,30,50,14,20,42,59,61,53,37,57,9,12,25,6,10,33,13,44,1"
printf "%s" "$bin2gram" | awk -F "," -v col="$1" '{print $col}'
}
random () {
# use system entropy for random functions, mod to length for range
limit=${1-255}
printf "%s" "$((RANDOM % limit))"
}
phase1 () {
acc1=0
stalks=$((stalks - 1))
l=$(random $stalks)
r=$((stalks - l))
ml1=$((l % 4))
if [ $ml1 -eq 0 ]; then
ml1=4
fi
mr1=$((r % 4))
if [ $mr1 -eq 0 ]; then
mr1=4
fi
stalks=$((stalks - ml1))
stalks=$((stalks - mr1))
acc1=$((acc1 + ml1))
acc1=$((acc1 + mr1))
if [ $acc1 -eq 4 ]; then
points=$((points + 2))
else
points=$((points + 3))
fi
}
phase2 () {
acc2=1
stalks=$((stalks - 1))
l=$(random $stalks)
r=$((stalks - l))
ml2=$((l % 4))
if [ $ml2 -eq 0 ]; then
ml2=4
fi
mr2=$((r % 4))
if [ $mr2 -eq 0 ]; then
mr2=4
fi
stalks=$((stalks - ml2))
stalks=$((stalks - mr2))
acc2=$((acc2 + ml2))
acc2=$((acc2 + mr2))
if [ $acc2 -eq 4 ]; then
points=$((points + 2))
else
points=$((points + 3))
fi
}
phase3 () {
acc3=1
stalks=$((stalks - 1))
l=$(random $stalks)
r=$((stalks - l))
ml3=$((l % 4))
if [ $ml3 -eq 0 ]; then
ml3=4
fi
mr3=$((r % 4))
if [ $mr3 -eq 0 ]; then
mr3=4
fi
stalks=$((stalks - ml3))
stalks=$((stalks - mr3))
acc3=$((acc3 + ml3))
acc3=$((acc3 + mr3))
if [ $acc3 -eq 4 ]; then
points=$((points + 2))
else
points=$((points + 3))
fi
}
phase4 () {
# build output picture and bitarray of hexagrams
pow=$(echo "2 ^ $((6 - i))" | bc)
case $points in
6)
hexagram=$(printf "━━━━━━━━━ ━━━ ━━━\n%s" "$hexagram")
hex1=$((hex1 + (1 * pow)))
hex2=$((hex2 + (0 * pow)))
return
;;
7)
hexagram=$(printf "━━━ ━━━ ━━━ ━━━\n%s" "$hexagram")
hex1=$((hex1 + (0 * pow)))
hex2=$((hex2 + (0 * pow)))
return
;;
8)
hexagram=$(printf "━━━━━━━━━ ━━━━━━━━━\n%s" "$hexagram")
hex1=$((hex1 + (1 * pow)))
hex2=$((hex2 + (1 * pow)))
return
;;
9)
hexagram=$(printf "━━━ ━━━ ━━━━━━━━━\n%s" "$hexagram")
hex1=$((hex1 + (0 * pow)))
hex2=$((hex2 + (1 * pow)))
return
;;
esac
}
main () {
i=6
while [ $i -gt 0 ];
do
stalks=50
points=0
stalks=$((stalks - 1))
phase1
phase2
phase3
phase4
i=$((i-1))
done
printf "20 text/gemini\r\n"
cat << END
\`\`\` I Ching
___ ____ _ _ ___ _ _ ____
|_ _| / ___| | | |_ _| \ | |/ ___|
| | | | | |_| || || \| | | _
| | | |___| _ || || |\ | |_| |
|___| \____|_| |_|___|_| \_|\____|
\`\`\`
Focus your mind on the question to be posed to the oracle. Refresh for a new reading.
* Don't ask the same question multiple times. The first answer is your response.
* Avoid frivolous questions. The oracle does not respond well.
* Avoid questions which could lead to harm.
END
printf "\`\`\` i ching hexagrams\n"
printf "%s\n" "$hexagram"
printf "\`\`\`\n\n"
printf "=> https://divination.com/iching/lookup/%s-2/ Hexagram #%s\n" "$(gethex $((hex1 + 1)))" "$(gethex $((hex1 + 1)))"
printf "=> https://divination.com/iching/lookup/%s-2/ Changing to Hexagram #%s\n" "$(gethex $((hex2 + 1)))" "$(gethex $((hex2 + 1)))"
printf "\n\n=> https://github.com/jamestomasino/iching Gemini CGI hacked together from this repo\n"
}
main "$@"

View File

@ -1,3 +1,4 @@
```
_______ __ __ __ __ __ __
| |__| .--| .-----. | |--| .---.-.----| |--.
|.| | | | | _ | -__|__| _ | | _ | __| <
@ -25,10 +26,11 @@
*$$@@@@@@$$$$$*` /@
`"*****"` /*
```
# Tilde.Black is proud to support the emerging Gemini protocol.
Tilde.Black is proud to support the emerging Gemini protocol.
=> gemini://zaibatsu.circumlunar.space Read more about Gemini
=> gemini://gemini.circumlunar.space/ Learn more about the Gemini protocol
=> gemini://black6kfjetfuzaeozz7fs53whh7xtd4e27telrf5fg5kgdt5ah5plad.onion/ tilde.black is available via tor
=> /cgi-bin/iching I Ching - Ask the oracle

View File

@ -1,2 +0,0 @@
[Users]
=> /users/fox ~fox

View File

@ -0,0 +1,12 @@
## Users
=> gemini://tilde.black:1965/users/fox/ ~fox
=> gemini://tilde.black:1965/users/bloat/ ~bloat
=> gemini://tilde.black:1965/users/styan/ ~styan
=> gemini://tilde.black:1965/users/brool/ ~brool
=> gemini://tilde.black:1965/users/zoid/ ~zoid
=> gemini://tilde.black:1965/users/whits/ ~whits
=> gemini://tilde.black:1965/users/smore/ ~smore
=> gemini://tilde.black:1965/users/scrin/ ~scrin
=> gemini://tilde.black:1965/users/tier/ ~tier
=> gemini://tilde.black:1965/users/betel/ ~betel
=> gemini://tilde.black:1965/users/devil/ ~devil

View File

@ -2,9 +2,18 @@ i [Users]
1 ~fox /users/fox
1 ~joist /users/joist
1 ~bloat /users/bloat
1 ~tempo /users/tempo
1 ~brool /users/brool
1 ~nixie /users/nixie
1 ~check /users/check
1 ~zoid /users/zoid
1 ~frail /users/frail
1 ~genin /users/genin
1 ~thrip /users/thrip
1 ~smore /users/smore
1 ~belly /users/belly
1 ~tolyl /users/tolyl
1 ~poet /users/poet
1 ~fruit /users/fruit
1 ~betel /users/betel
1 ~hoper /users/hoper

51
templates/mail/mail.tmpl Normal file
View File

@ -0,0 +1,51 @@
Subject: Welcome to the black
Welcome to tilde.black.
Your public key has been added to the following user accounts on
the system:
USERS
You may log into tilde.black using any or all of these identities.
Each one has a unique system password which you will find waiting
for you in the respective mailbox. The accounts are all capable of
gopher and web posting and will have access to all other services
on the site.
Some users may find it easiest to have a "primary" account and
only use the others rarely. Others may find it helpful to voice
opinions from many different angles. Use them as you see fit.
The system enables ssh on port 2222, not port 22. To connect
you'll need to specify the port in your ssh config, or on the
command line with the -p switch.
Access to ssh is also available over tor, which will help hide
your public IP from other members of tilde.black. The tor address
of the server is:
black6kfjetfuzaeozz7fs53whh7xtd4e27telrf5fg5kgdt5ah5plad.onion
It is also on port 2222. A guide for connecting to ssh over tor
can be found on the tilde.black website.
Also, mosh is also available as an alternative to ssh.
Unfortunately mosh is designed to run under UDP while tor is
limited to TCP connections. Therefore, if you want to protect your
IP while using mosh, you'll need to connect through another server
or VPN.
Both gopher and http are also available over tor.
Black runs net news with the rest of the tildeverse for message
forums. You can access net news by running slrn (use --create the
first time to generate a configuration file). All groups beginning
with tilde.* are publically shared with the rest of the
tildeverse. Those groups beginning with black.* are private to our
system.
Check the group 'black.general' for system updates and new
information.
- fox

View File

@ -0,0 +1,13 @@
Subject: Welcome to the black
Welcome ~USERNAME,
Your password for this username is "PASSWORD". please change it
when you log in for the first time with 'passwd' like so:
$ passwd
While we don't use passwords for logins, they can be used on the
system to change settings like your current shell (chsh).
- fox

101
templates/web/404.html Normal file
View File

@ -0,0 +1,101 @@
<!doctype html>
<html lang="en">
<head>
<title>Tilde Black - Page not found</title>
<meta charset=utf-8>
<meta http-equiv=X-UA-Compatible content="IE=edge">
<meta name=viewport content="shrink-to-fit=no,width=device-width,height=device-height,initial-scale=1,user-scalable=1">
<meta name="description" content="The black hole of the tildeverse">
<link rel='icon' href='/favicon.ico' type='image/x-icon' />
<style type="text/css">
@font-face {
font-family: 'blankaregular';
font-display: swap;
src: url('/assets/blanka-regular-webfont.woff2') format('woff2'),
url('/assets/blanka-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
html,
body {
position: relative;
width: 100%;
height: 100%;
margin: 0;
overflow-x: hidden;
background-color: #2f2f2f;
color: #f2f2f2;
font-size: 18px;
line-height: 1.4em;
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
scroll-behavior: smooth;
}
@media screen and (prefers-reduced-motion: reduce) {
html,
body {
scroll-behavior: auto;
}
}
a {
color: #fd9beb;
text-decoration: none;
display: inline-block;
}
p {
margin: 0 0 25px 0;
}
a::after {
content: '';
display: block;
width: 100%;
height: 1px;
transform: scaleX(0);
transform-origin: left center;
transition: background-color 0.7s cubic-bezier(0.23, 1, 0.32, 1), transform 1s cubic-bezier(0.23, 1, 0.32, 1);
background-color: #fd9beb;
}
a:hover::after {
transform: scaleX(1);
}
h1 {
font-family: "blankaregular";
font-size: 42px;
line-height: 1.4em;
text-align: center;
}
@media (min-width: 728px) {
h1 {
font-size: 60px;
}
}
.error {
display: grid;
justify-items: center;
align-items: center;
min-height: 80vh;
margin: 15px;
}
.error-block {
text-align: center;
}
</style>
<script defer src="/samizdat.js"></script>
</head>
<body>
<div class="error">
<div class="error-block">
<h1>Page not found</h1>
<p>We're sorry, but there is no page at this location. Why not <a href="javascript:history.back()">go back and try again</a>, or <a href="/">return to the homepage</a></p>
</div>
</div>
</body>
</html>

View File

@ -1,6 +0,0 @@
/* Close menu buttons on select */
Array.from(document.querySelectorAll('.link-grid a')).map( el => {
el.addEventListener('click', e => {
document.getElementById("toggle").checked = false;
})
})

View File

@ -2,9 +2,9 @@
@font-face {
font-family: 'blankaregular';
font-display: auto;
src: url('blanka-regular-webfont.woff2') format('woff2'),
url('blanka-regular-webfont.woff') format('woff');
font-display: swap;
src: url('/assets/blanka-regular-webfont.woff2') format('woff2'),
url('/assets/blanka-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@ -23,6 +23,14 @@ body {
font-size: 18px;
line-height: 1.4em;
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
scroll-behavior: smooth;
}
@media screen and (prefers-reduced-motion: reduce) {
html,
body {
scroll-behavior: auto;
}
}
a {
@ -90,26 +98,41 @@ h3 {
max-width: 100%;
}
#main-nav #toggle {
#main-nav .links {
display: none;
}
#main-nav #toggle:checked ~ section {
#main-nav:target .links {
display: block;
animation-name: fadeInFromNone;
animation-fill-mode: forwards;
animation-duration: 0.7s;
}
#main-nav #toggle:checked ~ label {
#main-nav:target .open {
display: none;
}
#main-nav:target .close {
display: block;
background-position: 0 -52px;
}
#main-nav label {
#main-nav .open {
display: block;
}
#main-nav .close {
display: none;
}
#main-nav .menubutton {
position: absolute;
top: 0;
left: 0;
display: block;
cursor: pointer;
line-height: 40px;
background-image: url('/assets/nav-toggle.png');
background-image: url('nav-toggle.png');
background-size: 50px 103px;
background-position: 0 0;
background-color: rgba(255, 255, 255, 0.2);
@ -119,10 +142,11 @@ h3 {
z-index: 5;
}
#main-nav section {
position: absolute;
top: 0;
left: 0;
#main-nav .menubutton::after {
content: none;
}
#main-nav .links {
width: 100vw;
max-width: 100%;
height: 100vh;
@ -135,34 +159,16 @@ h3 {
background-color: #222;
}
#main-nav section .link-grid {
display: grid;
#main-nav .links {
padding-bottom: 55px;
padding-top: 55px;
padding-left: 15vw;
padding-right: 15vw;
max-height: 100%;
grid-template-columns: 1fr 0.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 0.5fr 1fr;
grid-template-rows: auto auto;
grid-template-areas:
". . . nl nl nl nl nl nl . . ."
width: 100%;
}
#main-nav section .link-grid .links {
grid-area: nl;
padding-bottom: 55px;
height: 100%;
}
#main-nav section header {
text-align: center;
}
#main-nav section header img {
height: 22px;
padding-top: 17px;
margin-bottom: 50px;
}
#main-nav section .link-grid .links a {
#main-nav .links a {
display: block;
position: relative;
line-height: 60px;
@ -173,13 +179,14 @@ h3 {
text-align: center;
transition: color 0.7s cubic-bezier(0.23, 1, 0.32, 1);
font-family: "blankaregular";
z-index: 0;
}
#main-nav section .link-grid .links a:hover {
#main-nav .links a:hover {
color: #222;
}
#main-nav section .link-grid .links a::after {
#main-nav .links a::after {
content: '';
display: block;
position: absolute;
@ -193,7 +200,7 @@ h3 {
z-index: -1;
}
#main-nav section .link-grid .links a:hover::after {
#main-nav .links a:hover::after {
transform: scaleX(1);
}
@ -474,6 +481,7 @@ h3 {
margin: 0;
padding: 0;
text-align: center;
column-count: 2;
}
#users ul li {
@ -481,6 +489,23 @@ h3 {
padding: 0;
}
@media (min-width: 728px) {
#users ul {
column-count: 3;
}
}
@media (min-width: 1400px) {
#users ul {
column-count: 4;
}
}
#snowflake {
margin: 50px 0;
text-align: center;
}
/* ---------- FOOTER ---------- */
#footer {
@ -501,3 +526,56 @@ h3 {
#footer p {
margin-bottom: 5px;
}
@-webkit-keyframes fadeInFromNone {
0% {
opacity: 0;
}
1% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-moz-keyframes fadeInFromNone {
0% {
opacity: 0;
}
1% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-o-keyframes fadeInFromNone {
0% {
opacity: 0;
}
1% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeInFromNone {
0% {
opacity: 0;
}
1% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@ -1,3 +1,6 @@
<section id="snowflake">
<iframe src="/snowflake/snowflake.html" width="320" height="240" frameborder="0" scrolling="no"></iframe>
</section>
<section id="footer">
<div class="copy">
<p>Icons made by <a href="https://www.flaticon.com/authors/freepik"
@ -8,7 +11,7 @@
href="http://creativecommons.org/licenses/by/3.0/"
rel="noreferrer"
title="Creative Commons BY 3.0">CC 3.0 BY</a></p>
<p>&copy; 2019 Tilde Black</p>
<p>Tilde Black system pages are licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. Individual user pages are all rights reserved unless otherwise specified.</p>
</div>
</section>
</body>

View File

@ -6,32 +6,29 @@
<meta http-equiv=X-UA-Compatible content="IE=edge">
<meta name=viewport content="shrink-to-fit=no,width=device-width,height=device-height,initial-scale=1,user-scalable=1">
<meta name="description" content="The black hole of the tildeverse">
<link rel='stylesheet' href='/assets/styles.css'>
<link rel='stylesheet' href='assets/styles.css'>
<link rel='canonical' href='https://tilde.black/' />
<link rel='icon' href='favicon.ico' type='image/x-icon' />
<script defer type="text/javascript" src="/assets/scripts.js"></script>
<script defer src="/samizdat.js"></script>
</head>
<body>
<nav id='main-nav'>
<input type='checkbox' id='toggle' />
<label for='toggle'>&nbsp;</label>
<section>
<div class='link-grid'>
<div class='links'>
<a href='#intro'>Home</a>
<a href='#about'>About</a>
<a href='#docs'>Guides</a>
<a href='#features'>Features</a>
<a href='#users'>User Pages</a>
</div>
</div>
</section>
<nav id='main-nav' class='link-grid'>
<div class='links'>
<a href='#intro'>Home</a>
<a href='#about'>About</a>
<a href='#docs'>Guides</a>
<a href='#features'>Features</a>
<a href='#users'>User Pages</a>
</div>
<a class="menubutton open" href="#main-nav" aria-label="Open menu"></a>
<a class="menubutton close" href="#" aria-label="Close menu"></a>
</nav>
<section id="intro">
<h1>Tilde Black</h1>
<h2>The black hole of the tildeverse</h2>
<div class="icon"><img src="/assets/black-hole.svg" alt="Icon of a Black Hole"/></div>
<div class="icon"><img src="assets/black-hole.svg" alt="Icon of a Black Hole"/></div>
<div class="copy">
<p>This site is available on tor at: <a href="http://black6kfjetfuzaeozz7fs53whh7xtd4e27telrf5fg5kgdt5ah5plad.onion">black...jetfuz...onion</a></p>
<p>Tilde (~) servers are public access unix systems in the spirit of
<a href="https://tilde.club" rel="noreferrer">tilde.club</a>, which was created by Paul Ford
in 2014. Member status on a tilde server mainly involves some level of
@ -162,9 +159,10 @@
</div>
<ul>
<li>Secure web hosting</li>
<li>Gemini hosting</li>
<li>Gopher hosting</li>
<li>Email</li>
<li>Tor hidden service access</li>
<li>Tor hidden services (ssh, http, gopher, gemini) </li>
<li>NNTP private discussion boards</li>
<li>Guide authoring</li>
<li>Programming tools</li>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,103 @@
;(function(){
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
Gun.on('opt', function(root){
this.to.next(root);
var opt = root.opt;
if(root.once){ return }
if(!Gun.Mesh){ return }
if(false === opt.RTCPeerConnection){ return }
var env;
if(typeof window !== "undefined"){ env = window }
if(typeof global !== "undefined"){ env = global }
env = env || {};
var rtcpc = opt.RTCPeerConnection || env.RTCPeerConnection || env.webkitRTCPeerConnection || env.mozRTCPeerConnection;
var rtcsd = opt.RTCSessionDescription || env.RTCSessionDescription || env.webkitRTCSessionDescription || env.mozRTCSessionDescription;
var rtcic = opt.RTCIceCandidate || env.RTCIceCandidate || env.webkitRTCIceCandidate || env.mozRTCIceCandidate;
if(!rtcpc || !rtcsd || !rtcic){ return }
opt.RTCPeerConnection = rtcpc;
opt.RTCSessionDescription = rtcsd;
opt.RTCIceCandidate = rtcic;
opt.rtc = opt.rtc || {'iceServers': [
{url: 'stun:stun.l.google.com:19302'},
{url: "stun:stun.sipgate.net:3478"},
{url: "stun:stun.stunprotocol.org"},
{url: "stun:stun.sipgate.net:10000"},
{url: "stun:217.10.68.152:10000"},
{url: 'stun:stun.services.mozilla.com'}
]};
opt.rtc.dataChannel = opt.rtc.dataChannel || {ordered: false, maxRetransmits: 2};
opt.rtc.sdp = opt.rtc.sdp || {mandatory: {OfferToReceiveAudio: false, OfferToReceiveVideo: false}};
opt.announce = function(to){
root.on('out', {rtc: {id: opt.pid, to:to}}); // announce ourself
};
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root);
root.on('create', function(at){
this.to.next(at);
setTimeout(opt.announce, 1);
});
root.on('in', function(msg){
if(msg.rtc){ open(msg) }
this.to.next(msg);
});
function open(msg){
var rtc = msg.rtc, peer, tmp;
if(!rtc || !rtc.id){ return }
delete opt.announce[rtc.id]; /// remove after connect
if(tmp = rtc.answer){
if(!(peer = opt.peers[rtc.id] || open[rtc.id]) || peer.remoteSet){ return }
return peer.setRemoteDescription(peer.remoteSet = new opt.RTCSessionDescription(tmp));
}
if(tmp = rtc.candidate){
peer = opt.peers[rtc.id] || open[rtc.id] || open({rtc: {id: rtc.id}});
return peer.addIceCandidate(new opt.RTCIceCandidate(tmp));
}
//if(opt.peers[rtc.id]){ return }
if(open[rtc.id]){ return }
(peer = new opt.RTCPeerConnection(opt.rtc)).id = rtc.id;
var wire = peer.wire = peer.createDataChannel('dc', opt.rtc.dataChannel);
open[rtc.id] = peer;
wire.onclose = function(){
delete open[rtc.id];
mesh.bye(peer);
//reconnect(peer);
};
wire.onerror = function(err){};
wire.onopen = function(e){
//delete open[rtc.id];
mesh.hi(peer);
}
wire.onmessage = function(msg){
if(!msg){ return }
mesh.hear(msg.data || msg, peer);
};
peer.onicecandidate = function(e){ // source: EasyRTC!
if(!e.candidate){ return }
root.on('out', {'@': msg['#'], rtc: {candidate: e.candidate, id: opt.pid}});
}
peer.ondatachannel = function(e){
var rc = e.channel;
rc.onmessage = wire.onmessage;
rc.onopen = wire.onopen;
rc.onclose = wire.onclose;
}
if(tmp = rtc.offer){
peer.setRemoteDescription(new opt.RTCSessionDescription(tmp));
peer.createAnswer(function(answer){
peer.setLocalDescription(answer);
root.on('out', {'@': msg['#'], rtc: {answer: answer, id: opt.pid}});
}, function(){}, opt.rtc.sdp);
return;
}
peer.createOffer(function(offer){
peer.setLocalDescription(offer);
root.on('out', {'@': msg['#'], rtc: {offer: offer, id: opt.pid}});
}, function(){}, opt.rtc.sdp);
return peer;
}
});
var noop = function(){};
}());

View File

@ -0,0 +1,130 @@
/* ========================================================================= *\
|* === Stashing plugin using the Cache API === *|
\* ========================================================================= */
/**
* getting content from cache
*/
let getContentFromCache = (url) => {
console.log('Samizdat: getting from cache!')
return caches.open('v1')
.then((cache) => {
return cache.match(url)
})
.then((response) => {
if (typeof response === 'undefined') {
throw new Error('Resource not found in cache');
} else {
response.headers.forEach(function(v, k){
console.log('+-- Retrieved cached header: ', k, ' :: ', v)
});
// return the response
return response
}
})
}
/**
* add resources to cache
*
* implements the stash() Samizdat plugin method
*
* accepts either a Response
* or a string containing a URL
* or an Array of string URLs
*/
let cacheContent = (resource, key) => {
return caches.open('v1')
.then((cache) => {
if (typeof resource === 'string') {
// assume URL
console.log("(COMMIT_UNKNOWN) caching an URL")
return cache.add(resource)
} else if (Array.isArray(resource)) {
// assume array of URLs
console.log("(COMMIT_UNKNOWN) caching an Array of URLs")
return cache.addAll(resource)
} else {
// assume a Response
// which means we either have a Request in key, a string URL in key,
// or we can use the URL in resource.url
if ( (typeof key !== 'object') && ( (typeof key !== 'string') || (key === '') ) ) {
if (typeof resource.url !== 'string' || resource.url === '') {
throw new Error('No URL to work with!')
}
key = resource.url
}
// we need to create a new Response object
// with all the headers added explicitly
// otherwise the x-samizdat-* headers get ignored
var init = {
status: resource.status,
statusText: resource.statusText,
headers: {}
};
resource.headers.forEach(function(val, header){
init.headers[header] = val;
});
return resource
.blob()
.then((blob) => {
console.log("(COMMIT_UNKNOWN) caching a Response to: " + key)
return cache.put(key, new Response(
blob,
init
))
})
}
})
}
/**
* remove resources from cache
*
* implements the unstash() Samizdat plugin method
*
* accepts either a Response
* or a string containing a URL
* or an Array of string URLs
*/
let clearCachedContent = (resource) => {
return caches.open('v1')
.then((cache) => {
if (typeof resource === 'string') {
// assume URL
console.log("(COMMIT_UNKNOWN) deleting a cached URL")
return cache.delete(resource)
} else if (Array.isArray(resource)) {
// assume array of URLs
console.log("(COMMIT_UNKNOWN) deleting an Array of cached URLs")
return Promise.all(
resource.map((res)=>{
return cache.delete(res)
})
)
} else {
// assume a Response
// which means we have an URL in resource.url
console.log("(COMMIT_UNKNOWN) removing a Response from cache: " + resource.url)
return cache.delete(resource.url)
}
})
}
// initialize the SamizdatPlugins array
if (!Array.isArray(self.SamizdatPlugins)) {
self.SamizdatPlugins = new Array()
}
// and add ourselves to it
// with some additional metadata
self.SamizdatPlugins.push({
name: 'cache',
description: 'Locally cached responses, using the Cache API.',
version: 'COMMIT_UNKNOWN',
fetch: getContentFromCache,
stash: cacheContent,
unstash: clearCachedContent
})

View File

@ -0,0 +1,64 @@
/* ========================================================================= *\
|* === Regular HTTP(S) fetch() plugin === *|
\* ========================================================================= */
/**
* this plugin does not implement any push method
*/
/**
* getting content using regular HTTP(S) fetch()
*/
let fetchContent = (url) => {
console.log('Samizdat: regular fetch!')
return fetch(url, {cache: "reload"})
.then((response) => {
// 4xx? 5xx? that's a paddlin'
if (response.status >= 400) {
// throw an Error to fall back to Samizdat:
throw new Error('HTTP Error: ' + response.status + ' ' + response.statusText);
}
// all good, it seems
console.log("(COMMIT_UNKNOWN) Fetched:", response.url);
// we need to create a new Response object
// with all the headers added explicitly,
// since response.headers is immutable
var init = {
status: response.status,
statusText: response.statusText,
headers: {}
};
response.headers.forEach(function(val, header){
init.headers[header] = val;
});
// add the X-Samizdat-* headers to the mix
init.headers['X-Samizdat-Method'] = 'fetch'
init.headers['X-Samizdat-ETag'] = response.headers.get('ETag')
// return the new response, using the Blob from the original one
return response
.blob()
.then((blob) => {
return new Response(
blob,
init
)
})
})
}
// initialize the SamizdatPlugins array
if (!Array.isArray(self.SamizdatPlugins)) {
self.SamizdatPlugins = new Array()
}
// and add ourselves to it
// with some additional metadata
self.SamizdatPlugins.push({
name: 'fetch',
description: 'Just a regular HTTP(S) fetch()',
version: 'COMMIT_UNKNOWN',
fetch: fetchContent,
})

View File

@ -0,0 +1,592 @@
/* ========================================================================= *\
|* === Basic utils useful only in browser window === *|
\* ========================================================================= */
// create an object to hold everything that needs to be held globally
var samizdat = {
info: {},
status: false,
contentUnavailable: false,
cacheStale: false,
clientId: null
}
// some basic method stats
samizdat.methodStats = {}
// UI elements displaying the status for each local resource URL
samizdat.resourceDisplays = {}
/**
* creating a safe CSS class name from a string
*/
samizdat.safeClassName = (name) => {
return encodeURIComponent(name.toLowerCase()).replace(/%[0-9A-F]{2}/gi,'-')
}
/**
* creating the standalone Samizdat UI
*/
samizdat.addUI = () => {
var uiTemplate = document.createElement('template')
uiTemplate.innerHTML = `<div id="samizdat-ui">
<div class="samizdat-message-container"></div>
<div id="samizdat-ui-container" class="samizdat-status-service-worker">
<input type="checkbox" id="samizdat-ui-toggle"/>
<div class="samizdat-description">
<p><a href="https://samizdat.is/">Samizdat</a> is a tool that helps circumvent web censorship.<br/>If you are seeing this it means some content is blocked or unavailable.<br/>Samizdat will attempt to get it for you anyway.</p>
<div class="samizdat-status-display"></div>
</div>
<label for="samizdat-ui-toggle" class="samizdat-toggle"><div></div></label>
</div></div>`
var uiStyle = document.createElement('style')
uiStyle.innerHTML = `#samizdat-ui {
display:flex;
align-items: flex-end;
flex-direction:column-reverse;
flex-wrap:nowrap;
position:fixed;
top:0px;
right:0px;
visibility:hidden;
}
#samizdat-ui.content-unavailable,
#samizdat-ui:target {
visibility:visible;
}
#samizdat-ui .samizdat-message-container {
}
#samizdat-ui .samizdat-message {
font-size:90%;
text-align:center;
background:#dfd;
border-radius:1em;
box-shadow:0px 0px 3px #dfd;
padding:0.5em 2em 0.5em 1em;
transition: ease-in 0.5s opacity;
opacity: 1;
position: relative;
top:16px;
right:5px;
color: #060;
text-shadow: 0px 0px 2px white;
font-family: sans;
}
#samizdat-ui .samizdat-message::after {
display: block;
content: "x";
position: absolute;
right: 0.5em;
top: 0.7em;
font-size:90%;
border-radius: 100%;
width: 1em;
height: 1em;
line-height: 0.8em;
padding-left: 0.01em;
box-shadow: inset 0px 0px 2px #080;
transition: ease-in 0.5s color, ease-in 0.5s background-color, ease-in 0.5s box-shadow-color;
color: #080;
background:white;
}
#samizdat-ui .samizdat-message:hover::after {
background: #080;
color: white;
box-shadow: inset 0px 0px 2px black;
}
#samizdat-ui .samizdat-message:first-child::before {
display:block;
content:" ";
width:1em;
height:1em;
position:absolute;
right:1em;
top:-0.5em;
background:#dfd;
box-shadow:0px 0px 3px #dfd;
transform: rotate(45deg);
z-index:-1;
}
#samizdat-ui-container {
background:#ddd;
box-shadow:0px 0px 3px black;
border-bottom-left-radius:30px;
padding: 4px 4px 8px 8px;
display:flex;
flex-wrap:nowrap;
}
#samizdat-ui-container .samizdat-toggle {
width:32px;
height:32px;
background:url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA5MCA5MCIgeD0iMHB4IiB5PSIwcHgiPjx0aXRsZT5iYWxsMjwvdGl0bGU+PGcgZGF0YS1uYW1lPSJMYXllciAyIj48cGF0aCBkPSJNOTIuMTQsMzkuMjZhNDEuOCw0MS44LDAsMCwwLTEuMjktNUE0NSw0NSwwLDEsMCw0Ny40Nyw5M0g0OGE0NC4zNSw0NC4zNSwwLDAsMCw0LjY0LS4yNEE0NS4wNyw0NS4wNywwLDAsMCw5Myw0OCw0NC4yMSw0NC4yMSwwLDAsMCw5Mi4xNCwzOS4yNlpNMjksMjQuMTZBMzAuMzgsMzAuMzgsMCwwLDAsMTcuNTEsNDhhMi40MSwyLjQxLDAsMSwxLTQuODEsMEEzNS4xNCwzNS4xNCwwLDAsMSwyNiwyMC40MWEyLjQsMi40LDAsMSwxLDMsMy43NVptMTktNi42NWEzMC40NiwzMC40NiwwLDAsMC0xMC4yOSwxLjc3LDIuMjgsMi4yOCwwLDAsMS0uODEuMTUsMi40MSwyLjQxLDAsMCwxLS44MS00LjY4QTM1LjM4LDM1LjM4LDAsMCwxLDQ4LDEyLjdhMi40MSwyLjQxLDAsMSwxLDAsNC44MVoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0zIC0zKSI+PC9wYXRoPjwvZz48L3N2Zz4=') center center no-repeat;
display: block;
background-size:contain;
}
#samizdat-ui-container .samizdat-toggle > div {
width:100%;
height:100%;
background:url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjRjk3OTAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNTIgNTIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDUyIDUyIiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48Zz48cGF0aCBkPSJNMzYsMTEuMDI5NzU0Nmg3LjM2MjYwOTljLTAuMDc3ODgwOS0wLjEwMDIxOTctMC4xNTExODQxLTAuMjAzMzA4MS0wLjI0MjYxNDctMC4yOTQ3Mzg4bC03LjgyOTk1NjEtNy44MzAwMTcxICAgIEMzNS4yMDA4MDU3LDIuODE1NzY1NCwzNS4wOTg3NTQ5LDIuNzQzNDM4NywzNSwyLjY2NjcxNzV2Ny4zNjMwMzcxQzM1LDEwLjU4MTAyNDIsMzUuNDQ4NzMwNSwxMS4wMjk3NTQ2LDM2LDExLjAyOTc1NDZ6Ij48L3BhdGg+PC9nPjxnPjxwYXRoIGQ9Ik0zNiwxMy4wMjk3NTQ2Yy0xLjY1NDI5NjksMC0zLTEuMzQ1NzAzMS0zLTNWMi4wMjQ5OTM5SDExYy0xLjY0OTk2MzQsMC0zLDEuMzUwMDM2Ni0zLDN2NDEuOTUwMDEyMiAgICBjMCwxLjY2MDAzNDIsMS4zNTAwMzY2LDMsMywzaDMwYzEuNjUwMDI0NCwwLDMtMS4zMzk5NjU4LDMtM1YxMy4wMjk3NTQ2SDM2eiBNMjEsMTkuMDAyODk5MmgxMHYySDIxVjE5LjAwMjg5OTJ6IE0zMSw0My4wMDMzODc1ICAgIEgyMXYtMmgxMFY0My4wMDMzODc1eiBNMzcsMzYuMDAzMzg3NUgxNXYtMmgyMlYzNi4wMDMzODc1eiBNMzcsMjguMDAyODk5MkgxNXYtMmgyMlYyOC4wMDI4OTkyeiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+') center center no-repeat;
display: block;
background-size:50% 50%;
}
#samizdat-ui-container.active .samizdat-toggle {
animation-name: samizdat-ball-rolling;
animation-duration:10s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
#samizdat-ui-container #samizdat-ui-toggle {
display:none;
}
#samizdat-ui-container > div {
display:none;
}
#samizdat-ui-container > #samizdat-ui-toggle:checked ~ div {
display:block;
}
#samizdat-ui-container .samizdat-description > p {
font-size:80%;
margin-top: 0.5em;
margin-bottom: 0.5em;
margin-right: 1em;
text-align: right;
text-shadow: -1px -1px 0px #ccc, 1px 1px 0px #eee;
color: #666;
font-family: sans-serif;
}
#samizdat-ui-container .samizdat-description > p a {
color: #d70;
}
#samizdat-ui-container .samizdat-status-display {
justify-content: right;
display: flex;
padding-right: 0.5em;
}
/*
* these will be useful also outside the #samizdat-ui
* for example, if there is a .samizdat-status-display in the page's HTML
*/
.samizdat-status-display > li {
display:inline-block;
font-size:80%;
font-family: Monospace;
}
.samizdat-status-element {
font-weight: bold;
display: inline-block;
text-align: center;
text-decoration:none;
background:#bbb;
padding:0.4em 1em;
border-radius:0.6em;
color:#777;
box-shadow: inset 0px 0px 3px #777;
margin: 0.5em;
transition: background-color 1s ease, color 1s ease, box-shadow 1s ease;
}
.samizdat-status-element.active {
box-shadow: 0px 0px 3px #f80, 0px 0px 3px #a60;
color: #fff;
background: #e70;
}
@keyframes samizdat-ball-rolling {
from {transform:rotate(0deg)}
to {transform:rotate(359deg)}
}`
document.head.insertAdjacentElement('afterbegin', uiStyle)
document.body.insertAdjacentElement('afterbegin', uiTemplate.content.firstChild)
}
/**
* fetched resource display element
*/
samizdat.addFetchedResourceElements = (url, fetchedResourcesDisplays) => {
// make sure we have the container element to work with
if (typeof fetchedResourcesDisplays !== 'object') {
fetchedResourcesDisplays = document.getElementsByClassName("samizdat-fetched-resources-list")
}
var itemHTML = `<li class="samizdat-fetched-resources-item"><label>`
var foundSuccess = false
var pluginsHTML = ''
Object.keys(samizdat.methodStats).forEach((plugin)=>{
var pclass = samizdat.safeClassName(plugin)
if (typeof samizdat.info[url] !== "undefined" && typeof samizdat.info[url][plugin] !== "undefined") {
pclass = pclass + ' ' + samizdat.info[url][plugin].state;
foundSuccess = foundSuccess || (samizdat.info[url][plugin].state === "success")
}
pluginsHTML += `<span class="samizdat-fetched-resource-method ${pclass}">${plugin}</span>`
})
itemHTML += `<input type="checkbox" ${ foundSuccess ? 'checked="checked"' : 'disabled="disabled"' }/><span class="samizdat-fetched-resource-url"><span>${url}</span></span>${pluginsHTML}</label></li>`;
var item = document.createElement('template')
item.innerHTML = itemHTML;
samizdat.resourceDisplays[url] = new Array()
for (let frd of fetchedResourcesDisplays) {
samizdat.resourceDisplays[url].push(
frd.insertAdjacentElement('beforeend', item.content.firstChild.cloneNode(true))
)
}
}
/**
* creating/updating fetched resources data
*/
samizdat.updateFetchedResources = () => {
// getting these elements once instead of once per URL...
var fetchedResourcesDisplays = document.getElementsByClassName("samizdat-fetched-resources-list")
Object.keys(samizdat.info).forEach((url)=>{
// simplify
si = samizdat.info[url]
// if there are no status display elements for this URL...
if (typeof samizdat.resourceDisplays[url] === 'undefined') {
// ...create the elements
samizdat.addFetchedResourceElements(url, fetchedResourcesDisplays)
// otherwise, if si.method evaluates to true (i.e. is not an empty string nor null in this case)
} else {
// samizdat.methodStats has the most comprehensive list of methods used
Object.keys(samizdat.methodStats).forEach((method)=>{
var pclass = samizdat.safeClassName(method);
var foundSuccess = false
// do we have the method even?
if (typeof si[method] === "object") {
// is this a success?
if (si[method].state === "success") {
for (let rdisplay of samizdat.resourceDisplays[url]) {
if (! rdisplay.getElementsByClassName(pclass)[0].classList.contains('success')) {
// make sure the right classes are on
rdisplay.getElementsByClassName(pclass)[0].classList.remove('running')
rdisplay.getElementsByClassName(pclass)[0].classList.add('success')
// make sure the checkbox is checked
rdisplay.getElementsByTagName('input')[0].checked = true
rdisplay.getElementsByTagName('input')[0].disabled = false
}
}
// is this a running thing?
} else if (si[method].state === "running") {
for (let rdisplay of samizdat.resourceDisplays[url]) {
if (! rdisplay.getElementsByClassName(pclass)[0].classList.contains('running')) {
// make sure the right classes are on
rdisplay.getElementsByClassName(pclass)[0].classList.remove('success')
rdisplay.getElementsByClassName(pclass)[0].classList.add('running')
}
}
// nope, an error presumably
} else {
for (let rdisplay of samizdat.resourceDisplays[url]) {
// make sure the right classes are on
rdisplay.getElementsByClassName(pclass)[0].classList.remove('success')
rdisplay.getElementsByClassName(pclass)[0].classList.remove('running')
}
}
// clarly this method has not even been used for the resource
} else {
for (let rdisplay of samizdat.resourceDisplays[url]) {
// make sure the right classes are on
rdisplay.getElementsByClassName(pclass)[0].classList.remove('success')
rdisplay.getElementsByClassName(pclass)[0].classList.remove('running')
}
}
})
}
})
}
/**
* adding status display per plugin
*
* plugin - plugin name
* description - plugin description (optional; default: empty string)
* status - status text (optional; default: number of resources fetched
* using this plugin, based on methodStats)
*/
samizdat.addPluginStatus = (plugin, description='', status=null) => {
console.log('(COMMIT_UNKNOWN) addPluginStatus(' + plugin + ')')
var statusDisplays = document.getElementsByClassName("samizdat-status-display");
var pclass = encodeURIComponent(plugin.toLowerCase()).replace(/%[0-9A-F]{2}/gi,'-');
var pcount = 0;
if (typeof samizdat.methodStats[plugin] !== 'undefined') {
pcount = samizdat.methodStats[plugin];
}
for (let sd of statusDisplays) {
sd.insertAdjacentHTML('beforeend', `<li><abbr class="samizdat-status-element ${pcount ? 'active' : ''} samizdat-status-${pclass}" title="${description}">${plugin}: <span class="status">${status ? status : pcount}</span></abbr></li>`)
}
}
/**
* updating status display per plugin
*
* expects an object that contains at least `name` attribute
*/
samizdat.updatePluginStatus = (plugin) => {
//console.log('updatePluginStatus :: ' + plugin)
var pclass = samizdat.safeClassName(plugin);
//console.log('updatePluginStatus :: pclass: ' + pclass)
var statusDisplay = document.querySelectorAll(".samizdat-status-" + pclass + " > .status");
//console.log('updatePluginStatus :: statusDisplay: ' + typeof statusDisplay)
var pcount = 0;
if (typeof samizdat.methodStats[plugin] !== 'undefined') {
pcount = samizdat.methodStats[plugin]
}
for (let statusDisplay of document.querySelectorAll(".samizdat-status-" + pclass + " > .status")) {
statusDisplay.innerText = pcount
if ( (pcount === 0) && statusDisplay.parentElement.classList.contains('active')) {
statusDisplay.parentElement.classList.remove('active')
} else if ( (pcount > 0) && ! statusDisplay.parentElement.classList.contains('active')) {
statusDisplay.parentElement.classList.add('active')
}
}
}
/**
* toggling resource checkboxes (only if not disabled)
*/
samizdat.toggleResourceCheckboxes = () => {
document.querySelectorAll('.samizdat-fetched-resources-item input')
.forEach((el)=>{
el.checked = ! el.disabled && ! el.checked
})
}
/**
* stashing and unstashing resources
*
* stash param means "stash" if set to true (the default), "unstash" otherwise
*/
samizdat.stashOrUnstashResources = (stash=true) => {
// what are we doing?
operation = {
clientId: samizdat.clientId
}
// get the resources
var resources = []
document
.querySelectorAll('.samizdat-fetched-resources-item input:checked')
.forEach((el)=>{
resources.push(el.parentElement.querySelector('.samizdat-fetched-resource-url').innerText)
})
if (stash) {
operation.stash = [resources]
console.log('(COMMIT_UNKNOWN) Calling `stash()` on the service worker to stash the resources...')
} else {
operation.unstash = [resources]
console.log('(COMMIT_UNKNOWN) Calling `unstash()` on the service worker to unstash the resources...')
}
// RPC call on the service worker
return navigator
.serviceWorker
.controller
.postMessage(operation)
}
/**
* publishing certain resources to Gun+IPFS
*/
samizdat.publishResourcesToGunAndIPFS = () => {
var user = document.getElementById('samizdat-gun-user').value
var pass = document.getElementById('samizdat-gun-password').value
if (! user || ! pass) {
throw new Error("Gun user/password required!")
}
var resources = []
document.querySelectorAll('.samizdat-fetched-resources-item input:checked')
.forEach((el)=>{
resources.push(el.parentElement.querySelector('.samizdat-fetched-resource-url').innerText)
})
// call it!
console.log('(COMMIT_UNKNOWN) Calling `publish()` on the service worker to publish the resources...')
return navigator
.serviceWorker
.controller
.postMessage({
clientId: samizdat.clientId,
publish: [resources, user, pass]
})
}
/**
* display a Samizdat message
*/
samizdat.displayMessage = (msg) => {
// prepare the template
var messageBox = document.createElement('template')
messageBox.innerHTML = `<div class="samizdat-message">${msg}</div>`
// attach it to all samizdat-message-containers out there
for (let smc of document.getElementsByClassName('samizdat-message-container')) {
var msg = messageBox.content.firstChild.cloneNode(true)
msg.onclick = (e) => {
e.target.style.opacity=0
setTimeout(()=>{e.target.remove()}, 1000)
}
smc.insertAdjacentElement('beforeend', msg)
setTimeout(()=>{
msg.style.opacity=0
setTimeout(()=>{msg.remove()}, 1000)
}, 5000)
}
console.log(' +-- message shown!')
}
/**
* onload handler just to mark stuff as loaded
* for purposes of informing the user all is loaded
* when service worker messages us about it
*/
window.addEventListener('load', function() {
samizdat.status = "loaded";
/*
* status display: how did this file get fetched?
*
* yes, this code has to be directly here,
* since we want to know how *this exact file* got fetched
*/
if (typeof samizdat.info[window.location.href] === 'object') {
// service worker info
for (let samizdat_sw of document.querySelectorAll(".samizdat-status-service-worker")) {
samizdat_sw.className += " active";
try {
samizdat_sw.querySelector('.status').innerHTML = "yes";
} catch(e) {}
}
}
// was any content blocked so far?
if (samizdat.contentUnavailable) {
samizdat.displayMessage('Some content seems blocked or unavailable. Attempting to retrieve it via Samizdat.')
}
})
console.log('(COMMIT_UNKNOWN) DOMContentLoaded!')
// add the generic service worker "badge"
samizdat.addUI()
samizdat.addPluginStatus('service worker', 'A service worker is an event-driven worker that intercepts fetch events.', 'no')
/* ========================================================================= *\
|* === Service worker setup === *|
\* ========================================================================= */
if ('serviceWorker' in navigator) {
if (navigator.serviceWorker.controller) {
// Service worker already registered.
console.log('(COMMIT_UNKNOWN) Service Worker already registered.')
} else {
var scriptPath = document.currentScript.src
var scriptFolder = scriptPath.substr(0, scriptPath.lastIndexOf( '/' )+1 )
var serviceWorkerPath = scriptFolder + 'service-worker.js'
console.log('Service Worker script at: ' + serviceWorkerPath)
navigator.serviceWorker.register(serviceWorkerPath, {
scope: './'
}).then(function(reg) {
// Success.
console.log('(COMMIT_UNKNOWN) Service Worker registered.')
}).catch(error => {
console.log("(COMMIT_UNKNOWN) Error while registering a service worker: ", error)
})
}
// handling the messages from ServiceWorker
navigator.serviceWorker.addEventListener('message', event => {
console.log('SamizdatInfo received!')
if (event.data.url) {
console.log('+-- for:', event.data.url)
if (event.data.method) {
console.log(' +-- method:', event.data.method)
console.log(' +-- state :', event.data.state)
samizdat.info[event.data.url] = samizdat.info[event.data.url] || {}
samizdat.info[event.data.url][event.data.method] = event.data
// update method stats
if (typeof samizdat.methodStats[event.data.method] === 'undefined') {
// setup the stats
samizdat.methodStats[event.data.method] = 0
// but also we now know this method has not been seen before
// so set-up the plugin status display
samizdat.addPluginStatus(event.data.method)
}
if (event.data.state === "success") {
samizdat.methodStats[event.data.method]++
console.log(' +-- methodStats incremented to:', samizdat.methodStats[event.data.method])
samizdat.updatePluginStatus(event.data.method)
// if the method was `fetch`, and that was the first method, and the outcome is `error`, we *might* be blocked
} else if ( event.data.state === "error"
&& event.data.method === "fetch"
&& Object.keys(samizdat.info[event.data.url]).length === 1
&& Object.keys(samizdat.info[event.data.url])[0] === "fetch" ) {
// we seem to be blocked
document.getElementById('samizdat-ui').classList.add('content-unavailable')
// if contentUnavailable is false, that means this is the first time we hit a problem fetching
if (!samizdat.contentUnavailable) {
// mark it properly
samizdat.contentUnavailable = true
// if loaded, show the message to the user.
// if not, the message will be shown on `load` event anyway
if (samizdat.status === "loaded") {
samizdat.displayMessage('Some content seems blocked or unavailable. Attempting to retrieve it via Samizdat.')
}
}
}
// update the fetched resources display
// TODO: this updates *all* resources on each received message,
// TODO: and so is rather wasteful
samizdat.updateFetchedResources()
}
// we only want to mark that new content is available, and handle the message
// at allFetched event
if (event.data.fetchedDiffers) {
console.log(' +-- fetched version apparently differs from cached for:', event.data.url)
// record fo the URL
samizdat.info[event.data.url].cacheStale = true
// record gloally
samizdat.cacheStale = true
}
}
if (event.data.allFetched) {
if (samizdat.status === "loaded") {
// set the status so that we don't get the message doubled
samizdat.status = "complete"
// inform the user
if (samizdat.cacheStale) {
samizdat.displayMessage('Newer version of this page is available; please reload to see it.')
} else {
console.log('+-- all fetched!..')
samizdat.displayMessage('Fetching via Samizdat finished; no new content found.')
}
}
}
if (event.data.clientId) {
console.log('+-- got our clientId:', event.data.clientId)
samizdat.clientId = event.data.clientId
}
if (event.data.plugins) {
var msg = '+-- got the plugin list:'
event.data.plugins.forEach((p)=>{
msg += '\n +-- ' + p
// initialize methodStats
if (typeof samizdat.methodStats[p] === 'undefined') {
samizdat.methodStats[p] = 0
// set-up the plugin status display
samizdat.addPluginStatus(p)
}
})
console.log(msg)
}
if (event.data.serviceWorker) {
console.log('+-- got the serviceWorker version:', event.data.serviceWorker)
samizdat_sws = document.getElementsByClassName("samizdat-commit-service-worker");
for (let element of samizdat_sws) {
element.innerHTML = event.data.serviceWorker;
}
}
});
}

View File

@ -0,0 +1,513 @@
/*
* Samizdat Service Worker.
*
* Strategy (not fully implemented yet):
* 1. Try to load from main website.
* 2. If loading fails, load from Samizdat.
* 3. If loading is too slow, load from Samizdat.
* 4. If loaded content doesn't match authenticated versions, fall back to
* Samizdat.
*/
// initialize the SamizdatPlugins array
if (!Array.isArray(self.SamizdatPlugins)) {
self.SamizdatPlugins = new Array()
}
// load the Gun+IPFS plugin, and idb-keyval
// order in which plugins are loaded defines the order
// in which they are called!
self.importScripts(
"./plugins/fetch.js",
"./plugins/cache.js");
console.log('(COMMIT_UNKNOWN) SamizdatPlugins.length:', self.SamizdatPlugins.length)
/**
* fetch counter per clientId
*
* we need to keep track of active fetches per clientId
* so that we can inform a given clientId when we're completely done
*/
self.activeFetches = {}
/**
* decrement fetches counter
* and inform the correct clientId if all is finished done
*/
let decrementActiveFetches = (clientId) => {
// decrement the fetch counter for the client
self.activeFetches[clientId]--
console.log('+-- activeFetches[' + clientId + ']:', self.activeFetches[clientId])
if (self.activeFetches[clientId] === 0) {
console.log('(COMMIT_UNKNOWN) All fetches done!')
// inform the client
// client has to be smart enough to know if that is just temporary
// (and new fetches will fire in a moment, because a CSS file just
// got fetched) or not
clients.get(clientId).then((client)=>{
client.postMessage({
allFetched: true
})
})
.then(()=>{
console.log('(COMMIT_UNKNOWN) all-fetched message sent.')
})
}
}
/* ========================================================================= *\
|* === SamizdatResourceInfo === *|
\* ========================================================================= */
/**
* Samizdat resource info class
*
* keeps the values as long as the service worker is running,
* and communicates all changes to relevant clients
*
* clients are responsible for saving and keeping the values across
* service worker restarts, if that's required
*/
let SamizdatResourceInfo = class {
// actual values of the fields
// only used internally, and stored into the Indexed DB
values = {
url: '', // read only after initialization
clientId: null,
fetchError: null,
method: null,
state: null, // can be "error", "success", "running"
serviceWorker: 'COMMIT_UNKNOWN' // this will be replaced by commit sha in CI/CD; read-only
}
client = null;
/**
* constructor
* needed to set the URL and clientId
*/
constructor(url, clientId) {
// set it
this.values.url = url
this.values.clientId = clientId
// we might not have a non-empty clientId if it's a cross-origin fetch
if (clientId) {
// get the client from Client API based on clientId
clients.get(clientId).then((client)=>{
// set the client
this.client = client
// Send a message to the client.
this.client.postMessage(this.values);
})
}
}
/**
* update this.values and immediately postMessage() to the relevant client
*
* data - an object with items to set in this.values
*/
update(data) {
// debug
var msg = '(COMMIT_UNKNOWN) Updated SamizdatResourceInfo for: ' + this.values.url
// was there a change? if not, no need to postMessage
var changed = false
// update the properties that are read-write
Object
.keys(data)
.filter((k)=>{
return ['fetchError', 'method', 'state'].includes(k)
})
.forEach((k)=>{
msg += '\n+-- ' + k + ': ' + data[k]
if (this.values[k] !== data[k]) {
msg += ' (changed!)'
changed = true
}
this.values[k] = data[k]
})
console.log(msg)
// send the message to the client
if (this.client && changed) {
this.client.postMessage(this.values);
}
}
/**
* fetchError property
*/
get fetchError() {
return this.values.fetchError
}
/**
* method property
*/
get method() {
return this.values.method
}
/**
* state property
*/
get state() {
return this.values.state
}
/**
* serviceWorker property (read-only)
*/
get serviceWorker() {
return this.values.serviceWorker
}
/**
* url property (read-only)
*/
get url() {
return this.values.url
}
/**
* clientId property (read-only)
*/
get clientId() {
return this.values.clientId
}
}
/* ========================================================================= *\
|* === Main Brain of Samizdat === *|
\* ========================================================================= */
/**
* get a plugin by name
*
* this doesn't have to be super-performant, since we should never have more
* then a few plugins
* (let's see how long it takes for me to eat my own words here)
*/
let getSamizdatPluginByName = (name) => {
for (i=0; i<SamizdatPlugins.length; i++) {
if (SamizdatPlugins[i].name === name) {
return SamizdatPlugins[i]
}
}
return null
}
/**
* run a plugin's fetch() method
* while handling all the auxiliary stuff like saving info in reqInfo
*
* plugin - the plugin to use
* url - string containing the URL to fetch
* lastError - error thrown by the previous plugin, if any (default: null)
*/
let samizdatFetch = (plugin, url, reqInfo) => {
// status of the current method
reqInfo.update({
method: plugin.name,
state: "running"
})
// log stuff
console.log("(COMMIT_UNKNOWN) Samizdat handling URL:", url,
'\n+-- current method : ' + plugin.name)
// run the plugin
return plugin.fetch(url)
}
/**
* callign a samizdat plugin function
*
* call - method name to call
* args - arguments that will be passed to it
*/
let callOnSamizdatPlugin = (call, args) => {
// find the first method implementing the method
for (i=0; i<SamizdatPlugins.length; i++) {
if (typeof SamizdatPlugins[i][call] === 'function') {
console.log('(COMMIT_UNKNOWN) Calling plugin ' + SamizdatPlugins[i].name + '.' + call + '()')
// call it
return SamizdatPlugins[i][call].apply(null, args)
}
}
}
/**
* Cycles through all the plugins, in the order they got registered,
* and returns a Promise resolving to a Response in case any of the plugins
* was able to get the resource
*
* request - string containing the URL we want to fetch
* clientId - string containing the clientId of the requesting client
* useStashed - use stashed resources; if false, only pull resources from live sources
* doStash - stash resources once fetched successfully; if false, do not stash pulled resources automagically
* stashedResponse - TBD
*/
let getResourceThroughSamizdat = (request, clientId, useStashed=true, doStash=true, stashedResponse=null) => {
// clean the URL, removing any fragment identifier
var url = request.url.replace(/#.+$/, '');
// set-up reqInfo for the fetch event
var reqInfo = new SamizdatResourceInfo(url, clientId)
// fetch counter
self.activeFetches[clientId]++
// filter out stash plugins if need be
var SamizdatPluginsRun = SamizdatPlugins.filter((plugin)=>{
return (useStashed || typeof plugin.stash !== 'function')
})
/**
* this uses Array.reduce() to chain the SamizdatPlugins[]-generated Promises
* using the Promise the first registered plugin as the default value
*
* see: https://css-tricks.com/why-using-reduce-to-sequentially-resolve-promises-works/
*
* this also means that SamizdatPlugins[0].fetch() below will run first
* (counter-intutively!)
*
* we are slice()-ing it so that the first plugin is only run once; it is
* run in the initialValue parameter below already
*
* ref:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
*/
return SamizdatPluginsRun
.slice(1)
.reduce(
(prevPromise, currentPlugin)=>{
return prevPromise.catch((error)=>{
console.log("(COMMIT_UNKNOWN) Samizdat plugin error for:", url,
'\n+-- method : ' + reqInfo.method,
'\n+-- error : ' + error.toString())
// save info in reqInfo -- status of the previous method
reqInfo.update({
state: "error",
fetchError: error.toString()
})
return samizdatFetch(currentPlugin, url, reqInfo)
})
},
// this samizdatFetch() will run first
// all other promises generated by SamizdatPlugins[] will be chained on it
// using the catch() in reduce() above
// skipping this very first plugin by way of slice(1)
samizdatFetch(SamizdatPluginsRun[0], url, reqInfo)
)
.then((response)=>{
// we got a successful response
decrementActiveFetches(clientId)
// record the success
reqInfo.update({state:"success"})
// get the plugin that was used to fetch content
plugin = getSamizdatPluginByName(reqInfo.method)
// if it's a stashing plugin...
if (typeof plugin.stash === 'function') {
// we obviously do not want to stash
console.log('(COMMIT_UNKNOWN) Not stashing, since resource is already retrieved by a stashing plugin:', url);
// since we got the data from a stashing plugin,
// let's run the rest of plugins in the background to check if we can get a fresher resource
// and stash it in cache for later use
console.log('(COMMIT_UNKNOWN) starting background no-stashed fetch for:', url);
// event.waitUntil?
// https://stackoverflow.com/questions/37902441/what-does-event-waituntil-do-in-service-worker-and-why-is-it-needed/37906330#37906330
getResourceThroughSamizdat(request, clientId, false, true, response.clone())
// return the response so that stuff can keep happening
return response
// otherwise, let's see if we want to stash
// and if we already had a stashed version that differs
} else {
// do we have a stashed version that differs?
if (stashedResponse && stashedResponse.headers) {
// this is where we check if the response from whatever plugin we got it from
// is newer than what we've stashed
console.log('(COMMIT_UNKNOWN) checking freshness of stashed version of:', url,
'\n+-- stashed from :', stashedResponse.headers.get('X-Samizdat-Method'),
'\n+-- fetched using :', reqInfo.method,
'\n+-- stashed X-Samizdat-ETag :', stashedResponse.headers.get('X-Samizdat-ETag'),
'\n+-- fetched X-Samizdat-ETag :', response.headers.get('X-Samizdat-ETag'))
// if the method does not match, or if it matches but the ETag doesn't
// we have a different response
// which means *probably* fresher content
if ( ( stashedResponse.headers.get('X-Samizdat-Method') !== reqInfo.method )
|| ( stashedResponse.headers.get('X-Samizdat-ETag') !== response.headers.get('X-Samizdat-ETag') ) ) {
// inform!
console.log('(COMMIT_UNKNOWN) fetched version method or ETag differs from stashed for:', url)
clients.get(reqInfo.clientId).then((client)=>{
client.postMessage({
url: url,
fetchedDiffers: true
})
})
}
}
// do we want to stash?
if (doStash) {
// find the first stashing plugin
for (i=0; i<SamizdatPlugins.length; i++) {
if (typeof SamizdatPlugins[i].stash === 'function') {
// ok, now we're in business
console.log('(COMMIT_UNKNOWN) Stashing a successful fetch of:', url,
'\n+-- fetched using :', reqInfo.method,
'\n+-- stashing using :', SamizdatPlugins[i].name)
response.headers.forEach(function(v, k){
console.log('+-- Stashing header: ', k, ' :: ', v)
});
// var cacheRequest = new Request(request, {
// headers: new Headers({
// 'X-Samizdat-Method': reqInfo.method,
// 'X-Samizdat-Etag': response.headers['X-Samizdat-ETag']
// })
// })
// working on clone()'ed response so that the original one is not touched
// TODO: should a failed stashing break the flow here? probably not!
return SamizdatPlugins[i].stash(response.clone(), url)
.then((res)=>{
// original response will be needed further down
return response
})
}
}
}
}
// if we're here it means we went through the whole list of plugins
// and found not a single stashing plugin
// or we don't want to stash the resources in the first place
// that's fine, but let's make sure the response goes forth
return response
})
// a final catch... in case all plugins fail
.catch((err)=>{
console.log("(COMMIT_UNKNOWN) Samizdat also failed completely: ", err,
'\n+-- URL : ' + url)
// cleanup
reqInfo.update({
state: "error",
fetchError: err.toString()
})
// this is very naïve and should in fact be handled
// inside the relevant plugin, probably
// TODO: is this even needed?
reqInfo.update({method: null})
decrementActiveFetches(clientId)
// rethrow
throw err
})
}
/* ========================================================================= *\
|* === Setting up the event handlers === *|
\* ========================================================================= */
self.addEventListener('install', event => {
// TODO: Might we want to have a local cache?
// "COMMIT_UNKNOWN" will be replaced with commit ID
console.log("0. Installed Inter-Planetary Shotgun (commit: COMMIT_UNKNOWN).");
// TODO: should we do some plugin initialization here?
});
self.addEventListener('activate', event => {
console.log("1. Activated Inter-Planetary Shotgun (commit: COMMIT_UNKNOWN).");
// TODO: should we do some plugin initialization here?
});
self.addEventListener('fetch', event => {
// if event.resultingClientId is available, we need to use this
// otherwise event.clientId is what we want
// ref. https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/resultingClientId
var clientId = event.clientId
if (event.resultingClientId) {
clientId = event.resultingClientId
// yeah, we seem to have to send the client their clientId
// because there is no way to get that client-side
// and we need that for sane messaging later
//
// so let's also send the plugin list, why not
//
// *sigh* JS is great *sigh*
clients
.get(clientId)
.then((client)=>{
client.postMessage({
clientId: clientId,
plugins: SamizdatPlugins.map((p)=>{return p.name}),
serviceWorker: 'COMMIT_UNKNOWN'
})
})
}
// counter!
if (typeof self.activeFetches[clientId] !== "number") {
self.activeFetches[clientId] = 0
}
// info
console.log("(COMMIT_UNKNOWN) Fetching!",
"\n+-- url :", event.request.url,
"\n+-- clientId :", event.clientId,
"\n+-- resultingClientId:", event.resultingClientId,
"\n +-- activeFetches[" + clientId + "]:", self.activeFetches[clientId]
)
// External requests go through a regular fetch()
if (!event.request.url.startsWith(self.location.origin)) {
return void event.respondWith(fetch(event.request));
}
// Non-GET requests go through a regular fetch()
if (event.request.method !== 'GET') {
return void event.respondWith(fetch(event.request));
}
// GET requests to our own domain that are *not* #samizdat-info requests
// get handled by plugins in case of an error
return void event.respondWith(getResourceThroughSamizdat(event.request, clientId))
});
/**
* assumptions to be considered:
* every message contains clientId (so that we know where to respond if/when we need to)
*/
self.addEventListener('message', (event) => {
// inform
var msg = '(COMMIT_UNKNOWN) Message received!'
Object.keys(event.data).forEach((k)=>{
msg += '\n+-- key: ' + k + " :: val: " + event.data[k]
})
console.log(msg);
/*
* supporting stash(), unstash(), and publish() only
*/
if (event.data.stash || event.data.unstash || event.data.publish) {
if (event.data.stash) {
callOnSamizdatPlugin('stash', event.data.stash)
}
if (event.data.unstash) {
callOnSamizdatPlugin('unstash', event.data.unstash)
}
if (event.data.publish) {
callOnSamizdatPlugin('publish', event.data.publish)
}
}
});

View File

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path fill="black" d="M9 6a1 1 0 0 0-.293-.707l-3-3a1 1 0 0 0-1.414 1.414L6.586 6 4.293 8.293a1 1 0 0 0 1.414 1.414l3-3A1 1 0 0 0 9 6z"/></svg>

After

Width:  |  Height:  |  Size: 438 B

View File

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path fill="black" d="M9 6a1 1 0 0 0-.293-.707l-3-3a1 1 0 0 0-1.414 1.414L6.586 6 4.293 8.293a1 1 0 0 0 1.414 1.414l3-3A1 1 0 0 0 9 6z"/></svg>

After

Width:  |  Height:  |  Size: 438 B

View File

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path fill="white" d="M9 6a1 1 0 0 0-.293-.707l-3-3a1 1 0 0 0-1.414 1.414L6.586 6 4.293 8.293a1 1 0 0 0 1.414 1.414l3-3A1 1 0 0 0 9 6z"/></svg>

After

Width:  |  Height:  |  Size: 438 B

View File

@ -0,0 +1,148 @@
body {
color: black;
margin: 10px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
width: 300px;
font-size: 12px;
}
#active {
margin: 20px 0;
text-align: center;
}
#statusimg {
background-image: url("status-off.svg");
background-repeat: no-repeat;
background-position: center center;
min-height: 60px;
}
#statusimg.on {
background-image: url("status-on.svg");
}
#statusimg.on.running {
background-image: url("status-running.svg");
}
.b {
border-top: 1px solid gainsboro;
padding: 10px;
position: relative;
}
.b a {
color: inherit;
display: inline-block;
text-decoration: none;
}
.error {
color: firebrick;
}
.learn:before {
content : " ";
display: block;
position: absolute;
top: 12px;
background-image: url('arrowhead-right-12.svg');
width: 12px;
height: 12px;
opacity : 0.6;
z-index: 9999;
right: 0px;
margin-right: 10px;
}
/* Snowflake Status */
.transfering {
-webkit-animation:spin 8s linear infinite;
-moz-animation:spin 8s linear infinite;
animation:spin 8s linear infinite;
fill: BlueViolet;
}
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
/* Toggle */
.switch {
position: relative;
display: inline-block;
width: 30px;
height: 17px;
float: right;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
border-radius: 17px;
}
.slider:before {
position: absolute;
content: "";
height: 13px;
width: 13px;
left: 2px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: BlueViolet;
}
input:focus + .slider {
box-shadow: 0 0 1px BlueViolet;
}
input:checked + .slider:before {
-webkit-transform: translateX(13px);
-ms-transform: translateX(13px);
transform: translateX(13px);
}
/* Dark Mode Always */
body {
/* https://design.firefox.com/photon/visuals/color.html#dark-theme */
color: white;
background-color: #38383d;
}
#statusimg {
background-image: url("status-off-dark.svg");
}
#statusimg.on {
background-image: url("status-on-dark.svg");
}
#statusimg.on.running {
background-image: url("status-running.svg");
}
input:checked + .slider {
background-color: #cc80ff;
}
input:focus + .slider {
box-shadow: 0 0 1px #cc80ff;
}
.learn:before {
background-image: url('arrowhead-right-dark-12.svg');
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,86 @@
{
"appDesc": {
"message": "Snowflake is a WebRTC pluggable transport for Tor."
},
"popupEnabled": {
"message": "Enabled"
},
"popupLearnMore": {
"message": "Learn more"
},
"popupStatusOff": {
"message": "Snowflake is off"
},
"popupStatusOn": {
"message": "Number of users currently connected: $1"
},
"popupStatusReady": {
"message": "Your Snowflake is ready to help users circumvent censorship"
},
"popupWebRTCOff": {
"message": "WebRTC feature is not detected."
},
"popupBridgeUnreachable": {
"message": "Could not connect to the bridge."
},
"popupDescOn": {
"message": "Number of users your Snowflake has helped circumvent censorship in the last 24 hours: $1"
},
"popupRetry": {
"message": "Retry"
},
"popupRetrying": {
"message": "Retrying ..."
},
"badgeCookiesOff": {
"message": "Cookies are not enabled."
},
"websiteIntro": {
"message": "Snowflake is a system to defeat internet censorship. People who are censored can use Snowflake to access the internet. Their connection goes through Snowflake proxies, which are run by volunteers. For more detailed information about how Snowflake works see our <a href=\"https://trac.torproject.org/projects/tor/wiki/doc/Snowflake/\" data-msgid=\"__MSG_docWiki__\">documentation wiki</a>."
},
"docWiki": {
"message": "documentation wiki"
},
"browser": {
"message": "Browser"
},
"censoredUsers": {
"message": "If your internet access is censored, you should download <a href=\"https://www.torproject.org/download/\">Tor Browser</a>."
},
"extension": {
"message": "Extension"
},
"installExtension": {
"message": "If your internet access is <strong>not</strong> censored, you should consider installing the Snowflake extension to help users in censored networks. There is no need to worry about which websites people are accessing through your proxy. Their visible browsing IP address will match their Tor exit node, not yours."
},
"installFirefox": {
"message": "Install in Firefox"
},
"installChrome": {
"message": "Install in Chrome"
},
"reportingBugs": {
"message": "Reporting Bugs"
},
"fileBug": {
"message": "If you encounter problems with Snowflake as a client or a proxy, please consider filing a bug. To do so, you will have to,"
},
"sharedAccount": {
"message": "Either <a href=\"https://trac.torproject.org/projects/tor/register\">create an account</a> or <a href=\"https://trac.torproject.org/projects/tor/login\">log in</a> using the shared <b>cypherpunks</b> account with password <b>writecode</b>."
},
"bugTracker": {
"message": "<a href=\"https://trac.torproject.org/projects/tor/newticket?component=Circumvention%2FSnowflake\">File a ticket</a> using our bug tracker."
},
"descriptive": {
"message": "Please try to be as descriptive as possible with your ticket and if possible include log messages that will help us reproduce the bug. Consider adding keywords <em>snowflake-webextension</em> or <em>snowflake-client</em> to let us know how which part of the Snowflake system is experiencing problems."
},
"embed": {
"message": "Embed"
},
"possible": {
"message": "It is now possible to embed the Snowflake badge on any website:"
},
"looksLike": {
"message": "Which looks like this:"
}
}

View File

@ -0,0 +1,88 @@
/* exported Popup */
// Add or remove a class from elem.classList, depending on cond.
function setClass(elem, className, cond) {
if (cond) {
elem.classList.add(className);
} else {
elem.classList.remove(className);
}
}
class Popup {
constructor(getMsgFunc, changeFunc, retryFunc) {
this.getMsgFunc = getMsgFunc;
this.enabled = document.getElementById('enabled');
this.enabled.addEventListener('change', changeFunc);
this.retry = document.getElementById('retry');
this.retry.addEventListener('click', () => {
this.setStatusDesc(getMsgFunc('popupRetrying'));
this.setRetry(false);
setTimeout(retryFunc, 1000); // Just some UI feedback
});
this.div = document.getElementById('active');
this.statustext = document.getElementById('statustext');
this.statusdesc = document.getElementById('statusdesc');
this.img = document.getElementById('statusimg');
this.button = document.querySelector('.button');
}
setEnabled(enabled) {
setClass(this.img, 'on', enabled);
}
setActive(active) {
setClass(this.img, 'running', active);
}
setStatusText(txt) {
this.statustext.innerText = txt;
}
setStatusDesc(desc, error) {
this.statusdesc.innerText = desc;
setClass(this.statusdesc, 'error', error);
}
setButton(hide) {
this.button.style.display = hide ? 'none' : 'block';
}
setRetry(display) {
this.retry.style.display = display ? 'inline-block' : 'none';
}
setChecked(checked) {
this.enabled.checked = checked;
}
static fill(n, func) {
switch(n.nodeType) {
case 3: { // Node.TEXT_NODE
const m = /^__MSG_([^_]*)__$/.exec(n.nodeValue);
if (m) { n.nodeValue = func(m[1]); }
break;
}
case 1: // Node.ELEMENT_NODE
n.childNodes.forEach(c => Popup.fill(c, func));
break;
}
}
turnOn(clients, total) {
this.setChecked(true);
if (clients > 0) {
this.setStatusText(this.getMsgFunc('popupStatusOn', String(clients)));
} else {
this.setStatusText(this.getMsgFunc('popupStatusReady'));
}
this.setStatusDesc((total > 0) ? this.getMsgFunc('popupDescOn', String(total)) : '');
this.setEnabled(true);
this.setActive(this.active);
this.setButton(false);
this.setRetry(false);
}
turnOff(desc, error, retry) {
this.setChecked(false);
this.setStatusText(this.getMsgFunc('popupStatusOff'));
this.setStatusDesc(desc ? this.getMsgFunc(desc) : '', error);
this.setEnabled(false);
this.setActive(false);
this.setButton(error);
this.setRetry(retry);
}
missingFeature(desc) {
this.turnOff(desc, true, desc === 'popupBridgeUnreachable');
}
}

View File

@ -0,0 +1,31 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<!-- This should be essentially be a no-opt in the popup -->
<meta http-equiv="refresh" content="86400" />
<title>Snowflake</title>
<link rel="icon" id="icon" href="./toolbar-off.ico" />
<link rel="stylesheet" href="./embed.css" />
<script src="./popup.js"></script>
<script src="./embed.js"></script>
</head>
<body>
<div id="active">
<div id="statusimg"></div>
<p id="statustext">__MSG_popupStatusOff__</p>
<p id="statusdesc"></p>
<button type="button" id="retry">__MSG_popupRetry__</button>
</div>
<div class="b button">
<label id="toggle" for="enabled">__MSG_popupEnabled__</label>
<label class="switch">
<input id="enabled" type="checkbox" />
<span class="slider round"></span>
</label>
</div>
<div class="b learn">
<a target="_blank" href="https://snowflake.torproject.org/">__MSG_popupLearnMore__</a>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,31 +1,29 @@
#!/bin/sh
all_users=$(grep /home < /etc/passwd | awk -F: '{print $1}' | sort)
# To test, pass a directory path as a parameter. Output will go there
all_users=$(grep /home < "/etc/passwd" | cut -d ':' -f 1 | sort)
current_dir=$(dirname "$(readlink -f "$0")")
template_dir="${current_dir}/templates/web"
if [ -z "$1" ]; then
output_dir="/var/www/htdocs"
else
output_dir="$1"
fi
output_dir="${1:-"/var/www/htdocs"}"
{
cat "${template_dir}/head.partial.html"
# User pages section
printf "<section id=\"users\">\\n"
printf "<h3>User pages</h3>\\n"
printf "<ul>\\n"
printf "%s" "$all_users" | while read -r n; do
printf "%s" "${all_users}" | while read -r n; do
if [ -f "/var/www/htdocs/users/${n}/index.html" ]; then
printf "<li><a href='/users/%s'>~%s</a></li>\\n" "$n" "$n"
printf "<li><a href='/users/%s'>~%s</a></li>\\n" "${n}" "${n}"
fi
done
printf "</ul>\\n"
printf "</section>\\n"
cat "${template_dir}/foot.partial.html"
} > "$output_dir/index.html"
} > "${output_dir}/index.html"
cp "${template_dir}/404.html" "${output_dir}"
cp -R "${template_dir}/assets/" "${output_dir}"
cp -R "${template_dir}/samizdat/." "${output_dir}"
cp -R "${template_dir}/snowflake/" "${output_dir}"