summaryrefslogtreecommitdiffstats
path: root/internal/hole
diff options
context:
space:
mode:
Diffstat (limited to 'internal/hole')
-rw-r--r--internal/hole/hole.go1412
1 files changed, 25 insertions, 1387 deletions
diff --git a/internal/hole/hole.go b/internal/hole/hole.go
index 3b9a54d..3749f9b 100644
--- a/internal/hole/hole.go
+++ b/internal/hole/hole.go
@@ -16,422 +16,31 @@
// License along with roseh.moe. If not, see
// <https://www.gnu.org/licenses/>.
-package main
+package hole
import (
"archive/tar"
- "bytes"
"cmp"
"compress/gzip"
- "context"
- "crypto"
- "crypto/ecdsa"
"crypto/hkdf"
- "crypto/hmac"
"crypto/rand"
- "crypto/rsa"
"crypto/sha256"
- "crypto/x509"
- "embed"
- "encoding/base64"
"encoding/binary"
- "encoding/json"
- "encoding/xml"
"errors"
- "flag"
"fmt"
- "html/template"
"io"
"io/fs"
"log"
"mime/multipart"
- "net/http"
- "net/http/cgi"
- "net/http/httputil"
- "net/url"
"os"
- "path"
- "path/filepath"
- "slices"
- "strconv"
"strings"
- "sync"
"time"
- "unicode"
- "github.com/skip2/go-qrcode"
"roseh.moe/pkg/oae2"
"roseh.moe/pkg/wordlist"
)
-var (
- selfURL = urlFlag("url", "http://localhost:42069", "base URL of the server")
- secretsFile = flag.String("secrets", "secrets.xml", "secrets file")
- configFile = flag.String("config", "config.xml", "configuration file (XML, baby!!)")
- https = flag.String("https", "", "directory containing cert.pem and key.pem, or empty string to use unencrypted http")
- httpRedirect = flag.String("http-redirect", "", "port to serve HTTP redirect")
- holeTempDir = flag.String("hole-temp-dir", "/tmp/hole", "directory to store temporary files for the wormhole")
- nezuko = flag.String("nezuko", "/mnt/nezuko", "nezuko partition")
- cgit = flag.String("cgit", "", "directory containing cgit.cgi")
- cgitConfig = flag.String("cgit-config", "", "cgit config file")
- v = flag.Bool("v", true, "verbose")
-)
-
-type urlValue struct {
- URL *url.URL
-}
-
-func (v urlValue) String() string {
- if v.URL != nil {
- return v.URL.String()
- }
- return ""
-}
-
-func (v urlValue) Set(s string) error {
- u, err := url.Parse(s)
- if err != nil {
- return err
- }
- *v.URL = *u
- return nil
-}
-
-func urlFlag(name, def, doc string) *url.URL {
- u, err := url.Parse(def)
- if err != nil {
- panic(err)
- }
- flag.Var(urlValue{u}, name, doc)
- return u
-}
-
-func sigVerifier(keyBytes []byte) (func(msg, sig []byte) bool, error) {
- key, err := x509.ParsePKIXPublicKey(keyBytes)
- if err != nil {
- return nil, err
- }
- switch key := key.(type) {
- case *ecdsa.PublicKey:
- return func(msg, sig []byte) bool {
- hash := sha256.Sum256(msg)
- return ecdsa.VerifyASN1(key, hash[:], sig)
- }, nil
- case *rsa.PublicKey:
- return func(msg, sig []byte) bool {
- hash := sha256.Sum256(msg)
- return rsa.VerifyPKCS1v15(key, crypto.SHA256, hash[:], sig) == nil
- }, nil
- default:
- return nil, fmt.Errorf("unsupported key type")
- }
-}
-
-func verifyPubKey(pubKey, msg, sig []byte) error {
- verify, err := sigVerifier(pubKey)
- if err != nil {
- return err
- }
- if !verify(msg, sig) {
- return fmt.Errorf("message failed validation")
- }
- return nil
-}
-
-var secretKey []byte
-
-func loadSecrets() error {
- fileBytes, err := os.ReadFile(*secretsFile)
- if err != nil {
- return err
- }
- var secretsConfig struct {
- SecretKey string `xml:",attr"`
- }
- if err := xml.Unmarshal(fileBytes, &secretsConfig); err != nil {
- return fmt.Errorf("%s: %s", *secretsFile, err)
- }
- if secretsConfig.SecretKey == "" {
- return fmt.Errorf("%s: missing SecretKey", *secretsFile)
- }
- if secretKey, err = base64.StdEncoding.DecodeString(secretsConfig.SecretKey); err != nil {
- return fmt.Errorf("%s: bad SecretKey (try base64)", *secretsFile)
- }
- return nil
-}
-
-type packageConfig struct {
- Name string `xml:",attr"`
- URL string `xml:",attr"`
- Source string `xml:",attr"`
- Description string `xml:",attr"`
-}
-
-type commandConfig struct {
- Name string `xml:",attr"`
- URL string `xml:",attr"`
- Source string `xml:",attr"`
- Description string `xml:",attr"`
- Multi bool `xml:",attr"`
-}
-
-type passkey struct {
- ID string `xml:",attr"`
- Key string `xml:",attr"`
-}
-
-type serviceConfiguration struct {
- PackageRedirects []packageConfig `xml:"Redirects>Package"`
- CommandRedirects []commandConfig `xml:"Redirects>Command"`
- Passkey []passkey
- passkeys map[string][]byte
-}
-
-var (
- serviceConfigMu sync.Mutex
- serviceConfig *serviceConfiguration
- serviceConfigLastModified time.Time
-)
-
-func loadConfig() (*serviceConfiguration, error) {
- stat, err := os.Stat(*configFile)
- if err != nil {
- return nil, err
- }
- serviceConfigMu.Lock()
- oldConfig := serviceConfig
- configLastModified := serviceConfigLastModified
- serviceConfigMu.Unlock()
- newModTime := stat.ModTime()
- if !newModTime.After(configLastModified) {
- return oldConfig, nil
- }
- log.Printf("Reloading config file...")
- fileBytes, err := os.ReadFile(*configFile)
- if err != nil {
- return nil, err
- }
- config := new(serviceConfiguration)
- if err := xml.Unmarshal(fileBytes, config); err != nil {
- return nil, err
- }
- names := make(map[string]bool)
- config.passkeys = make(map[string][]byte)
- namesByID := make(map[string]string)
- for _, passkey := range config.Passkey {
- if names[passkey.ID] {
- return nil, fmt.Errorf("%s: duplicate name %q", *configFile, passkey.ID)
- }
- names[passkey.ID] = true
- keyBytes, err := base64.RawURLEncoding.DecodeString(strings.Map(func(r rune) rune {
- if unicode.IsSpace(r) {
- return -1
- }
- return r
- }, passkey.Key))
- if err != nil {
- return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err)
- }
- if len(keyBytes) < 16 {
- return nil, fmt.Errorf("%s: invalid key for passkey %q: bad user ID: too short", *configFile, passkey.ID)
- }
- id, key := keyBytes[:16], keyBytes[16:]
- if _, err := sigVerifier(key); err != nil {
- return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err)
- }
- if _, ok := config.passkeys[string(id)]; ok {
- return nil, fmt.Errorf("%s: passkeys %q and %q have duplicate ID %d", *configFile, namesByID[string(id)], passkey.ID, id)
- }
- config.passkeys[string(id)] = key
- namesByID[string(id)] = passkey.ID
- }
- slices.SortFunc(config.PackageRedirects, func(x, y packageConfig) int { return strings.Compare(x.Name, y.Name) })
- slices.SortFunc(config.CommandRedirects, func(x, y commandConfig) int { return strings.Compare(x.Name, y.Name) })
- serviceConfigMu.Lock()
- serviceConfig = config
- serviceConfigLastModified = newModTime
- serviceConfigMu.Unlock()
- return config, nil
-}
-
-func makeETag(content io.Reader) (string, error) {
- h := sha256.New()
- if _, err := io.Copy(h, content); err != nil {
- return "", err
- }
- return `W/"` + base64.RawURLEncoding.EncodeToString(h.Sum(nil)[:8]) + `"`, nil
-}
-
-var (
- //go:embed templates/outline.html.template
- outlineString string
- outlineTemplate = template.Must(template.New("outline").Funcs(template.FuncMap{
- "host": func() string {
- return selfURL.Host
- },
- "xmlEscape": func(s string) string {
- buf := new(strings.Builder)
- xml.EscapeText(buf, []byte(s))
- return buf.String()
- },
- }).Parse(outlineString))
-)
-
-func outlinedTemplate(s string) (*template.Template, error) {
- outline, err := outlineTemplate.Clone()
- if err != nil {
- return nil, err
- }
- t, err := outline.New("body").Parse(s)
- if err != nil {
- return nil, err
- }
- return t.Lookup("outline"), nil
-}
-
-type staticHandler struct {
- content, eTag string
-}
-
-func (h *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("ETag", h.eTag)
- http.ServeContent(w, r, "", time.Time{}, strings.NewReader(h.content))
-}
-
-func staticTemplate(tmpl *template.Template) (*staticHandler, error) {
- buf := new(strings.Builder)
- if err := tmpl.Execute(buf, nil); err != nil {
- return nil, err
- }
- content := buf.String()
- eTag, err := makeETag(strings.NewReader(content))
- if err != nil {
- return nil, err
- }
- return &staticHandler{
- content: buf.String(),
- eTag: eTag,
- }, nil
-}
-
-var (
- //go:embed templates/404.html.template
- notFoundString string
- notFoundTemplate = template.Must(template.New("notFound").Parse(notFoundString))
-)
-
-func notFound(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusNotFound)
- type args struct {
- Path string
- }
- if err := notFoundTemplate.Execute(w, args{Path: r.URL.Path}); err != nil {
- log.Printf("Warning: notFound: %s", err)
- }
-}
-
-//go:embed templates/index.html.template
-var indexString string
-
-//go:embed templates/games/index.html.template
-var gamesIndexString string
-
-//go:embed templates/games/pong.html.template
-var pongString string
-
-//go:embed templates/games/rms.html.template
-var stallmanShooterString string
-
-//go:embed static
-var staticFiles embed.FS
-
-var staticFileHashes = buildStaticFileHashes()
-
-func buildStaticFileHashes() map[string]string {
- hashMap := make(map[string]string)
- if err := fs.WalkDir(staticFiles, ".", func(path string, d fs.DirEntry, err error) error {
- if err != nil || d.IsDir() {
- return err
- }
- f, err := staticFiles.Open(path)
- if err != nil {
- return err
- }
- defer f.Close()
- if hashMap[path], err = makeETag(f); err != nil {
- return err
- }
- return nil
- }); err != nil {
- panic(fmt.Sprintf("Build static file hash map: %s", err))
- }
- return hashMap
-}
-
-func serveStaticFile(w http.ResponseWriter, r *http.Request, path string) {
- if eTag, ok := staticFileHashes[path]; ok {
- w.Header().Set("ETag", eTag)
- }
- http.ServeFileFS(w, r, staticFiles, path)
-}
-
-func static(w http.ResponseWriter, r *http.Request) {
- serveStaticFile(w, r, path.Clean(strings.TrimPrefix(r.URL.Path, "/")))
-}
-
-func favicon(w http.ResponseWriter, r *http.Request) {
- serveStaticFile(w, r, "static/favicon.ico")
-}
-
-func sharedFileHandler(w http.ResponseWriter, r *http.Request) {
- fp, err := filepath.Localize(path.Clean(strings.TrimPrefix(r.URL.Path, "/")))
- if err != nil {
- notFound(w, r)
- return
- }
- f, err := os.OpenInRoot(filepath.Join(*nezuko, "www"), fp)
- if err != nil {
- notFound(w, r)
- return
- }
- defer f.Close()
- stat, err := f.Stat()
- if err != nil {
- log.Printf("Warning: sharedFileHandler: %s", err)
- notFound(w, r)
- return
- }
- if stat.IsDir() {
- notFound(w, r)
- return
- }
- http.ServeContent(w, r, fp, stat.ModTime(), f)
-}
-
-func qrHandler(w http.ResponseWriter, r *http.Request) {
- text, ok := verifyHMAC(r.FormValue("t"), "qr")
- if !ok {
- http.Error(w, "Bad signature", http.StatusBadRequest)
- return
- }
- qr, err := qrcode.Encode(text, qrcode.Medium, 200)
- if err != nil {
- log.Printf("Error: failed to encode QR code: %s", err)
- http.Error(w, "Failed to encode QR code", http.StatusInternalServerError)
- return
- }
- http.ServeContent(w, r, "qr.png", time.Time{}, bytes.NewReader(qr))
-}
-
-var (
- //go:embed templates/wormhole.html.template
- wormholeTemplateString string
- wormholeTemplate = template.Must(outlinedTemplate(wormholeTemplateString))
-)
-
-type wormholeTemplateArgs struct {
- Err string
-}
+var HoleTempDir string
func makeHole() string {
const nWords = 10
@@ -461,7 +70,7 @@ func writeHoleFile(filename string) (_ string, _ *holeFileWriter, err error) {
if err != nil {
return "", nil, err
}
- f, err := os.Create(fmt.Sprintf("%s/%x", *holeTempDir, primaryKey))
+ f, err := os.Create(fmt.Sprintf("%s/%x", HoleTempDir, primaryKey))
if err != nil {
return "", nil, err
}
@@ -495,26 +104,26 @@ func (r *offsetReader) Seek(offset int64, whence int) (int64, error) {
return n - int64(r.offset), err
}
-type holeFileReader struct {
+type HoleFileReader struct {
*offsetReader
- f *os.File
+ F *os.File
}
-func (r *holeFileReader) Close() error {
- return r.f.Close()
+func (r *HoleFileReader) Close() error {
+ return r.F.Close()
}
-var errNoHole = errors.New("no such hole")
+var ErrNoHole = errors.New("no such hole")
-func openHoleFile(hole string) (_ string, _ *holeFileReader, err error) {
+func Open(hole string) (_ string, _ *HoleFileReader, err error) {
primaryKey, err := hkdf.Key(sha256.New, []byte(hole), nil, "", 32)
if err != nil {
return "", nil, err
}
- f, err := os.Open(fmt.Sprintf("%s/%x", *holeTempDir, primaryKey))
+ f, err := os.Open(fmt.Sprintf("%s/%x", HoleTempDir, primaryKey))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
- return "", nil, errNoHole
+ return "", nil, ErrNoHole
}
return "", nil, err
}
@@ -533,32 +142,11 @@ func openHoleFile(hole string) (_ string, _ *holeFileReader, err error) {
if _, err := io.ReadFull(r, fileName); err != nil {
return "", nil, err
}
- return string(fileName), &holeFileReader{&offsetReader{r, 8 + int(fileNameLen)}, f}, nil
-}
-
-func readHoleFile(w http.ResponseWriter, req *http.Request, hole string) error {
- fileName, r, err := openHoleFile(hole)
- if err != nil {
- return err
- }
- defer r.Close()
- w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+fileName)
- var modTime time.Time
- if stat, err := r.f.Stat(); err == nil {
- modTime = stat.ModTime()
- }
- http.ServeContent(w, req, fileName, modTime, r)
- return nil
-}
-
-func wormhole(w http.ResponseWriter, r *http.Request) {
- if err := wormholeTemplate.Execute(w, wormholeTemplateArgs{}); err != nil {
- log.Printf("Warning: wormhole: %s", err)
- }
+ return string(fileName), &HoleFileReader{&offsetReader{r, 8 + int(fileNameLen)}, f}, nil
}
func writeHoleFileToTarFile(tarWriter *tar.Writer, hole string, now time.Time) error {
- fileName, f, err := openHoleFile(hole)
+ fileName, f, err := Open(hole)
if err != nil {
return fmt.Errorf("read back hole file: %s", err)
}
@@ -579,9 +167,9 @@ func writeHoleFileToTarFile(tarWriter *tar.Writer, hole string, now time.Time) e
return nil
}
-var errNoFile = errors.New("no file")
+var ErrNoFile = errors.New("no file")
-func uploadWormhole(reader *multipart.Reader) (string, error) {
+func Upload(reader *multipart.Reader) (string, error) {
var holes []string
for {
part, err := reader.NextPart()
@@ -591,14 +179,14 @@ func uploadWormhole(reader *multipart.Reader) (string, error) {
if part.FormName() != "file" || part.FileName() == "" {
continue
}
- hole, err := writeSingleHoleFile(part.FileName(), part)
+ hole, err := WriteSingleFile(part.FileName(), part)
if err != nil {
return "", err
}
holes = append(holes, hole)
}
if len(holes) == 0 {
- return "", errNoFile
+ return "", ErrNoFile
}
if len(holes) == 1 {
return holes[0], nil
@@ -634,46 +222,7 @@ func uploadWormhole(reader *multipart.Reader) (string, error) {
return tarFileHole, nil
}
-var (
- //go:embed templates/wormhole-success.html.template
- wormholeSuccessTemplateString string
- wormholeSuccessTemplate = template.Must(outlinedTemplate(wormholeSuccessTemplateString))
-)
-
-type wormholeSuccessTemplateArgs struct {
- URL string
- QR string
-}
-
-func wormholeUpload(w http.ResponseWriter, r *http.Request) {
- reader, err := r.MultipartReader()
- if err != nil {
- http.Error(w, fmt.Sprintf("Not a multipart/form-data request: %s", err), http.StatusBadRequest)
- return
- }
- hole, err := uploadWormhole(reader)
- if err != nil {
- if err == errNoFile {
- w.WriteHeader(http.StatusBadRequest)
- if err := wormholeTemplate.Execute(w, wormholeTemplateArgs{Err: "don't forget to pick a file"}); err != nil {
- log.Printf("Warning: wormholeTemplate: %s", err)
- }
- return
- }
- log.Printf("Error: failed to upload hole file: %s", err)
- http.Error(w, "Internal error", http.StatusInternalServerError)
- return
- }
- downloadURL := selfURL.String() + "/wormhole/" + hole
- if err := wormholeSuccessTemplate.Execute(w, wormholeSuccessTemplateArgs{
- URL: downloadURL,
- QR: "/qr.png?t=" + url.QueryEscape(signMsg(downloadURL, "qr")),
- }); err != nil {
- log.Printf("Warning: wormholeSend: %s", err)
- }
-}
-
-func writeSingleHoleFile(filename string, r io.Reader) (string, error) {
+func WriteSingleFile(filename string, r io.Reader) (string, error) {
hole, holeWriter, err := writeHoleFile(filename)
if err != nil {
return "", fmt.Errorf("write hole file: %s", err)
@@ -688,35 +237,9 @@ func writeSingleHoleFile(filename string, r io.Reader) (string, error) {
return hole, nil
}
-func wormholeAPIUpload(w http.ResponseWriter, r *http.Request) {
- hole, err := writeSingleHoleFile(r.PathValue("name"), r.Body)
- if err != nil {
- log.Printf("Error: failed to write hole file: %s", err)
- http.Error(w, "Failed to write hole file", http.StatusInternalServerError)
- return
- }
- resultURL := fmt.Sprintf("%s/wormhole/%s", selfURL, hole)
- w.Header().Set("Content-Type", "text/plain")
- w.Header().Set("Location", resultURL)
- w.WriteHeader(http.StatusCreated)
- fmt.Fprintln(w, resultURL)
-}
-
-func wormholeDownload(w http.ResponseWriter, r *http.Request) {
- if err := readHoleFile(w, r, r.PathValue("hole")); err != nil {
- if err == errNoHole {
- notFound(w, r)
- return
- }
- log.Printf("Error: failed to read hole file: %s", err)
- http.Error(w, "Failed to read hole file", http.StatusInternalServerError)
- return
- }
-}
-
-func cleanHole() error {
+func Clean() error {
log.Printf("Cleaning hole...")
- holeDir, err := os.Open(*holeTempDir)
+ holeDir, err := os.Open(HoleTempDir)
if err != nil {
return err
}
@@ -733,7 +256,7 @@ func cleanHole() error {
if !stat.ModTime().Before(deadline) {
continue
}
- if err := os.Remove(*holeTempDir + "/" + file.Name()); err != nil {
+ if err := os.Remove(HoleTempDir + "/" + file.Name()); err != nil {
log.Printf("Error: clean hole: %s", err)
continue
}
@@ -747,894 +270,9 @@ func cleanHole() error {
}
}
-func mac(msg []byte, purpose string) []byte {
- mac := hmac.New(sha256.New, secretKey)
- io.WriteString(mac, purpose)
- io.WriteString(mac, ":")
- mac.Write(msg)
- return mac.Sum(nil)
-}
-
-func signMsg(msg, purpose string) string {
- return msg + base64.RawURLEncoding.EncodeToString(mac([]byte(msg), purpose))
-}
-
-func verifyHMAC(signedMsg, purpose string) (string, bool) {
- macSize := base64.RawURLEncoding.EncodedLen(sha256.Size)
- if len(signedMsg) < macSize {
- return "", false
- }
- msg, msgMAC := signedMsg[:len(signedMsg)-macSize], signedMsg[len(signedMsg)-macSize:]
- expectedMAC := base64.RawURLEncoding.EncodeToString(mac([]byte(msg), purpose))
- if !hmac.Equal([]byte(msgMAC), []byte(expectedMAC)) {
- return "", false
- }
- return msg, true
-}
-
-const (
- authVersion = "authv3"
- authCookieName = "__Secure-a"
-)
-
-func makeHTTPCookie(token string, expiry time.Time) *http.Cookie {
- return &http.Cookie{
- Name: authCookieName,
- Value: token,
- Path: "/",
- Domain: selfURL.Hostname(),
- Expires: expiry,
- Secure: true,
- HttpOnly: true,
- SameSite: http.SameSiteStrictMode,
- Partitioned: true,
- }
-}
-
-type authToken struct {
- expiry time.Time
- userID []byte
-}
-
-func attachCookie(w http.ResponseWriter, tok *authToken) {
- buf := binary.AppendVarint(nil, tok.expiry.Unix())
- buf = append(buf, tok.userID...)
- token := signMsg(base64.RawURLEncoding.EncodeToString(buf), authVersion)
- http.SetCookie(w, makeHTTPCookie(token, tok.expiry))
-}
-
-func cookieAuth(r *http.Request) bool {
- cookie, err := r.Cookie(authCookieName)
- if err != nil {
- return false
- }
- msg, ok := verifyHMAC(cookie.Value, authVersion)
- if !ok {
- return false
- }
- msgBytes, err := base64.RawURLEncoding.DecodeString(msg)
- if err != nil {
- return false
- }
- expiryUnix, n := binary.Varint(msgBytes)
- if n <= 0 {
- return false
- }
- msgBytes = msgBytes[n:]
- expiry := time.Unix(expiryUnix, 0)
- if time.Now().After(expiry) {
- return false
- }
- if len(msgBytes) > 0 {
- if len(msgBytes) < 16 {
- return false
- }
- userID := msgBytes
- config, err := loadConfig()
- if err != nil {
- log.Printf("Error: failed to load config: %s", err)
- return false
- }
- if _, ok := config.passkeys[string(userID)]; !ok {
- return false
- }
- }
- return true
-}
-
-func verifyRedirect(redirect string) bool {
- u, err := url.Parse(redirect)
- if err != nil {
- return false
- }
- switch u.Host {
- case
- "cinema." + selfURL.Host,
- "downloads." + selfURL.Host,
- selfURL.Host:
-
- return true
- }
- return false
-}
-
-const challengeExpiry = 10 * time.Minute
-
-var (
- challengeMu sync.Mutex
- challenges = make(map[string]time.Time)
-)
-
-var (
- //go:embed templates/login.html.template
- loginTemplateString string
- loginTemplate = template.Must(template.New("login").Parse(loginTemplateString))
-)
-
-type loginTemplateArgs struct {
- Challenge string
-}
-
-func serveLogin(w http.ResponseWriter, r *http.Request) {
- if cookieAuth(r) {
- redirect := r.FormValue("redirect")
- if !verifyRedirect(redirect) {
- redirect = "/"
- }
- http.Redirect(w, r, redirect, http.StatusSeeOther)
- return
- }
- buf := make([]byte, 32)
- rand.Read(buf)
- now := time.Now()
- challengeMu.Lock()
- if len(challenges) >= 100 {
- challengeMu.Unlock()
- http.Error(w, "Too many active requests", http.StatusTooManyRequests)
- return
- }
- challenges[string(buf)] = now
- challengeMu.Unlock()
- if err := loginTemplate.Execute(w, loginTemplateArgs{
- Challenge: base64.StdEncoding.EncodeToString(buf),
- }); err != nil {
- log.Printf("Warning: login: %s", err)
- }
-}
-
-func login(w http.ResponseWriter, r *http.Request) {
- user, err := base64.StdEncoding.DecodeString(r.FormValue("user"))
- if err != nil {
- http.Error(w, "Bad user", http.StatusBadRequest)
- return
- }
- authData, err := base64.StdEncoding.DecodeString(r.FormValue("auth-data"))
- if err != nil {
- http.Error(w, "Bad auth data", http.StatusBadRequest)
- return
- }
- clientData, err := base64.StdEncoding.DecodeString(r.FormValue("client-data"))
- if err != nil {
- http.Error(w, "Bad client data: invalid base64", http.StatusBadRequest)
- return
- }
- sig, err := base64.StdEncoding.DecodeString(r.FormValue("sig"))
- if err != nil {
- http.Error(w, "Bad sig", http.StatusBadRequest)
- return
- }
- config, err := loadConfig()
- if err != nil {
- log.Printf("Error: failed to load config: %s", err)
- http.Error(w, "Internal error", http.StatusInternalServerError)
- return
- }
- pubKey, ok := config.passkeys[string(user)]
- if !ok {
- http.Error(w, "No user with the given ID", http.StatusBadRequest)
- return
- }
- clientDataHash := sha256.Sum256(clientData)
- if err := verifyPubKey(pubKey, slices.Concat(authData, clientDataHash[:]), sig); err != nil {
- http.Error(w, "Challenge failed", http.StatusBadRequest)
- return
- }
- var cd struct {
- Type string `json:"type"`
- Challenge string `json:"challenge"`
- Origin string `json:"origin"`
- CrossOrigin bool `json:"crossOrigin"`
- }
- if err := json.Unmarshal(clientData, &cd); err != nil {
- http.Error(w, "Bad client data: invalid JSON", http.StatusBadRequest)
- return
- }
- challenge, err := base64.RawURLEncoding.DecodeString(cd.Challenge)
- if err != nil {
- http.Error(w, "Bad client data: bad challenge", http.StatusBadRequest)
- return
- }
- challengeMu.Lock()
- initiated, ok := challenges[string(challenge)]
- delete(challenges, string(challenge))
- challengeMu.Unlock()
- if !ok {
- http.Error(w, "Request already consumed", http.StatusConflict)
- return
- }
- if time.Since(initiated) > challengeExpiry {
- http.Error(w, "Challenge is too old. Please try again", http.StatusConflict)
- return
- }
- if cd.Type != "webauthn.get" || cd.Origin != selfURL.String() || cd.CrossOrigin {
- http.Error(w, "Bad client data", http.StatusBadRequest)
- return
- }
- expectedRPHash := sha256.Sum256([]byte(selfURL.Hostname()))
- const flagUserPresence, flagUserVerification = 0x01, 0x04
- if len(authData) < 37 || !bytes.Equal(authData[:32], expectedRPHash[:]) || authData[32]&(flagUserPresence|flagUserVerification) != (flagUserPresence|flagUserVerification) {
- http.Error(w, "Bad authenticator data", http.StatusBadRequest)
- return
- }
- attachCookie(w, &authToken{
- expiry: time.Now().Add(30 * 24 * time.Hour),
- userID: user,
- })
- redirect := r.FormValue("redirect")
- if !verifyRedirect(redirect) {
- redirect = "/"
- }
- http.Redirect(w, r, redirect, http.StatusSeeOther)
-}
-
-var (
- //go:embed templates/register/register.html.template
- registerTemplateString string
- registerTemplate = template.Must(outlinedTemplate(registerTemplateString))
-)
-
-type registerTemplateArgs struct {
- ID string
- Challenge string
-}
-
-func serveRegister(w http.ResponseWriter, r *http.Request) {
- buf := make([]byte, 32+16)
- rand.Read(buf)
- challenge, userID := buf[:32], buf[32:]
- if err := registerTemplate.Execute(w, registerTemplateArgs{
- ID: base64.StdEncoding.EncodeToString(userID),
- Challenge: base64.StdEncoding.EncodeToString(challenge),
- }); err != nil {
- log.Printf("Warning: serveRegister: %s", err)
- }
-}
-
-func wrap(s string) string {
- out := new(strings.Builder)
- for s != "" {
- if out.Len() > 0 {
- out.WriteString("\n\t ")
- }
- n := min(50, len(s))
- out.WriteString(s[:n])
- s = s[n:]
- }
- return out.String()
-}
-
-var (
- //go:embed templates/register/register-success.html.template
- registerSuccessTemplateString string
- registerSuccessTemplate = template.Must(outlinedTemplate(registerSuccessTemplateString))
-)
-
-type registerSuccessTemplateArgs struct {
- ID string
- Key string
-}
-
-func register(w http.ResponseWriter, r *http.Request) {
- username := r.FormValue("name")
- if username == "" {
- http.Error(w, "Passkey name is required", http.StatusBadRequest)
- return
- }
- userID, err := base64.StdEncoding.DecodeString(r.FormValue("id"))
- if err != nil {
- http.Error(w, "Invalid user ID", http.StatusBadRequest)
- return
- }
- key, err := base64.StdEncoding.DecodeString(r.FormValue("key"))
- if err != nil {
- http.Error(w, "Invalid key", http.StatusBadRequest)
- return
- }
- if err := registerSuccessTemplate.Execute(w, registerSuccessTemplateArgs{
- ID: r.FormValue("name"),
- Key: wrap(base64.RawURLEncoding.EncodeToString(slices.Concat(userID, key))),
- }); err != nil {
- log.Printf("Warning: register: %s", err)
- }
-}
-
-var (
- otpMu sync.Mutex
- otpLink string
- otpGenerated time.Time
-)
-
-var (
- //go:embed templates/otp.html.template
- otpTemplateString string
- otpTemplate = template.Must(outlinedTemplate(otpTemplateString))
-)
-
-type otpTemplateArgs struct {
- URL string
- QR string
-}
-
-func serveOTP(w http.ResponseWriter, r *http.Request) {
- const nWords = 5
- buf := make([]byte, 2*nWords)
- rand.Read(buf)
- words := make([]string, nWords)
- for i := range len(buf) / 2 {
- words[i] = wordlist.Words[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff]
- }
- l := strings.Join(words, "-")
- otpMu.Lock()
- otpLink = l
- otpGenerated = time.Now()
- otpMu.Unlock()
- otpURL := selfURL.String() + "/otp/" + l
- if err := otpTemplate.Execute(w, otpTemplateArgs{
- URL: otpURL,
- QR: "/qr.png?t=" + url.QueryEscape(signMsg(otpURL, "qr")),
- }); err != nil {
- log.Printf("Warning: serveOTP: %s", err)
- }
-}
-
-func otpLogin(w http.ResponseWriter, r *http.Request) {
- otpMu.Lock()
- myOTP := otpLink
- otpLink = ""
- generated := otpGenerated
- otpMu.Unlock()
- reqOTP := r.PathValue("otp")
- if reqOTP == "" {
- notFound(w, r)
- return
- }
- myOTPHash := sha256.Sum256([]byte(myOTP))
- reqOTPHash := sha256.Sum256([]byte(reqOTP))
- if !hmac.Equal(myOTPHash[:], reqOTPHash[:]) {
- notFound(w, r)
- return
- }
- if time.Since(generated) > 10*time.Minute {
- http.Error(w, "OTP is too old. Please generate a new link", http.StatusConflict)
- return
- }
- attachCookie(w, &authToken{expiry: time.Now().Add(24 * time.Hour)})
- fmt.Fprintf(w, "Temporary 24 hour token issued. You are now logged in.\n")
-}
-
-func logout(w http.ResponseWriter, r *http.Request) {
- http.SetCookie(w, makeHTTPCookie("!", time.Now()))
- fmt.Fprintf(w, "You are now logged out.\n")
-}
-
-var (
- //go:embed templates/note.html.template
- notepadString string
- notepadTemplate = template.Must(outlinedTemplate(notepadString))
-
- notepadContentsMu sync.Mutex
- notepadContents string
-)
-
-type notepadTemplateArgs struct {
- Content string
-}
-
-func notepad(w http.ResponseWriter, r *http.Request) {
- notepadContentsMu.Lock()
- currentContent := notepadContents
- notepadContentsMu.Unlock()
- if err := notepadTemplate.Execute(w, notepadTemplateArgs{Content: currentContent}); err != nil {
- log.Printf("Warning: notepad: %s", err)
- }
-}
-
-func autosave(w http.ResponseWriter, r *http.Request) {
- newContent := r.FormValue("content")
- notepadContentsMu.Lock()
- notepadContents = newContent
- notepadContentsMu.Unlock()
- if err := notepadTemplate.ExecuteTemplate(w, "saveIndicator", "Saved ✓"); err != nil {
- log.Printf("Warning: autosave: %s", err)
- }
-}
-
-func cmd(w http.ResponseWriter, r *http.Request) {
- serviceConfig, err := loadConfig()
- if err != nil {
- log.Printf("Error: failed to load config: %s", err)
- http.Error(w, "Failed to load config", http.StatusInternalServerError)
- return
- }
- sourceIdx, ok := slices.BinarySearchFunc(serviceConfig.CommandRedirects, r.PathValue("cmd"), func(cmd commandConfig, name string) int { return strings.Compare(cmd.Name, name) })
- if !ok {
- notFound(w, r)
- return
- }
- if r.FormValue("go-get") == "1" {
- fmt.Fprintf(w, "<meta name=\"go-import\" content=\"roseh.moe/cmd/%s %s\">\n",
- r.PathValue("cmd"),
- serviceConfig.CommandRedirects[sourceIdx].URL)
- if source := serviceConfig.CommandRedirects[sourceIdx].Source; source != "" {
- fmt.Fprintf(w, "<meta name=\"go-source\" content=\"roseh.moe/cmd/%s %s\">\n",
- r.PathValue("cmd"),
- source)
- }
- } else {
- http.Redirect(w, r, "https://pkg.go.dev/roseh.moe/cmd/"+r.PathValue("cmd"), http.StatusMovedPermanently)
- }
-}
-
-var (
- //go:embed templates/pkg-index.html.template
- pkgIndexString string
- pkgIndexTemplate = template.Must(outlinedTemplate(pkgIndexString))
-)
-
-type pkgIndexTemplateArgs struct {
- Packages []packageConfig
-}
-
-func pkgIndex(w http.ResponseWriter, r *http.Request) {
- serviceConfig, err := loadConfig()
- if err != nil {
- log.Printf("Error: failed to load config: %s", err)
- http.Error(w, "Failed to load config", http.StatusInternalServerError)
- return
- }
- if err := pkgIndexTemplate.Execute(w, pkgIndexTemplateArgs{Packages: serviceConfig.PackageRedirects}); err != nil {
- log.Printf("Warning: pkgIndex: %s", err)
- }
-}
-
-var (
- //go:embed templates/cmd-index.html.template
- cmdIndexString string
- cmdIndexTemplate = template.Must(outlinedTemplate(cmdIndexString))
-)
-
-type cmdIndexTemplateArgs struct {
- Commands []commandConfig
-}
-
-func cmdIndex(w http.ResponseWriter, r *http.Request) {
- serviceConfig, err := loadConfig()
- if err != nil {
- log.Printf("Error: failed to load config: %s", err)
- http.Error(w, "Failed to load config", http.StatusInternalServerError)
- return
- }
- if err := cmdIndexTemplate.Execute(w, cmdIndexTemplateArgs{Commands: serviceConfig.CommandRedirects}); err != nil {
- log.Printf("Warning: pkgIndex: %s", err)
- }
-}
-
-func pkg(w http.ResponseWriter, r *http.Request) {
- serviceConfig, err := loadConfig()
- if err != nil {
- log.Printf("Error: failed to load config: %s", err)
- http.Error(w, "Failed to load config", http.StatusInternalServerError)
- return
- }
- sourceIdx, ok := slices.BinarySearchFunc(serviceConfig.PackageRedirects, r.PathValue("pkg"), func(pkg packageConfig, name string) int { return strings.Compare(pkg.Name, name) })
- if !ok {
- notFound(w, r)
- return
- }
- if r.FormValue("go-get") == "1" {
- fmt.Fprintf(w, "<meta name=\"go-import\" content=\"roseh.moe/pkg/%s %s\">\n", r.PathValue("pkg"), serviceConfig.PackageRedirects[sourceIdx].URL)
- if source := serviceConfig.PackageRedirects[sourceIdx].Source; source != "" {
- fmt.Fprintf(w, "<meta name=\"go-source\" content=\"roseh.moe/pkg/%s %s\">\n",
- r.PathValue("pkg"),
- source)
- }
- } else {
- http.Redirect(w, r, "https://pkg.go.dev/roseh.moe/pkg/"+r.PathValue("pkg"), http.StatusMovedPermanently)
- }
-}
-
-type nopCloser struct {
- io.Writer
-}
-
-func (nopCloser) Close() error { return nil }
-
-func (w nopCloser) ReadFrom(r io.Reader) (int64, error) {
- return io.Copy(w.Writer, r)
-}
-
-type gzipResponseWriter struct {
- http.ResponseWriter
-
- w io.WriteCloser
- buf []byte
- code int
-}
-
-func (w *gzipResponseWriter) Unwrap() http.ResponseWriter {
- return w.ResponseWriter
-}
-
-func shouldGzip(w http.ResponseWriter) bool {
- disposition, _, _ := strings.Cut(w.Header().Get("Content-Disposition"), ";")
- length, lengthErr := strconv.Atoi(w.Header().Get("Content-Length"))
- return strings.HasPrefix(w.Header().Get("Content-Type"), "text/") &&
- !(disposition == "attachment" && w.Header().Get("Accept-Ranges") != "" ||
- w.Header().Get("Content-Encoding") != "" ||
- lengthErr == nil && length < 512)
-}
-
-func (w *gzipResponseWriter) ReadFrom(r io.Reader) (int64, error) {
- if w.w != nil {
- return io.Copy(w.w, r)
- }
- var nn int64
- if len(w.Header().Values("Content-Type")) == 0 {
- if w.buf == nil {
- w.buf = make([]byte, 0, 512)
- }
- n, err := io.ReadFull(r, w.buf[len(w.buf):cap(w.buf)])
- w.buf = w.buf[:len(w.buf)+n]
- nn += int64(n)
- if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
- return nn, err
- }
- if len(w.buf) < 512 {
- return nn, nil
- }
- contentType := http.DetectContentType(w.buf)
- w.Header().Set("Content-Type", contentType)
- }
- if shouldGzip(w.ResponseWriter) {
- var err error
- w.w, err = gzip.NewWriterLevel(w.ResponseWriter, gzip.BestSpeed)
- if err != nil {
- return nn, err
- }
- w.Header().Set("Content-Encoding", "gzip")
- w.Header().Del("Content-Length")
- w.Header().Del("Accept-Ranges")
- } else {
- w.w = nopCloser{w.ResponseWriter}
- }
- if w.code != 0 {
- w.ResponseWriter.WriteHeader(w.code)
- }
- if len(w.buf) > 0 {
- if n, err := w.w.Write(w.buf); err != nil {
- return max(int64(n)-(512-nn), 0), err
- }
- }
- n, err := io.Copy(w.w, r)
- nn += n
- return nn, err
-}
-
-func (w *gzipResponseWriter) Write(p []byte) (int, error) {
- n, err := w.ReadFrom(bytes.NewReader(p))
- return int(n), err
-}
-
-func (w *gzipResponseWriter) WriteHeader(statusCode int) {
- w.code = statusCode
-}
-
-func (w *gzipResponseWriter) close() {
- if w.w == nil {
- if w.code != 0 {
- w.ResponseWriter.WriteHeader(w.code)
- }
- w.ResponseWriter.Write(w.buf)
- } else {
- w.w.Close()
- }
-}
-
-type gzipHandler struct {
- h http.Handler
-}
-
-func (h *gzipHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if len(r.Header.Values("Range")) == 0 {
- for _, hdr := range r.Header.Values("Accept-Encoding") {
- for encoding := range strings.FieldsFuncSeq(hdr, func(r rune) bool { return r == ',' || unicode.IsSpace(r) }) {
- encoding, _, _ := strings.Cut(encoding, ";")
- if encoding == "gzip" {
- gzipWriter := &gzipResponseWriter{ResponseWriter: w}
- h.h.ServeHTTP(gzipWriter, r)
- gzipWriter.close()
- return
- }
- }
- }
- }
- h.h.ServeHTTP(w, r)
-}
-
-type authHandler struct {
- h http.Handler
- noRedirect bool
-}
-
-func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if !cookieAuth(r) {
- if h.noRedirect {
- http.Error(w, "not logged in", http.StatusUnauthorized)
- } else {
- u := *r.URL
- u.Host = r.Host
- u.Scheme = selfURL.Scheme
- http.Redirect(w, r, "//"+selfURL.Host+"/login?redirect="+url.QueryEscape(u.String()), http.StatusSeeOther)
- }
- return
- }
- h.h.ServeHTTP(w, r)
-}
-
-type timeoutHandler struct {
- h http.Handler
- timeout time.Duration
-}
-
-func (h *timeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- rc := http.NewResponseController(w)
- deadline := time.Now().Add(h.timeout)
- if err := rc.SetReadDeadline(deadline); err != nil {
- log.Printf("Error: set read deadline: %s", err)
- }
- // Give some extra time to write the error
- writeDeadline := deadline.Add(time.Minute)
- if err := rc.SetWriteDeadline(writeDeadline); err != nil {
- log.Printf("Error: set write deadline: %s", err)
- }
- ctx, cancel := context.WithDeadline(ctx, deadline)
- defer cancel()
- h.h.ServeHTTP(w, r.WithContext(ctx))
-}
-
-type loggingHandler struct {
- h http.Handler
-}
-
-func (h *loggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if *v {
- log.Printf("%q from %q", r.Method+" "+r.Host+r.URL.String(), r.RemoteAddr)
- }
- h.h.ServeHTTP(w, r)
-}
-
-type removeAuthCookieHandler struct {
- h http.Handler
-}
-
-type hstsHandler struct {
- h http.Handler
-}
-
-func (h *hstsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if *https != "" {
- w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
- }
- h.h.ServeHTTP(w, r)
-}
-
-func (h *removeAuthCookieHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- var cookies []*http.Cookie
- for _, c := range r.Cookies() {
- if c.Name != authCookieName {
- cookies = append(cookies, c)
- }
- }
- r.Header.Del("Cookie")
- for _, c := range cookies {
- r.AddCookie(c)
- }
- h.h.ServeHTTP(w, r)
-}
-
-func main() {
- flag.Parse()
-
- if err := loadSecrets(); err != nil {
- log.Fatal(err)
- }
-
- if err := os.MkdirAll(*holeTempDir, 0700); err != nil {
- log.Fatal(err)
- }
-
- go func() {
- for range time.NewTicker(2 * time.Hour).C {
- if err := cleanHole(); err != nil {
- log.Printf("Error: clean hole: %s", err)
- }
- }
- }()
-
- go func() {
- for now := range time.NewTicker(time.Hour).C {
- log.Printf("Cleaning expired challenges")
- challengeMu.Lock()
- for challenge, initiated := range challenges {
- if now.Sub(initiated) > challengeExpiry {
- delete(challenges, challenge)
- }
- }
- challengeMu.Unlock()
- }
- }()
-
- mux := http.NewServeMux()
- indexTemplate, err := outlinedTemplate(indexString)
- if err != nil {
- log.Fatal(err)
- }
- indexHandler, err := staticTemplate(indexTemplate)
- if err != nil {
- log.Fatal(err)
- }
- mux.Handle("GET /{$}", &timeoutHandler{h: indexHandler, timeout: 5 * time.Minute})
- gamesIndexTemplate, err := outlinedTemplate(gamesIndexString)
- if err != nil {
- log.Fatal(err)
- }
- gamesIndexHandler, err := staticTemplate(gamesIndexTemplate)
- if err != nil {
- log.Fatal(err)
- }
- mux.Handle("GET /games/{$}", &timeoutHandler{h: gamesIndexHandler, timeout: 5 * time.Minute})
- pongTemplate, err := template.New("pong").Parse(pongString)
- if err != nil {
- log.Fatal(err)
- }
- pongHandler, err := staticTemplate(pongTemplate)
- if err != nil {
- log.Fatal(err)
- }
- mux.Handle("GET /games/pong", &timeoutHandler{h: pongHandler, timeout: 5 * time.Minute})
- stallmanShooterTemplate, err := outlinedTemplate(stallmanShooterString)
- if err != nil {
- log.Fatal(err)
- }
- stallmanShooterHandler, err := staticTemplate(stallmanShooterTemplate)
- if err != nil {
- log.Fatal(err)
- }
- mux.Handle("GET /games/rms", &timeoutHandler{h: stallmanShooterHandler, timeout: 5 * time.Minute})
- mux.Handle("GET /qr.png", &timeoutHandler{h: http.HandlerFunc(qrHandler), timeout: 5 * time.Minute})
- mux.Handle("GET /wormhole", &timeoutHandler{h: http.HandlerFunc(wormhole), timeout: 5 * time.Minute})
- mux.HandleFunc("POST /wormhole", wormholeUpload)
- mux.HandleFunc("GET /wormhole/{hole}", wormholeDownload)
- mux.Handle("GET /login", &timeoutHandler{h: http.HandlerFunc(serveLogin), timeout: 5 * time.Minute})
- mux.Handle("POST /login", &timeoutHandler{h: http.HandlerFunc(login), timeout: 5 * time.Minute})
- mux.Handle("GET /logout", &timeoutHandler{h: http.HandlerFunc(logout), timeout: 5 * time.Minute})
- mux.Handle("GET /register", &timeoutHandler{h: http.HandlerFunc(serveRegister), timeout: 5 * time.Minute})
- mux.Handle("POST /register", &timeoutHandler{h: http.HandlerFunc(register), timeout: 5 * time.Minute})
- mux.Handle("GET /otp", &timeoutHandler{h: &authHandler{h: http.HandlerFunc(serveOTP)}, timeout: 5 * time.Minute})
- mux.Handle("GET /otp/{otp}", &timeoutHandler{h: http.HandlerFunc(otpLogin), timeout: 5 * time.Minute})
- mux.Handle("GET /notepad", &timeoutHandler{h: &authHandler{h: http.HandlerFunc(notepad)}, timeout: 5 * time.Minute})
- mux.Handle("POST /notepad", &timeoutHandler{h: &authHandler{h: http.HandlerFunc(autosave), noRedirect: true}, timeout: 5 * time.Minute})
- mux.Handle("GET /cmd/{$}", &timeoutHandler{h: http.HandlerFunc(cmdIndex), timeout: 5 * time.Minute})
- mux.Handle("GET /cmd/{cmd}", &timeoutHandler{h: http.HandlerFunc(cmd), timeout: 5 * time.Minute})
- mux.Handle("GET /pkg/{$}", &timeoutHandler{h: http.HandlerFunc(pkgIndex), timeout: 5 * time.Minute})
- mux.Handle("GET /pkg/{pkg}", &timeoutHandler{h: http.HandlerFunc(pkg), timeout: 5 * time.Minute})
- mux.Handle("GET /nezuko/", &timeoutHandler{h: &authHandler{h: http.StripPrefix("/nezuko/", http.FileServer(http.Dir(*nezuko)))}, timeout: 10 * time.Minute})
- mux.Handle("GET /static/", &timeoutHandler{h: http.HandlerFunc(static), timeout: 5 * time.Minute})
- mux.Handle("GET /favicon.ico", &timeoutHandler{h: http.HandlerFunc(favicon), timeout: 5 * time.Minute})
- apiMux := http.NewServeMux()
- apiMux.HandleFunc("PUT /api/wormhole/{name}", wormholeAPIUpload)
- mux.Handle("/api/", apiMux)
- mux.Handle("/", &timeoutHandler{h: http.HandlerFunc(sharedFileHandler), timeout: 5 * time.Minute})
- serverMux := http.NewServeMux()
- serverMux.Handle(selfURL.Hostname()+"/", &gzipHandler{mux})
- jellyfinURL, err := url.Parse("http://127.0.0.1:8096")
- if err != nil {
- log.Fatal(err)
- }
- serverMux.Handle("cinema."+selfURL.Hostname()+"/", &authHandler{h: &removeAuthCookieHandler{&httputil.ReverseProxy{
- Rewrite: func(r *httputil.ProxyRequest) {
- r.SetURL(jellyfinURL)
- r.SetXForwarded()
- },
- }}})
- codeMux := http.NewServeMux()
- cgitStaticFiles := http.FileServer(http.Dir(*cgit))
- codeMux.Handle("/cgit.css", &timeoutHandler{h: cgitStaticFiles, timeout: 5 * time.Minute})
- codeMux.Handle("/cgit.js", &timeoutHandler{h: cgitStaticFiles, timeout: 5 * time.Minute})
- codeMux.Handle("/robots.txt", &timeoutHandler{h: cgitStaticFiles, timeout: 5 * time.Minute})
- codeMux.Handle("/favicon.ico", &timeoutHandler{h: http.HandlerFunc(favicon), timeout: 5 * time.Minute})
- cgitConfigAbsPath, err := filepath.Abs(*cgitConfig)
- if err != nil {
- log.Fatal(err)
- }
- cgitCGI := &cgi.Handler{
- Path: filepath.Join(*cgit, "cgit.cgi"),
- Env: []string{"CGIT_CONFIG=" + cgitConfigAbsPath},
- }
- cgitCGI.PathLocationHandler = cgitCGI
- codeMux.Handle("/", &timeoutHandler{h: &removeAuthCookieHandler{cgitCGI}, timeout: 5 * time.Minute})
- serverMux.Handle("code."+selfURL.Hostname()+"/", &gzipHandler{codeMux})
- transmissionURL, err := url.Parse("http://127.0.0.1:9091")
- if err != nil {
- log.Fatal(err)
- }
- serverMux.Handle("downloads."+selfURL.Hostname()+"/", &authHandler{h: &removeAuthCookieHandler{&httputil.ReverseProxy{
- Rewrite: func(r *httputil.ProxyRequest) {
- r.SetURL(transmissionURL)
- r.SetXForwarded()
- },
- }}})
- server := &http.Server{
- Handler: &loggingHandler{http.NewCrossOriginProtection().Handler(&hstsHandler{serverMux})},
-
- ReadHeaderTimeout: 5 * time.Minute,
- IdleTimeout: 10 * time.Minute,
- }
-
- if *httpRedirect != "" {
- go func() {
- mux := http.NewServeMux()
- mux.Handle("GET /.well-known/acme-challenge/", &timeoutHandler{
- h: http.FileServer(http.Dir("/var/lib/acme/acme-challenge")),
- timeout: 5 * time.Minute,
- })
- mux.Handle("/", &timeoutHandler{
- h: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- u := *r.URL
- u.Scheme = selfURL.Scheme
- u.Host, _, _ = strings.Cut(r.Host, ":")
- if port := selfURL.Port(); port != "" {
- u.Host += ":" + port
- }
- http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
- }),
- timeout: time.Minute,
- })
- httpSrv := &http.Server{
- Addr: ":" + *httpRedirect,
- Handler: &loggingHandler{mux},
- ReadHeaderTimeout: 5 * time.Minute,
- IdleTimeout: 10 * time.Minute,
- }
- log.Printf("Serving HTTP redirect on %q", httpSrv.Addr)
- err := httpSrv.ListenAndServe()
- log.Printf("Error: HTTP redirect failed: %s", err)
- }()
- }
-
- port := selfURL.Port()
- if port == "" {
- port = "80"
- if *https != "" {
- port = "443"
- }
- }
- server.Addr = ":" + port
- log.Printf("Listening on %q", server.Addr)
- if *https != "" {
- log.Fatal(server.ListenAndServeTLS(*https+"/cert.pem", *https+"/key.pem"))
- } else {
- log.Fatal(server.ListenAndServe())
+func Init() error {
+ if err := os.MkdirAll(HoleTempDir, 0700); err != nil {
+ return err
}
+ return nil
}