Initial commit

This commit is contained in:
Laura Hausmann 2022-11-20 03:06:41 +01:00
commit 7a9d910d86
Signed by: zotan
GPG Key ID: D044E84C5BE01605
37 changed files with 4164 additions and 0 deletions

187
.gitignore vendored Normal file
View File

@ -0,0 +1,187 @@
/packages/
/_ReSharper.Caches/
# Created by https://www.toptal.com/developers/gitignore/api/rider,dotnetcore,jetbrains+all
# Edit at https://www.toptal.com/developers/gitignore?templates=rider,dotnetcore,jetbrains+all
### DotnetCore ###
# .NET Core build folders
bin/
obj/
# Common node modules locations
/node_modules
/wwwroot/node_modules
### JetBrains+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### JetBrains+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### Rider ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
# End of https://www.toptal.com/developers/gitignore/api/rider,dotnetcore,jetbrains+all
# Created by https://www.toptal.com/developers/gitignore/api/macos
# Edit at https://www.toptal.com/developers/gitignore?templates=macos
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# End of https://www.toptal.com/developers/gitignore/api/macos
database.db
.bearer_token

16
zotan.pw-web.sln Normal file
View File

@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zotan.pw-web", "zotan.pw-web\zotan.pw-web.csproj", "{C13FBB14-C41C-440F-BFB3-61E476E1902D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C13FBB14-C41C-440F-BFB3-61E476E1902D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C13FBB14-C41C-440F-BFB3-61E476E1902D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C13FBB14-C41C-440F-BFB3-61E476E1902D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C13FBB14-C41C-440F-BFB3-61E476E1902D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,91 @@
using LinqToDB;
using LinqToDB.Data;
using zotanpw_web.database;
using zotanpw_web.database.Tables;
namespace zotanpw_web;
public static class Migrations {
private const int DbVer = 1;
private static readonly List<Migration> _migrations = new() {
};
public static void RunMigrations() {
using var db = new Database.DbConn();
var ccolor = Console.ForegroundColor;
if (!db.DataProvider.GetSchemaProvider().GetSchema(db).Tables.Any()) {
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.Write("Running migration: ");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("Initialize Database");
db.CreateTable<DbInfo>();
db.CreateTable<TravelynxInfo>();
db.CreateTable<AlbumHistoryEntry>();
db.CreateTable<PlaylistHistoryEntry>();
db.InsertWithIdentity(new DbInfo { DbVer = DbVer });
}
else if (db.DataProvider.GetSchemaProvider().GetSchema(db).Tables.All(t => t.TableName != "DbInfo")) {
db.CreateTable<DbInfo>();
db.InsertWithIdentity(new DbInfo { DbVer = 0 });
}
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Database version: {db.DbInfo.ToList().First().DbVer}");
var migrationsToRun = _migrations.FindAll(p => p.IntroducedWithDbVer > db.DbInfo.First().DbVer);
if (migrationsToRun.Count == 0) {
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("No migrations to run.");
}
else {
new Migration(0, "BEGIN TRANSACTION").Run(db);
try {
migrationsToRun.ForEach(p => p.Run(db));
}
catch {
Console.ForegroundColor = ConsoleColor.DarkRed;
Console.WriteLine($"Migrating to database version {DbVer} failed.");
new Migration(0, "ROLLBACK TRANSACTION").Run(db);
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("Rolled back migrations.");
Environment.Exit(1);
}
new Migration(0, "COMMIT TRANSACTION").Run(db);
var newdb = new Database.DbConn();
var dbinfo = newdb.DbInfo.First();
dbinfo.DbVer = DbVer;
newdb.Update(dbinfo);
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine($"Database version is now: {DbVer}");
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Finished running migrations.");
}
Console.ForegroundColor = ccolor;
}
private class Migration {
private readonly string _sql;
public readonly int IntroducedWithDbVer;
public Migration(int introducedWithDbVer, string sql) {
IntroducedWithDbVer = introducedWithDbVer;
_sql = sql;
}
public void Run(DataConnection db) {
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.Write("Running migration: ");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(_sql);
db.Execute(_sql);
}
}
}

View File

@ -0,0 +1,25 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId) {
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

View File

@ -0,0 +1,23 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace zotanpw_web.Pages;
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel {
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
private readonly ILogger<ErrorModel> _logger;
public ErrorModel(ILogger<ErrorModel> logger) {
_logger = logger;
}
public void OnGet() {
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}

View File

@ -0,0 +1,162 @@
@page
@using zotanpw_web.database
@model IndexModel
@{
ViewData["title"] = "home";
var travelynx = new Database.DbConn().TravelynxInfo.ToList();
}
@section head
{
<h2 style="margin-bottom: 0;">Welcome internet user.</h2>
<small style="color:#aaa">An experimental http3 + v6o version of this page is available <a href="https://h3.zotan.pw">here</a>.</small>
}
<h1 id="about">About me</h1>
<p>Hey there, I'm <span style="color: #D291BC;">Laura</span> (~<span style="color: #957DAD">zotan</span>, she/they), and I'm a queer, anarchist & antifascist network engineer, system administrator and software developer.</p>
<p>
You can find me in hackerspaces across Germany, primarily in Berlin, Munich and Karlsruhe.
@if (travelynx.Any()) {
var status = travelynx.First();
if (status.CheckedIn) {
if (string.IsNullOrWhiteSpace(status.Train)) {
<span>Most recently, I was seen on <span style="color: #D291BC;">an unknon train somewhere in Germany</span>.</span>
}
else if (string.IsNullOrWhiteSpace(status.Destination)) {
<span>Most recently, I was seen on <span style="color: #D291BC;">@status.Train</span>.</span>
}
else {
<span>Most recently, I was seen on <span style="color: #D291BC;">@status.Train</span> on the way to <span style="color: #b295cf">@status.Destination</span>.</span>
}
}
else {
if (string.IsNullOrWhiteSpace(status.Destination)) {
<span>Most recently, I was seen at <span style="color: #b295cf;">an unknon train station in Germany</span>.</span>
}
else {
<span>Most recently, I was seen at <span style="color: #b295cf;">@status.Destination</span>.</span>
}
}
}
</p>
<p>Here you can find my contact info, crypto keys, projects, and a few links.</p>
@if (Request.Headers["X-Forwarded-For"] == "::1") {
<p style="background-color:#333; padding:5px 7px;">Many of my services are IPv6-only, and more are soon to follow. Your browser preferred IPv4 while connecting to this website, so please <a href="https://ip6.biz" target="_blank">check your connection</a> for IPv6 support before contacting me if your browser displays a network error.</p>
}
<h2 id="contact">Contact</h2>
<p>You can contact me via the following services:</p>
<ul>
<li>
<strong>Telegram:</strong> <a href="https://t.me/zotan" target="_blank">@@zotan</a>
</li>
<li>
<strong>Threema:&nbsp;</strong> <a href="https://threema.id/S59S9U8J" target="_blank">S59S9U8J</a>
</li>
<li>
<strong>Matrix:&nbsp;&nbsp;</strong> <a href="https://matrix.to/#/@@zotan:161.rocks" target="_blank">@@zotan:161.rocks</a>
</li>
<li>
<strong>Email:&nbsp;&nbsp;&nbsp;</strong> <a href="mailto:zotan@zotan.pw" target="_blank">zotan@zotan.pw</a> (for GPG see below)<br/>
</li>
</ul>
<h2 id="social">Profiles</h2>
<ul>
<li>
<a href="/blog">Blog</a>
</li>
<li>
<a href="/np">Now playing</a>
</li>
</ul>
<ul>
<li>
<a href="https://estrogen.network/@@zotan" target="_blank">Fediverse</a>
</li>
<li>
<a href="https://chaos.stream/profile/zotan" target="_blank">Live streaming</a> (occasional gaming and photo editing streams)
</li>
</ul>
<ul>
<li>
<a href="https://git.ztn.sh/zotan" target="_blank">Gitea</a>
</li>
<li>
<a href="https://github.com/zotanmew" target="_blank">GitHub</a>
</li>
</ul>
<h2 id="links">Network</h2>
<ul>
<li>
<a href="https://status.zotan.network" target="_blank">Network status</a>
</li>
<li>
<a href="https://status.zotan.services" target="_blank">Service status</a>
</li>
<li>
<a href="https://vnstat.zotan.services" target="_blank">Network load</a>
</li>
</ul>
<h2 id="photo">Photography</h2>
<ul>
<li><a href="https://zotan.photos" target="_blank">Portfolio</a> (my favourite shots)</li>
<li><a href="https://t.me/photolaura" target="_blank">Main photo channel</a> (all of my photos)</li>
<li><a href="https://t.me/photolaura_nofood" target="_blank">Secondary photo channel</a> (all of my photos that don't contain food)</li>
</ul>
<h2 id="projects-net">Projects - Networking</h2>
<ul>
<li><a href="https://zotan.network" target="_blank">AS211579</a> ~ zotan experimental networks</li>
</ul>
<h2 id="projects-web">Projects - Webservices</h2>
<ul>
<li><a href="https://chaos.stream" target="_blank">chaos.stream</a> - a chaos-community centric live streaming platform</li>
<li><a href="https://c3stream.de" target="_blank">c3stream.de</a> &nbsp;- a media.ccc.de mirror with added functionality</li>
<li><a href="https://ip6.biz" target="_blank">ip6.biz</a> &nbsp;&nbsp;&nbsp;&nbsp; - IPv6 (and IPv4) address info and connection test</li>
</ul>
<ul>
<li><a href="https://hc.ztn.sh" target="_blank">Healthchecks.io instance</a> (free, open signups)</li>
<li>
<a href="https://transit.ztn.sh" target="_blank">Öffisearch instance</a>
</li>
<li>
<a href="https://paste.ztn.sh" target="_blank">PrivateBin instance</a>
</li>
<li>
<a href="https://t.ztn.sh" target="_blank">Nitter instance</a>
</li>
<li>
<a href="https://meet.ztn.sh" target="_blank">Jitsi instance</a>
</li>
</ul>
<h2 id="projects-soft">Projects - Software</h2>
<ul>
<li><a href="https://git.ztn.sh/zotan/nginx-mod-rtmp" target="_blank">nginx-mod-rtmp</a> - fork of the original with fixed bugs and added functionality</li>
<li><a href="https://git.ztn.sh/zotan/rtmpdash" target="_blank">RTMPdash</a> - the software powering <a href="https://chaos.stream">chaos.stream</a></li>
<li><a href="https://git.ztn.sh/zotan/ip6.biz" target="_blank">ip6.biz</a> &nbsp;- the source code for the IPv6 toolbox mentioned above</li>
<li><a href="https://git.ztn.sh/zotan/monithor" target="_blank">monithor</a> - alerting and status page generator for InfluxDB</li>
<li><a href="https://git.ztn.sh/zotan/webmusic" target="_blank">webmusic</a> - a simple web player for your music</li>
<li><a href="https://git.ztn.sh/zotan/trainav" target="_blank">trainav</a> &nbsp;- experimental train journey planner</li>
<li><a href="https://git.ztn.sh/zotan/repomgr" target="_blank">repomgr</a> &nbsp;- experimental AUR buildserver and repo manager</li>
<li><a href="https://git.ztn.sh/zotan/autotag" target="_blank">autotag</a> &nbsp;- experimental music tag normalizer</li>
<li><a href="https://git.ztn.sh/zotan/mediamanager" target="_blank">mediamanager</a> - experimental media tracker</li>
</ul>
<h2 id="projects-hard">Projects - Hardware</h2>
<ul>
<li><a href="https://git.ztn.sh/zotan/esp32-co2-mhz19b" target="_blank">esp32-co2-mhz19b</a> - ESP32-based CO2 monitoring setup</li>
</ul>
<h2 id="papers">Papers</h2>
<ul>
<li>2019 - Comparison of music streaming services encryption concepts <a href="/files/StreamingCrypto.pdf" target="_blank">Download</a></li>
<li>2020 - Programming a modular Smart-Home-System (prescientific paper) <a href="/files/SmartHome.pdf" target="_blank">Download</a></li>
</ul>
<h2 id="crypto">Crypto</h2>
<p>I use the following GPG keys:</p>
<ul>
<li><strong>Primary (ECC):</strong> F8A5 DAC0 0E43 5119 2089 42F9 D044 E84C 5BE0 1605 <a href="/files/primary.gpg">Pubkey</a></li>
</ul>
@section postfooter {
<img src="/files/help.svg" height=450rem title="Designed by someone I love, ~fr2">
}

View File

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace zotanpw_web.Pages;
public class IndexModel : PageModel {
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger) {
_logger = logger;
}
public void OnGet() { }
}

View File

@ -0,0 +1,33 @@
@page "/np"
@model NowPlayingModel
@{
ViewData["title"] = "now playing";
}
<p>Here you can see what kind of music I've been listening to lately. This table is updated every couple minutes from multiple data sources.</p>
<p style="background-color:#333; padding:5px 7px;">Links to my music library / webplayer are accessible to authorized users only. If you think this includes you, but something isn't working, <a href="/#contact">DM me</a>.</p>
<ul>
<li>Albums</li>
</ul>
<h3>>> Albums</h3>
<table>
<th>Artist</th>
<th>Album</th>
<th>Link</th>
<tr>
<td>Röyksopp</td>
<td>Profound Mysteries</td>
<td><a href="https://music.zotan.services/?/R%C3%B6yksopp/Profound%20Mysteries">music.zotan.services</a></td>
</tr>
<tr>
<td>t+pazolite</td>
<td>Refactoring Travel</td>
<td><a href="https://music.apple.com/gb/album/%E3%83%AA%E3%83%95%E3%82%A1%E3%82%AF%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0-%E3%83%88%E3%83%A9%E3%83%99%E3%83%AB/1494424249">Apple Music</a></td>
</tr>
<tr>
<td>test</td>
<td>test</td>
<td>test</td>
</tr>
</table>

View File

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace zotanpw_web.Pages;
public class NowPlayingModel : PageModel {
public void OnGet() {
}
}

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="twitter:card" content="summary"/>
<meta name="twitter:title" content="@ViewData["card_title"]"/>
<meta name="twitter:description" content="@ViewData["card_desc"]"/>
<title>zotan.pw >> @ViewData["title"]</title>
<link rel="stylesheet" href="~/css/site.css"/>
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<h1>
<a href="/">zotan.pw >> @ViewData["Title"]</a>
</h1>
@await RenderSectionAsync("head", required: false)
</div>
</nav>
</header>
<div class="container">
<section id="main_content">
@RenderBody()
</section>
</div>
<div style="text-align: center;" >
@await RenderSectionAsync("prefooter", required: false)
<p style="color: #666666">--- Served by @Environment.MachineName running <a class="footerlink" href="https://git.ztn.sh/zotan/zotan.pw-web/commit/@Utils.LinkVersion" target="_blank">zotan.pw-web @Utils.Version</a> on .NET @Environment.Version ---</p>
@await RenderSectionAsync("postfooter", required: false)
</div>
<script src="~/js/site.js"></script>
</body>
</html>

View File

@ -0,0 +1,3 @@
@using zotanpw_web
@namespace zotanpw_web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View File

@ -0,0 +1,14 @@
@page "/blog"
@model BlogModel
@{
ViewData["title"] = "blog";
}
<p>Hey there, welcome to the blog of a disabled neurodivergent queer person unhappy with the state of the world.</p>
<p>This is where I post about things that make it somewhat fun, things that help me with life in general or just things I felt like sharing.</p>
<h1 id="posts">Posts</h1>
<ul>
@foreach (var post in BlogModel.Posts) {
<li><strong>@post.PublishedOn.ToString("yyyy-MM-dd")</strong> <a href="/blog/@post.Shorthand">@post.Title</a></li>
}
</ul>

View File

@ -0,0 +1,40 @@
using System.Text.RegularExpressions;
using Markdig;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace zotanpw_web.Pages.blog;
public class BlogModel : PageModel {
public static readonly List<BlogPost> Posts = new() {
new BlogPost("adhd-and-notes", "ADHD & Notetaking: an autistic perspective", DateOnly.Parse("2021-08-07")),
new BlogPost("ipv6-networking", "IPv6-native networking: a project report", DateOnly.Parse("2021-08-23")),
};
static BlogModel() {
Posts = Posts.OrderByDescending(p => p.PublishedOn).ToList();
}
public void OnGet() { }
public class BlogPost {
public readonly string Title;
public readonly string Shorthand;
public readonly DateOnly PublishedOn;
public int ReadTimeMinutes;
public string Content = "";
public BlogPost(string shorthand, string title, DateOnly publishedOn) {
Title = title;
PublishedOn = publishedOn;
Shorthand = shorthand;
UpdateContent();
}
public void UpdateContent() {
var markdownText = System.IO.File.ReadAllText($"Pages/blog/posts/{Shorthand}.md");
Content = Markdown.ToHtml(markdownText, new MarkdownPipelineBuilder().UseGenericAttributes().Build());
ReadTimeMinutes = Regex.Matches(markdownText, @"\b\w+\b").Count / 150;
}
}
}

View File

@ -0,0 +1,22 @@
@page "/blog/{post}"
@{
if (string.IsNullOrWhiteSpace((string)RouteData.Values["post"]!)) {
return;
}
var post = BlogModel.Posts.FirstOrDefault(p => p.Shorthand == (string)RouteData.Values["post"]!);
if (post == null) {
Response.Redirect("/Error");
return;
}
ViewData["title"] = $"blog >> {post.Shorthand}";
#if (DEBUG)
post.UpdateContent();
#endif
}
<b>@post.PublishedOn.ToString("yyyy-MM-dd")</b> - @Utils.a_an(post.ReadTimeMinutes) @post.ReadTimeMinutes minute read (150 wpm)
<h1 id="post">IPv6-native networking: a project report</h1>
<div align="justify">
@Html.Raw(post.Content)
</div>

View File

@ -0,0 +1,133 @@
If you are living with ADHD, diagnosed or not, the following things
might sound familiar: *"I forgot to write that down"*, *"I forgot to do
that"*, *"I don't remember that"*.
If you ask neurotypical people what they do to resolve that, they will
probably give you answers ranging from "Oh I just have it all in my
head" to "Just use a todo list / GTD system / bullet journal", both
equally unhelpful to most neurodiverse folks I know.
Reading [this article](https://xeiaso.net/blog/gtd-on-paper-2021-06-13){target="_blank"} by Xe inspired me to tackle this
problem for myself. (I highly encourage you to read the linked post
along with the rest of their blog)
Now, back to the topic at hand. As mentioned, there are many common
strategies for managing tasks and notes, many of which simply do not
work for me, but let's go through the why and try to find something that
works from there.
### Journals, paper and other physical ways of notetaking
The most immediate problem with this one is something many ADHD folks
will know very well - keeping track of the physical thing. Many times
have I lost track of notebooks, journals, diaries or anything related,
often times not finding them again to this day. For something I have to
rely on (physical extension of my brain, my memories, my thoughts),
that's bad. It's hard to forget your head, after all - even though a
certain figure of speech might suggest otherwise.
Another issue, that links more into the *autistic perspective* part of
the title, is the thing that many people like about paper - its
append-only nature. I have very specific ideas about how I want things
structured - and those ideas and needs vary with time and with the
contents of the page. You just can't (easily and realistically) re-write
the entire page every time those change, which makes paper inconvenient
at best and irritating at worst.
But there are also some wonderful things about paper, some even come as
a direct consequence of the problems I just described. You can just
start writing, there are no creative restrictions on what you can and
can't write, draw or otherwise do with the page, there is no fixed set
of design choices, style guidelines and whatnot. The append-only nature
also forces you to stop worrying about mistakes, and ideally should let
you be in full control of writing out thoughts.
To summarize: Paper is problematic because of the physical and
append-only nature, but can also great because of the freedom and
implicit restrictions it brings onto the table. What I then set out to
do is translating those concepts into the digital world, as closely
adhering to those concepts (and other things my brain likes that I
didn't cover in this post) as possible.
### Markdown and the digital world
Once you dabble with digital notetaking for more than a few minutes,
there's no way not to stumble upon Markdown. And there's good reason for
that, being an easy to understand, simple and human-readable (in
contrast to programmer-readable) markup language.
Those things also bring caveats with them, however. Simplicity
inherently means limitation, and that's also true here. There isn't that
much most Markdown renderers can do. Even worse, there is fragmentation
in the Markdown space, with plain Markdown, GitHub-flavored Markdown
(GFM) and MultiMarkdown as examples, not to speak of the variety of ways
different renderers for the same specification actually interpret
things.
Where does that leave us, then? I think Markdown is great, just not the
full story we need here. It's a great starting point, and that's why my
personal solution builds upon Markdown. So what is it that I currently
(and thus far successfully, i.e. there when I need it, how I need it, as
I want it) use?
It's a combination of [Obsidian](https://obsidian.md){target="_blank"} (a fancy
Markdown editor, self-proclaimed "second brain"), some plugins, a custom
theme, and most importantly, *not using the Markdown __renderer__*. You
might wonder how that works, isn't markdown supposed to be rendered? To
which I say - yes, but we can do better. The one thing you are losing
with that click of a button is control. Suddenly you have the version of
what you wrote in front of you that the renderer decided on, not how you
wanted it to look and feel. Which defeats the entire purpose of this
project, to get something that offers creative freedom close to physical
paper, without being convoluted to use.
### Putting it all together
So how does my setup look like exactly? Like
[this](/files/blog/adhd-and-notes.png){target="_blank"}. Let me
explain what you are looking at here. On the left there is a tree view
of the directory structure that is currently open in Obsidian.
From top to bottom: *Events* are things like conferences and similar,
*Journal* is where the daily notes go, *Knowledge* is a categorized map
of information and trivia that might be useful again in the future,
*Meta* is stuff relevant for debugging Obsidian itself, *Notes* are
uncategorized but titled notes, *People* is for keeping track of people
I know (for the non-ADHD people reading this, yes, this is necessary, I
regularly forget basic things about people very close to me), *Places*
is the same thing but for Places like restaurants and stuff (important
to keep track of what I eat and where to get it and stuff), *Projects*
is pretty self-explanatory, *Vault* is the "system folder" where all the
templates and attachments go, and *Zettelkasten* is for untitled notes.
I have a shortcut configured that will create one of those untitled
notes so I can just type out a thought and figure out what it's about
later.
The file that's open is the daily journal template. This is used to
automatically generate the daily journal entry when I click on a date in
the calendar applet you can see on the top right. I then type out basic
info about the day (where I was when I woke up, when I woke up) and move
over incomplete TODOs from yesterday's daily note. You will also notice
the text editor is, well, in edit mode, with a nice monospace font. This
allows me to customize the spacing of individual elements in the
documents however I want (just like paper), which would all get lost
when rendering to HTML.
### Summary and conclusion
This setup allows me to write freely, structure everything the same way
my brain is structured, keep track of what I've been doing, keep track
of things that still need to be done, and much more. Have I forgotten
about it? Yes - two times over the past month. In comparison to previous
methods, this is great! It's also fairly easy to reconstruct the past
day or two, so I think I'm doing okay.
I won't pretend that this system will work for everyone, but I do hope
that you will find some useful information in this writeup. If you have
any questions (or want me to help you with Markdown, Obsidian or any
other part of this setup), feel free to contact me. (Links for that are
on the [main site](/#contact))
I hope this post was interesting for you, being the first time I've ever
written one like it. If you have any comments on the blog or my writing
style or just this post in general, please contact me as well. In any
case, thanks for reading and have a wonderful day!

View File

@ -0,0 +1,170 @@
If you have reached this post, chances are you already know my [AS211579](https://zotan.network){target="_blank"} project.
This post serves as a summary of the things I learnt and the roadblocks
I had to overcome on the way to get the network to its current state.
### Goals and setup
For those who don't know the project, here's a quick recap. After having
dabbled a bit with [DN42](https://dn42.eu){target="_blank"} last year during the first lockdown, I wanted to do the real thing, in
the same global routing system your ISP is using to reach this very web
server. While this sounds unnecessarily convoluted and complicated to
accomplish, it was actually pretty easy and not that expensive. The easy
part is almost completely due to my knowledge gained by interacting with
DN42 and the awesome people in #dn42 on the [hackint](https://hackint.org){target="_blank"} IRC, who have
helped me with debugging the stupidest of mistakes. If you're interested
in any of the things I'm about to talk about, be sure to check out the
community behind it, it's seriously amazing.
Alright, we've established the goals, where do we go from here? I
quickly found a sponsoring AS (something you need for the *cheap* part)
and had all the documents on the way to RIPE (the regional internet
registry responsible for, among others, Europe and Asia). Once all that
was processed, a nice person from one of the many network group chats
I'm in (Wim, if you are reading this, thank you so much), hooked me up
with a free /40 IPv6 subnet for all my routing needs. Next, I got BGP
VMs. Good places to get them are either
[Vultr](https://vultr.com){target="_blank"} (cloud provider)
or various smaller providers like
[iFog](https://ifog.ch){target="_blank"}. Peering mostly
happened on [LocIX](https://locix.online){target="_blank"}.
From there, I provided connectivity to home routers, laptops and other
computer-y devices via WireGuard.
### Going a step further
Once my projects (mostly RPKI) eclipsed the performance level provided
by the VMs, I contacted the nice people at
[Meerfarbig](https://meerfarbig.net){target="_blank"} for a
dedicated machine. That one is the primary server running the network to
this day. If you are trying to set up a similar thing and are looking
for specifics, feel free to [contact me](/#contact) and I will give you
appropriate resources.
### The trials and tribulations of networking without IPv4
Now for the fun part, and the likely reason you are here in the first
place: all the things that broke along the way. You see, as you might be
able to tell from the title, my project goal was to set up an
IPv6-native network. That implies NAT64, DNS64, 464XLAT and a whole bag
of other fun things. But let's start at the beginning.
When you have IPv6-only networks, especially when talking about eyeball
networks (the kind mostly used for content consumption, e.g. viewing
webpages and their content), you will want a way to reach IPv4-only
servers. Many popular websites still presently don't support IPv6. At
time of writing, this includes GitHub, which is fairly important for
developing things. A less important (but still relevant) example is
Reddit. For now, we can't reach those websites. What do we do from here?
Our (first) solution is called NAT64, which translates packets between
IPv6 and IPv4, hence the name. The software I chose for this task is
[Jool](https://jool.mx){target="_blank"}. Setting it up was
fairly trivial, and I quickly set up three redundant NAT64-gateways that
announce the NAT64-WKP (well-known prefix, <span
class="highlight-bg">64:ff9b::/96</span>). So far so good, but how do we
get our systems to actually *use* those gateways?
For that, we need to look at DNS, which is responsible for translating
our domain names (e.g. <span class="highlight-bg">github.com</span>) to
an address we can connect to (e.g. <span
class="highlight-bg">140.82.121.3</span>). If we configure the resolver
that does that lookup to synthesize an AAAA record for our IPv4-only
domain, we can connect to it! And that's what I'm doing. Using
<a href="https://www.nlnetlabs.nl/projects/unbound/about/"
target="_blank">unbound</a>, the address <span
class="highlight-bg">140.82.121.3</span> (from the real A record) is
translated to <span class="highlight-bg">64:ff9b::140.82.121.3</span>,
or <span class="highlight-bg">64:ff9b::8c52:7903</span> in encoded form,
and returned as a synthesized AAAA record. Once we configure our system
to use this resolver, *most* IPv4-only sites work perfectly! If you are
asking why I said most there, I hope you are in for a ride.
### 464XLAT and questioning whether connecting computers together was a good idea
The answer to that question is no, obviously. And it keeps chipping away
at [my sanity](https://xkcd.com/2259/){target="_blank"}. But
since we're here, I might as well roll with it. So, what does 464XLAT
even mean? It's a specific combination of systems in place to ensure
connectivity to IPv4 hosts from IPv6-only networks. Namely, in addition
to DNS64 and the NAT64 gateway (called PLAT in this setup, provider-side
address translator), we need a second piece of software, the CLAT
(client/customer-side address translator).
Why, you ask? Because lots of rather popular software (for example,
Skype and Spotify) not only use IPv4, but hardcoded IPv4 literals. That
means that instead of connecting to <span
class="highlight-bg">somedomain.tld</span>, the software tries to
connect to <span class="highlight-bg">192.0.2.255</span>, which will
fail without a CLAT, since we are only capturing (and synthesizing AAAA
records for) DNS queries. A CLAT will take that packet and translate it
to an IPv6 packet destined for the DNS64-synthesized address of the
target host, the same one the resolver would have synthesized, had we
not used literals.
Okay, sounds simple enough, how do we do this? Most mobile operating
systems (Android and iOS) and some desktop operating systems (Windows)
support this natively, though support outside of cellular connections is
limited to non-existent. Since we are working with WireGuard here, this
won't help us. The solution I used here is giving all clients a private
IPv4 address, and instead running the CLAT on the router the tunnel
terminates on. For compatibility reasons I use addresses from the prefix
<span class="highlight-bg">100.64.0.0/10</span> meant for CGNAT (which
is almost what we are doing here) for this purpose.
### Trying to get it all to work
After the configuration part, getting NAT64 to run was fairly easy,
despite some initial issues with routing the NAT64-WKP. Once I turned on
464XLAT however, everything broke. <span class="highlight-bg">::1</span>
(the IPv6 loopback address) was unreachable. Traceroutes that shouldn't
even have gone through the 464XLAT stopped working. I ran into bugs in
Jool. Two fairly major ones, to be exact. However, the developers were
very helpful in debugging the problems, and got both of them resolved
within about two months (shoutouts to ydahhrk!). If you intend on
deploying a similar setup, I recommend going for the -git version, since
those bugs are fixed there already.
### One more thing
One last roadblock was wireguard-quick for macOS. For those unfamiliar
with these issues, I'm glad you didn't go through that debugging rabbit
hole. To start with, if you are tunneling all of your traffic and your
WiFi or Ethernet connection doesn't support IPv6 while your tunnel does,
macOS will sometimes decide to be smart and not attempt to request AAAA
records at all, thereby making the IPv6 connectivity of the tunnel
redundant. To work around this issue I created a very dodgy-looking
script that is run post-up by wireguard-quick, which creates a custom
network service for the tunnel interface. If you have this problem and
want this script, please [contact me](/#contact) directly as I don't
feel comfortable publishing something that terrible on my website.
Back to wireguard-quick, though. It turns out that the macOS version is
particularly dodgy, since the bypass for the "we only have one routing
table to work with" problem the devs went for is adding a more specific
override route for the tunnel endpoint, which depends on parsing
unchecked output of commands that print the routing table to determine
the default gateway. For reasons I can't explain, this breaks when you
create a custom network service as mentioned above, and it tries to set
the default gateway to <span class="highlight-bg">link#32</span> or
similar. This fails, and therefore the tunnel breaks, as tunnel traffic
is then routed back through the tunnel in an infinite loop. After
contacting the devs in the #wireguard channel on
[libera.chat](https://libera.chat){target="_blank"}, my patch
was accepted and this specific problem shouldn't occur anymore, though
that doesn't change the bodge that is the route monitor code of
wg-quick.
### Epilogue
That concludes the network setup. Everything is running smoothly and
thus far, no further bugs were found. I provide IPv6-tunnels for a few
friends who haven't reported any problems either, so at least for now I
think that this project is complete.
I have already removed IPv4 addresses from a few services I run, and I
hope to do so for the entirety of my online presence by the end of 2021,
maybe with a few exceptions for critical services used by friends from
Austria where major telcos still don't support IPv6.
Maybe the end of IPv4 is actually near, at least in my small corner of
the internet. Thanks for reading, and have a wonderful day.

44
zotan.pw-web/Program.cs Normal file
View File

@ -0,0 +1,44 @@
using System.Reflection;
using LinqToDB.Data;
using zotanpw_web;
using zotanpw_web.database;
DataConnection.DefaultSettings = new Database.Settings();
Migrations.RunMigrations();
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddSession(options => {
options.IdleTimeout = TimeSpan.MaxValue;
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
#if (DEBUG)
builder.Services.AddControllers().AddRazorRuntimeCompilation();
builder.Services.AddControllers();
#else
builder.Services.AddControllers();
#endif
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) {
app.UseExceptionHandler("/error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();

View File

@ -0,0 +1,23 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5073",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7119;http://localhost:5073",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,33 @@
using LinqToDB;
using Microsoft.AspNetCore.Mvc;
using zotanpw_web.database;
using zotanpw_web.database.Tables;
namespace zotanpw_web.Travelynx;
[ApiController]
[Route("/travelynx")]
public class Travelynx : Controller {
private static readonly string TravelynxSecret = System.IO.File.ReadAllLines(".bearer_token")[0];
[HttpPost]
public TravelynxInfo Update([FromBody] WebhookRequest rq) {
var token = Request.Headers.Authorization;
if (token == TravelynxSecret) {
var db = new Database.DbConn();
if (!db.TravelynxInfo.Any()) {
db.InsertWithIdentity(new TravelynxInfo { CheckedIn = false });
}
var status = db.TravelynxInfo.First();
status.CheckedIn = rq.Status.CheckedIn;
status.Train = $"{rq.Status.Train?.Type} {rq.Status.Train?.No}";
status.Destination = rq.Status.ToStation?.Name;
db.Update(status);
return status;
}
Response.StatusCode = 403;
return null!;
}
}

View File

@ -0,0 +1,43 @@
namespace zotanpw_web.Travelynx {
using System.Collections.Generic;
public class WebhookRequest {
public string Reason { get; set; }
public Status Status { get; set; }
}
public class Status {
public bool Deprecated { get; set; }
public bool CheckedIn { get; set; }
public Station? FromStation { get; set; }
public Station? ToStation { get; set; }
public List<IntermediateStop>? IntermediateStops { get; set; }
public Train? Train { get; set; }
public long ActionTime { get; set; }
}
public class Station {
public string? Name { get; set; }
public string? Ds100 { get; set; }
public long Uic { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public long ScheduledTime { get; set; }
public long RealTime { get; set; }
}
public class IntermediateStop {
public string? Name { get; set; }
public long ScheduledArrival { get; set; }
public long RealArrival { get; set; }
public long ScheduledDeparture { get; set; }
public long RealDeparture { get; set; }
}
public class Train {
public string? Type { get; set; }
public string? Line { get; set; }
public string? No { get; set; }
public string? Id { get; set; }
}
}

24
zotan.pw-web/Utils.cs Normal file
View File

@ -0,0 +1,24 @@
using System.Reflection;
namespace zotanpw_web;
public static class Utils {
public static readonly string Version =
((AssemblyInformationalVersionAttribute)Assembly.GetEntryAssembly()!.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false)[0])
.InformationalVersion[6..];
public static readonly string LinkVersion = Version[..8];
// Works up to 79, doubt i'll have >=80 minute read time blog posts
public static string a_an(int number) {
if (number >= 80)
throw new ArgumentOutOfRangeException();
return number switch {
8 => "an",
11 => "an",
18 => "an",
_ => "a"
};
}
}

View File

@ -0,0 +1,9 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,34 @@
using LinqToDB;
using LinqToDB.Configuration;
using LinqToDB.Data;
using zotanpw_web.database.Tables;
namespace zotanpw_web.database;
public class Database {
private class ConnectionStringSettings : IConnectionStringSettings {
public string ConnectionString { get; set; } = null!;
public string Name { get; set; } = null!;
public string ProviderName { get; set; } = null!;
public bool IsGlobal => false;
}
public class Settings : ILinqToDBSettings {
public IEnumerable<IDataProviderSettings> DataProviders => Enumerable.Empty<IDataProviderSettings>();
public string DefaultConfiguration => "SQLite";
public string DefaultDataProvider => "SQLite";
public IEnumerable<IConnectionStringSettings> ConnectionStrings {
get { yield return new ConnectionStringSettings { Name = "db", ProviderName = "SQLite", ConnectionString = @"Data Source=database.db;" }; }
}
}
public class DbConn : DataConnection {
public DbConn() : base("db") { }
public ITable<DbInfo> DbInfo => this.GetTable<DbInfo>();
public ITable<TravelynxInfo> TravelynxInfo => this.GetTable<TravelynxInfo>();
public ITable<AlbumHistoryEntry> AlbumHistory => this.GetTable<AlbumHistoryEntry>();
public ITable<PlaylistHistoryEntry> PlaylistHistory => this.GetTable<PlaylistHistoryEntry>();
}
}

View File

@ -0,0 +1,14 @@
using LinqToDB.Mapping;
#pragma warning disable CS8618
namespace zotanpw_web.database.Tables;
[Table(Name = "AlbumHistory")]
public class AlbumHistoryEntry {
[Column(Name = "EntryID"), PrimaryKey, Identity, NotNull] public int EntryId { get; set; }
[Column(Name = "DateTime"), NotNull] public DateTime DateTime { get; set; }
[Column(Name = "Artist"), NotNull] public string Artist { get; set; }
[Column(Name = "Title"), NotNull] public string Title { get; set; }
[Column(Name = "Source"), NotNull] public string Source { get; set; }
[Column(Name = "Link")] public string Link { get; set; }
}

View File

@ -0,0 +1,9 @@
using LinqToDB.Mapping;
namespace zotanpw_web.database.Tables;
[Table(Name = "DbInfo")]
public class DbInfo {
[Column(Name = "ID"), PrimaryKey, Identity, NotNull] public int Id { get; set; }
[Column(Name = "DbVer"), NotNull] public int DbVer { get; set; }
}

View File

@ -0,0 +1,14 @@
using LinqToDB.Mapping;
#pragma warning disable CS8618
namespace zotanpw_web.database.Tables;
[Table(Name = "PlaylistHistory")]
public class PlaylistHistoryEntry {
[Column(Name = "EntryID"), PrimaryKey, Identity, NotNull] public int EntryId { get; set; }
[Column(Name = "DateTime"), NotNull] public DateTime DateTime { get; set; }
[Column(Name = "Author"), NotNull] public string Artist { get; set; }
[Column(Name = "Title"), NotNull] public string Title { get; set; }
[Column(Name = "Source"), NotNull] public string Source { get; set; }
[Column(Name = "Link")] public string Link { get; set; }
}

View File

@ -0,0 +1,11 @@
using LinqToDB.Mapping;
namespace zotanpw_web.database.Tables;
[Table(Name = "Travelynx")]
public class TravelynxInfo {
[Column(Name = "ID"), PrimaryKey, Identity, NotNull] public int Id { get; set; }
[Column(Name = "CheckedIn"), NotNull] public bool CheckedIn { get; set; }
[Column(Name = "Train")] public string? Train { get; set; }
[Column(Name = "Destination")] public string? Destination { get; set; }
}

View File

@ -0,0 +1,372 @@
/*
generated by rouge http://rouge.jneen.net/
original base16 by Chris Kempson (https://github.com/chriskempson/base16)
*/
.highlight table td {
padding: 5px;
}
.highlight table pre {
margin: 0;
}
.highlight, .highlight .w {
color: #d0d0d0;
}
.highlight .err {
color: #151515;
background-color: #ac4142;
}
.highlight .c, .highlight .cd, .highlight .cm, .highlight .c1, .highlight .cs {
color: #888;
}
.highlight .cp {
color: #f4bf75;
}
.highlight .nt {
color: #f4bf75;
}
.highlight .o, .highlight .ow {
color: #d0d0d0;
}
.highlight .p, .highlight .pi {
color: #d0d0d0;
}
.highlight .gi {
color: #90a959;
}
.highlight .gd {
color: #ac4142;
}
.highlight .gh {
color: #6a9fb5;
font-weight: bold;
}
.highlight .k, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kv {
color: #aa759f;
}
.highlight .kc {
color: #d28445;
}
.highlight .kt {
color: #d28445;
}
.highlight .kd {
color: #d28445;
}
.highlight .s, .highlight .sb, .highlight .sc, .highlight .sd, .highlight .s2, .highlight .sh, .highlight .sx, .highlight .s1 {
color: #90a959;
}
.highlight .sr {
color: #75b5aa;
}
.highlight .si {
color: #8f5536;
}
.highlight .se {
color: #8f5536;
}
.highlight .nn {
color: #f4bf75;
}
.highlight .nc {
color: #f4bf75;
}
.highlight .no {
color: #f4bf75;
}
.highlight .na {
color: #6a9fb5;
}
.highlight .m, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .il, .highlight .mo, .highlight .mb, .highlight .mx {
color: #90a959;
}
.highlight .ss {
color: #90a959;
}
body {
margin: 0;
padding: 0;
background: #151515 0 0;
color: #eaeaea;
font-size: 16px;
line-height: 1.5;
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
}
/* General & 'Reset' Stuff */
.container {
width: 90%;
max-width: 800px;
margin: 0 auto;
}
section {
display: block;
margin: 20px 0 0 0;
}
h1, h2, h3, h4, h5, h6 {
margin: 20px 0 0 0;
}
header h2 {
margin: 0 0 0 0 !important;
}
li {
line-height: 1.4;
}
/* Header, <header>
header - container
h1 - project name
h2 - project description
*/
header {
background: rgba(0, 0, 0, 0.1);
width: 100%;
border-bottom: 1px dashed #b5e853;
padding: 20px 0;
margin: 0 0 40px 0;
}
header h1 {
font-size: 30px;
line-height: 1.5;
margin: 0 0 0 -40px;
font-weight: bold;
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
color: #b5e853;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), 0 0 5px rgba(181, 232, 83, 0.1), 0 0 10px rgba(181, 232, 83, 0.1);
letter-spacing: -1px;
-webkit-font-smoothing: antialiased;
}
header h1:before {
content: "./ ";
font-size: 24px;
}
header h2 {
font-size: 18px;
font-weight: 300;
color: #666;
}
#downloads .btn {
display: inline-block;
text-align: center;
margin: 0;
}
#tor-url {
font-size: smaller;
}
/* Main Content
*/
#main_content {
width: 100%;
-webkit-font-smoothing: antialiased;
}
section img {
max-width: 100%;
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
color: #b5e853;
letter-spacing: -0.03em;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), 0 0 5px rgba(181, 232, 83, 0.1), 0 0 10px rgba(181, 232, 83, 0.1);
}
#main_content h1 {
font-size: 30px;
}
#main_content h2 {
font-size: 24px;
}
#main_content h3 {
font-size: 18px;
margin-bottom: -5px;
}
#main_content h4 {
font-size: 14px;
}
#main_content h5 {
font-size: 12px;
text-transform: uppercase;
margin: 0 0 5px 0;
}
#main_content h6 {
font-size: 12px;
text-transform: uppercase;
color: #999;
margin: 0 0 5px 0;
}
dt {
font-style: italic;
font-weight: bold;
}
ul, p {
margin-top: 5px;
}
ul li {
list-style: none;
}
ul li:before {
content: " >> ";
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
font-size: 13px;
color: #b5e853;
margin-left: -37px;
margin-right: 8px;
line-height: 16px;
}
blockquote {
color: #aaa;
padding-left: 10px;
border-left: 1px dotted #666;
}
pre {
background: rgba(0, 0, 0, 0.9);
border: 1px solid rgba(255, 255, 255, 0.15);
padding: 10px;
font-size: 16px;
color: #b5e853;
border-radius: 2px;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
text-wrap: normal;
overflow: auto;
overflow-y: hidden;
}
table {
width: 100%;
margin: 0 0 20px 0;
}
th {
text-align: left;
border-bottom: 1px dashed #b5e853;
padding: 5px 10px;
}
td {
padding: 5px 10px;
}
hr {
height: 0;
border: 0;
border-bottom: 1px dashed #b5e853;
color: #b5e853;
}
/* Links
a, a:hover, a:visited
*/
a {
color: #63c0f5;
text-shadow: 0 0 5px rgba(104, 182, 255, 0.3);
text-underline-offset: 2px;
}
.footerlink {
color: #666666;
text-shadow: 0 0 5px rgba(104, 182, 255, 0.3);
text-underline-offset: 3px;
text-decoration-color: rgba(102, 102, 102, 0.3);
}
.footerlink:hover {
text-decoration-color: rgba(102, 102, 102, 0.7);
}
/* Clearfix */
.cf:before, .cf:after {
content: "";
display: table;
}
.cf:after {
clear: both;
}
.cf {
zoom: 1;
}
header h1 a {
text-decoration: none;
}
/* Scrollbar colors */
/* Works on Firefox */
* {
scrollbar-width: thin;
scrollbar-color: grey #151515;
}
/* Works on Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 6px;
}
*::-webkit-scrollbar-track {
background: #151515;
}
*::-webkit-scrollbar-thumb {
background-color: grey;
border-radius: 20px;
}
h1#post {
margin-top: 5px;
margin-bottom: 5px;
}
.highlight-bg {
background-color: #333;
padding: 1px 4px;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -0,0 +1,38 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEYJLXRxYJKwYBBAHaRw8BAQdAtmSNS3C7/M6kVd7fNIJ04Lv52MCSmq02udSA
7mncZWq0I0xhdXJhIEhhdXNtYW5uIDxsYXVyYUBoYXVzbWFubi5kZXY+iJkEExYI
AEECGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4ACGQEWIQT4pdrADkNRGSCJQvnQ
ROhMW+AWBQUCYdcZigUJBP7AMAAKCRDQROhMW+AWBZEBAQD/CrEQfkaCVi1SPrEN
5wmn/71F8fa170usNt5riNkfYwEAseybWYo+VmeE8tYa4sEBPFZoaPQAwM/wjwgf
zSJ7kQOIdQQQFggAHRYhBMvsHgj+kApKjUJoVV+LDAswTEVxBQJgktdxAAoJEF+L
DAswTEVxSyoBAM5EwfBuiELA4Atl7uXRVPvT1/qabFzr6Rs6FPRXXlRMAP9k78mo
IqG0JoITc0ZgnAUtN3blbMLW0I0smrYLu5CKC4kCMwQQAQgAHRYhBPG7exToWlhT
mi5PVl7B04/8MhMRBQJgktd+AAoJEF7B04/8MhMR0tgP/Rjigf7E6vNAJWJK6boF
AGMk2YeH6A98wYKWVjJX6tm4LxURrTfNGlovUuq27eE6Vedp2ZfqTfjNbp08g4x9
Kkratcw0aEr6zAf38ukZnxQZVqgUw5N4ZWdOD3j3Gdc/Oq4oSLua3egE0z/Q2hJM
r6zQc3y34ZDdVn22XqPuBZnaTwv0NIVXYUx6D/kwP+Yp4K4PDETVy1JotnBlvAu1
IE77DCWSd4aW0L6DBWbi9V15B7TyYpEooYtB6ayFOMoCWXKKnveNnXoD1Jy26THM
DjiWgngcJBuqOsetwNE3Bmt5eiaBUiWOSa6cO2RjaX+KOU2jEVMFZfOIYmE89PfV
Wso9s+K1KAF+656xTyDbl8iBYIUeM03/6/pRQiWfr0l8EVwaTtygMuXDeun2+nuv
6dXYxZkpSjCEeispkhHnoR6dRW86cr2WBYsMdioJ5z5ftyZ6lsM8ciMJYgq0QiAf
nDeE8hWtfhcjZLOsooEhljOtcek9cUpkP+hw3WbNkcTS9Xj/0rVEnzQh8EKzoGBG
OZAMclMUIOsz9U0JaVSbVedlQyT1X3ovIOaZ5yRJSWZ/3edNYmD26k16Aa/HwXRS
CbOPB52Vb67/kgcS9Cv+bfrN4kc2UynGPMNah6reSpJvGlmAHAc+0EXtBouOUAyJ
Qz6f5t7JvqR2CjynJ8OP3jAriJkEExYIAEECGwMFCQHhM4AFCwkIBwIGFQoJCAsC
BBYCAwECHgECF4AWIQT4pdrADkNRGSCJQvnQROhMW+AWBQUCYJLYDwIZAQAKCRDQ
ROhMW+AWBXnMAP9rntrchSROX0jzU9magm17YcbANjDL674jKC6LkOGk5AEAicKb
RKKPUG8pFNV2mOmgf35ogYJA3FinGxspTRy7CAy0H0xhdXJhIEhhdXNtYW5uIDx6
b3RhbkB6b3Rhbi5wdz6IlgQTFggAPgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIX
gBYhBPil2sAOQ1EZIIlC+dBE6Exb4BYFBQJh1xmKBQkE/sAwAAoJENBE6Exb4BYF
qfIBAI7yoj18pY8rSCIe0JwdsDamTu7i1tp9l601jcLWw4QPAP434R/1dh/sPL5X
vXigfmwbP8dg/ROpXbMNbP7DZIDyBoiWBBMWCAA+FiEE+KXawA5DURkgiUL50ETo
TFvgFgUFAmCS2AsCGwMFCQHhM4AFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ
0EToTFvgFgWVVgEA37FMZ8jCbML8GJT5hJZFFRVu69pE+R48EdERZL10PA0BAOrS
NVy89FoUtGZqQQXj/Gh/skzPCUWe8yoh7+Jqs8oNuDgEYJLXRxIKKwYBBAGXVQEF
AQEHQB6hjE6145oEWIw8ZlnvJgakZj8HAQJUfFbC+pzfcKMbAwEIB4h+BBgWCAAm
AhsMFiEE+KXawA5DURkgiUL50EToTFvgFgUFAmMwkdgFCQT+3ZoACgkQ0EToTFvg
FgUh2gEAieyZU0y/2CxC5vks8DcX4Q4xB2G19dNNIB8sHsgDDlsBAL36dDtt/irh
78Q59S49BEX+wx8AXInt2YOmeMQ8FsEA
=jz+5
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,4 @@
// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.

View File

@ -0,0 +1,81 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>zotanpw_web</RootNamespace>
</PropertyGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.js" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.js.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.min.js" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.min.js.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.js" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.js.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.min.js" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.min.js.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.js" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.js.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.min.js" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.min.js.map" />
<_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\LICENSE" />
<_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation\dist\additional-methods.js" />
<_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation\dist\additional-methods.min.js" />
<_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation\dist\jquery.validate.js" />
<_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation\dist\jquery.validate.min.js" />
<_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation\LICENSE.md" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="linq2db" Version="4.3.0" />
<PackageReference Include="Markdig" Version="0.30.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.0" />
<PackageReference Include="System.Data.SQLite" Version="1.0.115" />
</ItemGroup>
<ItemGroup>
<Folder Include="Pages\blog\posts" />
</ItemGroup>
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
<Exec Command="git describe --long --always --dirty --exclude=* --abbrev=8" ConsoleToMSBuild="True" IgnoreExitCode="False">
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput" />
</Exec>
</Target>
</Project>