2024-04-01 05:51:10 +00:00
|
|
|
// s3staticsites: servers http requests from an S3 bucket
|
|
|
|
// Copyright (C) 2024 Finn Herzfeld
|
|
|
|
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as
|
|
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
|
|
// License, or (at your option) any later version.
|
|
|
|
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
2024-02-18 22:30:11 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-04-08 20:21:27 +00:00
|
|
|
"context"
|
2024-02-18 22:30:11 +00:00
|
|
|
"io"
|
|
|
|
"net/http"
|
2024-04-08 20:21:27 +00:00
|
|
|
"strings"
|
2024-02-18 22:30:11 +00:00
|
|
|
|
|
|
|
minio "github.com/minio/minio-go/v7"
|
|
|
|
"golang.org/x/exp/slog"
|
|
|
|
)
|
|
|
|
|
|
|
|
func ListenAndServe(minioClient *minio.Client) error {
|
|
|
|
slog.Info("starting http server", "bind", config.Bind)
|
|
|
|
err := http.ListenAndServe(config.Bind, handler{minio: minioClient})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type handler struct {
|
|
|
|
minio *minio.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
domain := r.Host
|
|
|
|
path := r.URL.Path
|
2024-04-08 20:21:52 +00:00
|
|
|
if path == "" || path == "/" {
|
|
|
|
path = "index.html"
|
|
|
|
}
|
2024-02-18 22:30:11 +00:00
|
|
|
|
2024-04-08 20:21:27 +00:00
|
|
|
object, stat, err := h.get(r.Context(), domain, path)
|
2024-02-18 22:30:11 +00:00
|
|
|
if err != nil {
|
|
|
|
resp := minio.ToErrorResponse(err)
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
|
|
// TODO: custom 404 page
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
2024-04-01 06:20:10 +00:00
|
|
|
w.Write([]byte("404 not found"))
|
|
|
|
slog.Info("served 404", "domain", domain, "path", path)
|
2024-02-18 22:30:11 +00:00
|
|
|
return
|
|
|
|
}
|
2024-04-08 20:21:27 +00:00
|
|
|
slog.Error("error getting object from storage", "bucket", domain, "object", path, "err", err)
|
|
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
2024-02-18 22:30:11 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Add("Content-Type", stat.ContentType)
|
|
|
|
w.Header().Add("ETag", stat.ETag)
|
2024-07-06 07:54:32 +00:00
|
|
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
2024-02-18 22:30:11 +00:00
|
|
|
|
|
|
|
n, err := io.Copy(w, object)
|
|
|
|
if err != nil {
|
|
|
|
slog.Warn("error writting response", "error", err, "domain", domain, "path", path)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
slog.Info("served request", "domain", domain, "path", path, "size_bytes", n)
|
|
|
|
}
|
2024-04-08 20:21:27 +00:00
|
|
|
|
|
|
|
func (h handler) get(ctx context.Context, bucket string, path string) (*minio.Object, *minio.ObjectInfo, error) {
|
|
|
|
object, err := h.minio.GetObject(ctx, bucket, path, minio.GetObjectOptions{})
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
stat, err := object.Stat()
|
|
|
|
if err != nil {
|
|
|
|
resp := minio.ToErrorResponse(err)
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
|
|
if strings.HasSuffix(path, "/index.html") {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
if !strings.HasSuffix(path, "/") {
|
|
|
|
path = path + "/"
|
|
|
|
}
|
|
|
|
path = path + "index.html"
|
|
|
|
return h.get(ctx, bucket, path)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return object, &stat, nil
|
|
|
|
}
|