<feed xmlns='http://www.w3.org/2005/Atom'>
<title>skunky-art.git/app/config.go, branch main</title>
<subtitle>alternative deviantart frontend, no javascript. go.
</subtitle>
<id>http://git.krz.sh/krz/skunky-art.git/atom?h=main</id>
<link rel='self' href='http://git.krz.sh/krz/skunky-art.git/atom?h=main'/>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/'/>
<updated>2026-08-02T13:30:01+00:00</updated>
<entry>
<title>update org name</title>
<updated>2026-08-02T13:30:01+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-08-02T06:51:22+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/commit/?id=4178e3893f310425e76f6d4515af9e7d0ea36a0a'/>
<id>urn:sha1:4178e3893f310425e76f6d4515af9e7d0ea36a0a</id>
<content type='text'>
</content>
</entry>
<entry>
<title>fix: reject forged subdomains in the media proxy (#9)</title>
<updated>2026-07-15T16:41:18+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-15T16:41:18+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/commit/?id=184ce2c6dcdd047d6888fd243b9189c797f58d38'/>
<id>urn:sha1:184ce2c6dcdd047d6888fd243b9189c797f58d38</id>
<content type='text'>
* fix: make memcache concurrency-safe and stamp the version at link time

memcache was a crash waiting for traffic. Readers touched tempFS without
holding mx, while a per-entry goroutine deleted from it under the lock: a
concurrent map read and map write, which the runtime treats as a fatal error
that recover cannot catch. The option ships in config.example.json and was the
one cache key SETUP.md never documented, so it read like a free win to enable.

Put every map and field access behind the mutex, and age the whole map from one
janitor instead of a goroutine per cached file, each of which looped forever
holding its entry alive. mx is now a plain Mutex: every operation here mutates
something, and the old code took an RLock to write. Document the option, and
cover it with tests that run the readers, writers and janitor concurrently.

Split the disk/origin fetch out of DownloadAndSendMedia while there, so the
error path returns instead of falling through to write an empty body after the
error page.

Release.Version was hardcoded to 1.3.2, so images tagged v1.3.6 reported 1.3.2
from --help and /api/instance, and --help linked to the wrong release. Take it
from a main.version string the release workflow links in from the git tag.

* fix: reject forged subdomains in the media proxy

DownloadAndSendMedia built its upstream URL by concatenation, pasting the
subdomain segment of the request path straight into the host position. That
segment reaches the handler already percent-decoded, so it can carry "@", "#",
"?" and "/" — the characters that end a host. A request for

    /media/file/x@127.0.0.1:8080%2F/f/x.jpg

built a URL whose host parsed as 127.0.0.1:8080, with images-wixmp-x demoted to
userinfo, letting any caller aim the instance's fetcher at any address it could
reach, including services behind the firewall.

Validate the label against ^[a-zA-Z0-9-]+$ and refuse anything else with a 400.
Rejecting rather than escaping is what closes this: the label is the host, and
url.URL passes a host through verbatim, so building the URL structurally is not
sufficient on its own. DeviantArt's own media URLs use a hex-and-dash label, and
ParseMedia already splits on the first dot, so a legitimate label cannot contain
one.

Build the URL from url.URL fields as well, which escapes the path, and encode
the token argument, which reached the request unescaped.

Reported by CodeQL as go/request-forgery (CWE-918).</content>
</entry>
<entry>
<title>fix: refuse to start when the cache directory is not writable</title>
<updated>2026-07-15T16:13:25+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-15T16:13:25+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/commit/?id=8e8c03891a17798067a8da0a034725bf44ece8d0'/>
<id>urn:sha1:8e8c03891a17798067a8da0a034725bf44ece8d0</id>
<content type='text'>
An unwritable cache directory degraded silently: media still served, because
the download succeeds before the cache write is attempted, so the only symptom
was one "permission denied" line per request and a cache that never filled.
Every request re-fetched from the CDN.

Probe the directory at startup and exit with the uid and the chown that fixes
it. The container image runs as uid 10000, which is the usual cause with a
bind-mounted cache, so say so in the message and in both compose examples.
</content>
</entry>
<entry>
<title>fix: document exported API, fix naming, and harden Download</title>
<updated>2026-07-15T07:25:20+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-15T07:25:20+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/commit/?id=d6789522d3dd076f7c98dafa3e7d42df09252093'/>
<id>urn:sha1:d6789522d3dd076f7c98dafa3e7d42df09252093</id>
<content type='text'>
The remaining golangci-lint findings. These land together because the
Url -&gt; URL rename spans util.go, parsers.go and wrapper.go, and splitting
it would leave an intermediate commit that does not compile.

Download() carried the most serious bug here: try() only prints an error,
it does not return, so a failed request fell through to
resp.Body.Close() on a nil resp and panicked. Every failure path now
returns the zero Downloaded, and callers check Status. ReturnHTTPError
guards against the resulting status 0, which would otherwise panic
WriteHeader. Requests carry a context with a timeout (noctx), and a
download-proxy now routes through ProxiedTransport so it keeps the DA
throttle and timeouts.

Also:
- doc comments on all 48 exported symbols (revive's exported rule, with
  checkPrivateReceivers, since most of app is exported methods on the
  unexported skunkyart type), plus package docs in new doc.go files so
  both the embed and non-embed builds are covered.
- ST1003 naming: UrlBuilder -&gt; URLBuilder, id_search -&gt; idSearch,
  cache_config -&gt; cacheConfig, TXT_RAW -&gt; TxtRaw, mediaUrl -&gt; mediaURL.
- explicit json tags on structs that are unmarshaled (musttag); the
  hyphenated keys already had tags, the rest relied on case-insensitive
  fallback. Behaviour is unchanged.
- modernization: range-over-int, WaitGroup.Go, stale +build lines,
  interface{} -&gt; any, strings.Builder over string concatenation in a loop.
</content>
</entry>
<entry>
<title>fix(cache): correct max-size megabyte conversion</title>
<updated>2026-07-15T07:23:59+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-15T07:23:59+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/commit/?id=40d405f318a109aad5e7de7ad71fedcc04bb0e86'/>
<id>urn:sha1:40d405f318a109aad5e7de7ad71fedcc04bb0e86</id>
<content type='text'>
max-size is documented in SETUP.md as megabytes, but the conversion was
`*= 1024 ^ 2`. In Go `^` is XOR, not exponentiation, so this multiplied
by 1026 rather than 1048576 -- a configured max-size of 200 produced a
~205 KB cap instead of 200 MB, wiping the cache almost immediately.

This changes runtime behaviour: the cache will now grow to the size the
config actually asks for.
</content>
</entry>
<entry>
<title>deps: migrate to github.com/zerolabsco/devianter v0.3.1</title>
<updated>2026-07-15T01:22:04+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-15T01:22:04+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/commit/?id=0fdc45a668d95d603a9013f90392f6bc966c72f9'/>
<id>urn:sha1:0fdc45a668d95d603a9013f90392f6bc966c72f9</id>
<content type='text'>
Repoint the import path from git.macaw.me/skunky/devianter, replacing a
placeholder v0.0.0 require that had no go.sum entry and did not resolve.

v0.3.1 reorders the last two return values of PerformSearch, Group.Get
and Group.Gallery from (..., error, Error) to (..., Error, error);
adapt the call sites. The two types differ, so the compiler enforces
this.
</content>
</entry>
<entry>
<title>Add da.opnxng.com + lost-skunk.cc is now in Finland</title>
<updated>2025-04-07T10:53:47+00:00</updated>
<author>
<name>lost+skunk</name>
<email>me@lost-skunk.cc</email>
</author>
<published>2025-04-07T10:53:47+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/commit/?id=210c69e48c2af4d1fdb53c0654ed33a21688696c'/>
<id>urn:sha1:210c69e48c2af4d1fdb53c0654ed33a21688696c</id>
<content type='text'>
</content>
</entry>
<entry>
<title>Переключатель кеша в озу, небольшие улучшения ксс, фикс нсфв, фикс максимального размера кеша</title>
<updated>2025-02-22T12:30:29+00:00</updated>
<author>
<name>lost+skunk</name>
<email>skunky@macaw.me</email>
</author>
<published>2025-02-22T12:30:29+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/commit/?id=048bb470ab8dc3adce83d2e615f3ab369efa2199'/>
<id>urn:sha1:048bb470ab8dc3adce83d2e615f3ab369efa2199</id>
<content type='text'>
</content>
</entry>
<entry>
<title>v1.3.2</title>
<updated>2024-09-23T06:38:32+00:00</updated>
<author>
<name>lost+skunk</name>
<email>skunky@ebloid.ru</email>
</author>
<published>2024-09-23T06:38:32+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/commit/?id=191984b31ef228c4077904a2831a2af593352670'/>
<id>urn:sha1:191984b31ef228c4077904a2831a2af593352670</id>
<content type='text'>
</content>
</entry>
<entry>
<title>отображение ошибок</title>
<updated>2024-09-04T17:22:58+00:00</updated>
<author>
<name>lost+skunk</name>
<email>skunky@ebloid.ru</email>
</author>
<published>2024-09-04T17:22:58+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/skunky-art.git/commit/?id=c39399403e031a8c1ad53969770e4261d2c9e7aa'/>
<id>urn:sha1:c39399403e031a8c1ad53969770e4261d2c9e7aa</id>
<content type='text'>
</content>
</entry>
</feed>
