krz/skunky-art

Alternative privacy frontend for DeviantArt.

clone: git clone https://gitbay.org/krz/skunky-art.git

main: app/router.go · raw

  1package app
  2
  3import (
  4	"io"
  5	"net/http"
  6	url "net/url"
  7	"skunkyart/static"
  8	"strconv"
  9	"strings"
 10	"time"
 11)
 12
 13// Router registers the single catch-all handler that dispatches every path, then
 14// serves until the process exits. It does not return on success.
 15func Router() {
 16	parsepath := func(path string) map[int]string {
 17		if l := len(CFG.URI); len(path) > l {
 18			path = path[l-1:]
 19		} else {
 20			path = "/"
 21		}
 22
 23		parsedpath := make(map[int]string)
 24		for x := 0; true; x++ {
 25			slash := strings.Index(path, "/") + 1
 26			content := path[:slash]
 27			path = path[slash:]
 28			if slash == 0 {
 29				parsedpath[x] = path
 30				break
 31			}
 32			parsedpath[x] = content[:slash-1]
 33		}
 34		return parsedpath
 35	}
 36
 37	next := func(path map[int]string, from int) string {
 38		var out strings.Builder
 39		for x, l := from, len(path)-1; x <= l; x++ {
 40			out.WriteString(path[x])
 41			if x != l {
 42				out.WriteString("/")
 43			}
 44		}
 45		return out.String()
 46	}
 47
 48	open := func(name string) []byte {
 49		file, err := static.Templates.Open(name)
 50		if err != nil {
 51			try(err)
 52			return nil
 53		}
 54		defer func() { try(file.Close()) }()
 55
 56		fileReaded, err := io.ReadAll(file)
 57		if err != nil {
 58			try(err)
 59			return nil
 60		}
 61		return fileReaded
 62	}
 63
 64	// the function that drives everything
 65	handle := func(w http.ResponseWriter, r *http.Request) {
 66		path := parsepath(r.URL.Path)
 67
 68		// Per-request, not a package global: requests arrive concurrently on
 69		// different hosts and ports (bots hitting a proxy's alternate ports, for
 70		// one), and a shared global lets one request's host leak into another's
 71		// rendered URLs. Those URLs then point at a different origin, which this
 72		// handler's own default-src 'self' CSP blocks.
 73		host := "http://" + r.Host
 74		if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" {
 75			host = "https://" + r.Host
 76		}
 77
 78		var skunky = skunkyart{Version: Release.Version, Host: host}
 79		skunky._pth = r.URL.Path
 80
 81		skunky.Args = r.URL.Query()
 82		arg := skunky.Args.Get
 83		p, _ := strconv.Atoi(arg("p"))
 84
 85		skunky.Endpoint = path[1]
 86		skunky.API.main = &skunky
 87		skunky.Writer = w
 88		skunky.BasePath = CFG.URI
 89		skunky.Lang = ResolveLang(r.Header.Get("Accept-Language"))
 90		skunky.QueryRaw = arg("q")
 91		skunky.Query = url.QueryEscape(skunky.QueryRaw)
 92		skunky.Page = p
 93
 94		if t := arg("type"); len(t) > 0 {
 95			skunky.Type = rune(t[0])
 96		}
 97
 98		if arg("atom") == "true" {
 99			skunky.Atom = true
100		}
101
102		if CFG.Proxy {
103			w.Header().Add("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'")
104		} else {
105			w.Header().Add("Content-Security-Policy", "default-src 'self'; img-src 'self' *.wixmp.com; script-src 'none'; style-src 'self' 'unsafe-inline'")
106		}
107
108		w.Header().Add("X-Frame-Options", "DENY")
109
110		switch skunky.Endpoint {
111		// main
112		case "":
113			skunky.ExecuteTemplate("index.htm", "html", &CFG.URI)
114		case "about":
115			skunky.Templates.About = About
116			skunky.ExecuteTemplate("about.htm", "html", &skunky)
117		case "post":
118			skunky.Deviation(path[2], path[3])
119		case "search":
120			skunky.Search()
121		case "dd":
122			skunky.DD()
123		case "group_user":
124			skunky.GRUser()
125
126		// media
127		case "media":
128			switch path[2] {
129			case "file":
130				if a := arg("filename"); a != "" {
131					skunky.SetFilename(a)
132				}
133				skunky.DownloadAndSendMedia(path[3], next(path, 4))
134			case "emojitar":
135				skunky.Emojitar(path[3])
136			}
137		case "stylesheet":
138			w.Header().Add("Content-Type", "text/css")
139			_, _ = w.Write(open("css/skunky.css"))
140			// "auto" is the stylesheet as written: dark, with a light palette
141			// behind prefers-color-scheme. Forcing a theme means re-declaring
142			// that palette unconditionally, which outranks the media query
143			// because it comes later with equal specificity.
144			if css := forcedThemeCSS(); css != "" {
145				_, _ = w.Write([]byte(css))
146			}
147		case "favicon.ico":
148			_, _ = w.Write(open("images/logo.png"))
149
150		// API
151		case "api":
152			w.Header().Add("Content-Type", "application/json")
153			switch path[2] {
154			case "instance":
155				skunky.API.Info()
156			case "random":
157				skunky.API.Random()
158			case "search":
159				skunky.API.Search()
160			case "post":
161				skunky.API.Post(path[3], path[4])
162			default:
163				skunky.API.Error("Not Found", 404)
164			}
165
166		// 404
167		default:
168			skunky.ReturnHTTPError(404)
169		}
170	}
171
172	http.HandleFunc("/", handle)
173	println("SkunkyArt is listening on", CFG.Listen)
174
175	// Explicit timeouts: the bare http.ListenAndServe has none, so a slow client
176	// can hold a connection (and its handler) open indefinitely. WriteTimeout is
177	// generous because media proxying streams large files through a handler.
178	srv := &http.Server{
179		Addr:              CFG.Listen,
180		ReadHeaderTimeout: 10 * time.Second,
181		ReadTimeout:       30 * time.Second,
182		WriteTimeout:      120 * time.Second,
183		IdleTimeout:       120 * time.Second,
184	}
185	tryWithExitStatus(srv.ListenAndServe(), 1)
186}