From a4d92fecd374d6e3274ff4770b70076cc79b3a0f Mon Sep 17 00:00:00 2001 From: protokey Date: Sat, 10 Oct 2026 19:54:42 +0400 Subject: [PATCH] feat: English version of the site, CV as text - add English language: config, menus, i18n, translations of all pages and posts - CV page renders the full resume in both languages with PDF download buttons - remove salary from CV PDFs, Telegram is @xs0k0lx everywhere, English level B2 - fix broken CV menu link, update theme submodule to Public/blowfish Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + .gitmodules | 2 +- config/_default/languages.en.toml | 22 ++ config/_default/menus.en.toml | 17 + config/_default/menus.ru.toml | 2 +- content/_index.en.md | 16 + content/about/index.en.md | 50 +++ content/about/index.md | 2 +- content/blog/_index.en.md | 4 + content/blog/hello/index.en.md | 21 + content/blog/hello/index.md | 2 +- content/blog/how-to-make-blog/index.en.md | 369 ++++++++++++++++++ .../index.en.md | 152 ++++++++ .../blog/zero-setup-debian-server/index.en.md | 187 +++++++++ content/code-style.en.md | 115 ++++++ content/cv/Alexander_Sokolov_CV_EN.pdf | Bin 6421 -> 5080 bytes content/cv/Alexander_Sokolov_CV_RU.pdf | Bin 74511 -> 73415 bytes content/cv/index.en.md | 94 +++++ content/cv/index.md | 91 ++++- i18n/en.yaml | 1 + i18n/ru.yaml | 1 + layouts/partials/home/custom.html | 2 +- themes/blowfish | 2 +- 23 files changed, 1145 insertions(+), 8 deletions(-) create mode 100644 config/_default/languages.en.toml create mode 100644 config/_default/menus.en.toml create mode 100644 content/_index.en.md create mode 100644 content/about/index.en.md create mode 100644 content/blog/_index.en.md create mode 100644 content/blog/hello/index.en.md create mode 100644 content/blog/how-to-make-blog/index.en.md create mode 100644 content/blog/robotex-browser-automation-api/index.en.md create mode 100644 content/blog/zero-setup-debian-server/index.en.md create mode 100644 content/code-style.en.md create mode 100644 content/cv/index.en.md create mode 100644 i18n/en.yaml create mode 100644 i18n/ru.yaml diff --git a/.gitignore b/.gitignore index 8f88d7f..3c3d5aa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ public/ .obsidian/ content/.obsidian +.claude/ diff --git a/.gitmodules b/.gitmodules index f1c5772..7aae44f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "themes/blowfish"] path = themes/blowfish - url = https://git.sokolab.xyz/Sokolab/blowfish.git + url = https://git.sokolab.xyz/Public/blowfish.git diff --git a/config/_default/languages.en.toml b/config/_default/languages.en.toml new file mode 100644 index 0000000..d85ad44 --- /dev/null +++ b/config/_default/languages.en.toml @@ -0,0 +1,22 @@ +locale = "en" +label = "English" +weight = 2 +title = "Sokolab" + +[params] + displayName = "EN" + isoCode = "en" + rtl = false + dateFormat = "2 January 2006" + description = "Software and hardware products for people and businesses" + +[params.author] + name = "Alexander Sokolov aka s0k0l" + image = "img/me.jpg" + headline = "Scraping, bots and high-load web services. Information security." + bio = "I emulate user behavior, extract data and protect information. I take projects from idea to release." + links = [ + { email = "mailto:xalex.sokolov@yahoo.com" }, + { telegram = "https://t.me/xs0k0lx" }, + { gitea = "https://git.sokolab.xyz/s0k0l" }, + ] diff --git a/config/_default/menus.en.toml b/config/_default/menus.en.toml new file mode 100644 index 0000000..686a6f3 --- /dev/null +++ b/config/_default/menus.en.toml @@ -0,0 +1,17 @@ +# -- Main Menu -- +# See menus.ru.toml for the parameter reference. + +[[main]] + name = "Blog" + pageRef = "blog" + weight = 10 + +[[main]] + name = "About" + pageRef = "about" + weight = 20 + +[[main]] + name = "CV" + pageRef = "cv" + weight = 30 diff --git a/config/_default/menus.ru.toml b/config/_default/menus.ru.toml index 98b828d..3257b17 100644 --- a/config/_default/menus.ru.toml +++ b/config/_default/menus.ru.toml @@ -22,7 +22,7 @@ [[main]] name = "Резюме" - pageRef = "резюме" + pageRef = "cv" weight = 30 #[[main]] diff --git a/content/_index.en.md b/content/_index.en.md new file mode 100644 index 0000000..2ed1c9e --- /dev/null +++ b/content/_index.en.md @@ -0,0 +1,16 @@ +--- +title: "Alexander Sokolov" +card: + name: "Alexander Sokolov" + nick: "s0k0l" + role: "Scrapers, bots, websites and services. High load and infosec." + avatar: "img/me.jpg" # assets/img/me.jpg + status: + state: open # open | busy | away + text: "Open to projects and work" + contacts: + - { label: "Telegram", url: "https://t.me/xs0k0lx", icon: "telegram" } + - { label: "Email", url: "mailto:xalex.sokolov@yahoo.com", icon: "email" } + - { label: "Git", url: "https://git.sokolab.xyz/s0k0l", icon: "gitea" } + more: "/about/" +--- diff --git a/content/about/index.en.md b/content/about/index.en.md new file mode 100644 index 0000000..b66b774 --- /dev/null +++ b/content/about/index.en.md @@ -0,0 +1,50 @@ +--- +title: "About me" +date: 2026-10-08T19:18:00+04:00 +--- + +## Who I am + +My name is Alexander Sokolov, online I go by s0k0l. I'm a generalist IT specialist: user behavior emulation, data extraction and information security. I write scrapers and bots that get past anti-bot protection, build websites and network services, and secure infrastructure and development. + +I prefer to work from a spec and take a project from idea to MVP or release. If there is no spec, I'll write it myself: do the research, pick the technologies, estimate complexity and timelines. I work by my own [Software Development Standards](/code-style.md) and always start with requirements and architecture, not with code. + +My result is a solved problem. Source code is the proof that the solution works. If we agreed on something, I will do everything to keep my promise. + +## What I do + +I create software and hardware products for people and businesses that help them live better and work more comfortably. + +### Software: bots, services, automation + +I'm drawn to tasks around the web and Linux: + +- website scraping and anti-bot bypass +- website bots and chatbots +- high-load systems built on queues and microservices +- websites, web applications and services +- accepting crypto payments directly, on-chain, with no intermediaries +- information security for servers and development + +My detailed track record is in the [CV](/cv/index.md). + +### Hardware: devices, electronics, prototypes + +I learned microcontroller programming because I dream of building robots. In the meantime I built a hardware password manager, ProtoKey. I plan to keep growing in this direction, I have plenty of ideas for useful devices. + +## Where to find me + +Everything alive is here now: + +- [Blog](/blog/) - articles on development, servers and automation +- [git.sokolab.xyz](https://git.sokolab.xyz/s0k0l) - my code and examples + +I used to be active on other platforms too. I barely visit them now, but I keep the links, they show where I started. + +**[Stack Overflow](https://ru.stackoverflow.com/users/362019/alex)** (in Russian). For many years I answered questions there and asked colleagues myself. The site is almost empty now, AI answers these questions instead. + +**[Medium](https://alexandrsokolov-41020.medium.com/)**. My first blog. I wrote about development there, but someone else's platform got old fast, so now I write here. + +**[GitHub](https://github.com/alexsok-bit/)**. Code for the Medium articles and old projects. New code lives on my own git. + +**[Mozilla Add-ons](https://addons.mozilla.org/en-US/firefox/user/16605688/)**. When I was building data collection tools, I needed to adapt Firefox to my tasks. I wrote a couple of add-ons for that and published them in the public catalog. I made them for myself, but other people started using them, and they still leave comments. diff --git a/content/about/index.md b/content/about/index.md index a462413..26b74b8 100644 --- a/content/about/index.md +++ b/content/about/index.md @@ -26,7 +26,7 @@ date: 2026-10-08T19:18:00+04:00 - прием оплаты криптой напрямую, on-chain переводом без посредников - информационная безопасность серверов и разработки -Подробный послужной список в [резюме](/резюме/index.md). +Подробный послужной список в [резюме](/cv/index.md). ### Железо: устройства, электроника, прототипы diff --git a/content/blog/_index.en.md b/content/blog/_index.en.md new file mode 100644 index 0000000..7438f84 --- /dev/null +++ b/content/blog/_index.en.md @@ -0,0 +1,4 @@ +--- +title: "Blog" +description: "Notes on projects, software and hardware" +--- diff --git a/content/blog/hello/index.en.md b/content/blog/hello/index.en.md new file mode 100644 index 0000000..e0c7e38 --- /dev/null +++ b/content/blog/hello/index.en.md @@ -0,0 +1,21 @@ +--- +title: "I launched my website" +date: 2026-10-05T21:14:48+04:00 +draft: false +tags: ["server", "hugo"] +summary: "How I set up a website and Gitea on my own server" +--- + +Today I launched [sokolab.xyz](https://sokolab.xyz/), a landing page and a blog. Everything runs on my own server in Docker. Here I'll publish reports on my work, useful things about computers and internet technologies, maybe some reviews and lecture courses. + +The home page is a landing page with a summary about me and my statuses. The blog page has all posts in chronological order. And if a course appears, or my work turns into some kind of product, it will get its own subdomain where it can live its own life. + +## How it works + +The site is built with Hugo, the sources live in my [Gitea](https://git.sokolab.xyz/Public/site). + +![Diagram](schema.jpeg) + +- I write a post in Markdown; +- I do `git push`; +- a minute later it's on the site. diff --git a/content/blog/hello/index.md b/content/blog/hello/index.md index 01f06da..3ba77ee 100644 --- a/content/blog/hello/index.md +++ b/content/blog/hello/index.md @@ -12,7 +12,7 @@ summary: "Как я поднял сайт и Gitea на своём сервер ## Как это устроено -Сайт собран на Hugo, исходники лежат в моей [Gitea](https://git.sokolab.xyz/s0k0l). +Сайт собран на Hugo, исходники лежат в моей [Gitea](https://git.sokolab.xyz/Public/site). ![Схема](schema.jpeg) diff --git a/content/blog/how-to-make-blog/index.en.md b/content/blog/how-to-make-blog/index.en.md new file mode 100644 index 0000000..3d1b7cb --- /dev/null +++ b/content/blog/how-to-make-blog/index.en.md @@ -0,0 +1,369 @@ +--- +title: How a programmer can start their own blog +date: 2026-10-08T19:22:00+04:00 +draft: false +tags: + - hugo + - server +summary: How to set up your own blog +--- + +## Intro + +The first thing you need is a server. Just google "Debian VPS" and pick from what comes up. + +**Important:** if you're in Russia, it's better to choose a hosting provider and a server in your own country. Then search Yandex for "аренда виртуального сервера Debian". + +While the server is being set up, you have about an hour to buy a domain name and register a Cloudflare account. The first one is obvious, and CF is needed to hide your server's IP address from site visitors. This definitely improves server security, but it has to be done before the domain is first pointed at the server. So after buying the domain, go to the control panel (where you bought it) and change the NS servers to the ones Cloudflare gives you after you add the project in your account. After that it takes some time (up to 3 days) for the domain to be re-parked, since the whole internet has to learn about it. But after that no visitor will be able to find out your site's IP, unless you leak it, of course. We'll talk about that some other time. + +What you have now: +1. the IP and root password +2. a domain name parked at CF +3. records set up in CF: + 1. yourdomain.com - IP - the root record, the domain name opens the blog home page + 2. git.yourdomain.com - IP - a subdomain for Gitea + +## Let's go + +[Initial Debian server setup ->](/blog/zero-setup-debian-server/) + +Install Docker with + +``` +curl -fsSL https://get.docker.com -o install-docker.sh +sh install-docker.sh +``` + +Log out of the server, because now we need to prepare everything locally. + +On your computer, install Hugo from the GitHub repository, because the Debian package repository has an old version. + +https://github.com/gohugoio/hugo/releases + +Install Git on your computer to work with the repository. + +On the server we'll run 2 applications in Docker: +- **git** - Gitea, a git service, something like GitHub +- **hugo** - the site builder and Caddy. Caddy serves the finished pages and is also the only entry point from outside, Gitea traffic goes through it too + +Hugo builds blog pages from Markdown files, converting everything to HTML. The workflow looks like this: +1. create an md file in the repository +2. fill it in, commit and push +3. the builder in Docker checks the repository once a minute and sees a new commit +4. it builds the site and swaps the old version for the new one + +Okay, now pick a place on your computer and create an `apps` folder, where we'll keep the configuration of the Docker applications on the server. The final structure will look like this: + +``` +apps/ +├── git/ +│ └── docker-compose.yaml +└── hugo/ + ├── docker-compose.yaml + ├── caddy.conf + ├── dot_env + ├── .gitignore + └── data/ + └── builder/ + └── build.sh +``` + +There are only configs here. All data (repositories, certificates, the built site) lives in Docker named volumes, not in folders next to the configs. This way `apps` can safely be kept in git and copied to the server without fear of overwriting data. I'll cover backups below. + +Both applications talk over a shared `web` network. Only Caddy exposes ports to the outside. + +### git + +`apps/git/docker-compose.yaml`: + +```yaml +# Git application for docker-server + +services: + gitea: + image: gitea/gitea:28.0-rootless + restart: unless-stopped + environment: + GITEA__server__DOMAIN: git.yourdomain.com + GITEA__server__ROOT_URL: https://git.yourdomain.com/ + GITEA__server__DISABLE_SSH: "true" + GITEA__database__DB_TYPE: sqlite3 + GITEA__service__DISABLE_REGISTRATION: "true" + volumes: + - data:/var/lib/gitea + - config:/etc/gitea + networks: [ web ] + + +networks: + web: + external: true + + +volumes: + data: + config: +``` + +We use the rootless image, so inside the container Gitea doesn't run as root. This is where named volumes really fit: when creating a volume, Docker sets its owner from the image, and you don't have to `chown` anything by hand. + +We don't expose any ports, only Caddy reaches Gitea over the `web` network. SSH is disabled because Cloudflare doesn't proxy it, and we don't want to expose the server's IP. We'll push over https. Registration is closed so nobody but you can create accounts there. + +### hugo + +`apps/hugo/docker-compose.yaml`: + +```yaml +# Landing page and blog site on Hugo + +services: + builder: + image: ghcr.io/gohugoio/hugo:v0.167.0 + restart: unless-stopped + user: root + entrypoint: [ "sh", "/build.sh" ] + environment: + REPO_URL: ${REPO_URL} + volumes: + - ./data/builder/build.sh:/build.sh:ro + - src:/src + - public:/public + networks: [ web ] # to reach git-gitea-1 + + web: + image: caddy:2.10.2-alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./caddy.conf:/etc/caddy/Caddyfile:ro + - public:/srv:ro + - caddy_data:/data + - caddy_config:/config + networks: [ web ] # to proxy to git-gitea-1 + + +networks: + web: + external: true + + +volumes: + src: + public: + caddy_data: + caddy_config: +``` + +You don't need to build your own image. `builder` is the official Hugo image, it already includes git. Use the same version as on your computer, you can check it with `hugo version`. `user: root` is needed because the image runs as a regular user by default, while Docker creates volumes as root. + +`web` is Caddy. It faces the outside on 80 and 443, serves the site from the `public` volume shared with `builder`, and proxies the git subdomain to Gitea. It keeps its certificates in `caddy_data`. + +`apps/hugo/caddy.conf`: + +``` +yourdomain.com { + tls internal + encode gzip + root * /srv/live + file_server + + handle_errors { + rewrite * /404.html + file_server + } +} + +www.yourdomain.com { + tls internal + redir https://yourdomain.com{uri} permanent +} + +git.yourdomain.com { + tls internal + reverse_proxy git-gitea-1:3000 +} +``` + +The site is served from `/srv/live`, why exactly from there will become clear from the build script. `handle_errors` makes non-existent pages show the theme's nice 404 instead of an empty response. `www` simply redirects to the main domain so the site has a single address. + +Docker Compose builds the name `git-gitea-1` itself from the folder name and the service name: folder `git`, service `gitea`, first instance. That's why it matters that the folder is named exactly like that. + +`tls internal` means Caddy issues a certificate for itself. The browser never sees it, because visitors connect to Cloudflare, and Cloudflare connects to us. So in the Cloudflare panel, under SSL/TLS, set the mode to **Full**. Not Flexible, otherwise traffic from Cloudflare to the server goes unencrypted, and not Full (strict), which won't accept such a certificate. + +`apps/hugo/dot_env` is a template for the variables: + +``` +REPO_URL=http://git-gitea-1:3000//site.git +``` + +On the server you copy it to `.env` and fill in your username and repository. The `.env` itself doesn't go into git, that's what the `.gitignore` next to it with a single `.env` line is for. The builder clones the repository straight from the Gitea container over the internal network, without going out to the internet. If the repository is private, add a token: `http://:@git-gitea-1:3000//site.git`. The token is issued in the Gitea user settings, under Applications. In that case hide the file from prying eyes with `chmod 600 .env`. + +`apps/hugo/data/builder/build.sh`: + +```sh +#!/bin/sh +# Once a minute: new commit -> build into /public/next -> swap /public/live. +# Build error - the previous site stays. +# The repository address is taken from REPO_URL on every start: change in .env + redeploy = new source. +cd /src +if [ -d .git ]; then + git remote set-url origin "$REPO_URL" +else + git clone --recurse-submodules "$REPO_URL" . || exit 1 +fi +last='' +while true; do + if git fetch -q origin HEAD && git reset -q --hard FETCH_HEAD \ + && git submodule sync -q --recursive && git submodule update -q --init --recursive; then + rev=$(git rev-parse HEAD) + if [ "$rev" != "$last" ]; then + rm -rf /public/next + if hugo --minify -d /public/next; then + rm -rf /public/old + [ -d /public/live ] && mv /public/live /public/old + mv /public/next /public/live + last=$rev + echo "published $rev" + else + echo "build of $rev failed, site unchanged" + fi + fi + fi + sleep 60 +done +``` + +The main trick of the script is that the site is built into a separate `next` folder, and only if the build succeeds does it replace the working `live` one. If you push a broken commit, the site simply stays as it was, and the log shows what broke. The previous version is kept in `old` in case you need to roll back quickly. + +`git reset --hard` is used here on purpose instead of `git pull`. If you force push or rewrite history, a regular pull will break, while reset just takes whatever is in the repository. And `set-url` at startup lets you change the source: edit `REPO_URL` in `.env`, restart the container, and the builder pulls from the new place. + +## Uploading to the server + +The configs are ready, let's send the folder to the server. `` is the host name from `~/.ssh/config` that we set up in the previous article: + +``` +s0k0l:~$ scp -r apps :/opt/ +``` + +Before starting, we need to open the firewall for the site. In the previous article we closed all incoming traffic, and I warned there that Docker lives by its own rules. Let's sort it out now. + +Open `/etc/nftables.conf` on the server. In the `input` chain, uncomment the line for the site: + +``` + tcp dport { 80, 443 } accept +``` + +And change the `forward` chain to this: + +``` + chain forward { + type filter hook forward priority 0; policy drop; + + ct state established,related accept + ct status dnat accept + iifname "docker0" accept + iifname "br-*" accept + } +} +``` + +The thing is, traffic to containers goes not through `input` but through `forward`. With our strict `policy drop`, containers can neither accept connections nor reach the internet. These rules let through traffic to the ports Docker published (in our case only Caddy's 80 and 443) and outgoing traffic from the containers themselves. Everything else is still closed. + +Check and apply the same way as last time, with a timer just in case: + +``` +nft -c -f /etc/nftables.conf +(sleep 120 && nft flush ruleset) & +nft -f /etc/nftables.conf +``` + +Check that ssh is alive, cancel the timer with `kill %1` and restart Docker: + +``` +systemctl restart docker +``` + +This is mandatory. Our config starts with `flush ruleset`, which also wipes the rules Docker created for itself. After a restart Docker creates them again. Remember this: every time you restart nftables, restart Docker too. + +## Launch + +Create the shared network through which the containers will see each other: + +``` +docker network create web +``` + +Bring up Gitea and, for now, only Caddy without the builder. The builder has nothing to clone until there's a repository in Gitea: + +``` +cd /opt/apps/git && docker compose up -d +cd /opt/apps/hugo && cp dot_env .env && docker compose up -d web +``` + +Open `https://git.yourdomain.com` in the browser. Gitea will show the initial setup page, the database is already set in the config. At the bottom of the page, in the administrator account section, create your user. We closed registration, so this is the only way to get an account. + +Create a `site` repository in Gitea. Now push the site there from your computer: + +``` +s0k0l:~$ cd my-blog +s0k0l:~$ git remote add origin https://git.yourdomain.com//site.git +s0k0l:~$ git push -u origin main +``` + +If the theme is included as a git submodule, the theme repository must also be reachable by the builder at the URL in `.gitmodules`. The easiest way is to make a mirror of the theme in your Gitea and put its address in `.gitmodules`. + +Put your real username into `/opt/apps/hugo/.env` and start the builder: + +``` +cd /opt/apps/hugo && docker compose up -d +``` + +Check that it built the site: + +``` +docker logs -f hugo-builder-1 +``` + +If `published ` shows up in the log, open `https://yourdomain.com`, the blog is working. + +## Backups + +Since the data lives in named volumes, you can't see it in `apps`. You can list them with `docker volume ls`. Two of them matter here: `git_data` and `git_config`, they hold all the repositories and Gitea settings. The site doesn't need backing up, it's built from the repository, and Caddy will issue certificates again. + +Back up Gitea: + +``` +docker compose -f /opt/apps/git/docker-compose.yaml stop +docker run --rm -v git_data:/data -v git_config:/config -v /root:/backup alpine \ + tar czf /backup/gitea-$(date +%F).tgz /data /config +docker compose -f /opt/apps/git/docker-compose.yaml start +``` + +We stop Gitea during the backup so the SQLite database isn't written halfway. Pull the finished archive to your machine with `scp`. + +## How to write now + +The whole process looks like this: + +1. create a post with `hugo new content blog/my-post/index.md` +2. write it and preview it locally with `hugo server` +3. commit and push +4. a minute later the post is on the site + +For convenience I added a `Makefile` to the repository so I don't have to type the commands by hand: + +```makefile +all: + hugo new content $(path) + +publish: + git add . + git commit -m 'chore: $(msg)' + git push +``` + +A new post is `make path=blog/my-post/index.md`, publishing is `make publish msg="new post"`. + +That's it. A server, your own git, a blog and auto-deploy, all on one inexpensive VPS with no third-party services except Cloudflare. diff --git a/content/blog/robotex-browser-automation-api/index.en.md b/content/blog/robotex-browser-automation-api/index.en.md new file mode 100644 index 0000000..305a311 --- /dev/null +++ b/content/blog/robotex-browser-automation-api/index.en.md @@ -0,0 +1,152 @@ +--- +title: Robotex, a Browser Automation API +date: 2026-10-09T01:59:39+04:00 +draft: false +tags: + - parsing + - anti-bot + - api + - saas +summary: How a bot can scrape a site behind anti-bot protection or with heavy JS +aliases: + - /blog/robotex-brouser-automation-api/ +--- + +Every website bot sooner or later runs into one of three problems: + +1. a captcha +2. anti-bot protection +3. heavy JS that has to be rendered to submit a form + +A spider that walks a site with raw http requests is helpless here. There's only one solution: launch a browser and let it handle the hard part. But the browser has to be hidden too. Under the hood it sends the same http requests as a Python spider, it just does so from the context of running JS. And that context is full of markers that tell the site someone is controlling the browser. + +For this task I wrote a microservice, [Robotex](https://git.sokolab.xyz/s0k0l/robotex). Below I'll explain how it works and why it's needed when Scrapling and FlareSolverr already exist. + +## What was used before and now + +I used to go with Selenium. It runs Firefox through geckodriver over the Marionette protocol and gives itself away quite noticeably: `navigator.webdriver`, driver traces in the page environment and so on. Almost all of that can be hidden with an add-on that adjusts the page context before the site loads. That's what I did, but it's a constant race against every new check. + +Now there are tools that solve this at the level of the browser itself: + +- **Camoufox** - a Firefox build where fingerprints (screen, fonts, WebGL, core count, etc.) are spoofed in the engine code rather than through JS. +- **Patchright** - a patched Playwright that removes the CDP leaks anti-bots use to detect Chromium automation. + +Scrapling works on top of them. It has a ready-made mode for getting through anti-bot pages, and I borrowed its algorithm. It turned out that passing Cloudflare isn't about solving a puzzle. If the browser environment looks human and the IP is clean, it's enough to wait for the widget and click the checkbox with a random offset and delay. The whole difficulty is correctly detecting that you're facing a check, and making sure the browser no longer looks suspicious by the time it clicks. + +For small volumes without accounts, Scrapling is enough. Problems start when accounts come in. Then the IP, User-Agent, cookies and browser fingerprint have to be tied into one profile and live together. Change the proxy but keep the old cookies, and the account goes into verification or gets banned. The protection starts nagging you with captchas, and accounts die. + +## What Robotex is + +Robotex takes over all the browser work: it gets past the anti-bot, runs a scenario on the page and hands the bot cookies it can use to keep browsing the site with plain requests. + +It differs from FlareSolverr and Byparr in that those can only open a page and get `cf_clearance`. Robotex runs a scenario: fill a form, click, solve a captcha, wait for an element. And it differs from Scrapling in that it's not a library inside your process but a separate service. The bot can be written in anything, all it needs is an http client. + +Under the hood are FastAPI and a real Chrome driven by Patchright. I ported the browser wrapper from Scrapling. I started with Camoufox but eventually settled on Chrome. + +## How it works + +The bot sends a scenario: + +``` +POST /v1/solve +Content-Type: application/json +X-API-Key: + +{ + "url": "https://example.com/login", + "actions": [ + {"type": "fill", "selector": "#email", "value": "user@example.com"}, + {"type": "fill", "selector": "#password", "value": "..."}, + {"type": "mouse_move", "selector": "#submit"}, + {"type": "click", "selector": "#submit"}, + {"type": "wait_for", "selector": ".dashboard", "timeout": 30} + ], + "proxy": "socks5://user:pass@host:port", + "html": true, + "timeout": 120 +} +``` + +The scenario fills in the login form and waits for the dashboard to load. Besides these actions there are `submit` (a click that waits for navigation to a new page), `delay`, `inner_html` (grab a piece of the page) and `captcha`, more on that below. + +Since the request has no `session_id` cookie, the service creates a new browser profile and returns it in the `Set-Cookie` header. The profile is stored on disk: cookies, local storage and fingerprint. The browser locale and timezone are matched to the proxy's geo, so the IP and the environment don't contradict each other. When the bot needs a browser again, it sends this cookie, and Robotex continues in the same profile. To the site it's still the same user. + +The response: + +``` +{ + "status": "ok", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", + "cookies": [ + {"name": "sessionid", "value": "...", "domain": ".example.com", "path": "/"} + ], + "actions": [ + {"type": "fill", "ok": true}, + {"type": "fill", "ok": true}, + {"type": "mouse_move", "ok": true}, + {"type": "click", "ok": true}, + {"type": "wait_for", "ok": true} + ], + "html": "......" +} +``` + +From there the bot browses the site on its own, with these cookies, **the same User-Agent and through the same proxy**. If an action fails, `status` will be `err`, and `actions` shows at which step and why. + +### Captcha + +If a captcha is expected on the form, add a step for it and a recognition service key (2captcha is supported right now): + +``` +{ + "url": "https://example.com/login", + "actions": [ + {"type": "fill", "selector": "#email", "value": "user@example.com"}, + {"type": "fill", "selector": "#password", "value": "..."}, + {"type": "captcha", "selector": "#captcha", "value": "hcaptcha"}, + {"type": "click", "selector": "#submit"}, + {"type": "wait_for", "selector": ".dashboard", "timeout": 30} + ], + "captcha": "<2captcha key>", + "proxy": "socks5://user:pass@host:port" +} +``` + +The captcha will be solved before the submit click. + +### Anti-bot page + +An anti-bot page can appear at any moment, or not at all. So it's not a scenario step but a setting for the whole request: + +``` +{ + "url": "https://example.com/login", + "actions": [ ... ], + "captcha_type": "cloudflare", + "captcha_detect_locator": "#anti-bot-page-only-id", + "captcha_success_locator": "#site-only-id", + "captcha": "<2captcha key>", + "proxy": "socks5://user:pass@host:port" +} +``` + +`captcha_detect_locator` is a selector that exists only on the check page, `captcha_success_locator` exists only on the site itself. Right after the page loads and before running the scenario, Robotex checks whether there's a barrier in front of it. If there is, it gets through on its own: waits, clicks the checkbox. If the check escalated to a full image captcha, it sends it to the recognition service. If the check pops up in the middle of a scenario, there's a separate `pass_challenge` action for that. + +For heavy sites there are a couple more useful options: `disable_resources` turns off loading of images and fonts, `blocked_domains` cuts requests to unneeded domains like analytics, and `cookies` lets you pass in ready-made cookies. + +## Limitations + +There's no silver bullet here: + +- This is an early MVP. API key checks, a task queue and billing are still planned +- a browser eats hundreds of megabytes of memory, so one instance currently handles 10 concurrent sessions +- proxy quality matters a lot, no fingerprint will help on a dirty IP +- Turnstile doesn't pass on every site, I'm still working on stability. Anti-bots get updated, and getting through a specific site has to be tested and maintained + +On the upside, the bot stays lightweight: it needs a browser only where there's no way around it, and the rest of the time it works with fast http requests. + +## Code + +Sources: [git.sokolab.xyz/s0k0l/robotex](https://git.sokolab.xyz/s0k0l/robotex). The `docs` folder also has the spec for the anti-bot bypass module, if you're curious how it works inside. + +Use it only on sites where you have the right to do so. diff --git a/content/blog/zero-setup-debian-server/index.en.md b/content/blog/zero-setup-debian-server/index.en.md new file mode 100644 index 0000000..7c3fabb --- /dev/null +++ b/content/blog/zero-setup-debian-server/index.en.md @@ -0,0 +1,187 @@ +--- +title: "Initial Debian server setup" +date: 2026-10-08T19:29:38+04:00 +draft: false +tags: [server, linux] +summary: "What to do with a Debian server after you buy it. An article for Linux users." +--- + +Usually, after you buy a server, you get an email with an IP address and the root password. That's enough to connect to the server over ssh and start setting it up. + +Just 3 steps: +1. System update +2. ssh setup +3. nftables setup + +Step zero is preparing ssh keys and a config for the server. To generate keys, run: + +``` +s0k0l:~$ ssh-keygen +Generating public/private ed25519 key pair. +Enter file in which to save the key (/~/.ssh/id_ed25519): +``` + +It asks for a path for the key. You can just type the key name you want, and the pair will be created in the directory you ran the command from. The first time you can skip this and press Enter. + +The second question is about a passphrase for the key. For ssh keys to production infrastructure it's better to set one. If you don't want to, just hit Enter. + +Once the hosting provider brings the server up, copy the key over with + +``` +s0k0l:~$ ssh-copy-id root@ +/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed +/usr/bin/ssh-copy-id: INFO: 3 key(s) remain to be installed -- if you are prompted now it is to install the new keys +root@'s password: +``` + +Enter the password and you should see a success message suggesting you connect to the server, which is exactly what we'll do. But first add a new entry to `~/.ssh/config`: + +``` +Host + HostName + Port 22 + IdentityFile ~/.ssh/id_ed25519 +``` + +Now you can connect by name + +``` +ssh +``` + +On the server, the first thing to do is run an update: + +``` +apt update && apt dist-upgrade -y && reboot +``` + +This updates the whole system and reboots it. Wait about 5 minutes and try to connect again. + +Now we need to configure ssh so that nobody else can connect to the server on the default port or log in with a password. This is the most important part. + +Open `/etc/ssh/sshd_config` for editing and find the parameters for the listen address and port. + +``` +Port 5872 +AddressFamily inet +ListenAddress +#ListenAddress :: +``` + +Set them like this to strictly define how the server can be reached. We have an ipv4 address, so we can put it in the config explicitly. And the port should be changed to a random one in the range from 1024 to 65535. To check which ports are already taken, run: + +``` +ss -tlnup +``` + +If the port you want isn't in the output, it's free. + +Next, find the following in the ssh config: + +``` +PermitRootLogin prohibit-password + +PubkeyAuthentication yes + +PasswordAuthentication no +``` + +These settings disable ssh password authentication for all users, including root (PermitRootLogin). + +After saving the config, restart the ssh service. Stay in your session until you've connected in parallel. + +``` +systemctl restart ssh +``` + +Open another terminal, fix the port in `~/.ssh/config` and connect to the server. If the connection works, you can close the first terminal. SSH is done. + +The last step is setting up nftables. It's the standard firewall in Debian, iptables is no longer needed. + +The rules live in a single file, `/etc/nftables.conf`. The principle of a strict config is simple: all incoming traffic is denied, we allow only what is actually needed. Outgoing traffic stays open, otherwise updates and DNS will break. + +Open `/etc/nftables.conf` and replace its contents entirely: + +``` +#!/usr/sbin/nft -f + +flush ruleset + +table inet filter { + chain input { + type filter hook input priority 0; policy drop; + + # let established connections through, drop garbage + ct state established,related accept + ct state invalid drop + + # loopback interface + iif lo accept + + # ping, but no more than 5 per second + ip protocol icmp icmp type echo-request limit rate 5/second accept + ip protocol icmp accept + meta l4proto ipv6-icmp accept + + # ssh on our port, no more than 10 new connections per minute from one ip + tcp dport 5872 ct state new meter ssh_limit { ip saddr limit rate 10/minute } accept + + # uncomment if the server will host a website + # tcp dport { 80, 443 } accept + } + + chain forward { + type filter hook forward priority 0; policy drop; + } + + chain output { + type filter hook output priority 0; policy accept; + } +} +``` + +Change port `5872` to the one you set in `sshd_config`. This is the most important line in the config, a mistake here will lock you out of the server. + +First, check the config for errors without applying anything: + +``` +nft -c -f /etc/nftables.conf +``` + +If the command printed nothing, the syntax is fine. Now a safety net in case we do lock ourselves out. Start a timer that will flush all rules in 2 minutes: + +``` +(sleep 120 && nft flush ruleset) & +``` + +And apply the config: + +``` +nft -f /etc/nftables.conf +``` + +Then, as with ssh, open another terminal and connect. If you got in, everything is fine, cancel the timer: + +``` +kill %1 +``` + +If you didn't get in, just wait 2 minutes, the rules will be flushed automatically and you can calmly look for the mistake. + +To see what is actually applied right now, run: + +``` +nft list ruleset +``` + +All that's left is to enable loading the rules at boot, otherwise after a reboot the server will be left without a firewall: + +``` +systemctl enable --now nftables +``` + +If the server will run Docker, keep in mind that it writes its own rules and can open container ports bypassing your config. That's a topic for a separate article. + +That's it. The server is updated, ssh only lets you in with a key and on a non-standard port, and the firewall blocks everything we haven't explicitly allowed. From here you can start installing your services. + +Disclaimer: the timer trick is needed because a firewall can drop even an already established connection. The example config has the line `ct state established,related accept`, which means "allow connections that are already established". So in theory there shouldn't be a disconnect. diff --git a/content/code-style.en.md b/content/code-style.en.md new file mode 100644 index 0000000..4a28c6d --- /dev/null +++ b/content/code-style.en.md @@ -0,0 +1,115 @@ +--- +title: "Software Development Standards" +date: 2026-10-08T19:18:00+04:00 +--- + +These are the rules I write code by. They are based on Robert Martin's "Clean Code" and PEP 8 for Python. It's a living document, I extend it as I learn things the hard way. + +The main idea is simple: code is read far more often than it is written. So I write it for the person who opens the file six months from now. Often that person is me. + +## 0. No spec, no result + +Any work starts with requirements, not with code. + +- First I pin down what problem we're solving and how we'll know it's solved. +- Then the architecture: components, data, the boundaries between them. +- Only after that, code. If requirements change along the way, I update the spec first, then the code. + +## 1. Code style: PEP 8 + my rules + +PEP 8 is the baseline standard. My rules work on top of it, like the cascade in CSS: everything I haven't overridden is inherited from PEP 8 as is. There's one override. + +**Tabs instead of spaces.** One level of nesting is one character. Everyone sees indentation at the width they set in their editor, and nothing changes in the file. I never mix spaces and tabs in one file, Python 3 won't allow it anyway. + +Everything else follows PEP 8: line length, blank lines between functions and classes, import order, spaces around operators. + +So I don't have to keep this in my head, a tool checks and fixes the style. I use ruff: + +```toml +# pyproject.toml +[tool.ruff] +line-length = 100 + +[tool.ruff.format] +indent-style = "tab" + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "B", "UP"] +ignore = ["W191"] # W191 complains about tabs, for us it's a deliberate choice +``` + +IDE: JetBrains (PyCharm). The project settings have tabs enabled and ruff runs on save. + +## 2. Names + +A name should answer three questions: why it exists, what it does and how it's used. If a name needs a comment, it's a bad name. + +- Variables and functions in `snake_case`, classes in `PascalCase`, constants in `UPPER_CASE`. +- Functions are named with a verb: `fetch_page`, `parse_price`, `send_report`. Classes and variables with a noun: `Browser`, `proxy_pool`. +- No abbreviations or single-letter names. Exceptions: `i` in a short loop and `e` for an exception. +- One concept, one word. If the project has `fetch`, then `get`, `load` and `retrieve` don't show up next to it for the same thing. +- No type encoding in names: `users`, not `users_list` or `lst_users`. +- Boolean variables read like a question: `is_ready`, `has_proxy`, `can_retry`. + +## 3. Functions + +- **Small.** A function should fit on the screen without scrolling. If it doesn't, it can be split. +- **Do one thing.** If a function can't be described in one sentence without the word "and", it's two functions. +- **One level of abstraction.** A function either orchestrates a process and calls other functions, or works with details. Not both at once. +- **Few arguments.** Ideally zero to two. Three or more is a reason to group them into a dataclass. +- **No flag arguments.** `render(page, True)` is hard to read. Better two functions: `render_full` and `render_preview`. +- **No hidden side effects.** If a function is called `check_session`, it shouldn't create a new session along the way. +- **Command or query.** A function either changes state or returns data. Not both. + +## 4. Comments + +The best comment is the one that wasn't needed because the code is clear as it is. + +I write a comment when I need to explain **why**, not **what**: + +```python +# Cloudflare returns 403 on the first request without a cookie, so we always make two +response = session.get(url) +``` + +I don't write: +- comments that retell the code +- commented-out code. That's what git history is for +- a change log and authorship in the file header. That's git's job too + +A docstring is required for a module's public functions and classes. For internal ones, it depends. + +## 5. Error handling + +- Exceptions instead of return codes. A function doesn't return `None` or `-1` when something goes wrong, it raises an exception. +- I catch specific exceptions, never a bare `except:` or `except Exception: pass`. +- My own exceptions for my domain: `ProxyBannedError`, `CaptchaError`. They tell you what happened without reading the stack trace. +- I don't return `None` where a collection is expected. An empty list is better than a `None` check at every call site. +- I handle an error where I know what to do with it. If I don't know, I let it go up. + +## 6. Classes and modules + +- **Small classes with a single responsibility.** A class should have one reason to change. If a class talks to the network, parses HTML and writes to the database, that's three classes. +- **High cohesion.** A class's methods work with its fields. If a method doesn't touch `self`, it most likely doesn't belong in this class. +- **Dependencies from outside.** A class receives the browser, session or database client in its constructor instead of creating them itself. That makes it easy to test and swap. +- **Law of Demeter.** I talk only to immediate neighbors. `order.customer.address.city` is a sign that a method is needed. +- **Boundaries with third-party code.** I wrap third-party libraries in my own thin layer. If the library has to be replaced, the change happens in one place. + +## 7. Tests + +- Tests are code too, and the same cleanliness requirements apply. +- A test checks one thing, and its name shows it: `test_returns_empty_list_when_page_has_no_items`. +- Every test has the same structure: arrange, act, assert. +- Tests are fast, independent of each other and give the same result on every run. No trips to the real internet in unit tests. +- I reproduce a bug with a test first, then fix it. + +## 8. The Boy Scout rule + +I leave code cleaner than I found it. There's no need to rewrite everything at once. It's enough to rename one unclear variable or extract one piece into a function every time I touch a file. + +## 9. Git + +- One commit, one logical change. +- Commit messages in the conventional commits format: `feat:`, `fix:`, `refactor:`, `docs:`, `chore:`. +- Code that doesn't pass the linter and tests doesn't get into the main branch. +- Secrets, keys and `.env` never get into the repository. diff --git a/content/cv/Alexander_Sokolov_CV_EN.pdf b/content/cv/Alexander_Sokolov_CV_EN.pdf index da046afbb1376077defd2c8e863f1acc36e3db11..258def22590176ef00b4c2bf006d3c4cf626e83b 100644 GIT binary patch literal 5080 zcmcIoc{tST+b^;-2wAcuLkJVIk0DET*{ZQKmSF~?F{7EW78Qr=LbfPnUy|%(?^F(n zEZHKmWGQQoQkHk-{Lb&Z{W|aa{;unNulM=q`+Yvoz2En9UC({XUeGg8f+`~bvQKNC z)&gXKHdG>=P9Ota1A%6~j6)z)8HQGd0RT`S1W0l90;sElEdzZBU;_%74z}=hr8757 z1Pa!{(FlLQMg)={flhS8f%VC56g-ja0k$EMwaGN%Uwd<$H$w(gQxiZSXw$ zB^?S0|L?TGe$WE{Ny`PC2LY_h@P$C8(}1dsQw-RQfG6T~C;<)-#ullHREDV_Q9v{T zs*Fafq8-5(a8w30P~{I|41q@RrMeMljKDCRXMSKnrZX6Ze?Vr~FknwcK!9lGuO?%G zF%&wEP5?r{mQ);>!N`EL$z%$h=HU2)sR#!Bj}iY@Q=v@b{vT}pZw&mwF62*k>Eh@( z62*fN77v0M1y2B5(Fo=gGT{$^_Kz_Az>fM)c5Pp}Cxt4nO(F!~$an%3XhFF|AyNDk zz`9fdj!vYI^%!xI*HeQ*U??aAq5^|JR3Ru3MCB9&a_Ubih5)R*FPY{;a3i`C33#l6 z0@#EQ=ue^IY4QqShDHCgkPB1_-q(#lmB$c#C{(&B&K1b4dsCt-6-NyODgm+b6dwZl zA6aG!XB52`!HxcRt(HVOiST!CfV$QS%uM_E5b$6F9EnEwAsWmg`Z>uoMnU>A3JeA| zA>wHc%#y_bnbm;#u{@aN^dsm0?Lz)w67v%LDcF=i_Mm$LVJJ8ZY(ON@2~@BFiIEaL z0^?dU-JsDKiSh;nWYyTcrOu#^?xs8nw(B);*IQaK&WE)S)NMMfQ4T4i9Ff1s#A@rX z-bzg=pBy-#@G{=)jKC^3`siVnxE0O9^SaIo1@d$g@MiVEvHL?C!o6 z{v>Q2{h&x|iFevPBYhez_Dgus&OsB$E*(*m`AjMipq4s}$rKXaYx0-x2if5z)P(cz zpUkZ>H6cXzmfM#to~j#?(cKZkj_J*v(odRO^o#n;)#a3sM27oID=P2oGDrRX3B3hrk3tN+C*W8n@u0QT+ zQCqI`9xJnIkfFt$pOF*)aA@^Ie}S=Ql8Q(^K2cgylhf*!!?}?AduyFOXYEJAWzwXL zZeN}*c>7HDY_!Bc{w})SL@<9WTdK@GGsQw9hEUp;d1w*xdSKY zUN<^S)N2+oDI-+E!4oOv)!jIFab*||bVZn0AN0Cw}zt|?`wn%mO*g}Et6_$y^Q@WDYi6|*rDrxGHkkyIv{K3+&S|UgCYz1*<<*wZ| zkyqEa1t#o}Esk3u#wbxs<%;LBp`q103Rk`9DJJu*vakxpbAUobM57M9 zrt$ETn|+vaj-4@MC0iv4SjEfxf|X(D|4+ zfe*q<+(b~i8cxW-XSHJ#nwJTD{KKgnW4q znQ(1se?4=5GUwr8ix)tan`7e?A-~t1bAjN|)=v@;{T){ElKy;?vfD0i%0QMCI>ar- zrQw&lK{UVEGpC1aEsuxY)cx@|YOgN0Jx~;i2 z++{VsHpQQtBOv1f@s#4~@c@g5zdKvR<&3S!{FM#5bK^}*-P%MexZE8zL&rvz*cR@rAoV zlZ#o6tq$!Nt)_@&0x$PCi&7+BEJjo60oPN7eibj1Bu6&`-| z+k98Z-tn8JmEy>+&GLBR5l@GHKl|K%5sAJBruuAJQk2nARe>~N@eh!hhTd@X_Z9-p zpks$A1m4Kg=tIk5ih%072AmRzv?Ge%wd;3{^~&cD9I~Usn|jEdJX;MD;4R+jVNcX7-PpLPm%%;L>><}BT1`0 zFG@o0ZCSSCT5e}^k-~L@W#>xl5N%wy6f)x{FV;+76g{b2Z7Z@aQbmTle&wtR(dGXt zKa5BV_0^nakGRQM|31Mw=Qv00;-O3njP=i&s;-(SNzz1qE*1QT(t;V*Pp9Vmh>)$7~|Gj%RNESRq&nj{Pg@o z)9$;G`nS8vB+cLSEgvQ+$l369dQJsb4SR*ChhX?ohE54@TQvmF=v=ks0X#7+v-OU= z#7e)%R-XYlvX-ZKHht*EoX#M`r>vRW&vmFAD}5B?s0|N~wq4hLW}0(uj^>kLW3PVI z)>hJI&?7ucxJY5R@yaW=Rft1^kvUw?HbXfNu=-URLCiTP!6yB_|={P%XqcZ zI|ViTcDXK@C+}Yv(J1|Oac_LC_Yp-#&?edKkU0OhC65NastiS~9ZdG1-sTCD!A+h4 zT0XJ^c2mj8WSyTDA>Frx*rbB>XAR|?wF}_W2kx6Ce$(UUy738gDR~ zWyg4p^9un3FRf+uxIMnIvFe^V|3xmryF>>JF1cYh!J6&j_x*ZI_UKyv;INeaLv&KT*75!#n|->9#$NDhq4;p~(o&|)&UeZ3 z9j&0PcrddW{PRiq&$jT7KIdPbqi9vszj=)|r{XA?YJw5_UE3oiw_T0`qY6MZ=bp?d zPxR}Yax8Y{zTBoXGKlKjSoH^*hgu_hR5G7es)$7e0I93W|9dC}qiaITfn z&`aZe=no&Zj?63CU-;Gev`9r(@vHK|5qQu_XRMo-ybP?@&b409@F6b8vq8H)i#!xv zm{9@LV`(pa%A&~K8k^tV&T`cLj!Z5e#kj;5qo#X{x> zOOj&6huR$EH$!i@f_jF^AbD3{-FcCtuI=%qB>H7syG)k9qor(9#CZ$6$zTBj_B_jd zS#}nEN}&63H0aKrPW*S{OWlA1Lzm@8ws%vbjA)PG>i)|^p^-uscZO;VKX-ga$!T$J znO<`Te*Rvrk_^cT*`*@GN-rXIQE_{hJ_~3qjP^LM3rHFoCN3fQfIWUZ&n5x8Qnq_l zgm+HYQZkOaXBBr1u{#>(6urbgx_LIJht03hDYr|wK7E+$bXz4XZdSO-(scMF4`v5% zYG(2A$$o>;rJCdQG7j333DTocr`3e4n^&fiBw!1#vx2Xkdf<5a!B&m`q{FvIoE0y% ztk(HrZ#P8bTV6@?N==M=mo8xK++}9{oX5|sWbU)9{qmVM{F+mxIeUx#&GKj|?gJB2 z0_qP`@34&-_uRoAH{B{LAL{T_kY;N!v8{Ugg45ys-FN#1e$^i$5x=lGRA5ut1lV8a ziYncr6yqyS#?+OcH8oi_d^}rXYw>&x-K;rE+q26&|AIHSl(Vs^tVV;L*zQd9?p>8I z49Z>5e$6+z60H;I-3i2uJkGadKXIshIWg0^YD>M67vHG47n-{(wTbd6KYYb1Rn>#5 zK3~r5_b>W6nXY1;EJ~p`0okmmLz7snOW|!*-6rljd7=kfX`3XZn|IUj~$6cVKiSJg&B^pLuG1_o$dm_7uK(o~1( zqanK5NT@1A2L@9`>8og~ARuT19bFx$uC_J|rOBKJ(C9cS{g3z}RG=_`tgQZd1HgX( DcxsQ) literal 6421 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 D^*cPQ diff --git a/content/cv/Alexander_Sokolov_CV_RU.pdf b/content/cv/Alexander_Sokolov_CV_RU.pdf index 53cb3d79df73919a502036c160e5d380d7405943..28338fbde7928573bb957bbb5c88b5240375b160 100644 GIT binary patch delta 19711 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 delta 20237 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 Ct0=($ diff --git a/content/cv/index.en.md b/content/cv/index.en.md new file mode 100644 index 0000000..cdf191c --- /dev/null +++ b/content/cv/index.en.md @@ -0,0 +1,94 @@ +--- +title: "CV: Senior Python Developer & Web Scraping" +date: 2026-10-08T19:18:00+04:00 +--- + +{{< button href="/cv/Alexander_Sokolov_CV_EN.pdf" target="_blank" >}}Download PDF{{< /button >}} +{{< button href="/cv/Alexander_Sokolov_CV_RU.pdf" target="_blank" >}}PDF на русском{{< /button >}} + +## Alexander Sokolov + +**Senior Python Developer (Backend, Data Collection)** + +33 years old. Full time, remote. Not open to relocation or business travel. + +- Email: [xalex.sokolov@yahoo.com](mailto:xalex.sokolov@yahoo.com) +- Telegram: [@xs0k0lx](https://t.me/xs0k0lx) (preferred) + +## About me + +Python developer with over 8 years of commercial experience. Main specialization: large scale data collection and anti-bot bypass. I also have backend experience with enterprise systems on Django. + +I work with TDD. I can take a task end to end: design the architecture, implement it, deploy it and support it. I try to solve not only my own ticket but also the team's problems when I see they can be removed, like I did with environment deployment at Tsifra. + +How I can help: + +- data collection systems that handle large volumes and adapt easily to changes on the sources +- bypassing Cloudflare, captchas and browser fingerprint detection, browser automation +- backend and integrations on Django and Flask, queues, Redis, PostgreSQL +- development infrastructure: deployment, environments, CI/CD + +Besides Python I build embedded devices in C++, so I have a good understanding of security and how systems work at a low level. + +## Work experience + +### ProtoKey, own project + +**Developer** · 2025 - present + +Hardware password manager. Took the project from idea to preorders on Planeta.ru (Russian crowdfunding platform). + +- Developed C++ firmware for ESP32-S3: 3.5" touchscreen, password input over USB HID (the device works as a keyboard), web interface over Wi-Fi. +- Implemented data protection: AES-256 encryption, Secure Boot and flash encryption, fully autonomous operation without any cloud. +- Learned circuit design, soldering and hardware debugging on my own. + +### Tsifra + +**Python Developer** · February 2022 - August 2024 + +Core team of an international enterprise system for monitoring mining equipment. The Django web panel receives sensor readings, manages their state and visualizes data, the system generates analytical reports for mines. + +- Moved the KVS storage from Django ORM to Redis. Wrote a custom database manager that redirects ORM queries to Redis through configuration, with no changes to application code. +- On my own initiative built a project deployment tool. Because of the many layout options, the local environment used to be assembled slowly and by hand. After rollout the project deploys with one command on a clean Linux, and switching between production environment variants works the same way. Frontend developers became able to keep their test backends up to date on their own. +- New features, bug fixes, refactoring, test coverage, code review. + +### KRIPTON + +**Python Developer** · June 2019 - January 2022 + +BreachReport project, a forum monitoring startup. Responsible for the data collection system, from design to operation. + +- Designed and built a system collecting data from 100+ forums as a set of microservices. +- Created a Scrapy based framework: a universal forum crawling algorithm, plugins for user behavior emulation and captcha bypass, a convenient API for developers. Adding a new forum came down to configuring ready made tools, which noticeably sped up scaling. +- Developed stateless microservices for data processing, media downloading and browser management on Flask and Django with Dramatiq and RabbitMQ queues. Load up to 10 million messages per day. +- Compared an async API against queues and chose queues as the more efficient solution for this load. +- Took part in setting up and supporting servers and CI/CD. + +### Freelance + +**Python Developer** · January 2016 - February 2019 + +Turnkey development: discussing the task with the client, writing the spec, estimating time and cost, implementation and delivery. + +- Scrapers and bots for websites, Telegram bots, websites, system utilities for Linux. + +## Skills + +- **Backend:** Python, Django, Flask, PostgreSQL, Redis, RabbitMQ, Dramatiq, Celery +- **Data collection:** Scrapy, Playwright, Selenium, anti-bot and captcha bypass +- **Infrastructure:** Linux (Debian, Ubuntu), Docker, Git, CI/CD +- **Practices:** TDD, microservice architecture, code review +- **Embedded:** C++, ESP32, USB HID, cryptography +- **Languages:** Russian (native), English (B2, conversational) + +## Professional activity + +- [ru.stackoverflow](https://ru.stackoverflow.com/users/362019/alex): 140+ answers, 2700+ reputation, questions on Python, Django and scraping +- [GitHub](https://github.com/alexsok-bit): tools for Selenium and hCaptcha bypass +- [Medium](https://alexandrsokolov-41020.medium.com/): 9 technical articles on Selenium, captcha bypass, caching in Python and Linux +- Twitch: development streams, from Python backend to C++ firmware + +## Education + +**Samara State Transport University** +Applied Mathematics and Computer Science, incomplete higher education diff --git a/content/cv/index.md b/content/cv/index.md index 2b635a0..0bf0d7a 100644 --- a/content/cv/index.md +++ b/content/cv/index.md @@ -2,6 +2,93 @@ title: "Резюме: Senior Python Developer & Web Scraping" date: 2026-10-08T19:18:00+04:00 --- -[CV EN](Alexander_Sokolov_CV_EN.pdf) -[Резюме РУ](Alexander_Sokolov_CV_RU.pdf) +{{< button href="/cv/Alexander_Sokolov_CV_RU.pdf" target="_blank" >}}Скачать PDF{{< /button >}} +{{< button href="/cv/Alexander_Sokolov_CV_EN.pdf" target="_blank" >}}PDF in English{{< /button >}} + +## Соколов Александр Александрович + +**Senior Python-разработчик (бэкенд, сбор данных)** + +33 года, Самара. Полная занятость, удаленно. Не готов к переезду и командировкам. + +- Email: [xalex.sokolov@yahoo.com](mailto:xalex.sokolov@yahoo.com) +- Telegram: [@xs0k0lx](https://t.me/xs0k0lx) (предпочтительный способ связи) + +## Обо мне + +Python-разработчик с опытом коммерческой разработки более 8 лет. Основная специализация: сбор данных в больших объемах и обход антибот-защит. Также имею опыт бэкенд-разработки enterprise-систем на Django. + +Работаю по TDD. Могу взять задачу целиком: спроектировать архитектуру, реализовать, развернуть и сопровождать. Стараюсь решать не только свой тикет, но и проблемы команды, если вижу, что их можно убрать, как было с развертыванием окружений в Цифре. + +Чем могу быть полезен: + +- системы сбора данных, которые выдерживают большой объем и легко адаптируются к изменениям на источниках +- обход Cloudflare, капч и детекта по отпечатку браузера, автоматизация браузеров +- бэкенд и интеграции на Django и Flask, очереди, Redis, PostgreSQL +- инфраструктура для разработки: развертывание, окружения, CI/CD + +Помимо Python разрабатываю встраиваемые устройства на C++, поэтому хорошо понимаю вопросы безопасности и работу систем на низком уровне. + +## Опыт работы + +### ProtoKey, собственный проект + +**Разработчик** · 2025 - настоящее время + +Аппаратный менеджер паролей. Провел проект от идеи до предзаказов на Planeta.ru. + +- Разработал прошивку на C++ для ESP32-S3: сенсорный экран 3.5", ввод паролей по USB-HID (устройство работает как клавиатура), веб-интерфейс по Wi-Fi. +- Реализовал защиту данных: шифрование AES-256, Secure Boot и шифрование флеш-памяти, полностью автономная работа без облака. +- Самостоятельно освоил схемотехнику, пайку и отладку железа. + +### Цифра + +**Python-разработчик** · Февраль 2022 - Август 2024 + +Команда core международной enterprise-системы мониторинга карьерной техники. Веб-панель на Django принимает показания датчиков, управляет их состоянием и визуализирует данные, система формирует аналитические отчеты для карьеров. + +- Перевел KVS-хранилище с Django ORM на Redis. Написал собственный менеджер базы данных, который перенаправляет запросы ORM в Redis через конфигурацию, без изменений в коде приложения. +- По собственной инициативе создал инструмент развертывания проекта. Из-за большого числа вариантов компоновки локальное окружение раньше собиралось долго и вручную. После внедрения проект разворачивается одной командой на чистом Linux, а между вариантами продакшен-окружений можно переключаться так же. Фронтенд-разработчики получили возможность самостоятельно поддерживать тестовые бэкенды в актуальном состоянии. +- Разработка нового функционала, исправление ошибок, рефакторинг, покрытие тестами, код-ревью. + +### КРИПТОН + +**Python-разработчик** · Июнь 2019 - Январь 2022 + +Проект BreachReport, стартап по мониторингу форумов. Отвечал за систему сбора данных: от проектирования до эксплуатации. + +- Спроектировал и разработал систему сбора данных со 100+ форумов в виде набора микросервисов. +- Создал фреймворк на базе Scrapy: универсальный алгоритм обхода форумов, плагины для имитации поведения пользователя и обхода капчи, удобный API для разработчиков. Подключение нового форума свелось к настройке готовых инструментов, что заметно ускорило масштабирование. +- Разработал stateless-микросервисы для обработки данных, загрузки медиаконтента и управления браузерами на Flask и Django с очередями Dramatiq и RabbitMQ. Нагрузка до 10 млн сообщений в сутки. +- Провел сравнение асинхронного API и очередей, по результатам выбрал очереди как более эффективное решение для этой нагрузки. +- Участвовал в настройке и поддержке серверов и CI/CD. + +### Фриланс + +**Python-разработчик** · Январь 2016 - Февраль 2019 + +Разработка под ключ: обсуждение задачи с заказчиком, подготовка ТЗ, оценка сроков и стоимости, реализация и сдача. + +- Парсеры и боты для сайтов, Telegram-боты, веб-сайты, системные утилиты для Linux. + +## Навыки + +- **Backend:** Python, Django, Flask, PostgreSQL, Redis, RabbitMQ, Dramatiq, Celery +- **Сбор данных:** Scrapy, Playwright, Selenium, обход антибот-защит и капч +- **Инфраструктура:** Linux (Debian, Ubuntu), Docker, Git, CI/CD +- **Практики:** TDD, микросервисная архитектура, код-ревью +- **Embedded:** C++, ESP32, USB-HID, криптография +- **Языки:** русский (родной), английский (B2) + +## Профессиональная активность + +- [ru.stackoverflow](https://ru.stackoverflow.com/users/362019/alex): 140+ ответов, репутация 2700+, вопросы по Python, Django и парсингу +- [GitHub](https://github.com/alexsok-bit): инструменты для Selenium и обхода hCaptcha +- [Medium](https://alexandrsokolov-41020.medium.com/): 9 технических статей о Selenium, обходе капчи, кешировании в Python и Linux +- Twitch: стримы разработки, от бэкенда на Python до прошивок на C++ + +## Образование + +**Самарский государственный университет путей сообщения** +Прикладная математика и информатика, неоконченное высшее diff --git a/i18n/en.yaml b/i18n/en.yaml new file mode 100644 index 0000000..48427cd --- /dev/null +++ b/i18n/en.yaml @@ -0,0 +1 @@ +moreAboutMe: "More about me →" diff --git a/i18n/ru.yaml b/i18n/ru.yaml new file mode 100644 index 0000000..a20fe8c --- /dev/null +++ b/i18n/ru.yaml @@ -0,0 +1 @@ +moreAboutMe: "Подробнее обо мне →" diff --git a/layouts/partials/home/custom.html b/layouts/partials/home/custom.html index c716922..700e215 100644 --- a/layouts/partials/home/custom.html +++ b/layouts/partials/home/custom.html @@ -32,6 +32,6 @@ {{ end }} - {{ with $p.more }}Подробнее обо мне →{{ end }} + {{ with $p.more }}{{ i18n "moreAboutMe" }}{{ end }} \ No newline at end of file diff --git a/themes/blowfish b/themes/blowfish index a062452..ebe683e 160000 --- a/themes/blowfish +++ b/themes/blowfish @@ -1 +1 @@ -Subproject commit a062452786c5ea45b61bb196a628c46f50d2e073 +Subproject commit ebe683e9c2668972b71bc011e61e0004ff8e7689