2022-01-16 12:49:45 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/hex"
|
2022-01-19 20:46:05 +00:00
|
|
|
"encoding/json"
|
2022-01-16 12:49:45 +00:00
|
|
|
"flag"
|
2022-01-19 20:46:05 +00:00
|
|
|
"git.gammaspectra.live/S.O.N.G/FinalCommander/content"
|
|
|
|
"git.gammaspectra.live/S.O.N.G/FinalCommander/utilities"
|
2022-01-18 18:54:39 +00:00
|
|
|
"git.gammaspectra.live/S.O.N.G/MakyuuIchaival"
|
|
|
|
"git.gammaspectra.live/S.O.N.G/MakyuuIchaival/httputils"
|
|
|
|
"git.gammaspectra.live/S.O.N.G/MakyuuIchaival/tlsutils"
|
|
|
|
"github.com/cloudflare/circl/sign/ed25519"
|
2022-01-16 12:49:45 +00:00
|
|
|
"github.com/mroth/weightedrand"
|
|
|
|
"github.com/multiformats/go-multihash"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
var privateKey ed25519.PrivateKey
|
|
|
|
|
2022-01-18 18:54:39 +00:00
|
|
|
var debugOutput = false
|
2022-01-19 20:46:05 +00:00
|
|
|
var contentServers []*content.Server
|
2022-01-16 12:49:45 +00:00
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
var db *content.Database
|
2022-01-16 12:49:45 +00:00
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
func selectNextContentServer(skip []int) *content.Server {
|
2022-01-16 12:49:45 +00:00
|
|
|
inSkip := func(i int) bool {
|
|
|
|
for _, c := range skip {
|
|
|
|
if i == c {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
var choosingList []weightedrand.Choice
|
|
|
|
for _, c := range contentServers {
|
2022-01-19 20:46:05 +00:00
|
|
|
if !inSkip(c.Index) && c.GetCheckResult() {
|
2022-01-16 12:49:45 +00:00
|
|
|
choosingList = append(choosingList, weightedrand.NewChoice(c, c.Weight))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
chooser, err := weightedrand.NewChooser(choosingList...)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
return chooser.Pick().(*content.Server)
|
2022-01-16 12:49:45 +00:00
|
|
|
}
|
|
|
|
|
2022-01-18 18:54:39 +00:00
|
|
|
func setOtherHeaders(ctx *httputils.RequestContext) {
|
|
|
|
ctx.SetResponseHeader("Server", "FinalCommander")
|
|
|
|
ctx.SetResponseHeader("Vary", "Content-Encoding")
|
|
|
|
ctx.SetResponseHeader("X-Content-Type-Options", "nosniff")
|
|
|
|
ctx.SetResponseHeader("X-Robots-Tags", "noindex, nofollow, notranslate")
|
|
|
|
ctx.SetResponseHeader("Referrer-Policy", "origin")
|
2022-01-16 12:49:45 +00:00
|
|
|
}
|
2022-01-18 18:54:39 +00:00
|
|
|
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", "*")
|
2022-01-16 12:49:45 +00:00
|
|
|
|
|
|
|
//CORP, COEP, COOP
|
2022-01-18 18:54:39 +00:00
|
|
|
ctx.SetResponseHeader("Cross-Origin-Embedder-Policy", "require-corp")
|
|
|
|
ctx.SetResponseHeader("Cross-Origin-Resource-Policy", "cross-origin")
|
|
|
|
ctx.SetResponseHeader("Cross-Origin-Opener-Policy", "unsafe-none")
|
2022-01-16 12:49:45 +00:00
|
|
|
}
|
|
|
|
|
2022-01-18 18:54:39 +00:00
|
|
|
func handle(ctx *httputils.RequestContext) {
|
|
|
|
if len(ctx.GetRequestHeader("Host")) > 0 && ctx.GetRequestHeader("Host") == ctx.GetTLSServerName() { //Prevents rebinding / DNS stuff
|
|
|
|
ctx.SetResponseCode(http.StatusNotFound)
|
2022-01-16 12:49:45 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-18 18:54:39 +00:00
|
|
|
if ctx.IsGet() || ctx.IsHead() {
|
|
|
|
if debugOutput {
|
|
|
|
log.Printf("Serve %s", ctx.GetPath())
|
|
|
|
}
|
|
|
|
setOtherHeaders(ctx)
|
|
|
|
setCORSHeaders(ctx)
|
|
|
|
pathElements := strings.Split(ctx.GetPath(), "/")
|
2022-01-16 12:49:45 +00:00
|
|
|
if len(pathElements) < 3 {
|
2022-01-18 18:54:39 +00:00
|
|
|
ctx.SetResponseCode(http.StatusBadRequest)
|
2022-01-16 12:49:45 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
hashType := strings.ToLower(pathElements[1])
|
|
|
|
|
|
|
|
hash, err := hex.DecodeString(pathElements[2])
|
|
|
|
if err != nil {
|
2022-01-18 18:54:39 +00:00
|
|
|
ctx.SetResponseCode(http.StatusBadRequest)
|
2022-01-16 12:49:45 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-18 18:54:39 +00:00
|
|
|
var mh multihash.Multihash
|
2022-01-16 12:49:45 +00:00
|
|
|
if hashType == "sha256" && len(hash) == 32 {
|
2022-01-18 18:54:39 +00:00
|
|
|
mh, _ = multihash.Encode(hash, multihash.SHA2_256)
|
2022-01-16 12:49:45 +00:00
|
|
|
} else if hashType == "md5" && len(hash) == 16 {
|
2022-01-18 18:54:39 +00:00
|
|
|
mh, _ = multihash.Encode(hash, multihash.MD5)
|
2022-01-16 12:49:45 +00:00
|
|
|
} else {
|
2022-01-19 20:46:05 +00:00
|
|
|
ctx.SetResponseCode(http.StatusNotImplemented)
|
2022-01-16 12:49:45 +00:00
|
|
|
return
|
|
|
|
}
|
2022-01-19 20:46:05 +00:00
|
|
|
key := content.NewHashIdentifierFromMultihash(mh)
|
2022-01-16 12:49:45 +00:00
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
var skip []int
|
|
|
|
|
|
|
|
entry := getContentEntry(key)
|
|
|
|
|
|
|
|
if len(pathElements) > 3 {
|
|
|
|
|
|
|
|
if pathElements[3] == "information" {
|
|
|
|
ctx.SetResponseHeader("Content-Type", "application/json")
|
|
|
|
if entry != nil {
|
|
|
|
b, _ := json.Marshal(struct {
|
|
|
|
Known bool `json:"known"`
|
|
|
|
CID string `json:"cid"`
|
|
|
|
AccessTime int64 `json:"accessTime"`
|
|
|
|
CheckTime int64 `json:"checkTime"`
|
|
|
|
InvalidCount int `json:"invalidCount"`
|
|
|
|
}{
|
|
|
|
Known: true,
|
|
|
|
CID: entry.CID().String(),
|
|
|
|
AccessTime: entry.AccessTime,
|
|
|
|
CheckTime: entry.CheckTime,
|
|
|
|
InvalidCount: len(entry.InvalidList),
|
|
|
|
})
|
|
|
|
|
|
|
|
ctx.ServeBytes(b)
|
|
|
|
} else {
|
|
|
|
b, _ := json.Marshal(struct {
|
|
|
|
Known bool `json:"known"`
|
|
|
|
CID string `json:"cid"`
|
|
|
|
}{
|
|
|
|
Known: false,
|
|
|
|
CID: key.CID().String(),
|
|
|
|
})
|
|
|
|
|
|
|
|
ctx.ServeBytes(b)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
//TODO: update entries with these instant returns
|
|
|
|
data, err := MakyuuIchaival.Bech32Encoding.DecodeString(pathElements[3])
|
|
|
|
if err != nil {
|
|
|
|
ctx.SetResponseCode(http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
skip = utilities.DecodeIntegerList(data)
|
2022-01-16 12:49:45 +00:00
|
|
|
}
|
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
if entry != nil {
|
|
|
|
for _, ci := range skip {
|
|
|
|
if !entry.InInvalidList(ci) {
|
|
|
|
skip = append(skip, ci)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
contentServer := selectNextContentServer(skip)
|
|
|
|
if contentServer == nil {
|
|
|
|
ctx.SetResponseCode(http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
2022-01-16 12:49:45 +00:00
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
ctx.DoRedirect(contentServer.GetContentURL(entry, privateKey, skip), http.StatusFound)
|
|
|
|
} else {
|
|
|
|
contentServer := selectNextContentServer(skip)
|
|
|
|
if contentServer == nil {
|
|
|
|
ctx.SetResponseCode(http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
2022-01-16 12:49:45 +00:00
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
//TODO: only trigger this when we don't get a 404
|
|
|
|
go func() {
|
|
|
|
|
|
|
|
//e.CheckTime = time.Now().UTC().Unix() + 3600*24*365 //force a check next year as necessary
|
|
|
|
|
|
|
|
var newInvalidList []int
|
|
|
|
var e *content.Entry
|
|
|
|
|
|
|
|
for _, c := range contentServers {
|
|
|
|
if !c.GetCheckResult() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
result := c.CheckEntryKey(key, privateKey)
|
|
|
|
if result != nil {
|
|
|
|
if e == nil {
|
|
|
|
e = &content.Entry{
|
|
|
|
Key: *result,
|
|
|
|
Version: 0,
|
|
|
|
AccessTime: time.Now().UTC().Unix(),
|
|
|
|
CheckTime: time.Now().UTC().Unix() + 3600*24*3, // Check sooner after addition, not all servers might have it yet
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
newInvalidList = append(newInvalidList, c.Index)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if e != nil {
|
|
|
|
if !key.IsKey() { //Check for entry already existing
|
|
|
|
entry := getContentEntry(&e.Key)
|
|
|
|
|
|
|
|
if entry != nil {
|
|
|
|
e = entry
|
|
|
|
}
|
|
|
|
//Add alias mapping
|
|
|
|
_ = db.SetAlias(&content.Alias{
|
|
|
|
Key: *key,
|
|
|
|
Identifier: e.Key,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
e.AccessTime = time.Now().UTC().Unix()
|
|
|
|
e.InvalidList = newInvalidList
|
|
|
|
_ = db.SetEntry(e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
ctx.DoRedirect(contentServer.GetHashURL(mh, privateKey, skip), http.StatusFound)
|
|
|
|
}
|
2022-01-18 18:54:39 +00:00
|
|
|
} else if ctx.IsOptions() {
|
|
|
|
setOtherHeaders(ctx)
|
|
|
|
setCORSHeaders(ctx)
|
|
|
|
ctx.SetResponseCode(http.StatusNoContent)
|
2022-01-16 12:49:45 +00:00
|
|
|
} else {
|
2022-01-18 18:54:39 +00:00
|
|
|
ctx.SetResponseCode(http.StatusNotImplemented)
|
2022-01-16 12:49:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
func getContentEntry(key *content.HashIdentifier) *content.Entry {
|
|
|
|
entry := db.GetEntry(*key)
|
|
|
|
|
|
|
|
if entry != nil {
|
|
|
|
entry.UpdateAccessTime(db)
|
|
|
|
|
|
|
|
if entry.NeedsCheck() {
|
|
|
|
go func() {
|
|
|
|
b, _ := entry.Encode() //Encode/decode to copy object
|
|
|
|
e := content.DecodeEntry(entry.Key, b)
|
|
|
|
e.CheckTime = time.Now().UTC().Unix() + 3600*24*365 //force a check next year as necessary
|
|
|
|
|
|
|
|
var newInvalidList []int
|
|
|
|
|
|
|
|
for _, c := range contentServers {
|
|
|
|
if !c.GetCheckResult() {
|
|
|
|
if e.InInvalidList(c.Index) { //Keep old result if down
|
|
|
|
newInvalidList = append(newInvalidList, c.Index)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if c.CheckEntryKey(&e.Key, privateKey) == nil {
|
|
|
|
newInvalidList = append(newInvalidList, c.Index)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
e.InvalidList = newInvalidList
|
|
|
|
_ = db.SetEntry(e)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return entry
|
|
|
|
}
|
|
|
|
|
2022-01-16 12:49:45 +00:00
|
|
|
func checkContentServers() {
|
|
|
|
for _, c := range contentServers {
|
2022-01-19 20:46:05 +00:00
|
|
|
c.Check()
|
2022-01-16 12:49:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
//TODO: OCSP
|
2022-01-16 14:25:17 +00:00
|
|
|
certificatePath := flag.String("certificate", "", "Path to SSL certificate file.")
|
|
|
|
keypairPath := flag.String("keypair", "", "Path to SSL key file.")
|
2022-01-19 20:46:05 +00:00
|
|
|
databasePath := flag.String("dbpath", "database", "Path to key/value database.")
|
2022-01-16 12:49:45 +00:00
|
|
|
|
|
|
|
listenAddress := flag.String("listen", ":7777", "Address/port to lisent on.")
|
|
|
|
|
|
|
|
weightedServerList := flag.String("servers", "", "Weighted list of servers to use. All use HTTPs. Format address:PORT/WEIGHT,[...]")
|
|
|
|
|
|
|
|
sniAddressOption := flag.String("sni", "", "Define SNI address if desired. Empty will serve any requests regardless.")
|
|
|
|
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
privateKeyEnv := os.Getenv("PRIVATE_KEY")
|
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
if privateKeyEnv != "" {
|
|
|
|
log.Print("PRIVATE_KEY is deprecated. Use PRIVATE_SEED instead with seed")
|
|
|
|
privateKey, _ := MakyuuIchaival.Bech32Encoding.DecodeString(privateKeyEnv)
|
|
|
|
|
|
|
|
log.Printf("Private Ed25519 seed (keep safe!): %s", MakyuuIchaival.Bech32Encoding.EncodeToString(ed25519.PrivateKey(privateKey).Seed()))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
privateSeedEnv := os.Getenv("PRIVATE_SEED")
|
|
|
|
|
|
|
|
if privateSeedEnv == "" {
|
|
|
|
log.Print("No PRIVATE_SEED environment variable specified, generating new identity")
|
2022-01-16 12:49:45 +00:00
|
|
|
publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
|
|
|
|
|
2022-01-18 18:54:39 +00:00
|
|
|
log.Printf("Public Ed25519 key (share this): %s", MakyuuIchaival.Bech32Encoding.EncodeToString(publicKey))
|
2022-01-19 20:46:05 +00:00
|
|
|
log.Printf("Private Ed25519 seed (keep safe!): %s", MakyuuIchaival.Bech32Encoding.EncodeToString(privateKey.Seed()))
|
2022-01-16 12:49:45 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
privateSeed, err := MakyuuIchaival.Bech32Encoding.DecodeString(privateSeedEnv)
|
2022-01-16 12:49:45 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2022-01-19 20:46:05 +00:00
|
|
|
|
|
|
|
if len(privateSeed) != ed25519.SeedSize {
|
|
|
|
log.Fatal("Wrong Private key length")
|
|
|
|
}
|
|
|
|
|
|
|
|
privateKey = ed25519.NewKeyFromSeed(privateSeed)
|
2022-01-16 12:49:45 +00:00
|
|
|
publicKey := make([]byte, ed25519.PublicKeySize)
|
2022-01-19 20:46:05 +00:00
|
|
|
copy(publicKey, privateKey[ed25519.PublicKeySize:])
|
2022-01-18 18:54:39 +00:00
|
|
|
log.Printf("Loaded Private Ed25519 key, Public %s", MakyuuIchaival.Bech32Encoding.EncodeToString(publicKey))
|
2022-01-16 12:49:45 +00:00
|
|
|
|
2022-01-19 20:46:05 +00:00
|
|
|
db, err = content.OpenDatabase(*databasePath)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
2022-01-16 12:49:45 +00:00
|
|
|
}
|
2022-01-19 20:46:05 +00:00
|
|
|
defer db.Close()
|
2022-01-16 12:49:45 +00:00
|
|
|
|
|
|
|
for i, s := range strings.Split(*weightedServerList, ",") {
|
2022-01-19 20:46:05 +00:00
|
|
|
cs, err := content.NewContentServerFromArgument(s, i)
|
2022-01-16 12:49:45 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2022-01-19 20:46:05 +00:00
|
|
|
contentServers = append(contentServers, cs)
|
2022-01-16 12:49:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
checkContentServers()
|
|
|
|
|
2022-01-17 09:48:32 +00:00
|
|
|
go func() {
|
|
|
|
ticker := time.NewTicker(1 * time.Minute)
|
|
|
|
for _ = range ticker.C {
|
|
|
|
checkContentServers()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2022-01-18 18:54:39 +00:00
|
|
|
tlsConfiguration, err := tlsutils.NewTLSConfiguration(*certificatePath, *keypairPath, *sniAddressOption)
|
2022-01-16 12:49:45 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2022-01-16 14:25:17 +00:00
|
|
|
|
2022-01-18 18:54:39 +00:00
|
|
|
server := &httputils.Server{
|
|
|
|
ListenAddress: *listenAddress,
|
|
|
|
TLSConfig: tlsConfiguration,
|
|
|
|
EnableHTTP2: true,
|
|
|
|
EnableHTTP3: true,
|
|
|
|
Handler: handle,
|
|
|
|
Debug: debugOutput,
|
2022-01-16 12:49:45 +00:00
|
|
|
}
|
|
|
|
|
2022-01-18 18:54:39 +00:00
|
|
|
server.Serve()
|
2022-01-16 12:49:45 +00:00
|
|
|
}
|