Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
727f8f6e9c | ||
|
|
a813326312 | ||
|
|
5a885e3781 | ||
|
|
f4113033ac | ||
|
|
5f9796cc11 | ||
|
|
b228fec07e | ||
|
|
ea0d6a85fd | ||
|
|
49c61b6b91 | ||
|
|
e4050cbf05 | ||
|
|
bc7cb26b61 | ||
|
|
c498ce1d52 | ||
|
|
c96357c83a | ||
|
|
eec4e359ea | ||
|
|
85d345bac2 | ||
|
|
a86a3c7b47 | ||
|
|
fd1e379fcf | ||
|
|
a17b2d76dc | ||
|
|
44a2d83ae6 | ||
|
|
fc7b75b930 | ||
|
|
ac98212fa2 | ||
|
|
758fc0b828 | ||
|
|
22106dcd7d | ||
|
|
e48e0644ba | ||
|
|
917e3d6089 | ||
|
|
23896bced5 | ||
|
|
54607982cf | ||
|
|
996cc53e56 | ||
|
|
4d6900ccf9 | ||
|
|
cfab5962e4 | ||
|
|
aed4af0868 | ||
|
|
f106ffe22d | ||
|
|
986d35376e | ||
|
|
7e1f995e7e | ||
|
|
fc250b31b6 | ||
|
|
3c61a3c419 | ||
|
|
d718172c23 | ||
|
|
83e25907a8 | ||
|
|
a36da4af6f | ||
|
|
93ee2a943e |
29 changed files with 5802 additions and 404 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
21
astro.config.mjs
Normal file
21
astro.config.mjs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
// Relative asset URLs, so the site works wherever it is hosted, e.g. https://berlin.ccc.de/kik/
|
||||
build: { format: 'file', assetsPrefix: '.', inlineStylesheets: 'never' },
|
||||
compressHTML: false,
|
||||
vite: {
|
||||
// The pages' styles, which Astro bundles while building the pages, readable rather than minified
|
||||
build: { cssMinify: false, rolldownOptions: { output: { assetFileNames: 'styles.css' } } },
|
||||
environments: {
|
||||
client: {
|
||||
build: {
|
||||
rolldownOptions: {
|
||||
// The page's script and the terminal it loads
|
||||
output: { entryFileNames: 'loader.js', chunkFileNames: 'terminal.js' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
var e=(function(){let e=typeof document<`u`&&document.createElement(`link`).relList;return e&&e.supports&&e.supports(`modulepreload`)?`modulepreload`:`preload`})(),t=function(e,t){return new URL(e,t).href},n={},r=function(e){return e.pathname.endsWith(`.css`)},i=function(e,t,n){if(t in e)return e[t];let r=n();if(!r){e[t]=void 0;return}let i=r.then(()=>{e[t]=void 0},n=>{throw e[t]=void 0,n});return e[t]=i,i},a=function(a,o,s){let c=Promise.resolve();if(o&&o.length>0){let a,l=document.querySelector(`meta[property=csp-nonce]`),u=l?.nonce||l?.getAttribute(`nonce`);function d(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function f(e){return import.meta.resolve?new URL(import.meta.resolve(e)):new URL(e,import.meta.url)}c=d(o.map(o=>{o=t(o,s);let c=f(o),l=r(c);return i(n,c.href,()=>{if(a===void 0){a={all:new Set,styles:new Set};let e=document.getElementsByTagName(`link`);for(let t=e.length-1;t>=0;t--){let n=e[t];a.all.add(n.href),n.rel===`stylesheet`&&a.styles.add(n.href)}}if((l?a.styles:a.all).has(c.href))return;let t=document.createElement(`link`);if(t.rel=l?`stylesheet`:e,l||(t.as=`script`),t.crossOrigin=``,t.href=c.href,u&&t.setAttribute(`nonce`,u),document.head.appendChild(t),l)return new Promise((e,n)=>{t.addEventListener(`load`,e),t.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${c}`)))})})}).filter(e=>e!==void 0))}function l(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return c.then(e=>{for(let t of e||[])t.status===`rejected`&&l(t.reason);return a().catch(l)})},o=document.getElementById(`term-input`),s=o.parentElement,c;addEventListener(`keydown`,e=>{if(!(e.target!==document.body||e.key.length!==1||e.key===` `||e.metaKey||e.ctrlKey!==e.altKey)){if(!c)return o.focus();e.preventDefault(),c.focus(),c.input(e.key)}}),s.addEventListener(`click`,()=>o.focus()),o.addEventListener(`input`,()=>o.style.width=`${o.value.length}ch`),o.addEventListener(`keydown`,e=>{if(e.key!==`Enter`||e.isComposing)return;e.preventDefault();let t=o.value;o.replaceWith(t),s.querySelector(`.cursor`).remove(),a(async()=>{let{start:e}=await import(`./terminal.js`);return{start:e}},[],import.meta.url).then(({start:e})=>e(s,t)).then(e=>c=e).catch(e=>s.append(` [${e.message}]`))});
|
||||
3716
package-lock.json
generated
Normal file
3716
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
18
package.json
Normal file
18
package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "kik-website",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^2.4.0",
|
||||
"@noble/curves": "^2.4.0",
|
||||
"@noble/hashes": "^2.4.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"astro": "^7.4.2"
|
||||
}
|
||||
}
|
||||
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
114
server/README.md
Normal file
114
server/README.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# The shell server
|
||||
|
||||
The website's terminal connects to a shell server, whose `root` user logs in at `wss://web.nowhere.wirs.ing` and
|
||||
gets a shell on the box.
|
||||
|
||||
## The security model
|
||||
|
||||
The box is an isolated, disposable VPS at a random hoster. We assume that it can be compromised and that this is
|
||||
acceptable: the worst case is that the gimmick on the website stops working. Nothing else is on the box. The
|
||||
website and the websocket bridge that carries the connection run on other servers, so a compromised shell server
|
||||
cannot get at them.
|
||||
|
||||
The `root` user on the box is a normal Windows account without administrator rights. It may do a lot of things
|
||||
there, including things we did not plan for. We do not try to contain it and we do not try to undo what it does.
|
||||
We only keep the basic user journey working.
|
||||
|
||||
## What we keep working
|
||||
|
||||
Anything that breaks the following user journey is not acceptable:
|
||||
|
||||
- The user can connect with the SSH key as `root`.
|
||||
- The prompt: `root@khaos-im-kiez:~#`, which `~/.zsh/.zshrc` sets.
|
||||
- The home directory the user ends up in: `/home/root`. They cannot change where it is, and they cannot delete it.
|
||||
- `cat README`, `cat README.en`, `cat CODE_OF_CONDUCT` and `cat CODE_OF_CONDUCT.en` always work. The four files
|
||||
are always there and always regular text files.
|
||||
|
||||
So in summary running `ssh root@nowhere.wirs.ing cat README` must always succeed and display the contents of the file.
|
||||
|
||||
Anything thats not specified above (like the content of README for example) is NOT part of the user journey.
|
||||
|
||||
## What we do not keep working
|
||||
|
||||
As long as the user story above does not break, we do not guarantee the functionality of anything else on the server. However the cleanup task should still try to best-effort restore everything to a working condition.
|
||||
|
||||
## Packages
|
||||
|
||||
The root user owns the whole MSYS2 installation and can install packages with pacman. That means they can also
|
||||
replace or delete the very programs the login needs. So the sshd, the login shell `zsh` and `cat` are kept as a
|
||||
second, protected copy in `C:\msys2\opt\kik`, which only SYSTEM and the administrators may write. The sshd runs as
|
||||
SYSTEM from that copy and the login shell is that copy's `zsh`, with `/opt/kik` first on the path, so the journey
|
||||
keeps working no matter what the user does to their own `/usr/bin`. Replacing `/usr/bin/sshd` cannot touch the
|
||||
running server, so the key on the website still never grants administrator access.
|
||||
|
||||
The copy has to be the *same* MSYS2 installation, not a second one: `C:\msys2\opt\kik` is two levels below
|
||||
`C:\msys2`, so its runtime resolves the same root and the login shell and the user's own tools share one cygwin
|
||||
runtime. A separate installation does not work - two runtimes in one process tree corrupt each other's file
|
||||
descriptors and pacman cannot even lock its database.
|
||||
|
||||
The runtime itself is not among the protected copies. A program and the shell that starts it have to use the same
|
||||
runtime DLL, or the program does not run at all - a second copy of it in one process is refused. The login path
|
||||
therefore uses the one in `/usr/bin`, next to the user's own programs, and the user may replace and upgrade it.
|
||||
What is protected instead is that it is the one the package says it is: pacman checks the files of `msys2-runtime`,
|
||||
and the cleanup job puts the package back if it was changed - while the server is stopped, before it starts it
|
||||
again, because the server runs as SYSTEM and would otherwise run code the user of the website wrote.
|
||||
|
||||
`/etc/passwd` (the login shell and home directory) and `/etc/ssh` (the host keys and config) are read-only to the
|
||||
user as well; everything else in the installation is theirs.
|
||||
|
||||
## The cleanup tasks
|
||||
|
||||
Three scheduled tasks run as SYSTEM, all at startup, and the user cannot stop any of them:
|
||||
|
||||
- `KiK sshd` runs the sshd from `C:\msys2\opt\kik`. It replaces the installation's own `msys2-sshd` service, which
|
||||
is disabled.
|
||||
- `KiK restore shell files` runs every 30 minutes and rebuilds the protected copy from its trusted originals,
|
||||
re-applies the permissions, and puts the home directory, the shell configuration, the website's key and the four
|
||||
texts back. Its second job is to best-effort revert other mockery and bring a fresh server to the expected state.
|
||||
- `KiK watch the server` runs every two minutes and starts the server when nothing listens on port 22. The task of
|
||||
the server only starts it again when it failed, never when it was stopped, so a cleanup job that is killed while
|
||||
it has the server stopped would otherwise take the site down with it.
|
||||
|
||||
The cleanup job only restarts the server when it has to: when it does not listen at all, when it is older than its
|
||||
configuration, or when the protected copy or the runtime has to be repaired - those cannot be replaced while the
|
||||
server holds them open. A working server is left alone, because every start can fail, and failed starts have kept
|
||||
the site down for hours. The job also never restarts the server while somebody is connected: a session that is open
|
||||
keeps the port, so a server that is started then could not bind it.
|
||||
|
||||
The server is only down for the short part of the job that needs it down: the protected copy cannot be replaced
|
||||
while the server holds it open. Everything else - the home directory, the four texts, the permissions of the
|
||||
installation - is put back while the server runs, because on a slow disk that takes minutes and the site should
|
||||
have a shell the whole time.
|
||||
|
||||
The task that runs the server ignores a start while it thinks it is still running, so a start that is dropped
|
||||
leaves the site without a shell. The cleanup job therefore tries three times, killing the processes of the task
|
||||
in between, before it gives up.
|
||||
|
||||
The deployment does not restart the server. It runs in a session, and that session keeps the port, so a server that
|
||||
starts during a deployment cannot bind it. The new configuration and the restored programs therefore take effect
|
||||
at the next run of the cleanup job, which happens within half an hour.
|
||||
|
||||
The server logs to `C:\ProgramData\kik-restore\sshd.log`. The log is appended to, not overwritten, because it is
|
||||
the only trace of what the server did: a server that is restarted would otherwise take the evidence of an outage
|
||||
with it. It is rotated when it grows past a hundred megabytes, which takes years. The event logs of windows are
|
||||
made bigger as well, the security log to a gigabyte: the scanners of the remote desktop alone fill a hundred
|
||||
megabytes of it in a week.
|
||||
|
||||
A start of the server that fails leaves nothing in that log. So windows records the history of the scheduled tasks
|
||||
and the start and end of every process, and when the server does not come up, the cleanup job writes the processes
|
||||
and the state of the server's task to `C:\ProgramData\kik-restore\failed-starts.log`.
|
||||
|
||||
Only keys may log in over SSH. The box stands in the open internet and both of its accounts have a password.
|
||||
|
||||
The files that have to stay belong to the administrators, so the user cannot delete them, and they cannot change the
|
||||
shell configuration or the key the website logs in with either. The shell reads its configuration from `~/.zsh`, so
|
||||
the startup files the user could write in the home directory are ignored.
|
||||
|
||||
## Installing the cleanup on the server
|
||||
|
||||
To deploy the cleanup task an the server, run:
|
||||
|
||||
server/scripts/deploy.sh
|
||||
|
||||
The script logs in as `Administrator` - whose shell is a Windows one - copies the scripts and the resources to the
|
||||
Administrator's home directory and runs the installer there.
|
||||
1
server/resources/admin/.ssh/authorized_keys
Normal file
1
server/resources/admin/.ssh/authorized_keys
Normal file
|
|
@ -0,0 +1 @@
|
|||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIBTHzLm8QMhHIo7kFAvtAFnqpspeR3L3gM8kLoG1137 lennart@erms
|
||||
2
server/resources/home/.ssh/authorized_keys
Normal file
2
server/resources/home/.ssh/authorized_keys
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDweEH8bhZAHVaAedhTVDb2j/oMEn3vm97xCwCw9LC9 root@nowhere.wirs.ing cygwin-2222
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIBTHzLm8QMhHIo7kFAvtAFnqpspeR3L3gM8kLoG1137 lennart@erms
|
||||
40
server/resources/home/.zsh/.zshrc
Normal file
40
server/resources/home/.zsh/.zshrc
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# The protected copies of zsh and cat (and the server itself) live here; keeping them first means the basic
|
||||
# journey - the prompt and reading the four texts - survives even if the user breaks their own /usr/bin copies.
|
||||
path=(/opt/kik $path)
|
||||
|
||||
# History shared between sessions.
|
||||
HISTFILE=~/.zsh_history HISTSIZE=10000 SAVEHIST=10000
|
||||
setopt share_history hist_ignore_dups hist_ignore_space interactive_comments auto_cd
|
||||
|
||||
# Completion: menu, case-insensitive, colored. -C skips compaudit, which is slow on
|
||||
# Windows and meaningless under MSYS2's noacl mounts (rm ~/.zcompdump after adding completions).
|
||||
autoload -Uz compinit && compinit -C
|
||||
zstyle ':completion:*' menu select
|
||||
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
|
||||
zstyle ':completion:*' list-colors ${(s.:.)LS_COLORS}
|
||||
|
||||
# Emacs keys; Up/Down search history for what is already typed.
|
||||
bindkey -e
|
||||
autoload -Uz up-line-or-beginning-search down-line-or-beginning-search
|
||||
zle -N up-line-or-beginning-search && zle -N down-line-or-beginning-search
|
||||
bindkey '^[[A' up-line-or-beginning-search '^[OA' up-line-or-beginning-search
|
||||
bindkey '^[[B' down-line-or-beginning-search '^[OB' down-line-or-beginning-search
|
||||
|
||||
# Prompt: user@host:directory, git branch, exit status of the last command if it failed.
|
||||
# A literal '#' as on Linux: zsh's %# would show '%', since this root is not uid 0.
|
||||
# The branch is read from .git/HEAD directly: starting git on every prompt is slow on Windows.
|
||||
precmd() {
|
||||
local d=$PWD head=
|
||||
until [[ -e $d/.git || $d == / ]]; do d=${d:h}; done
|
||||
[[ -f $d/.git/HEAD ]] && read -r head < $d/.git/HEAD
|
||||
[[ $head == 'ref: '* ]] && git_branch=${head#ref: refs/heads/} || git_branch=${head:0:7}
|
||||
print -Pn '\e]0;%n@%m: %~\a'
|
||||
}
|
||||
setopt prompt_subst
|
||||
PROMPT='%F{green}%n@%m%f:%F{blue}%~%f${git_branch:+ %5F($git_branch)%f}%(?.. %F{red}[%?]%f)# '
|
||||
|
||||
alias ls='ls --color=auto' ll='ls -lah' grep='grep --color=auto'
|
||||
|
||||
# Process monitor: htop does not exist for MSYS2 (its /proc only shows MSYS2 processes).
|
||||
# bottom is a native Windows program; winpty lets it draw in the pty, as Server 2016 has no ConPTY.
|
||||
alias btm='winpty /ucrt64/bin/btm'
|
||||
7
server/resources/home/.zshenv
Normal file
7
server/resources/home/.zshenv
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Preset what MSYS2's /etc/profile would otherwise start a helper process for (~90 ms each here).
|
||||
# TZ has to match the Windows timezone (W. Europe Standard Time).
|
||||
export LANG=en_US.UTF-8 TZ=Europe/Berlin
|
||||
|
||||
# The shell reads its configuration from here, not from the home directory: the user may not change it, and the
|
||||
# files in the home directory that zsh would read otherwise (.zprofile, .zlogin) are ignored.
|
||||
export ZDOTDIR=~/.zsh
|
||||
31
server/resources/home/CODE_OF_CONDUCT
Normal file
31
server/resources/home/CODE_OF_CONDUCT
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
┌─[38;2;31;154;63m┤ [0m[1mCode of Conduct[0m[38;2;31;154;63m ├[0m────────────────────────────────────────────────────────────┐
|
||||
│ Khaos im Kiez ist eine Veranstaltung des Chaos Computer Club Berlin e.V. │
|
||||
│ (CCCB). Damit sich alle wohlfühlen, gelten diese Regeln für alle │
|
||||
│ Teilnehmenden, Vortragenden und Helfenden. │
|
||||
│ │
|
||||
│ [38;2;31;154;63m> [0m[1mSei nett zueinander.[0m Geh respektvoll und freundlich miteinander um, auch │
|
||||
│ wenn du anderer Meinung bist. │
|
||||
│ [38;2;31;154;63m> [0m[1mKeine Diskriminierung.[0m Rassismus, Sexismus, Queerfeindlichkeit, Ableismus │
|
||||
│ und andere menschenverachtende Haltungen haben hier keinen Platz. │
|
||||
│ [38;2;31;154;63m> [0m[1mKeine Belästigung.[0m Dazu gehören Beleidigungen, Bedrohungen, Stalking, │
|
||||
│ unerwünschte körperliche Berührungen und unerwünschte sexuelle │
|
||||
│ Aufmerksamkeit. Ein Nein ist ein Nein. │
|
||||
│ [38;2;31;154;63m> [0m[1mFrag vor dem Fotografieren.[0m Fotos, Video- und Tonaufnahmen von Personen nur │
|
||||
│ mit deren Einverständnis. Das gilt auch für Talks und Workshops. │
|
||||
│ [38;2;31;154;63m> [0m[1mAchte auf Geräte und Eigentum.[0m Fass fremde Geräte nicht ohne Erlaubnis an. │
|
||||
│ Geh pfleglich mit den Räumen und dem Inventar um. │
|
||||
│ [38;2;31;154;63m> [0m[1mSei neugierig, nicht übergriffig.[0m Chaos ja, Grenzenüberschreitungen nein. │
|
||||
│ │
|
||||
│ [1mHausrecht:[0m Die Orga hat Hausrecht. Bei Verstößen gegen diese Regeln kann sie │
|
||||
│ Verwarnungen aussprechen, Personen der Veranstaltung verweisen oder ein │
|
||||
│ Hausverbot erteilen. Den Anweisungen der Orga ist Folge zu leisten. │
|
||||
│ │
|
||||
│ [1mProbleme?[0m Wenn dir etwas passiert oder du etwas beobachtest, sprich eine │
|
||||
│ Person aus der Orga an oder schreib an aprl [at] berlin [dot] ccc [dot] de. │
|
||||
│ Hinweise werden vertraulich behandelt. │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[38;2;31;154;63m──────────────────────────────────────────────────────────────────────────────────[0m
|
||||
]8;;https://berlin.ccc.de/kik/\[38;2;31;154;63mStartseite[0m]8;;\[38;2;31;154;63m · [0m]8;;https://berlin.ccc.de/page/impressum/\[38;2;31;154;63mImpressum[0m]8;;\
|
||||
|
||||
30
server/resources/home/CODE_OF_CONDUCT.en
Normal file
30
server/resources/home/CODE_OF_CONDUCT.en
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
┌─[38;2;31;154;63m┤ [0m[1mCode of Conduct[0m[38;2;31;154;63m ├[0m────────────────────────────────────────────────────────────┐
|
||||
│ Khaos im Kiez is an event of the Chaos Computer Club Berlin e.V. (CCCB). To │
|
||||
│ make everyone feel comfortable, these rules apply to all attendees, speakers │
|
||||
│ and helpers. │
|
||||
│ │
|
||||
│ [38;2;31;154;63m> [0m[1mBe nice to each other.[0m Treat each other respectfully and kindly, even when │
|
||||
│ you disagree. │
|
||||
│ [38;2;31;154;63m> [0m[1mNo discrimination.[0m Racism, sexism, queerphobia, ableism and other hateful │
|
||||
│ attitudes have no place here. │
|
||||
│ [38;2;31;154;63m> [0m[1mNo harassment.[0m This includes insults, threats, stalking, unwanted physical │
|
||||
│ contact and unwanted sexual attention. No means no. │
|
||||
│ [38;2;31;154;63m> [0m[1mAsk before taking pictures.[0m Photos, video and audio recordings of people │
|
||||
│ only with their consent. This also applies to talks and workshops. │
|
||||
│ [38;2;31;154;63m> [0m[1mRespect devices and property.[0m Do not touch other people's devices without │
|
||||
│ permission. Treat the rooms and furniture with care. │
|
||||
│ [38;2;31;154;63m> [0m[1mBe curious, not intrusive.[0m Chaos yes, crossing boundaries no. │
|
||||
│ │
|
||||
│ [1mHouse rules:[0m The organizers have the right to exclude people from the premises │
|
||||
│ (Hausrecht). In case of violations they may issue warnings, ask people to │
|
||||
│ leave, or ban them from the event. Follow the organizers' instructions. │
|
||||
│ │
|
||||
│ [1mProblems?[0m If something happens to you or you notice something, talk to a │
|
||||
│ member of the organizing team or write to aprl [at] berlin [dot] ccc [dot] de. │
|
||||
│ Reports are treated confidentially. │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[38;2;31;154;63m──────────────────────────────────────────────────────────────────────────────────[0m
|
||||
]8;;https://berlin.ccc.de/kik/en.html\[38;2;31;154;63mHome[0m]8;;\[38;2;31;154;63m · [0m]8;;https://berlin.ccc.de/page/impressum/\[38;2;31;154;63mLegal notice[0m]8;;\
|
||||
|
||||
79
server/resources/home/README
Normal file
79
server/resources/home/README
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
|
||||
[1mKHAOS IM [0m[1m[38;2;255;42;42mKIEZ[0m
|
||||
[38;2;31;154;63mCCC Berlin · 28. – 29.11.2026[0m
|
||||
|
||||
[38;2;31;154;63m──────────────────────────────────────────────────────────────────────────────────[0m
|
||||
]8;;https://berlin.ccc.de/kik/#infos\[1] Infos]8;;\ ]8;;https://berlin.ccc.de/kik/#mitmachen\[2] Mitmachen]8;;\ ]8;;https://berlin.ccc.de/kik/#anfahrt\[3] Anfahrt]8;;\
|
||||
[38;2;31;154;63m──────────────────────────────────────────────────────────────────────────────────[0m
|
||||
|
||||
┌─[38;2;31;154;63m┤ [0m[1m1 Infos[0m[38;2;31;154;63m ├[0m────────────────────────────────────────────────────────────────────┐
|
||||
│ Khaos im Kiez ist ein kleines Chaos-Event, back to the roots: inspiriert von │
|
||||
│ anderen kleinen Chaos-Events. Ein Wochenende zum Chillen und um die Seite von │
|
||||
│ Chaos-Events zu erleben, für die bei größeren Events oft keine Zeit mehr │
|
||||
│ bleibt. │
|
||||
│ │
|
||||
│ Du kannst einen Talk oder Workshop halten, an deinen Projekten schrauben, │
|
||||
│ gemeinsam hacken, löten, basteln oder einfach vorbeikommen, zuhören und Leute │
|
||||
│ kennenlernen. Alle sind willkommen, die Lust auf $Dinge, Neugier und Chaos │
|
||||
│ mitbringen. │
|
||||
│ │
|
||||
│ [38;2;31;154;63mWas : [0mKhaos im Kiez (KiK) │
|
||||
│ [38;2;31;154;63mWann : [0mSa 28.11. – So 29.11.2026, 11:00 – 23:59 │
|
||||
│ [38;2;31;154;63mWo : [0mMarienstraße 11, 10117 Berlin │
|
||||
│ [38;2;31;154;63mWer : [0mChaos Computer Club Berlin │
|
||||
│ [38;2;31;154;63mPreis : [0m0,00 € (Ticket trotzdem nötig, ~50 Plätze) │
|
||||
│ [38;2;31;154;63mFahrplan : [0m]8;;https://cfp.berlin.ccc.de\cfp.berlin.ccc.de]8;;\ │
|
||||
│ [38;2;31;154;63mEssen : [0mEwiges Frühstück, vegan │
|
||||
│ [38;2;31;154;63mBarrierefrei : [0mNicht barrierefrei. │
|
||||
│ [38;2;31;154;63mRegeln : [0m]8;;https://berlin.ccc.de/kik/coc.html\Code of Conduct]8;;\ │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─[38;2;31;154;63m┤ [0m[1m2 Mitmachen[0m[38;2;31;154;63m ├[0m────────────────────────────────────────────────────────────────┐
|
||||
│ [1mTicket:[0m Anmeldung über den Ticketshop. Das Ticket ist kostenlos, aber die │
|
||||
│ Plätze sind begrenzt. │
|
||||
│ │
|
||||
│ ]8;;https://tickets.berlin.ccc.de/cccb/kik-v0.1/\[38;2;255;42;42m[ Ticket sichern ][0m]8;;\ │
|
||||
│ │
|
||||
│ [1mTalks & Workshops:[0m Reiche deinen Beitrag im Call for Participation ein und │
|
||||
│ schau dir später den Fahrplan an. │
|
||||
│ │
|
||||
│ ]8;;https://cfp.berlin.ccc.de/kik-v01/cfp\[ Talk einreichen ]]8;;\ ]8;;https://cfp.berlin.ccc.de/kik-v01/schedule\[ Fahrplan ]]8;;\ │
|
||||
│ │
|
||||
│ [1mSonstiges:[0m Du willst anders helfen oder hast Fragen? Schreib an aprl [at] │
|
||||
│ berlin [dot] ccc [dot] de. │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─[38;2;31;154;63m┤ [0m[1m3 Anfahrt[0m[38;2;31;154;63m ├[0m──────────────────────────────────────────────────────────────────┐
|
||||
│ [38;2;31;154;63mAdresse : [0mClubräume im Hinterhof der Marienstraße 11, 10117 Berlin │
|
||||
│ [38;2;31;154;63mKoordinaten : [0m52,52194° N, 13,38261° O │
|
||||
│ [38;2;31;154;63mFahrrad : [0mIm Innenhof gibt es eine begrenzte Zahl an Fahrradständern. Auf │
|
||||
│ dem Weg liegen viele Fahrradstraßen, nutz die am besten, z. B. │
|
||||
│ mit ]8;;http://www.bbbike.de/\BBBike.de]8;;\. │
|
||||
│ [38;2;31;154;63mÖPNV : [0mDer Club liegt direkt beim S+U-Bahnhof Friedrichstraße (auch │
|
||||
│ Regionalbahnen). Vom oberirdischen S-Bahn-Steig (S3/S5/S7) │
|
||||
│ nimmst du den Ausgang Schiffbauerdamm und gehst unter der │
|
||||
│ Bahnbrücke über die Spree. Über die Albrechtstraße kommst du │
|
||||
│ zur Marienstraße. │
|
||||
│ [38;2;31;154;63mFernverkehr : [0mAussteigen in Berlin Hauptbahnhof, dann mit S-Bahn oder │
|
||||
│ Regionalbahn Richtung Osten eine Station bis Friedrichstraße. │
|
||||
│ Bahnhof Richtung Spree verlassen, über die Fußgängerbrücke und │
|
||||
│ der Albrechtstraße bis zur Stichstraße Marienstraße folgen. │
|
||||
│ [38;2;31;154;63mFlugzeug : [0mBER: Airport Express (RE7, RB14) oder S9 bis Friedrichstraße, │
|
||||
│ alternativ FEX bis Hauptbahnhof (dann wie Fernverkehr). │
|
||||
│ Achtung: Terminal 5 (früher SXF) ist nicht mit dem Rest des BER │
|
||||
│ verbunden, dort fährt nur die S9. Flugplatz Strausberg (EDAY): │
|
||||
│ S7 direkt bis Friedrichstraße. │
|
||||
│ [38;2;31;154;63mAuto : [0mZufahrt von Hauptbahnhof oder Brandenburger Tor über die │
|
||||
│ Luisenstraße. Von der Friedrichstraße entweder über │
|
||||
│ Schiffbauerdamm und Albrechtstraße (Außengastronomie und │
|
||||
│ Kopfsteinpflaster) oder über Reinhardstraße und Luisenstraße │
|
||||
│ (die Albrechtstraße ist nur in Gegenrichtung befahrbar). │
|
||||
│ [38;2;31;154;63mParken : [0mAchtung: Es gibt kaum Parkplätze, und die Marienstraße und alle │
|
||||
│ umliegenden Straßen sind parkraumbewirtschaftet. │
|
||||
│ │
|
||||
│ ]8;;https://www.openstreetmap.org/node/3872556357#map=20/52.5217003/13.3828041&layers=H\[ Karte (OpenStreetMap) ]]8;;\ │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[38;2;31;154;63m──────────────────────────────────────────────────────────────────────────────────[0m
|
||||
]8;;https://berlin.ccc.de/page/impressum/\[38;2;31;154;63mImpressum[0m]8;;\[38;2;31;154;63m · [0m]8;;https://berlin.ccc.de/\[38;2;31;154;63mCCC Berlin[0m]8;;\
|
||||
|
||||
77
server/resources/home/README.en
Normal file
77
server/resources/home/README.en
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
|
||||
[1mKHAOS IM [0m[1m[38;2;255;42;42mKIEZ[0m
|
||||
[38;2;31;154;63mCCC Berlin · 2026-11-28 – 2026-11-29[0m
|
||||
|
||||
[38;2;31;154;63m──────────────────────────────────────────────────────────────────────────────────[0m
|
||||
]8;;https://berlin.ccc.de/kik/en.html#infos\[1] Info]8;;\ ]8;;https://berlin.ccc.de/kik/en.html#join\[2] Join in]8;;\ ]8;;https://berlin.ccc.de/kik/en.html#getting-here\[3] Getting here]8;;\
|
||||
[38;2;31;154;63m──────────────────────────────────────────────────────────────────────────────────[0m
|
||||
|
||||
┌─[38;2;31;154;63m┤ [0m[1m1 Info[0m[38;2;31;154;63m ├[0m─────────────────────────────────────────────────────────────────────┐
|
||||
│ Khaos im Kiez is a small chaos event, back to the roots: inspired by other │
|
||||
│ small chaos events. A weekend to chill and to experience the side of chaos │
|
||||
│ events that often gets no time at larger events. │
|
||||
│ │
|
||||
│ Give a talk or workshop, work on your projects, hack together, solder, tinker, │
|
||||
│ or simply drop by, listen and meet people. Everyone who brings a love of │
|
||||
│ $things, curiosity and a bit of chaos is welcome. │
|
||||
│ │
|
||||
│ [38;2;31;154;63mWhat : [0mKhaos im Kiez (KiK) │
|
||||
│ [38;2;31;154;63mWhen : [0mSat 2026-11-28 – Sun 2026-11-29, 11:00 – 23:59 │
|
||||
│ [38;2;31;154;63mWhere : [0mMarienstraße 11, 10117 Berlin, Germany │
|
||||
│ [38;2;31;154;63mWho : [0mChaos Computer Club Berlin │
|
||||
│ [38;2;31;154;63mPrice : [0m€0.00 (ticket still required, ~50 seats) │
|
||||
│ [38;2;31;154;63mSchedule : [0m]8;;https://cfp.berlin.ccc.de\cfp.berlin.ccc.de]8;;\ │
|
||||
│ [38;2;31;154;63mFood : [0mEternal breakfast, vegan │
|
||||
│ [38;2;31;154;63mAccessibility : [0mNot accessible. │
|
||||
│ [38;2;31;154;63mRules : [0m]8;;https://berlin.ccc.de/kik/coc-en.html\Code of Conduct]8;;\ │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─[38;2;31;154;63m┤ [0m[1m2 Join in[0m[38;2;31;154;63m ├[0m──────────────────────────────────────────────────────────────────┐
|
||||
│ [1mTicket:[0m Register via the ticket shop. The ticket is free, but seats are │
|
||||
│ limited. │
|
||||
│ │
|
||||
│ ]8;;https://tickets.berlin.ccc.de/cccb/kik-v0.1/\[38;2;255;42;42m[ Get ticket ][0m]8;;\ │
|
||||
│ │
|
||||
│ [1mTalks & workshops:[0m Submit your proposal via the call for participation and │
|
||||
│ check the schedule later. │
|
||||
│ │
|
||||
│ ]8;;https://cfp.berlin.ccc.de/kik-v01/cfp\[ Submit a talk ]]8;;\ ]8;;https://cfp.berlin.ccc.de/kik-v01/schedule\[ Schedule ]]8;;\ │
|
||||
│ │
|
||||
│ [1mOther:[0m Want to help otherwise or have questions? Write to aprl [at] berlin │
|
||||
│ [dot] ccc [dot] de. │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─[38;2;31;154;63m┤ [0m[1m3 Getting here[0m[38;2;31;154;63m ├[0m─────────────────────────────────────────────────────────────┐
|
||||
│ [38;2;31;154;63mAddress : [0mClub rooms in the back yard of Marienstraße 11, 10117 Berlin │
|
||||
│ [38;2;31;154;63mCoordinates : [0m52.52194° N, 13.38261° E │
|
||||
│ [38;2;31;154;63mBicycle : [0mA limited number of bike racks are in the courtyard. Many │
|
||||
│ bicycle streets lead there, preferably use those, e.g. with │
|
||||
│ ]8;;http://www.bbbike.de/\BBBike.de]8;;\. │
|
||||
│ [38;2;31;154;63mLocal transit : [0mThe club is right next to S+U Friedrichstraße (regional trains │
|
||||
│ stop there too). From the elevated S-Bahn platform (S3/S5/S7) │
|
||||
│ take the Schiffbauerdamm exit and walk across the Spree │
|
||||
│ beneath the railway bridge. Albrechtstraße leads to │
|
||||
│ Marienstraße. │
|
||||
│ [38;2;31;154;63mLong distance : [0mGet off at Berlin Hauptbahnhof, then take any S-Bahn or │
|
||||
│ regional train eastbound one stop to Friedrichstraße. Leave │
|
||||
│ the station towards the Spree, cross the footbridge and follow │
|
||||
│ Albrechtstraße to the dead-end Marienstraße. │
|
||||
│ [38;2;31;154;63mAir : [0mBER: Airport Express (RE7, RB14) or S9 to Friedrichstraße, or │
|
||||
│ FEX to Hauptbahnhof (then as above). Note: Terminal 5 │
|
||||
│ (formerly SXF) is not connected to the rest of BER, only the │
|
||||
│ S9 runs there. Strausberg airfield (EDAY): S7 direct to │
|
||||
│ Friedrichstraße. │
|
||||
│ [38;2;31;154;63mCar : [0mAccess from Hauptbahnhof or Brandenburger Tor via │
|
||||
│ Luisenstraße. From Friedrichstraße either via Schiffbauerdamm │
|
||||
│ and Albrechtstraße (outdoor seating and cobblestones) or via │
|
||||
│ Reinhardstraße and Luisenstraße (Albrechtstraße is one-way the │
|
||||
│ other direction). │
|
||||
│ [38;2;31;154;63mParking : [0mWarning: parking is scarce, and Marienstraße and all │
|
||||
│ surrounding streets are subject to paid parking zones. │
|
||||
│ │
|
||||
│ ]8;;https://www.openstreetmap.org/node/3872556357#map=20/52.5217003/13.3828041&layers=H\[ Map (OpenStreetMap) ]]8;;\ │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[38;2;31;154;63m──────────────────────────────────────────────────────────────────────────────────[0m
|
||||
]8;;https://berlin.ccc.de/page/impressum/\[38;2;31;154;63mLegal notice[0m]8;;\[38;2;31;154;63m · [0m]8;;https://berlin.ccc.de/\[38;2;31;154;63mCCC Berlin[0m]8;;\
|
||||
|
||||
67
server/scripts/deploy.sh
Executable file
67
server/scripts/deploy.sh
Executable file
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env bash
|
||||
# Deploys the cleanup job to the shell server and installs it there.
|
||||
#
|
||||
# server/scripts/deploy.sh [-i KEYFILE] [-h HOST] [--uninstall]
|
||||
#
|
||||
# The job runs on the box as SYSTEM, never as the root user. The Administrator account sets it up; its shell is a
|
||||
# Windows one, so the commands we run there are Windows commands.
|
||||
#
|
||||
# The Administrator logs in with the personal key of the deployer, from the agent or from -i. The key of the website
|
||||
# never gets administrator rights: it only logs in as the root user.
|
||||
set -euo pipefail
|
||||
|
||||
HOST=nowhere.wirs.ing
|
||||
USER=Administrator
|
||||
KEYFILE=''
|
||||
UNINSTALL=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
-i) KEYFILE=$2; shift 2 ;;
|
||||
-h) HOST=$2; shift 2 ;;
|
||||
--uninstall) UNINSTALL=1; shift ;;
|
||||
*) echo "usage: $(basename "$0") [-i KEYFILE] [-h HOST] [--uninstall]" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
server="$repo/server"
|
||||
remote='C:\Users\Administrator\kik' # where the installer is put, in the home of the Administrator
|
||||
remotePath='/c/Users\Administrator\kik' # the same place, as the file transfer sees it
|
||||
temporary=()
|
||||
|
||||
# The host key is the one the website pins, so that the deployment cannot be intercepted.
|
||||
expected=$(sed -n "s/.*const HOST_KEY = '\([^']*\)'.*/\1/p" "$repo/src/scripts/terminal.ts")
|
||||
known=$(mktemp)
|
||||
temporary+=("$known")
|
||||
trap 'rm -f "${temporary[@]}"' EXIT
|
||||
ssh-keyscan -t ed25519 "$HOST" > "$known" 2>/dev/null
|
||||
actual=$(ssh-keygen -lf "$known" | awk '{print $2}')
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "the host key of $HOST is $actual, but $expected is expected" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The site logs in with the first key in the authorized_keys of the user on the box, so it has to be the right one.
|
||||
sitekey=$(ssh-keygen -lf "$server/resources/home/.ssh/authorized_keys" | awk 'NR==1 {print $2}')
|
||||
if [ "$sitekey" != 'SHA256:KUEv4d4+75h41l0OGTWCA2FbtEOQZ047ujkeh+kkFW0' ]; then
|
||||
echo "the key of the website is not the first one in server/resources/home/.ssh/authorized_keys" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
options=(-o "UserKnownHostsFile=$known" -o StrictHostKeyChecking=yes)
|
||||
if [ -n "$KEYFILE" ]; then options+=(-i "$KEYFILE"); fi
|
||||
ssh=(ssh "${options[@]}" "$USER@$HOST")
|
||||
scp=(scp "${options[@]}")
|
||||
|
||||
if [ "$UNINSTALL" = 1 ]; then
|
||||
"${ssh[@]}" "if exist $remote\\scripts\\install_restore_task.ps1 (powershell -NoProfile -ExecutionPolicy Bypass -File $remote\\scripts\\install_restore_task.ps1 -Uninstall) else (echo Nothing is installed.)"
|
||||
exit
|
||||
fi
|
||||
|
||||
# The installer looks for the resources next to itself, so both directories go to the server. What was there
|
||||
# before is removed, so that files that are gone from the repository do not stay on the box.
|
||||
"${ssh[@]}" "if exist $remote rmdir /s /q $remote"
|
||||
"${ssh[@]}" "mkdir $remote"
|
||||
"${scp[@]}" -r "$server/scripts" "$server/resources" "$USER@$HOST:$remotePath/"
|
||||
"${ssh[@]}" "powershell -NoProfile -ExecutionPolicy Bypass -File $remote\\scripts\\install_restore_task.ps1"
|
||||
605
server/scripts/install_restore_task.ps1
Normal file
605
server/scripts/install_restore_task.ps1
Normal file
|
|
@ -0,0 +1,605 @@
|
|||
# Sets up the shell server so the website's root user can install packages with pacman, while the basic user
|
||||
# journey - logging in with the key, the prompt, the home directory and the four text files - cannot be broken.
|
||||
#
|
||||
# The whole MSYS2 installation belongs to the root user: they may write everywhere and run pacman. The login path
|
||||
# the journey depends on - the sshd, the login shell zsh and cat - is kept as a protected copy in C:\msys2\opt\kik
|
||||
# that only SYSTEM and the administrators may write. The sshd runs as SYSTEM from that copy (the scheduled task
|
||||
# "KiK sshd"), so replacing /usr/bin/sshd does not touch the running server. A second scheduled task,
|
||||
# "KiK restore shell files", puts the home directory, the four texts and the protected copy back every 30 minutes.
|
||||
#
|
||||
# opt/kik works because its MSYS2 runtime resolves the same installation root (C:\msys2) as /usr/bin, so the login
|
||||
# shell and the user's own tools share one cygwin runtime. A separate second installation cannot: two runtimes in
|
||||
# one process tree break each other's file descriptors, and pacman then cannot even lock its database.
|
||||
#
|
||||
# Run this in an elevated PowerShell on the server, with this script and the ../resources directory next to it:
|
||||
# powershell -ExecutionPolicy Bypass -File .\install_restore_task.ps1
|
||||
# Remove the tasks and the protected copy again (the installation goes back to a normal cygrunsrv sshd) with:
|
||||
# powershell -ExecutionPolicy Bypass -File .\install_restore_task.ps1 -Uninstall
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$HomeDir = 'C:\msys2\home\root',
|
||||
[switch]$Uninstall
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$msys = 'C:\msys2'
|
||||
$kik = "$msys\opt\kik"
|
||||
$installDir = 'C:\ProgramData\kik-restore'
|
||||
$copiesDir = Join-Path $installDir 'files'
|
||||
$binDir = Join-Path $installDir 'bin'
|
||||
$sshdBat = Join-Path $installDir 'run-sshd.bat'
|
||||
$restoreTask = 'KiK restore shell files'
|
||||
$sshdTask = 'KiK sshd'
|
||||
$watchTask = 'KiK watch the server'
|
||||
$service = 'msys2-sshd'
|
||||
|
||||
# The programs the journey needs, kept out of the user's reach. sshd-session and sshd-auth are the per-connection
|
||||
# helpers OpenSSH 10 re-executes; zsh is the login shell; cat reads the four texts.
|
||||
$loginBinaries = "$msys\usr\bin\sshd.exe", "$msys\usr\lib\ssh\sshd-session.exe", "$msys\usr\lib\ssh\sshd-auth.exe",
|
||||
"$msys\usr\bin\zsh.exe", "$msys\usr\bin\cat.exe"
|
||||
|
||||
# The runtime is not among them: a program and the shell that starts it have to use the same runtime, or the
|
||||
# program does not run at all. The login path therefore uses the one in /usr/bin, and the user may replace it -
|
||||
# pacman checks that it is the one the package says it is, and the cleanup job puts it back if it is not.
|
||||
$runtimeFiles = '/usr/bin/msys-2.0.dll'
|
||||
|
||||
# The copies the task restores from and the script it runs both live in $installDir, which only SYSTEM and the
|
||||
# administrators may read or write.
|
||||
$restoreScript = @'
|
||||
# Puts the shell server back into shape and keeps the protected login path in place. Runs as SYSTEM every 30 minutes
|
||||
# and at startup; the root user can neither stop it nor change the copies it restores from.
|
||||
param([string]$HomeDir = 'C:\msys2\home\root')
|
||||
$msys = 'C:\msys2'
|
||||
$kik = "$msys\opt\kik"
|
||||
$log = Join-Path $PSScriptRoot 'restore.log'
|
||||
$copies = Join-Path $PSScriptRoot 'files\home'
|
||||
$binDir = Join-Path $PSScriptRoot 'bin'
|
||||
|
||||
# The account of the shell server's root user, whose id the files in its home directory belong to.
|
||||
$passwd = Get-Content "$msys\etc\passwd" -ErrorAction SilentlyContinue | Where-Object { $_ -like 'root:*' }
|
||||
$sid = if ($passwd) { ($passwd -split ':')[4].Split(',')[-1] }
|
||||
|
||||
$system = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-18')
|
||||
$admins = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544')
|
||||
|
||||
function Write-Log($message) {
|
||||
"$(Get-Date -Format s) $message" | Out-File -FilePath $log -Append
|
||||
}
|
||||
|
||||
# Removes a file, a directory or a link, but never what a link points to.
|
||||
function Remove-Anything($path) {
|
||||
$item = Get-Item -LiteralPath $path -Force
|
||||
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
||||
if ($item.PSIsContainer) { [IO.Directory]::Delete($path) } else { [IO.File]::Delete($path) }
|
||||
} elseif ($item.PSIsContainer) {
|
||||
Remove-Item -LiteralPath $path -Recurse -Force
|
||||
} else {
|
||||
Remove-Item -LiteralPath $path -Force
|
||||
}
|
||||
}
|
||||
|
||||
# A path that only SYSTEM and the administrators own. $userRights is what the root user may do: 'None' (the ssh keys
|
||||
# and config), 'Read' (the account database) or 'ReadAndExecute' (the login programs it has to run).
|
||||
function Set-LockedAcl($path, $forDirectory, $userRights) {
|
||||
$acl = Get-Acl -LiteralPath $path
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($rule) }
|
||||
$acl.SetOwner($admins)
|
||||
$inherit = if ($forDirectory) { 'ContainerInherit,ObjectInherit' } else { 'None' }
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($system, 'FullControl', $inherit, 'None', 'Allow')))
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($admins, 'FullControl', $inherit, 'None', 'Allow')))
|
||||
if ($userRights -ne 'None' -and $sid) {
|
||||
$user = New-Object System.Security.Principal.SecurityIdentifier($sid)
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($user, $userRights, $inherit, 'None', 'Allow')))
|
||||
}
|
||||
Set-Acl -LiteralPath $path -AclObject $acl
|
||||
}
|
||||
|
||||
# The website logs in with a key in the authorized_keys of the user. The user may keep keys of their own, so only
|
||||
# the key of the website is put back.
|
||||
function Restore-Keys($target, $source) {
|
||||
$required = @(Get-Content -LiteralPath $source | Where-Object { $_.Trim() })
|
||||
$blobs = $required | ForEach-Object { ($_ -split '\s+')[1] }
|
||||
$keys = @()
|
||||
if (Test-Path -LiteralPath $target) {
|
||||
$keys = @(Get-Content -LiteralPath $target | Where-Object { $_.Trim() -and $blobs -notcontains ($_ -split '\s+')[1] })
|
||||
}
|
||||
[IO.File]::WriteAllText($target, (($keys + $required) -join "`n") + "`n")
|
||||
}
|
||||
|
||||
# The shell configuration and the key the website logs in with must not change, the four texts may be edited.
|
||||
$readOnly = '.zshrc', '.zshenv', 'authorized_keys'
|
||||
|
||||
# The permissions of the home directory and of the files in it that have to stay: the user may read them and write
|
||||
# the ones that may change, and delete their own files, but not the home directory itself and not the files the
|
||||
# website needs. Only SYSTEM and the administrators own them, so the user cannot take the permissions away again.
|
||||
function Set-HomeAcl($path, $forDirectory, $writable) {
|
||||
if (-not $sid) { return }
|
||||
try {
|
||||
$user = New-Object System.Security.Principal.SecurityIdentifier($sid)
|
||||
$acl = Get-Acl -LiteralPath $path
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($rule) }
|
||||
$acl.SetOwner($admins)
|
||||
$none = [System.Security.AccessControl.InheritanceFlags]::None
|
||||
$children = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
||||
$inheritOnly = [System.Security.AccessControl.PropagationFlags]::InheritOnly
|
||||
$full = [System.Security.AccessControl.FileSystemRights]::FullControl
|
||||
$some = [System.Security.AccessControl.FileSystemRights]::ReadAndExecute -bor [System.Security.AccessControl.FileSystemRights]::Write
|
||||
$read = [System.Security.AccessControl.FileSystemRights]::ReadAndExecute
|
||||
$inherit = if ($forDirectory) { $children } else { $none }
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $system, $full, $inherit, 'None', 'Allow'))
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $admins, $full, $inherit, 'None', 'Allow'))
|
||||
$userRights = if ($writable) { $some } else { $read }
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $user, $userRights, $none, 'None', 'Allow'))
|
||||
if ($forDirectory) {
|
||||
# the files in it are the user's own, they may delete them
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $user, $full, $children, $inheritOnly, 'Allow'))
|
||||
}
|
||||
Set-Acl -LiteralPath $path -AclObject $acl
|
||||
} catch {
|
||||
Write-Log "the permissions of $path are not set: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# The home directory is the one from /etc/passwd, which the user cannot change, but which they can delete. A new one
|
||||
# gets MSYS2's default files from /etc/skel, which MSYS2 itself only copies into a home that does not exist yet.
|
||||
function New-Home($path) {
|
||||
New-Item -ItemType Directory -Path $path | Out-Null
|
||||
Set-HomeAcl $path $true $true
|
||||
Get-ChildItem -LiteralPath "$msys\etc\skel" -Force | Copy-Item -Destination $path -Recurse -Force
|
||||
}
|
||||
|
||||
# The server is only stopped when it has to be: when it does not run, when it is older than its configuration, or
|
||||
# when it holds open what has to be repaired - its programs in opt/kik or the runtime. A working server is left alone,
|
||||
# because every start can fail, and a failed start has kept the site down for hours.
|
||||
#
|
||||
# Stopping the task does not take the server with it, so its processes are killed by hand and the task is waited
|
||||
# for: it ignores a new instance while one is running, so a start would be dropped.
|
||||
function Stop-Sshd {
|
||||
Stop-ScheduledTask -TaskName 'KiK sshd' -ErrorAction SilentlyContinue
|
||||
Get-CimInstance Win32_Process -Filter "Name='cmd.exe'" | Where-Object { $_.CommandLine -match 'cmd\.exe\s+/c\s+"?C:\\ProgramData\\kik-restore\\run-sshd\.bat' } |
|
||||
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
||||
Get-Process -Name sshd -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
# the server leaves the processes of the sessions it has served behind, and they hold the runtime and the
|
||||
# protected programs open, so those cannot be replaced while they are there
|
||||
Get-Process -Name 'sshd-session' -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
$waited = 0
|
||||
while ((Get-ScheduledTask -TaskName 'KiK sshd').State -ne 'Ready' -and $waited -lt 30) { Start-Sleep 1; $waited++ }
|
||||
$waited = 0
|
||||
while ((Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue) -and $waited -lt 30) { Start-Sleep 1; $waited++ }
|
||||
}
|
||||
|
||||
# What the box looks like when the server did not come up. Nothing else records a failed start, and the reboot that
|
||||
# ends the outage takes the evidence with it.
|
||||
function Save-Snapshot($attempt) {
|
||||
$task = Get-ScheduledTask -TaskName 'KiK sshd'
|
||||
$info = $task | Get-ScheduledTaskInfo
|
||||
$lines = @("== $(Get-Date -Format s) attempt ${attempt}: the task is $($task.State), last run $($info.LastRunTime), result 0x$('{0:X}' -f $info.LastTaskResult)")
|
||||
$lines += Get-NetTCPConnection -LocalPort 22 -ErrorAction SilentlyContinue | ForEach-Object { "port 22: $($_.LocalAddress) $($_.State) pid $($_.OwningProcess)" }
|
||||
$lines += Get-CimInstance Win32_Process | Sort-Object CreationDate | ForEach-Object {
|
||||
'{0:MM-dd HH:mm:ss} {1} pid {2} parent {3} session {4}: {5}' -f $_.CreationDate, $_.Name, $_.ProcessId, $_.ParentProcessId, $_.SessionId, $_.CommandLine
|
||||
}
|
||||
$lines | Out-File -FilePath (Join-Path $PSScriptRoot 'failed-starts.log') -Append
|
||||
}
|
||||
|
||||
# The server is started again and waited for: a deployment or a crash must never leave the site without a shell.
|
||||
# The task ignores a new instance while it thinks one is running, so when the port stays closed its processes are
|
||||
# killed and it is started again.
|
||||
function Start-Sshd {
|
||||
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||
Start-ScheduledTask -TaskName 'KiK sshd'
|
||||
$waited = 0
|
||||
while (-not (Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue) -and $waited -lt 30) { Start-Sleep 1; $waited++ }
|
||||
if (Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue) { return $true }
|
||||
Write-Log "the server did not come up (attempt $attempt)"
|
||||
Save-Snapshot $attempt
|
||||
Stop-Sshd
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Runs a program and gives up after a while: pacman can wait for the lock of its database, and the server must not
|
||||
# stay down because of it. The exit code is returned, or -1 when it did not finish in time.
|
||||
function Invoke-Bounded($program, $arguments, $seconds) {
|
||||
$process = Start-Process -FilePath $program -ArgumentList $arguments -NoNewWindow -PassThru `
|
||||
-RedirectStandardOutput (Join-Path $env:TEMP 'kik-pacman.out') -RedirectStandardError (Join-Path $env:TEMP 'kik-pacman.err')
|
||||
# the handle is taken while the process runs, otherwise its exit code is not available afterwards
|
||||
$null = $process.Handle
|
||||
if (-not $process.WaitForExit($seconds * 1000)) {
|
||||
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
||||
return -1
|
||||
}
|
||||
return $process.ExitCode
|
||||
}
|
||||
|
||||
$netError = $null
|
||||
$connections = @(Get-NetTCPConnection -LocalPort 22 -ErrorAction SilentlyContinue -ErrorVariable netError)
|
||||
# An open session keeps the port, so the server is only stopped when there is none. A query that fails is not a
|
||||
# reason to disturb a session. The processes of the sessions are left behind by the server when a client goes away,
|
||||
# so they say nothing about whether anybody is connected.
|
||||
$busy = @($connections | Where-Object { $_.State -eq 'Established' }).Count -gt 0
|
||||
$mayStop = -not $netError -and -not $busy
|
||||
# The runtime of the installation belongs to the user's programs and is not protected: a program and the shell that
|
||||
# starts it have to use the same one, or the program does not run at all. What has to be protected is that it is the
|
||||
# one the package says it is, because the server runs as SYSTEM. The check takes a while, so it runs while the
|
||||
# server is up - the server must not wait for it.
|
||||
$runtimeBroken = $false
|
||||
if ($mayStop) {
|
||||
$runtimeBroken = (Invoke-Bounded "$msys\usr\bin\pacman.exe" @('-Qkk', 'msys2-runtime') 120) -ne 0
|
||||
}
|
||||
|
||||
# The home directory and the four texts: the server does not hold them open, so they are put back while it runs.
|
||||
# On a slow disk this takes minutes, and the site has a shell the whole time.
|
||||
if (-not (Test-Path -LiteralPath $HomeDir)) {
|
||||
New-Home $HomeDir
|
||||
Write-Log "created $HomeDir"
|
||||
} elseif ((Get-Item -LiteralPath $HomeDir -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
||||
Remove-Anything $HomeDir
|
||||
New-Home $HomeDir
|
||||
Write-Log "replaced the link $HomeDir"
|
||||
} else {
|
||||
Set-HomeAcl $HomeDir $true $true
|
||||
}
|
||||
|
||||
foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) {
|
||||
$target = Join-Path $HomeDir $copy.FullName.Substring($copies.Length).TrimStart('\')
|
||||
try {
|
||||
$dir = Split-Path -Parent $target
|
||||
if ((Test-Path -LiteralPath $dir) -and -not (Get-Item -LiteralPath $dir -Force).PSIsContainer) {
|
||||
Remove-Anything $dir
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null }
|
||||
Set-HomeAcl $dir $true $true
|
||||
if (Test-Path -LiteralPath $target) {
|
||||
$item = Get-Item -LiteralPath $target -Force
|
||||
if ($item.PSIsContainer -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
||||
# anything but a file is replaced, so that the files are always the text files they should be
|
||||
Remove-Anything $target
|
||||
} else {
|
||||
# the user may have locked the file; taking it back lets us write it
|
||||
takeown /f $target /a | Out-Null
|
||||
}
|
||||
}
|
||||
if ($copy.Name -eq 'authorized_keys') {
|
||||
Restore-Keys $target $copy.FullName
|
||||
} else {
|
||||
Copy-Item -LiteralPath $copy.FullName -Destination $target -Force
|
||||
}
|
||||
Set-HomeAcl $target $false ($readOnly -notcontains $copy.Name)
|
||||
# the user may have made the file read-only for themselves, the four texts are theirs to change
|
||||
(Get-Item -LiteralPath $target -Force).IsReadOnly = $false
|
||||
Write-Log "restored $($copy.Name)"
|
||||
} catch {
|
||||
Write-Log "failed $($copy.Name) : $_"
|
||||
}
|
||||
}
|
||||
|
||||
# The whole installation belongs to the user, so pacman can write it. The home directory keeps its own protected
|
||||
# permissions because they are set without inheritance.
|
||||
if ($sid) { icacls $msys /grant "*${sid}:(OI)(CI)F" | Out-Null }
|
||||
|
||||
# The protected login path: the copy the sshd and the login shell run from. The user cannot write it, so even after
|
||||
# they replace or remove their own copies in /usr/bin the server and the journey keep working. The trusted originals
|
||||
# are in bin\; anything missing from opt/kik or different from its original is put back. Replacing them is the only
|
||||
# work that needs the server to be stopped, because a running server holds its programs open.
|
||||
$stale = @(if (Test-Path -LiteralPath $binDir) {
|
||||
Get-ChildItem -LiteralPath $binDir -File -Force | Where-Object {
|
||||
$target = Join-Path $kik $_.Name
|
||||
$_.Name -ne 'msys-2.0.dll' -and (-not (Test-Path -LiteralPath $target) -or (Get-Item -LiteralPath $target -Force).Length -ne $_.Length)
|
||||
}
|
||||
})
|
||||
# A deployment writes the configuration but cannot restart the server, so a server that is older than its
|
||||
# configuration is restarted here.
|
||||
$listener = Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
$outdated = $listener -and @(Get-Item -LiteralPath "$msys\etc\ssh\sshd_config", (Join-Path $PSScriptRoot 'run-sshd.bat') |
|
||||
Where-Object { $_.LastWriteTime -gt (Get-Process -Id $listener.OwningProcess).StartTime }).Count -gt 0
|
||||
$restart = $mayStop -and (-not $listener -or $outdated -or $runtimeBroken -or $stale.Count -gt 0)
|
||||
|
||||
if ($restart) {
|
||||
Stop-Sshd
|
||||
Remove-Item "$kik\msys-2.0.dll" -Force -ErrorAction SilentlyContinue
|
||||
if ($runtimeBroken) {
|
||||
# the package is put back while the server is stopped: the file cannot be replaced while it is mapped.
|
||||
# The pattern is quoted, or the runtime expands it to the files of the working directory.
|
||||
Invoke-Bounded "$msys\usr\bin\pacman.exe" @('-S', '--noconfirm', '--overwrite', "'*'", 'msys2-runtime') 300 | Out-Null
|
||||
Write-Log 'restored the runtime of the installation'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $kik)) { New-Item -ItemType Directory -Path $kik | Out-Null }
|
||||
foreach ($file in $stale) {
|
||||
try {
|
||||
Copy-Item -LiteralPath $file.FullName -Destination (Join-Path $kik $file.Name) -Force
|
||||
Write-Log "restored opt/kik/$($file.Name)"
|
||||
} catch {
|
||||
# a process the server left behind may still hold it open; it is replaced in one of the next runs
|
||||
Write-Log "failed opt/kik/$($file.Name) : $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# The permissions do not need the server to be stopped, so they are put back on every run.
|
||||
try {
|
||||
if (Test-Path -LiteralPath $binDir) {
|
||||
New-Item -ItemType Directory -Force -Path "$kik\dev\shm", "$kik\dev\mqueue" | Out-Null
|
||||
Set-LockedAcl $kik $true 'ReadAndExecute'
|
||||
# /dev is scratch: the user may write it so POSIX shared memory works, the rest of opt/kik stays read-only
|
||||
if ($sid) { icacls "$kik\dev" /grant "*${sid}:(OI)(CI)M" | Out-Null }
|
||||
}
|
||||
|
||||
# The account database and the ssh keys and config the SYSTEM server reads: the user may read passwd (programs
|
||||
# map ids through it) but change none of it, and has no business in /etc/ssh at all.
|
||||
$rootShell = '/opt/kik/zsh.exe'
|
||||
$pw = "$msys\etc\passwd"
|
||||
$lines = Get-Content $pw
|
||||
if ($lines | Where-Object { $_ -match '^root:' -and $_ -notmatch ':/opt/kik/zsh\.exe$' }) {
|
||||
($lines | ForEach-Object { if ($_ -match '^root:') { $_ -replace ':[^:]*$', ":$rootShell" } else { $_ } }) | Set-Content $pw -Encoding ascii
|
||||
Write-Log 'set the root login shell to the protected zsh'
|
||||
}
|
||||
Set-LockedAcl $pw $false 'Read'
|
||||
if (Test-Path "$msys\etc\ssh") { Set-LockedAcl "$msys\etc\ssh" $true 'None' }
|
||||
} catch {
|
||||
Write-Log "the protected login path is not fully in place: $_"
|
||||
}
|
||||
|
||||
if ($restart) {
|
||||
if (Start-Sshd) { Write-Log 'restarted the sshd' } else { Write-Log 'the sshd does not listen on port 22' }
|
||||
}
|
||||
'@
|
||||
|
||||
# The server is watched, because the cleanup job may be killed while it has the server stopped: the task of the
|
||||
# server only starts it again when it failed, never when it was stopped. The watcher is the only thing that brings
|
||||
# the site back when that happens.
|
||||
$watchScript = @'
|
||||
# Starts the server when it is not running. The cleanup job stops it while it puts the protected files back, and it
|
||||
# can be killed while the server is down - the task of the server does not start it again then.
|
||||
$log = Join-Path $PSScriptRoot 'watch.log'
|
||||
if (Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue) { return }
|
||||
if ((Get-ScheduledTask -TaskName 'KiK restore shell files').State -eq 'Running') {
|
||||
# the cleanup job starts the server itself when it is done, but it must not hold it down for ever
|
||||
$running = (Get-Date) - (Get-ScheduledTaskInfo -TaskName 'KiK restore shell files').LastRunTime
|
||||
if ($running.TotalMinutes -lt 3) { return }
|
||||
}
|
||||
if ((Get-ScheduledTask -TaskName 'KiK sshd').State -eq 'Running') {
|
||||
# the task is running but nothing listens, so it is stuck and has to be started anew
|
||||
Stop-ScheduledTask -TaskName 'KiK sshd'
|
||||
Start-Sleep 5
|
||||
}
|
||||
Start-ScheduledTask -TaskName 'KiK sshd'
|
||||
"$(Get-Date -Format s) the server was not running and was started" | Out-File -FilePath $log -Append
|
||||
'@
|
||||
|
||||
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw 'Run this in an elevated PowerShell.'
|
||||
}
|
||||
|
||||
$system = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-18')
|
||||
$admins = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544')
|
||||
|
||||
$passwd = Get-Content "$msys\etc\passwd" -ErrorAction SilentlyContinue | Where-Object { $_ -like 'root:*' }
|
||||
if (-not $passwd) { throw "No root user in $msys\etc\passwd." }
|
||||
$sid = ($passwd -split ':')[4].Split(',')[-1]
|
||||
|
||||
# A previous install may have locked opt/kik, the account database and the ssh keys. The administrators keep
|
||||
# full control over them, so reset the permissions to inherited now and let this run write freely; the restore
|
||||
# task locks everything again at the end.
|
||||
$prevEA = $ErrorActionPreference; $ErrorActionPreference = 'SilentlyContinue'
|
||||
foreach ($p in "$msys\opt\kik", "$msys\etc\ssh", "$msys\etc\passwd") {
|
||||
if (Test-Path -LiteralPath $p) { icacls $p /reset /t /q | Out-Null }
|
||||
}
|
||||
Get-ChildItem "$msys\etc\ssh", "$msys\etc\passwd" -Force | ForEach-Object { try { $_.IsReadOnly = $false } catch {} }
|
||||
$ErrorActionPreference = $prevEA
|
||||
|
||||
function Set-LockedAcl($path, $forDirectory, $userRights) {
|
||||
$acl = Get-Acl -LiteralPath $path
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($rule) }
|
||||
$acl.SetOwner($admins)
|
||||
$inherit = if ($forDirectory) { 'ContainerInherit,ObjectInherit' } else { 'None' }
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($system, 'FullControl', $inherit, 'None', 'Allow')))
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($admins, 'FullControl', $inherit, 'None', 'Allow')))
|
||||
if ($userRights -ne 'None') {
|
||||
$user = New-Object System.Security.Principal.SecurityIdentifier($sid)
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($user, $userRights, $inherit, 'None', 'Allow')))
|
||||
}
|
||||
Set-Acl -LiteralPath $path -AclObject $acl
|
||||
}
|
||||
|
||||
# The DLLs an MSYS2 program loads, as Windows paths under C:\msys2.
|
||||
function Get-Libraries($program) {
|
||||
$paths = @()
|
||||
$posix = ($program -replace '^C:\\msys2', '') -replace '\\', '/'
|
||||
foreach ($line in & "$msys\usr\bin\ldd.exe" $posix 2>$null) {
|
||||
foreach ($m in [regex]::Matches($line, '/usr/[^\s]+\.dll')) { $paths += $m.Value }
|
||||
}
|
||||
$paths | Select-Object -Unique | Where-Object { $_ -notin $runtimeFiles } | ForEach-Object { $msys + ($_ -replace '/', '\') }
|
||||
}
|
||||
|
||||
if ($Uninstall) {
|
||||
foreach ($t in $restoreTask, $sshdTask, $watchTask) { Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction SilentlyContinue }
|
||||
if (Test-Path -LiteralPath $installDir) { Remove-Item -LiteralPath $installDir -Recurse -Force }
|
||||
if (Test-Path -LiteralPath $kik) {
|
||||
icacls $kik /reset /t /q 2>$null | Out-Null
|
||||
Remove-Item -LiteralPath $kik -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
# the installation's own cygrunsrv sshd takes over again
|
||||
$pw = "$msys\etc\passwd"
|
||||
(Get-Content $pw | ForEach-Object { if ($_ -match '^root:') { $_ -replace ':[^:]*$', ':/usr/bin/zsh' } else { $_ } }) | Set-Content $pw -Encoding ascii
|
||||
icacls $pw /reset | Out-Null
|
||||
if (Test-Path "$msys\etc\ssh") { icacls "$msys\etc\ssh" /reset /t /q 2>$null | Out-Null }
|
||||
Set-Service $service -StartupType Automatic -ErrorAction SilentlyContinue
|
||||
Start-Service $service -ErrorAction SilentlyContinue
|
||||
Write-Host "Removed the tasks and $installDir; the $service service serves ssh again."
|
||||
return
|
||||
}
|
||||
|
||||
$resources = Join-Path $PSScriptRoot '..\resources'
|
||||
if (-not (Get-ChildItem -LiteralPath $resources -Recurse -File -Force)) { throw "No files in $resources." }
|
||||
|
||||
$prepared = Get-Item -LiteralPath $installDir -Force -ErrorAction SilentlyContinue
|
||||
if ($prepared -and $prepared.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "$installDir is a link, refusing to install." }
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $copiesDir, $binDir | Out-Null
|
||||
|
||||
# Only SYSTEM and the administrators may read or write the copies, the trusted login programs and the scripts.
|
||||
$acl = New-Object System.Security.AccessControl.DirectorySecurity
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
foreach ($owner in $system, $admins) {
|
||||
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($owner, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
|
||||
}
|
||||
$acl.SetOwner($admins)
|
||||
Set-Acl -LiteralPath $installDir -AclObject $acl
|
||||
|
||||
foreach ($item in Get-ChildItem -LiteralPath $installDir -Recurse -Force) {
|
||||
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "$($item.FullName) is a link, refusing to install." }
|
||||
}
|
||||
Remove-Item -LiteralPath $copiesDir -Recurse -Force
|
||||
New-Item -ItemType Directory -Force -Path $copiesDir | Out-Null
|
||||
Copy-Item -Path (Join-Path $resources '*') -Destination $copiesDir -Recurse -Force
|
||||
|
||||
# The Administrator logs in with the key of the people who deploy, which is in the resources. The keys that are
|
||||
# already there stay.
|
||||
$adminKeys = 'C:\Users\Administrator\.ssh\authorized_keys'
|
||||
$required = @(Get-Content -LiteralPath (Join-Path $resources 'admin\.ssh\authorized_keys') | Where-Object { $_.Trim() })
|
||||
$keys = @()
|
||||
if (Test-Path -LiteralPath $adminKeys) { $keys = @(Get-Content -LiteralPath $adminKeys | Where-Object { $_.Trim() }) }
|
||||
if (-not ($keys | Where-Object { ($_ -split '\s+')[1] -eq ($required[0] -split '\s+')[1] })) {
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $adminKeys) | Out-Null
|
||||
[IO.File]::WriteAllText($adminKeys, (($keys + $required) -join "`n") + "`n")
|
||||
}
|
||||
[IO.File]::WriteAllText((Join-Path $installDir 'restore.ps1'), $restoreScript)
|
||||
[IO.File]::WriteAllText((Join-Path $installDir 'watch.ps1'), $watchScript)
|
||||
|
||||
# The trusted login programs: the sshd, its helpers, the login shell and cat, with every DLL they load. The restore
|
||||
# task puts these into opt/kik, which the user cannot write.
|
||||
Remove-Item "$binDir\*" -Force -ErrorAction SilentlyContinue
|
||||
$files = @{}
|
||||
foreach ($b in $loginBinaries) {
|
||||
if (-not (Test-Path -LiteralPath $b)) { throw "$b does not exist." }
|
||||
$files[$b] = $true
|
||||
foreach ($lib in Get-Libraries $b) { $files[$lib] = $true }
|
||||
}
|
||||
foreach ($f in $files.Keys) { Copy-Item -LiteralPath $f -Destination $binDir -Force }
|
||||
|
||||
# Remote Desktop is the way in for the administrator, and the port is open to the internet. It needs network level
|
||||
# authentication, so that a scanner cannot get a session before it has proven that it knows the password.
|
||||
$rdpTcp = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
|
||||
$nla = (Get-ItemProperty -Path $rdpTcp -Name UserAuthentication).UserAuthentication
|
||||
Set-ItemProperty -Path $rdpTcp -Name UserAuthentication -Value 1 -Type DWord
|
||||
if ($nla -ne 1) {
|
||||
# the setting only counts when the service starts again, which drops the sessions that are open
|
||||
Restart-Service TermService -Force
|
||||
}
|
||||
|
||||
# Only the administrator may log in over Remote Desktop. The user of the website is in no group that may log in
|
||||
# over it, and it must never be an administrator - that is taken away if it somehow got there.
|
||||
$rootAccount = Get-LocalUser -Name 'root' -ErrorAction SilentlyContinue
|
||||
if ($rootAccount) {
|
||||
foreach ($group in 'Administrators', 'Remote Desktop Users') {
|
||||
$member = Get-LocalGroupMember $group -ErrorAction SilentlyContinue | Where-Object { $_.SID -eq $rootAccount.SID }
|
||||
if ($member) {
|
||||
Remove-LocalGroupMember $group $member -Confirm:$false
|
||||
Write-Host "The root user was in '$group' and has been removed from it."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# The sshd reads its configuration and finds its helpers in opt/kik; nothing here depends on the user's /usr.
|
||||
$sshdConfig = "$msys\etc\ssh\sshd_config"
|
||||
Set-Content -LiteralPath $sshdConfig -Encoding ascii -Value @(
|
||||
'# The website logs in as root, the deployers as Administrator. Both come in over the websocket bridge.',
|
||||
'Port 22',
|
||||
'AllowUsers root Administrator',
|
||||
'# Only keys may log in: the box is in the open internet and the accounts have passwords.',
|
||||
'PasswordAuthentication no',
|
||||
'KbdInteractiveAuthentication no',
|
||||
'# Files are protected by Windows ACLs; under MSYS2''s noacl mounts the POSIX modes sshd checks are fake.',
|
||||
'StrictModes no',
|
||||
'Subsystem sftp internal-sftp',
|
||||
'# The server and its per-connection helpers are the protected copies, not the ones the user can replace.',
|
||||
'SshdSessionPath /opt/kik/sshd-session.exe',
|
||||
'SshdAuthPath /opt/kik/sshd-auth.exe'
|
||||
)
|
||||
|
||||
# The sshd runs as SYSTEM from the protected copy, with opt/kik first on the path so its own DLLs and the session's
|
||||
# programs (zsh, cat) are the protected ones. The log stays in the install directory, out of the user's reach.
|
||||
# The log is appended to, not overwritten: it is the only trace of what the server did, and a server that is
|
||||
# restarted would otherwise take the evidence of the outage with it. It is rotated at a hundred megabytes, which
|
||||
# takes years to reach.
|
||||
Set-Content -LiteralPath $sshdBat -Encoding ascii -Value @(
|
||||
'@echo off',
|
||||
'set PATH=C:\msys2\opt\kik;C:\msys2\usr\bin;%PATH%',
|
||||
'rem Keep the previous log if it grew too big; the current one is appended to.',
|
||||
'if exist C:\ProgramData\kik-restore\sshd.log for %%f in (C:\ProgramData\kik-restore\sshd.log) do if %%~zf GEQ 104857600 move /y C:\ProgramData\kik-restore\sshd.log C:\ProgramData\kik-restore\sshd.log.1',
|
||||
'C:\msys2\opt\kik\sshd.exe -D -e >> C:\ProgramData\kik-restore\sshd.log 2>&1'
|
||||
)
|
||||
|
||||
# The event logs of windows are made bigger as well: they are the only record of what the box itself did. The
|
||||
# security log gets the most, because the scanners of the remote desktop alone fill a hundred megabytes in a week.
|
||||
foreach ($eventLog in 'System', 'Application') {
|
||||
wevtutil sl $eventLog /ms:104857600 | Out-Null
|
||||
}
|
||||
wevtutil sl Security /ms:1073741824 | Out-Null
|
||||
|
||||
# A start of the server that fails leaves no trace in its own log, so windows records what the task of the server
|
||||
# launched and how each process ended: the history of the scheduled tasks, and the start and end of every process.
|
||||
wevtutil sl Microsoft-Windows-TaskScheduler/Operational /e:true /ms:104857600 | Out-Null
|
||||
auditpol /set '/subcategory:{0CCE922B-69AE-11D9-BED3-505054503030},{0CCE922C-69AE-11D9-BED3-505054503030}' /success:enable | Out-Null
|
||||
|
||||
# The installation's own cygrunsrv sshd service would fight for port 22; the scheduled task owns the sshd instead.
|
||||
Set-Service $service -StartupType Disabled -ErrorAction SilentlyContinue
|
||||
Stop-Service $service -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$atSystem = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
|
||||
$sshdAction = New-ScheduledTaskAction -Execute $sshdBat
|
||||
$sshdSettings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Seconds 0) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
Register-ScheduledTask -TaskName $sshdTask -Action $sshdAction -Trigger (New-ScheduledTaskTrigger -AtStartup) -Principal $atSystem -Settings $sshdSettings -Force | Out-Null
|
||||
|
||||
$restoreAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$installDir\restore.ps1`" -HomeDir `"$HomeDir`""
|
||||
$restoreTriggers = @(
|
||||
New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 30) -RepetitionDuration (New-TimeSpan -Days 3650)
|
||||
New-ScheduledTaskTrigger -AtStartup
|
||||
)
|
||||
$restoreSettings = New-ScheduledTaskSettingsSet -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 30) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
Register-ScheduledTask -TaskName $restoreTask -Action $restoreAction -Trigger $restoreTriggers -Principal $atSystem -Settings $restoreSettings -Force | Out-Null
|
||||
|
||||
# The watcher looks after the server every two minutes, so that a cleanup job that is killed while it has the server
|
||||
# stopped does not take the site down with it.
|
||||
$watchAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$installDir\watch.ps1`""
|
||||
$watchTriggers = @(
|
||||
New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 2) -RepetitionDuration (New-TimeSpan -Days 3650)
|
||||
New-ScheduledTaskTrigger -AtStartup
|
||||
)
|
||||
$watchSettings = New-ScheduledTaskSettingsSet -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
Register-ScheduledTask -TaskName $watchTask -Action $watchAction -Trigger $watchTriggers -Principal $atSystem -Settings $watchSettings -Force | Out-Null
|
||||
|
||||
# The server is started and waited for: it must listen on port 22, or the site has no shell.
|
||||
function Start-Sshd {
|
||||
Start-ScheduledTask -TaskName $sshdTask
|
||||
$waited = 0
|
||||
while (-not (Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue) -and $waited -lt 30) { Start-Sleep 1; $waited++ }
|
||||
return [bool](Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
# The restore task builds opt/kik and sets every permission; run it before starting the sshd that depends on it.
|
||||
Start-ScheduledTask -TaskName $restoreTask
|
||||
Start-Sleep -Seconds 6
|
||||
$waited = 0
|
||||
while ((Get-ScheduledTask -TaskName $restoreTask).State -eq 'Running' -and $waited -lt 60) { Start-Sleep 2; $waited += 2 }
|
||||
|
||||
# The server is not restarted here. It cannot be: the deployment runs in a session, and the port is still in use by
|
||||
# that session, so a server that starts now cannot bind it. The cleanup job restarts the server when nobody is
|
||||
# connected, and the new configuration and the restored programs take effect then. A server that is not running at
|
||||
# all is started here, because then there is no session that could hold the port.
|
||||
if (-not (Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue)) {
|
||||
if (-not (Start-Sshd)) {
|
||||
throw "The sshd does not listen on port 22; the task '$sshdTask' says: $((Get-ScheduledTask -TaskName $sshdTask).State), $((Get-ScheduledTaskInfo -TaskName $sshdTask).LastTaskResult)"
|
||||
}
|
||||
}
|
||||
if (-not (Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue)) {
|
||||
throw "The sshd does not listen on port 22."
|
||||
}
|
||||
|
||||
Write-Host "The sshd runs as SYSTEM from C:\msys2\opt\kik (task '$sshdTask'); '$restoreTask' keeps it and the home directory in shape."
|
||||
Get-Content (Join-Path $installDir 'restore.log') -Tail 10 -ErrorAction SilentlyContinue
|
||||
Get-CimInstance Win32_Process -Filter "Name='sshd.exe'" | ForEach-Object { $_.CommandLine }
|
||||
337
server/scripts/make_files.py
Normal file
337
server/scripts/make_files.py
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
# Builds the text files for the home directory of the shell server's root user in ../resources/home: README,
|
||||
# README.en, CODE_OF_CONDUCT and CODE_OF_CONDUCT.en as the website's pages in terminal text with escape sequences.
|
||||
# The other files there are edited by hand.
|
||||
# Usage: python3 server/scripts/make_files.py
|
||||
import pathlib
|
||||
import textwrap
|
||||
|
||||
ESC = '\x1b'
|
||||
RESET = f'{ESC}[0m'
|
||||
BOLD = f'{ESC}[1m'
|
||||
DIM = f'{ESC}[38;2;31;154;63m' # the page's --dim
|
||||
HOT = f'{ESC}[38;2;255;42;42m' # the page's --hot
|
||||
WIDTH = 82 # the web terminal's width, as wide as the page column
|
||||
INNER = WIDTH - 4 # text width inside "│ " ... " │"
|
||||
SITE = 'https://berlin.ccc.de/kik/'
|
||||
IMPRESSUM = 'https://berlin.ccc.de/page/impressum/'
|
||||
TICKETS = 'https://tickets.berlin.ccc.de/cccb/kik-v0.1/'
|
||||
CFP = 'https://cfp.berlin.ccc.de/kik-v01/cfp'
|
||||
SCHEDULE = 'https://cfp.berlin.ccc.de/kik-v01/schedule'
|
||||
MAP = 'https://www.openstreetmap.org/node/3872556357#map=20/52.5217003/13.3828041&layers=H'
|
||||
|
||||
|
||||
def link(url, text, style=''):
|
||||
return f'{ESC}]8;;{url}{ESC}\\{style}{text}{RESET if style else ""}{ESC}]8;;{ESC}\\'
|
||||
|
||||
|
||||
class Line:
|
||||
"""A line of text with its visible width, so boxes can be padded around styled text."""
|
||||
def __init__(self, text='', width=None):
|
||||
self.text, self.width = text, len(text) if width is None else width
|
||||
|
||||
def __add__(self, other):
|
||||
return Line(self.text + other.text, self.width + other.width)
|
||||
|
||||
|
||||
def styled(text, style):
|
||||
return Line(f'{style}{text}{RESET}', len(text))
|
||||
|
||||
|
||||
def linked(url, text, style=''):
|
||||
return Line(link(url, text, style), len(text))
|
||||
|
||||
|
||||
def joined(parts, gap=' '):
|
||||
line = parts[0]
|
||||
for part in parts[1:]:
|
||||
line = line + Line(gap) + part
|
||||
return line
|
||||
|
||||
|
||||
def wrap(text, width, links=()):
|
||||
"""Wraps plain text, then turns the given (text, url) pairs into links where they appear."""
|
||||
lines = []
|
||||
for raw in textwrap.wrap(text, width, break_on_hyphens=False, break_long_words=False):
|
||||
rendered = raw
|
||||
for needle, url in links:
|
||||
rendered = rendered.replace(needle, link(url, needle))
|
||||
lines.append(Line(rendered, len(raw)))
|
||||
return lines
|
||||
|
||||
|
||||
def paragraph(text, links=(), lead=None):
|
||||
"""A paragraph, optionally starting with a bold lead-in like "Ticket:"."""
|
||||
full = f'{lead} {text}' if lead else text
|
||||
lines = wrap(full, INNER, links)
|
||||
if lead:
|
||||
first = lines[0]
|
||||
lines[0] = Line(first.text.replace(lead, f'{BOLD}{lead}{RESET}', 1), first.width)
|
||||
return lines
|
||||
|
||||
|
||||
def pairs(items, label, links=()):
|
||||
"""A key-value list like the page's: dim "Label :" column, values wrapped with a hanging indent."""
|
||||
lines = []
|
||||
for key, value in items:
|
||||
wrapped = wrap(value, INNER - label, links)
|
||||
lines.append(styled(f'{key} :'.ljust(label), DIM) + wrapped[0])
|
||||
lines += [Line(' ' * label) + rest for rest in wrapped[1:]]
|
||||
return lines
|
||||
|
||||
|
||||
def bullets(items):
|
||||
"""A list like the page's: dim "> " marker, bold title, text wrapped with a hanging indent."""
|
||||
lines = []
|
||||
for title, text in items:
|
||||
wrapped = wrap(f'{title} {text}', INNER - 2)
|
||||
first = wrapped[0]
|
||||
lines.append(styled('> ', DIM) + Line(first.text.replace(title, f'{BOLD}{title}{RESET}', 1), first.width))
|
||||
lines += [Line(' ') + rest for rest in wrapped[1:]]
|
||||
return lines
|
||||
|
||||
|
||||
def box(title, lines):
|
||||
rule = WIDTH - 2 - len(f'─┤ {title} ├')
|
||||
out = [Line('┌─') + styled('┤ ', DIM) + styled(title, BOLD) + styled(' ├', DIM) + Line('─' * rule + '┐')]
|
||||
out += [Line('│ ') + line + Line(' ' * (INNER - line.width) + ' │') for line in lines]
|
||||
out.append(Line('└' + '─' * (WIDTH - 2) + '┘'))
|
||||
return out
|
||||
|
||||
|
||||
def centered(line):
|
||||
return Line(' ' * ((WIDTH - line.width) // 2)) + line
|
||||
|
||||
|
||||
def nav(line):
|
||||
return [styled('─' * WIDTH, DIM), line, styled('─' * WIDTH, DIM)]
|
||||
|
||||
|
||||
def footer(parts):
|
||||
line = linked(parts[0][1], parts[0][0], DIM)
|
||||
for text, url in parts[1:]:
|
||||
line = line + styled(' · ', DIM) + linked(url, text, DIM)
|
||||
return [styled('─' * WIDTH, DIM), centered(line)]
|
||||
|
||||
|
||||
def page(lines):
|
||||
lines = [Line(), *lines, Line()]
|
||||
for line in lines:
|
||||
assert line.width <= WIDTH + 2, (line.width, line.text)
|
||||
return ''.join(f'{line.text}\n' for line in lines)
|
||||
|
||||
|
||||
def readme(t):
|
||||
label = 3 + max(len(key) for key, _ in t['infos'] + t['route'])
|
||||
return page([
|
||||
styled('KHAOS IM ', BOLD) + styled('KIEZ', BOLD + HOT),
|
||||
styled(t['date'], DIM),
|
||||
Line(),
|
||||
*nav(joined([linked(f'{SITE}{t["page"]}#{anchor}', f'[{n}] {title}') for n, title, anchor in t['sections']])),
|
||||
Line(),
|
||||
*box('{} {}'.format(*t['sections'][0][:2]), [
|
||||
*paragraph(t['intro'][0]),
|
||||
Line(),
|
||||
*paragraph(t['intro'][1]),
|
||||
Line(),
|
||||
*pairs(t['infos'], label, links=[('cfp.berlin.ccc.de', 'https://cfp.berlin.ccc.de'), ('Code of Conduct', f'{SITE}{t["coc"]}')]),
|
||||
]),
|
||||
Line(),
|
||||
*box('{} {}'.format(*t['sections'][1][:2]), [
|
||||
*paragraph(t['ticket'], lead=t['ticket_lead']),
|
||||
Line(),
|
||||
linked(TICKETS, f'[ {t["get_ticket"]} ]', HOT),
|
||||
Line(),
|
||||
*paragraph(t['talks'], lead=t['talks_lead']),
|
||||
Line(),
|
||||
joined([linked(CFP, f'[ {t["submit"]} ]'), linked(SCHEDULE, f'[ {t["schedule"]} ]')]),
|
||||
Line(),
|
||||
*paragraph(t['other'], lead=t['other_lead']),
|
||||
]),
|
||||
Line(),
|
||||
*box('{} {}'.format(*t['sections'][2][:2]), [
|
||||
*pairs(t['route'], label, links=[('BBBike.de', 'http://www.bbbike.de/')]),
|
||||
Line(),
|
||||
linked(MAP, f'[ {t["map"]} ]'),
|
||||
]),
|
||||
Line(),
|
||||
*footer(t['footer']),
|
||||
])
|
||||
|
||||
|
||||
def code_of_conduct(t):
|
||||
return page([
|
||||
*box('Code of Conduct', [
|
||||
*paragraph(t['intro']),
|
||||
Line(),
|
||||
*bullets(t['rules']),
|
||||
Line(),
|
||||
*paragraph(t['house'], lead=t['house_lead']),
|
||||
Line(),
|
||||
*paragraph(t['problems'], lead=t['problems_lead']),
|
||||
]),
|
||||
Line(),
|
||||
*footer(t['footer']),
|
||||
])
|
||||
|
||||
|
||||
DE = {
|
||||
'page': '', 'coc': 'coc.html',
|
||||
'date': 'CCC Berlin · 28. – 29.11.2026',
|
||||
'sections': [(1, 'Infos', 'infos'), (2, 'Mitmachen', 'mitmachen'), (3, 'Anfahrt', 'anfahrt')],
|
||||
'intro': [
|
||||
'Khaos im Kiez ist ein kleines Chaos-Event, back to the roots: inspiriert von anderen kleinen Chaos-Events. '
|
||||
'Ein Wochenende zum Chillen und um die Seite von Chaos-Events zu erleben, für die bei größeren Events oft '
|
||||
'keine Zeit mehr bleibt.',
|
||||
'Du kannst einen Talk oder Workshop halten, an deinen Projekten schrauben, gemeinsam hacken, löten, basteln '
|
||||
'oder einfach vorbeikommen, zuhören und Leute kennenlernen. Alle sind willkommen, die Lust auf $Dinge, '
|
||||
'Neugier und Chaos mitbringen.',
|
||||
],
|
||||
'infos': [
|
||||
('Was', 'Khaos im Kiez (KiK)'),
|
||||
('Wann', 'Sa 28.11. – So 29.11.2026, 11:00 – 23:59'),
|
||||
('Wo', 'Marienstraße 11, 10117 Berlin'),
|
||||
('Wer', 'Chaos Computer Club Berlin'),
|
||||
('Preis', '0,00 € (Ticket trotzdem nötig, ~50 Plätze)'),
|
||||
('Fahrplan', 'cfp.berlin.ccc.de'),
|
||||
('Essen', 'Ewiges Frühstück, vegan'),
|
||||
('Barrierefrei', 'Nicht barrierefrei.'),
|
||||
('Regeln', 'Code of Conduct'),
|
||||
],
|
||||
'ticket_lead': 'Ticket:',
|
||||
'ticket': 'Anmeldung über den Ticketshop. Das Ticket ist kostenlos, aber die Plätze sind begrenzt.',
|
||||
'get_ticket': 'Ticket sichern',
|
||||
'talks_lead': 'Talks & Workshops:',
|
||||
'talks': 'Reiche deinen Beitrag im Call for Participation ein und schau dir später den Fahrplan an.',
|
||||
'submit': 'Talk einreichen', 'schedule': 'Fahrplan',
|
||||
'other_lead': 'Sonstiges:',
|
||||
'other': 'Du willst anders helfen oder hast Fragen? Schreib an aprl [at] berlin [dot] ccc [dot] de.',
|
||||
'route': [
|
||||
('Adresse', 'Clubräume im Hinterhof der Marienstraße 11, 10117 Berlin'),
|
||||
('Koordinaten', '52,52194° N, 13,38261° O'),
|
||||
('Fahrrad', 'Im Innenhof gibt es eine begrenzte Zahl an Fahrradständern. Auf dem Weg liegen viele '
|
||||
'Fahrradstraßen, nutz die am besten, z. B. mit BBBike.de.'),
|
||||
('ÖPNV', 'Der Club liegt direkt beim S+U-Bahnhof Friedrichstraße (auch Regionalbahnen). Vom oberirdischen '
|
||||
'S-Bahn-Steig (S3/S5/S7) nimmst du den Ausgang Schiffbauerdamm und gehst unter der Bahnbrücke über '
|
||||
'die Spree. Über die Albrechtstraße kommst du zur Marienstraße.'),
|
||||
('Fernverkehr', 'Aussteigen in Berlin Hauptbahnhof, dann mit S-Bahn oder Regionalbahn Richtung Osten eine '
|
||||
'Station bis Friedrichstraße. Bahnhof Richtung Spree verlassen, über die Fußgängerbrücke und '
|
||||
'der Albrechtstraße bis zur Stichstraße Marienstraße folgen.'),
|
||||
('Flugzeug', 'BER: Airport Express (RE7, RB14) oder S9 bis Friedrichstraße, alternativ FEX bis Hauptbahnhof '
|
||||
'(dann wie Fernverkehr). Achtung: Terminal 5 (früher SXF) ist nicht mit dem Rest des BER '
|
||||
'verbunden, dort fährt nur die S9. Flugplatz Strausberg (EDAY): S7 direkt bis Friedrichstraße.'),
|
||||
('Auto', 'Zufahrt von Hauptbahnhof oder Brandenburger Tor über die Luisenstraße. Von der Friedrichstraße '
|
||||
'entweder über Schiffbauerdamm und Albrechtstraße (Außengastronomie und Kopfsteinpflaster) oder '
|
||||
'über Reinhardstraße und Luisenstraße (die Albrechtstraße ist nur in Gegenrichtung befahrbar).'),
|
||||
('Parken', 'Achtung: Es gibt kaum Parkplätze, und die Marienstraße und alle umliegenden Straßen sind '
|
||||
'parkraumbewirtschaftet.'),
|
||||
],
|
||||
'map': 'Karte (OpenStreetMap)',
|
||||
'footer': [('Impressum', IMPRESSUM), ('CCC Berlin', 'https://berlin.ccc.de/')],
|
||||
}
|
||||
|
||||
EN = {
|
||||
'page': 'en.html', 'coc': 'coc-en.html',
|
||||
'date': 'CCC Berlin · 2026-11-28 – 2026-11-29',
|
||||
'sections': [(1, 'Info', 'infos'), (2, 'Join in', 'join'), (3, 'Getting here', 'getting-here')],
|
||||
'intro': [
|
||||
'Khaos im Kiez is a small chaos event, back to the roots: inspired by other small chaos events. A weekend to '
|
||||
'chill and to experience the side of chaos events that often gets no time at larger events.',
|
||||
'Give a talk or workshop, work on your projects, hack together, solder, tinker, or simply drop by, listen and '
|
||||
'meet people. Everyone who brings a love of $things, curiosity and a bit of chaos is welcome.',
|
||||
],
|
||||
'infos': [
|
||||
('What', 'Khaos im Kiez (KiK)'),
|
||||
('When', 'Sat 2026-11-28 – Sun 2026-11-29, 11:00 – 23:59'),
|
||||
('Where', 'Marienstraße 11, 10117 Berlin, Germany'),
|
||||
('Who', 'Chaos Computer Club Berlin'),
|
||||
('Price', '€0.00 (ticket still required, ~50 seats)'),
|
||||
('Schedule', 'cfp.berlin.ccc.de'),
|
||||
('Food', 'Eternal breakfast, vegan'),
|
||||
('Accessibility', 'Not accessible.'),
|
||||
('Rules', 'Code of Conduct'),
|
||||
],
|
||||
'ticket_lead': 'Ticket:',
|
||||
'ticket': 'Register via the ticket shop. The ticket is free, but seats are limited.',
|
||||
'get_ticket': 'Get ticket',
|
||||
'talks_lead': 'Talks & workshops:',
|
||||
'talks': 'Submit your proposal via the call for participation and check the schedule later.',
|
||||
'submit': 'Submit a talk', 'schedule': 'Schedule',
|
||||
'other_lead': 'Other:',
|
||||
'other': 'Want to help otherwise or have questions? Write to aprl [at] berlin [dot] ccc [dot] de.',
|
||||
'route': [
|
||||
('Address', 'Club rooms in the back yard of Marienstraße 11, 10117 Berlin'),
|
||||
('Coordinates', '52.52194° N, 13.38261° E'),
|
||||
('Bicycle', 'A limited number of bike racks are in the courtyard. Many bicycle streets lead there, preferably '
|
||||
'use those, e.g. with BBBike.de.'),
|
||||
('Local transit', 'The club is right next to S+U Friedrichstraße (regional trains stop there too). From the '
|
||||
'elevated S-Bahn platform (S3/S5/S7) take the Schiffbauerdamm exit and walk across the Spree '
|
||||
'beneath the railway bridge. Albrechtstraße leads to Marienstraße.'),
|
||||
('Long distance', 'Get off at Berlin Hauptbahnhof, then take any S-Bahn or regional train eastbound one stop '
|
||||
'to Friedrichstraße. Leave the station towards the Spree, cross the footbridge and follow '
|
||||
'Albrechtstraße to the dead-end Marienstraße.'),
|
||||
('Air', 'BER: Airport Express (RE7, RB14) or S9 to Friedrichstraße, or FEX to Hauptbahnhof (then as above). '
|
||||
'Note: Terminal 5 (formerly SXF) is not connected to the rest of BER, only the S9 runs there. '
|
||||
'Strausberg airfield (EDAY): S7 direct to Friedrichstraße.'),
|
||||
('Car', 'Access from Hauptbahnhof or Brandenburger Tor via Luisenstraße. From Friedrichstraße either via '
|
||||
'Schiffbauerdamm and Albrechtstraße (outdoor seating and cobblestones) or via Reinhardstraße and '
|
||||
'Luisenstraße (Albrechtstraße is one-way the other direction).'),
|
||||
('Parking', 'Warning: parking is scarce, and Marienstraße and all surrounding streets are subject to paid '
|
||||
'parking zones.'),
|
||||
],
|
||||
'map': 'Map (OpenStreetMap)',
|
||||
'footer': [('Legal notice', IMPRESSUM), ('CCC Berlin', 'https://berlin.ccc.de/')],
|
||||
}
|
||||
|
||||
COC_DE = {
|
||||
'intro': 'Khaos im Kiez ist eine Veranstaltung des Chaos Computer Club Berlin e.V. (CCCB). Damit sich alle '
|
||||
'wohlfühlen, gelten diese Regeln für alle Teilnehmenden, Vortragenden und Helfenden.',
|
||||
'rules': [
|
||||
('Sei nett zueinander.', 'Geh respektvoll und freundlich miteinander um, auch wenn du anderer Meinung bist.'),
|
||||
('Keine Diskriminierung.', 'Rassismus, Sexismus, Queerfeindlichkeit, Ableismus und andere menschenverachtende '
|
||||
'Haltungen haben hier keinen Platz.'),
|
||||
('Keine Belästigung.', 'Dazu gehören Beleidigungen, Bedrohungen, Stalking, unerwünschte körperliche '
|
||||
'Berührungen und unerwünschte sexuelle Aufmerksamkeit. Ein Nein ist ein Nein.'),
|
||||
('Frag vor dem Fotografieren.', 'Fotos, Video- und Tonaufnahmen von Personen nur mit deren Einverständnis. '
|
||||
'Das gilt auch für Talks und Workshops.'),
|
||||
('Achte auf Geräte und Eigentum.', 'Fass fremde Geräte nicht ohne Erlaubnis an. Geh pfleglich mit den Räumen '
|
||||
'und dem Inventar um.'),
|
||||
('Sei neugierig, nicht übergriffig.', 'Chaos ja, Grenzenüberschreitungen nein.'),
|
||||
],
|
||||
'house_lead': 'Hausrecht:',
|
||||
'house': 'Die Orga hat Hausrecht. Bei Verstößen gegen diese Regeln kann sie Verwarnungen aussprechen, Personen '
|
||||
'der Veranstaltung verweisen oder ein Hausverbot erteilen. Den Anweisungen der Orga ist Folge zu leisten.',
|
||||
'problems_lead': 'Probleme?',
|
||||
'problems': 'Wenn dir etwas passiert oder du etwas beobachtest, sprich eine Person aus der Orga an oder schreib an '
|
||||
'aprl [at] berlin [dot] ccc [dot] de. Hinweise werden vertraulich behandelt.',
|
||||
'footer': [('Startseite', SITE), ('Impressum', IMPRESSUM)],
|
||||
}
|
||||
|
||||
COC_EN = {
|
||||
'intro': 'Khaos im Kiez is an event of the Chaos Computer Club Berlin e.V. (CCCB). To make everyone feel '
|
||||
'comfortable, these rules apply to all attendees, speakers and helpers.',
|
||||
'rules': [
|
||||
('Be nice to each other.', 'Treat each other respectfully and kindly, even when you disagree.'),
|
||||
('No discrimination.', 'Racism, sexism, queerphobia, ableism and other hateful attitudes have no place here.'),
|
||||
('No harassment.', 'This includes insults, threats, stalking, unwanted physical contact and unwanted sexual '
|
||||
'attention. No means no.'),
|
||||
('Ask before taking pictures.', 'Photos, video and audio recordings of people only with their consent. This '
|
||||
'also applies to talks and workshops.'),
|
||||
('Respect devices and property.', "Do not touch other people's devices without permission. Treat the rooms "
|
||||
'and furniture with care.'),
|
||||
('Be curious, not intrusive.', 'Chaos yes, crossing boundaries no.'),
|
||||
],
|
||||
'house_lead': 'House rules:',
|
||||
'house': 'The organizers have the right to exclude people from the premises (Hausrecht). In case of violations '
|
||||
"they may issue warnings, ask people to leave, or ban them from the event. Follow the organizers' "
|
||||
'instructions.',
|
||||
'problems_lead': 'Problems?',
|
||||
'problems': 'If something happens to you or you notice something, talk to a member of the organizing team or '
|
||||
'write to aprl [at] berlin [dot] ccc [dot] de. Reports are treated confidentially.',
|
||||
'footer': [('Home', f'{SITE}en.html'), ('Legal notice', IMPRESSUM)],
|
||||
}
|
||||
|
||||
out = pathlib.Path(__file__).parent.parent / 'resources' / 'home'
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
for name, text in [('README', readme(DE)), ('README.en', readme(EN)),
|
||||
('CODE_OF_CONDUCT', code_of_conduct(COC_DE)), ('CODE_OF_CONDUCT.en', code_of_conduct(COC_EN))]:
|
||||
(out / name).write_text(text, encoding='utf-8')
|
||||
1
src/components/Prompt.astro
Normal file
1
src/components/Prompt.astro
Normal file
|
|
@ -0,0 +1 @@
|
|||
<p class="prompt">root@khaos-im-kiez:<span class="cwd">~</span># <slot /></p>
|
||||
36
src/components/Terminal.astro
Normal file
36
src/components/Terminal.astro
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
import Prompt from './Prompt.astro';
|
||||
---
|
||||
<Prompt><input id="term-input" aria-label="Terminal" autocomplete="off" autocapitalize="off" spellcheck="false" enterkeyhint="send"><span class="cursor"></span></Prompt>
|
||||
|
||||
<script>
|
||||
// A simple text input for the first command. The real terminal is only loaded once that command is sent.
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
|
||||
const input = document.getElementById('term-input') as HTMLInputElement;
|
||||
const prompt = input.parentElement!;
|
||||
let terminal: Terminal | undefined;
|
||||
|
||||
// Typing anywhere on the page goes to the prompt, later to the terminal. Shortcuts, Space (scrolling) and focused links are left alone.
|
||||
addEventListener('keydown', (e) => {
|
||||
if (e.target !== document.body || e.key.length !== 1 || e.key === ' ' || e.metaKey || e.ctrlKey !== e.altKey) return;
|
||||
if (!terminal) return input.focus(); // the key itself then lands in the input
|
||||
e.preventDefault();
|
||||
terminal.focus();
|
||||
terminal.input(e.key);
|
||||
});
|
||||
prompt.addEventListener('click', () => input.focus());
|
||||
input.addEventListener('input', () => (input.style.width = `${input.value.length}ch`));
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Enter' || e.isComposing) return;
|
||||
e.preventDefault();
|
||||
const command = input.value;
|
||||
input.replaceWith(command);
|
||||
prompt.querySelector('.cursor')!.remove(); // the terminal's cursor continues on the next line
|
||||
import('../scripts/terminal')
|
||||
.then(({ start }) => start(prompt, command))
|
||||
.then((started) => (terminal = started))
|
||||
.catch((error) => prompt.append(` [${error.message}]`));
|
||||
});
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue