OrbitalBeat/OrbitalBeat.go

793 lines
21 KiB
Go

package main
import (
"bytes"
"crypto/tls"
"database/sql"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"git.gammaspectra.live/S.O.N.G/MakyuuIchaival"
"git.gammaspectra.live/S.O.N.G/MakyuuIchaival/contentmessage"
"git.gammaspectra.live/S.O.N.G/MakyuuIchaival/httputils"
"git.gammaspectra.live/S.O.N.G/MakyuuIchaival/tlsutils"
"github.com/cloudflare/circl/sign/ed25519"
"github.com/ipfs/go-cid"
_ "github.com/lib/pq"
"github.com/multiformats/go-multihash"
"log"
"net/http"
"net/url"
"os"
"path"
"runtime"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
"time"
)
type ContentCacheEntry struct {
Entry ContentEntry
AccessTime time.Time
}
var dbHandle *sql.DB
var sha256Statement *sql.Stmt
var md5Statement *sql.Stmt
var statsStatement *sql.Stmt
var fdlimit int
var objectCacheMutex sync.RWMutex
var objectCache = make(map[string]*ContentCacheEntry)
var trustedPublicKeys []ed25519.PublicKey
var debugOutput = false
var programVersion = "unknown"
type statistics struct {
Served servedStatistics `json:"served"`
ContentCache contentCacheStatistics `json:"content_cache"`
SignatureCache signatureCacheStatistics `json:"signature_cache"`
Http struct {
Code httpCodeStatistics `json:"code"`
Protocol httpProtocolStatistics `json:"protocol"`
} `json:"http"`
Tls struct {
Version tlsVersionStatistics `json:"version"`
Cipher tlsCipherStatistics `json:"cipher"`
} `json:"tls"`
}
type servedStatistics struct {
TotalBytesServed uint64 `json:"bytes"`
TotalHashesServed uint64 `json:"hashes"`
TotalContentRequestsServed uint64 `json:"content_requests"`
}
type contentCacheStatistics struct {
ContentCacheHit uint64 `json:"cache_hit"`
ContentCacheMiss uint64 `json:"cache_miss"`
}
type signatureCacheStatistics struct {
SignatureCacheHit uint64 `json:"cache_hit"`
SignatureCacheMiss uint64 `json:"cache_miss"`
}
type httpCodeStatistics struct {
Code200 uint64 `json:"200"`
Code204 uint64 `json:"204"`
Code206 uint64 `json:"206"`
Code302 uint64 `json:"302"`
Code400 uint64 `json:"400"`
Code403 uint64 `json:"403"`
Code404 uint64 `json:"404"`
Code501 uint64 `json:"501"`
}
type httpProtocolStatistics struct {
Http10 uint64 `json:"http_10"`
Http11 uint64 `json:"http_11"`
Http20 uint64 `json:"http_20"`
Http30 uint64 `json:"http_30"`
Other uint64 `json:"other"`
}
type tlsVersionStatistics struct {
Tls12 uint64 `json:"tls_12"`
Tls13 uint64 `json:"tls_13"`
Other uint64 `json:"other"`
}
type tlsCipherStatistics struct {
ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 uint64 `json:"ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"`
ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 uint64 `json:"ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"`
ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 uint64 `json:"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"`
ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 uint64 `json:"ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"`
ECDHE_RSA_WITH_AES_256_GCM_SHA384 uint64 `json:"ECDHE_RSA_WITH_AES_256_GCM_SHA384"`
ECDHE_RSA_WITH_AES_128_GCM_SHA256 uint64 `json:"ECDHE_RSA_WITH_AES_128_GCM_SHA256"`
CHACHA20_POLY1305_SHA256 uint64 `json:"CHACHA20_POLY1305_SHA256"`
AES_128_GCM_SHA256 uint64 `json:"AES_128_GCM_SHA256"`
AES_256_GCM_SHA384 uint64 `json:"AES_256_GCM_SHA384"`
Other uint64 `json:"other"`
}
var globalStatistics statistics
func getFirstValidContentEntry(entries *[]ContentEntry) *ContentEntry {
for _, entry := range *entries {
stat, err := os.Stat(entry.Path)
if err == nil && (entry.Size == 0 || uint64(stat.Size()) == entry.Size) { //TODO: update Size if not found
copiedEntry := entry
copiedEntry.Size = uint64(stat.Size())
return &copiedEntry
}
}
return nil
}
func GetMimeTypeFromExtension(ext string) string {
if len(ext) > 0 {
switch strings.ToLower(ext[1:]) {
//Audio types
case "flac":
return "audio/flac"
case "mp3":
return "audio/mpeg;codecs=mp3"
case "m4a":
return "audio/mp4"
case "mka":
return "audio/x-matroska"
case "ogg":
return "audio/ogg"
case "opus":
return "audio/opus"
case "tta":
return "audio/tta"
case "aac":
return "audio/aac"
case "alac":
return "audio/alac"
case "wav":
return "audio/wav"
case "ape":
return "audio/ape"
//Video types
case "mkv":
return "video/x-matroska"
case "webm":
return "video/webm"
case "mp4", "m4v":
return "video/mp4"
case "ogv":
return "video/ogg"
case "ts":
return "video/mp2t"
case "avi":
return "video/x-msvideo"
//Image types
case "png":
return "image/png"
case "jpg", "jpeg", "jfif":
return "image/jpeg"
case "gif":
return "image/gif"
case "svg":
return "image/svg+xml"
case "tif", "tiff":
return "image/tiff"
case "webp":
return "image/webp"
case "bmp":
return "image/bmp"
//Text types
case "txt":
return "text/plain"
case "log":
return "text/x-log"
case "accurip":
return "text/x-accurip"
case "cue":
return "text/x-cue"
case "toc":
return "text/x-toc"
//Text subtitles
case "lrc":
return "text/x-subtitle-lrc"
case "ssa":
return "text/x-subtitle-ssa"
case "ass":
return "text/x-subtitle-ass"
case "srt":
return "text/x-subtitle-subrip"
//Web types
case "js":
return "text/javascript"
case "wasm":
return "application/wasm"
case "json":
return "application/json"
case "html":
return "text/html"
case "css":
return "text/css"
case "ttf":
return "font/ttf"
case "otf":
return "font/otf"
case "woff":
return "font/woff"
case "woff2":
return "font/woff2"
}
}
return "application/octet-stream"
}
func handleQueryRequest(ctx httputils.RequestContext, identifier cid.Cid, extraArguments []string) {
major, minor, _ := http.ParseHTTPVersion(ctx.GetProtocol())
switch major {
case 1:
switch minor {
case 0:
atomic.AddUint64(&globalStatistics.Http.Protocol.Http10, 1)
case 1:
atomic.AddUint64(&globalStatistics.Http.Protocol.Http11, 1)
default:
atomic.AddUint64(&globalStatistics.Http.Protocol.Other, 1)
}
case 2:
switch minor {
case 0:
atomic.AddUint64(&globalStatistics.Http.Protocol.Http20, 1)
default:
atomic.AddUint64(&globalStatistics.Http.Protocol.Other, 1)
}
case 3:
switch minor {
case 0:
atomic.AddUint64(&globalStatistics.Http.Protocol.Http30, 1)
default:
atomic.AddUint64(&globalStatistics.Http.Protocol.Other, 1)
}
default:
atomic.AddUint64(&globalStatistics.Http.Protocol.Other, 1)
}
switch ctx.GetTLSVersion() {
case tls.VersionTLS12:
atomic.AddUint64(&globalStatistics.Tls.Version.Tls12, 1)
case tls.VersionTLS13:
atomic.AddUint64(&globalStatistics.Tls.Version.Tls13, 1)
default:
if debugOutput {
log.Printf("Unknown TLS version %d", ctx.GetTLSVersion())
}
atomic.AddUint64(&globalStatistics.Tls.Version.Other, 1)
}
switch ctx.GetTLSCipher() {
//TLS 1.2 ciphers
case tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
atomic.AddUint64(&globalStatistics.Tls.Cipher.ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, 1)
case tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
atomic.AddUint64(&globalStatistics.Tls.Cipher.ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 1)
case tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
atomic.AddUint64(&globalStatistics.Tls.Cipher.ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 1)
case tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
atomic.AddUint64(&globalStatistics.Tls.Cipher.ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, 1)
case tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:
atomic.AddUint64(&globalStatistics.Tls.Cipher.ECDHE_RSA_WITH_AES_256_GCM_SHA384, 1)
case tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
atomic.AddUint64(&globalStatistics.Tls.Cipher.ECDHE_RSA_WITH_AES_128_GCM_SHA256, 1)
//TLS 1.3 ciphers
case tls.TLS_CHACHA20_POLY1305_SHA256:
atomic.AddUint64(&globalStatistics.Tls.Cipher.CHACHA20_POLY1305_SHA256, 1)
case tls.TLS_AES_128_GCM_SHA256:
atomic.AddUint64(&globalStatistics.Tls.Cipher.AES_128_GCM_SHA256, 1)
case tls.TLS_AES_256_GCM_SHA384:
atomic.AddUint64(&globalStatistics.Tls.Cipher.AES_256_GCM_SHA384, 1)
default:
if debugOutput {
log.Printf("Unknown TLS cipher %d", ctx.GetTLSCipher())
}
atomic.AddUint64(&globalStatistics.Tls.Cipher.Other, 1)
}
cTime := time.Now()
var cacheEntry = tryGetCacheEntryForIdentifier(identifier)
if cacheEntry == nil {
result := getEntriesForCID(identifier)
entry := getFirstValidContentEntry(&result)
if entry != nil {
cacheEntry = getCacheEntryForContentEntry(entry, identifier)
}
atomic.AddUint64(&globalStatistics.ContentCache.ContentCacheMiss, 1)
ctx.AddTimingInformational("ec", "Content Cache MISS")
} else {
atomic.AddUint64(&globalStatistics.ContentCache.ContentCacheHit, 1)
ctx.AddTimingInformational("ec", "Content Cache HIT")
}
pTime := cTime
cTime = time.Now()
ctx.AddTiming("e", "Content Entry", cTime.Sub(pTime))
if cacheEntry == nil {
var origin string
if refUrl, err := url.Parse(ctx.GetRequestHeader("Referer")); err != nil && len(ctx.GetRequestHeader("Referer")) > 0 {
origin = "https://" + refUrl.Host
} else if len(ctx.GetRequestHeader("Origin")) > 0 {
origin = ctx.GetRequestHeader("Origin")
} else if len(extraArguments) > 1 {
origin = "https://" + extraArguments[1]
}
//Try to redirect back to origin
if len(extraArguments) > 0 && origin != "" {
mh, _ := multihash.Decode(identifier.Hash())
var kind string
if mh.Code == multihash.SHA2_256 {
kind = "sha256"
} else if mh.Code == multihash.MD5 {
kind = "md5"
}
if kind != "" {
atomic.AddUint64(&globalStatistics.Http.Code.Code302, 1)
ctx.DoRedirect(fmt.Sprintf("%s/%s/%s/%s", origin, kind, hex.EncodeToString(mh.Digest), strings.Join(extraArguments, "/")), http.StatusFound)
return
}
}
atomic.AddUint64(&globalStatistics.Http.Code.Code404, 1)
ctx.SetResponseCode(http.StatusNotFound)
return
}
mh, _ := multihash.Decode(cacheEntry.Entry.Identifier.Hash())
file, err := os.Open(cacheEntry.Entry.Path)
if err != nil {
atomic.AddUint64(&globalStatistics.Http.Code.Code404, 1)
ctx.SetResponseCode(http.StatusNotFound)
return
}
ctx.SetResponseHeader("X-Request-CID", identifier.String())
ctx.SetResponseHeader("ETag", fmt.Sprintf("\"%s\"", cacheEntry.Entry.Identifier.String()))
if mh.Code == multihash.SHA2_256 {
ctx.SetResponseHeader("Digest", fmt.Sprintf("sha-256=%s", base64.StdEncoding.EncodeToString(mh.Digest)))
}
ctx.SetResponseHeader("Cache-Control", "public, max-age=2592000, immutable")
filename := path.Base(cacheEntry.Entry.Path)
//TODO: setting to hide filename
ctx.SetResponseHeader("Content-Disposition", fmt.Sprintf("inline; filename*=utf-8''%s", url.PathEscape(filename)))
pTime = cTime
cTime = time.Now()
ctx.AddTiming("s", "Content Serve", cTime.Sub(pTime))
mime := GetMimeTypeFromExtension(path.Ext(cacheEntry.Entry.Path))
if len(mime) > 0 {
ctx.SetResponseHeader("Content-Type", mime)
}
atomic.AddUint64(&globalStatistics.Served.TotalContentRequestsServed, 1)
ss := &statsStream{
stream: httputils.NewStreamFromFile(file),
}
runtime.SetFinalizer(ss, (*statsStream).Close)
if len(ctx.GetRequestHeader("range")) > 0 {
atomic.AddUint64(&globalStatistics.Http.Code.Code206, 1)
} else {
atomic.AddUint64(&globalStatistics.Http.Code.Code200, 1)
}
ctx.ServeStream(ss)
}
type statsStream struct {
stream httputils.DefinedStream
byteCount int64
}
func (ss *statsStream) Read(p []byte) (n int, err error) {
ss.byteCount += int64(len(p))
return ss.stream.Read(p)
}
func (ss *statsStream) Seek(offset int64, whence int) (int64, error) {
return ss.stream.Seek(offset, whence)
}
func (ss *statsStream) Close() error {
atomic.AddUint64(&globalStatistics.Served.TotalBytesServed, uint64(ss.byteCount))
ss.byteCount = 0
return ss.stream.Close()
}
func (ss *statsStream) Size() int64 {
return ss.stream.Size()
}
func (ss *statsStream) ModTime() time.Time {
return ss.stream.ModTime()
}
func setOtherHeaders(ctx httputils.RequestContext) {
ctx.SetResponseHeader("Server", "OrbitalBeat")
ctx.SetResponseHeader("Vary", "Content-Encoding")
ctx.SetResponseHeader("Strict-Transport-Security", "max-age=31536000")
ctx.SetResponseHeader("X-Content-Type-Options", "nosniff")
ctx.SetResponseHeader("X-Robots-Tags", "noindex, nofollow, notranslate")
for k, v := range ctx.GetExtraHeaders() {
ctx.SetResponseHeader(k, v)
}
}
func setCORSHeaders(ctx httputils.RequestContext) {
ctx.SetResponseHeader("Access-Control-Allow-Credentials", "true")
ctx.SetResponseHeader("Access-Control-Max-Age", "7200") //Firefox caps this to 86400, Chrome to 7200. Default is 5 seconds (!!!)
ctx.SetResponseHeader("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS")
ctx.SetResponseHeader("Access-Control-Allow-Headers", "DNT,ETag,Origin,Accept,Accept-Language,X-Requested-With,Range")
ctx.SetResponseHeader("Access-Control-Allow-Origin", "*")
ctx.SetResponseHeader("Access-Control-Expose-Headers", "*")
//CORP, COEP, COOP
ctx.SetResponseHeader("Cross-Origin-Embedder-Policy", "require-corp")
ctx.SetResponseHeader("Cross-Origin-Resource-Policy", "cross-origin")
ctx.SetResponseHeader("Cross-Origin-Opener-Policy", "unsafe-none")
}
func getCacheEntryForContentEntry(entry *ContentEntry, originalIdentifier cid.Cid) *ContentCacheEntry {
cacheEntry := tryGetCacheEntryForIdentifier(entry.Identifier)
if cacheEntry != nil {
return cacheEntry
}
objectCacheMutex.Lock()
defer objectCacheMutex.Unlock()
if len(objectCache) >= fdlimit {
//Find oldest value, remove it
var item *ContentCacheEntry
for _, e := range objectCache {
if item == nil || e.AccessTime.Before(item.AccessTime) {
item = e
}
}
if item != nil {
delete(objectCache, item.Entry.Identifier.String())
}
}
c := &ContentCacheEntry{
Entry: *entry,
AccessTime: time.Now(),
}
//TODO: make it not require a mutex
objectCache[entry.Identifier.String()] = c
if originalIdentifier.String() != entry.Identifier.String() {
objectCache[originalIdentifier.String()] = c
}
atomic.AddUint64(&globalStatistics.Served.TotalHashesServed, 1)
return c
}
func tryGetCacheEntryForIdentifier(identifier cid.Cid) *ContentCacheEntry {
objectCacheMutex.RLock()
defer objectCacheMutex.RUnlock()
cacheEntry, ok := objectCache[identifier.String()]
if ok {
cacheEntry.AccessTime = time.Now().UTC()
return cacheEntry
}
return nil
}
func IsTrustedPublicKey(key ed25519.PublicKey) bool {
for _, k := range trustedPublicKeys {
if bytes.Compare(k, key) == 0 {
return true
}
}
return false
}
func handle(ctx httputils.RequestContext) {
if len(ctx.GetHost()) > 0 && len(ctx.GetTLSServerName()) > 0 && strings.Split(ctx.GetHost(), ":")[0] != ctx.GetTLSServerName() { //Prevents rebinding / DNS stuff
atomic.AddUint64(&globalStatistics.Http.Code.Code404, 1)
ctx.SetResponseCode(http.StatusNotFound)
return
}
cTime := time.Now()
if _, ok := ctx.(*httputils.FastHTTPContext); ok {
ctx.AddTiming("c", "Connection", ctx.GetRequestTime().Sub(ctx.GetConnectionTime()))
}
ctx.AddTiming("r", "Request Handler", cTime.Sub(ctx.GetRequestTime()))
if ctx.IsGet() || ctx.IsHead() {
if debugOutput {
log.Printf("Serve %s", ctx.GetPath())
}
setOtherHeaders(ctx)
setCORSHeaders(ctx)
if ctx.GetPath() == "/stats" {
ctx.SetResponseHeader("Content-Type", "application/json")
atomic.AddUint64(&globalStatistics.Http.Code.Code200, 1)
ctx.SetResponseCode(http.StatusOK)
statsStruct := struct {
Version string `json:"version"`
Statistics *statistics `json:"statistics"`
Database struct {
TotalEntries uint64 `json:"entries"`
TotalSize uint64 `json:"size"`
} `json:"database"`
}{
Version: programVersion,
Statistics: &globalStatistics,
}
if rows, err := statsStatement.Query(); err == nil {
defer rows.Close()
if rows.Next() {
rows.Scan(&statsStruct.Database.TotalEntries, &statsStruct.Database.TotalSize)
}
}
statBytes, _ := json.MarshalIndent(statsStruct, "", " ")
ctx.ServeBytes(statBytes)
return
}
pathElements := strings.Split(ctx.GetPath(), "/")
if len(pathElements) < 2 {
atomic.AddUint64(&globalStatistics.Http.Code.Code400, 1)
ctx.SetResponseCode(http.StatusBadRequest)
return
}
messageBytes, err := MakyuuIchaival.Bech32Encoding.DecodeString(pathElements[1])
if err != nil {
atomic.AddUint64(&globalStatistics.Http.Code.Code400, 1)
ctx.SetResponseCode(http.StatusBadRequest)
return
}
message := contentmessage.DecodeContentMessage(messageBytes)
if message == nil {
atomic.AddUint64(&globalStatistics.Http.Code.Code400, 1)
ctx.SetResponseCode(http.StatusBadRequest)
return
}
if !IsTrustedPublicKey(message.PublicKey) {
atomic.AddUint64(&globalStatistics.Http.Code.Code403, 1)
ctx.SetResponseCode(http.StatusForbidden)
return
}
pTime := cTime
cTime := time.Now()
ctx.AddTiming("d", "Decode", cTime.Sub(pTime))
result, cacheHit := message.Verify()
pTime = cTime
cTime = time.Now()
if cacheHit {
atomic.AddUint64(&globalStatistics.SignatureCache.SignatureCacheHit, 1)
ctx.AddTimingInformational("vc", "Ed25519 Cache HIT")
} else {
atomic.AddUint64(&globalStatistics.SignatureCache.SignatureCacheMiss, 1)
ctx.AddTimingInformational("vc", "Ed25519 Cache MISS")
}
ctx.AddTiming("v", "Ed25519 Verify", cTime.Sub(pTime))
if !result {
atomic.AddUint64(&globalStatistics.Http.Code.Code403, 1)
ctx.SetResponseCode(http.StatusForbidden)
return
}
if debugOutput {
log.Printf("Serving CID %s", message.Identifier.String())
}
handleQueryRequest(ctx, message.Identifier, pathElements[2:])
} else if ctx.IsOptions() {
setOtherHeaders(ctx)
setCORSHeaders(ctx)
atomic.AddUint64(&globalStatistics.Http.Code.Code204, 1)
ctx.SetResponseCode(http.StatusNoContent)
} else {
atomic.AddUint64(&globalStatistics.Http.Code.Code501, 1)
ctx.SetResponseCode(http.StatusNotImplemented)
}
}
type ContentEntry struct {
Identifier cid.Cid
Path string
Size uint64
}
func handleQuery(rows *sql.Rows, err error) []ContentEntry {
if err != nil {
log.Print(err)
return []ContentEntry{}
}
defer rows.Close()
var result []ContentEntry
for rows.Next() {
var entry ContentEntry
var sha256 multihash.Multihash
var size sql.NullInt64
err := rows.Scan(&entry.Path, &size, &sha256)
if err != nil {
log.Print(err)
break
}
if size.Valid {
entry.Size = uint64(size.Int64)
}
mh, _ := multihash.Encode(sha256, multihash.SHA2_256)
entry.Identifier = cid.NewCidV1(cid.Raw, mh)
result = append(result, entry)
}
return result
}
func getEntriesForCID(identifier cid.Cid) []ContentEntry {
mh, _ := multihash.Decode(identifier.Hash())
if mh.Code == multihash.SHA2_256 {
return handleQuery(sha256Statement.Query(mh.Digest))
} else if mh.Code == multihash.MD5 {
return handleQuery(md5Statement.Query(mh.Digest))
}
return []ContentEntry{}
}
func main() {
certificatePath := flag.String("certificate", "", "Path to SSL certificate file.")
keypairPath := flag.String("keypair", "", "Path to SSL key file.")
pgConnStr := flag.String("connstr", "", "Postgres connection string for postgres database")
listenAddress := flag.String("listen", ":7777", "Address/port to lisent on.")
trustedKeys := flag.String("trusted_keys", "", "Trusted list of public keys, comma separated.")
sniAddressOption := flag.String("sni", "", "Define SNI address if desired. Empty will serve any requests regardless.")
fdLimitOption := flag.Int("fdlimit", 512, "Maximum number of lingering cached open files.")
debugOption := flag.Bool("debug", false, "Output debug information.")
http2Option := flag.Bool("http2", false, "Enable HTTP/2")
http3Option := flag.Bool("http3", false, "Enable HTTP/3")
signatureCacheLimitOption := flag.Int("siglimit", 4096, "Maximum number of lingering valid signature cache results.")
flag.Parse()
fdlimit = *fdLimitOption
contentmessage.SetMessageCacheLimit(*signatureCacheLimitOption)
debugOutput = *debugOption
var err error
if dInfo, ok := debug.ReadBuildInfo(); ok {
for _, s := range dInfo.Settings {
if s.Key == "vcs.revision" {
programVersion = s.Value
} else if s.Key == "vcs.modified" && s.Value == "true" {
programVersion += "-dev"
}
}
}
for _, k := range strings.Split(*trustedKeys, ",") {
var publicKey ed25519.PublicKey
publicKey, err = MakyuuIchaival.Bech32Encoding.DecodeString(strings.Trim(k, " "))
if err != nil {
log.Fatal(err)
}
if len(publicKey) != ed25519.PublicKeySize {
continue
}
trustedPublicKeys = append(trustedPublicKeys, publicKey)
log.Printf("Added public key %s", strings.Trim(k, " "))
}
dbHandle, err = sql.Open("postgres", *pgConnStr)
if err != nil {
log.Fatal(err)
}
defer dbHandle.Close()
sha256Statement, err = dbHandle.Prepare("SELECT path, size, sha256 FROM entries WHERE sha256 = $1;")
if err != nil {
log.Fatal(err)
}
defer sha256Statement.Close()
md5Statement, err = dbHandle.Prepare("SELECT path, size, sha256 FROM entries WHERE md5 = $1;")
if err != nil {
log.Fatal(err)
}
defer md5Statement.Close()
statsStatement, err = dbHandle.Prepare("SELECT COUNT(*) AS count, SUM(size) as size FROM entries;")
if err != nil {
log.Fatal(err)
}
defer statsStatement.Close()
tlsConfiguration, err := tlsutils.NewTLSConfiguration(*certificatePath, *keypairPath, strings.ToLower(*sniAddressOption))
if err != nil {
log.Fatal(err)
}
server := &httputils.Server{
ReadTimeout: time.Second * 15,
IdleTimeout: time.Minute * 5,
ListenAddress: *listenAddress,
TLSConfig: tlsConfiguration,
EnableHTTP2: *http2Option,
EnableHTTP3: *http3Option,
Handler: handle,
Debug: debugOutput,
}
server.Serve()
}