krz/skunky-art

Alternative privacy frontend for DeviantArt.

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

v1.3.5: 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.QueryRaw = arg("q")
 90		skunky.Query = url.QueryEscape(skunky.QueryRaw)
 91		skunky.Page = p
 92
 93		if t := arg("type"); len(t) > 0 {
 94			skunky.Type = rune(t[0])
 95		}
 96
 97		if arg("atom") == "true" {
 98			skunky.Atom = true
 99		}
100
101		if CFG.Proxy {
102			w.Header().Add("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'")
103		} else {
104			w.Header().Add("Content-Security-Policy", "default-src 'self'; img-src 'self' *.wixmp.com; script-src 'none'; style-src 'self' 'unsafe-inline'")
105		}
106
107		w.Header().Add("X-Frame-Options", "DENY")
108
109		switch skunky.Endpoint {
110		// main
111		case "":
112			skunky.ExecuteTemplate("index.htm", "html", &CFG.URI)
113		case "about":
114			skunky.Templates.About = About
115			skunky.ExecuteTemplate("about.htm", "html", &skunky)
116		case "post":
117			skunky.Deviation(path[2], path[3])
118		case "search":
119			skunky.Search()
120		case "dd":
121			skunky.DD()
122		case "group_user":
123			skunky.GRUser()
124
125		// media
126		case "media":
127			switch path[2] {
128			case "file":
129				if a := arg("filename"); a != "" {
130					skunky.SetFilename(a)
131				}
132				skunky.DownloadAndSendMedia(path[3], next(path, 4))
133			case "emojitar":
134				skunky.Emojitar(path[3])
135			}
136		case "stylesheet":
137			w.Header().Add("Content-Type", "text/css")
138			_, _ = w.Write(open("css/skunky.css"))
139		case "favicon.ico":
140			_, _ = w.Write(open("images/logo.png"))
141
142		// API
143		case "api":
144			w.Header().Add("Content-Type", "application/json")
145			switch path[2] {
146			case "instance":
147				skunky.API.Info()
148			case "random":
149				skunky.API.Random()
150			default:
151				skunky.API.Error("Not Found", 404)
152			}
153
154		// 404
155		default:
156			skunky.ReturnHTTPError(404)
157		}
158	}
159
160	http.HandleFunc("/", handle)
161	println("SkunkyArt is listening on", CFG.Listen)
162
163	// Explicit timeouts: the bare http.ListenAndServe has none, so a slow client
164	// can hold a connection (and its handler) open indefinitely. WriteTimeout is
165	// generous because media proxying streams large files through a handler.
166	srv := &http.Server{
167		Addr:              CFG.Listen,
168		ReadHeaderTimeout: 10 * time.Second,
169		ReadTimeout:       30 * time.Second,
170		WriteTimeout:      120 * time.Second,
171		IdleTimeout:       120 * time.Second,
172	}
173	tryWithExitStatus(srv.ListenAndServe(), 1)
174}