diff --git a/.gitignore b/.gitignore index 7209c54..ae0914e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ # Go workspace file go.work + +# jetbrains editor +.idea \ No newline at end of file diff --git a/cmd/federation.go b/cmd/federation.go index 46b924c..1911d15 100644 --- a/cmd/federation.go +++ b/cmd/federation.go @@ -11,17 +11,24 @@ import ( ) func main() { + // bootstrap logger + logger := log.NewLogger("main") + // Load connection string from .env file err := godotenv.Load() if err != nil { - log.Warn(fmt.Sprintf("failed to load env, %v", err)) + logger.Warn(fmt.Sprintf("failed to load env, %v", err)) } + conn, err := db.Connect() if err != nil { - log.Fatal("failed connecting to db", err) + logger.Fatal("failed connecting to db", err) } defer conn.Close() db.RunMigrations(conn) - http.RunServer() + + s := http.NewServer(logger) + s.RunServer() + os.Exit(0) } diff --git a/internal/db/migrate.go b/internal/db/migrate.go index 2c3ad75..dd628f1 100644 --- a/internal/db/migrate.go +++ b/internal/db/migrate.go @@ -17,18 +17,19 @@ import ( var migrations embed.FS func RunMigrations(db *sql.DB) { - log.Debug("Running migrations...") + logger := log.NewLogger("db migrations") + logger.Debug("Running migrations...") driver, err := mysql.WithInstance(db, &mysql.Config{}) if err != nil { - log.Fatal("Error getting MySQL driver", err) + logger.Fatal("Error getting MySQL driver", err) } source, _ := iofs.New(migrations, "migrations") m, err := migrate.NewWithInstance("iofs", source, "mysql", driver) if err != nil { - log.Fatal("Error connecting to database", err) + logger.Fatal("Error connecting to database", err) } if err := m.Up(); err != nil && fmt.Sprintf("%s", err) != "no change" { - log.Fatal("Error running migrations", err) + logger.Fatal("Error running migrations", err) } - log.Debug("Done!") + logger.Debug("Done!") } diff --git a/internal/http/routes/activity.go b/internal/http/activity.go similarity index 69% rename from internal/http/routes/activity.go rename to internal/http/activity.go index 4edc242..996db35 100644 --- a/internal/http/routes/activity.go +++ b/internal/http/activity.go @@ -1,14 +1,12 @@ -package routes +package http import ( "context" "encoding/json" + "fmt" "net/http" "sublinks/sublinks-federation/internal/activitypub" "sublinks/sublinks-federation/internal/lemmy" - "sublinks/sublinks-federation/internal/log" - - "fmt" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -16,18 +14,18 @@ import ( "github.com/gorilla/mux" ) -func SetupActivityRoutes(r *mux.Router) { - r.HandleFunc("/activities/{action}/{id}", getActivityHandler).Methods("GET") +func (server *Server) SetupActivityRoutes() { + server.Router.HandleFunc("/activities/{action}/{id}", server.getActivityHandler).Methods("GET") } -func getActivityHandler(w http.ResponseWriter, r *http.Request) { +func (server *Server) getActivityHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) var content []byte switch vars["action"] { case "create": - obj, err := GetPostActivityObject(vars["id"]) + obj, err := server.GetPostActivityObject(vars["id"]) if err != nil { - log.Error("Error reading object", err) + server.Logger.Error("Error reading object", err) w.WriteHeader(http.StatusInternalServerError) return } @@ -54,12 +52,12 @@ func getActivityHandler(w http.ResponseWriter, r *http.Request) { w.Write(content) } -func GetPostActivityObject(id string) (*activitypub.Post, error) { +func (server *Server) GetPostActivityObject(id string) (*activitypub.Post, error) { ctx := context.Background() c := lemmy.GetLemmyClient(ctx) post, err := c.GetPost(ctx, id) if err != nil { - log.Error("Error reading post", err) + server.Logger.Error("Error reading post", err) return nil, err } return activitypub.ConvertPostToApub(post), nil diff --git a/internal/http/apub.go b/internal/http/apub.go new file mode 100644 index 0000000..0f34218 --- /dev/null +++ b/internal/http/apub.go @@ -0,0 +1,32 @@ +package http + +import ( + "net/http" +) + +func (server *Server) SetupApubRoutes() { + server.Router.HandleFunc("/users/{user}/inbox", server.getInboxHandler).Methods("GET") + server.Router.HandleFunc("/users/{user}/inbox", server.postInboxHandler).Methods("POST") + server.Router.HandleFunc("/users/{user}/outbox", server.getOutboxHandler).Methods("GET") + server.Router.HandleFunc("/users/{user}/outbox", server.postOutboxHandler).Methods("POST") +} + +func (server *Server) getInboxHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Add("content-type", "application/activity+json") +} + +func (server *Server) postInboxHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Add("content-type", "application/activity+json") +} + +func (server *Server) getOutboxHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Add("content-type", "application/activity+json") +} + +func (server *Server) postOutboxHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Add("content-type", "application/activity+json") +} diff --git a/internal/http/middleware.go b/internal/http/middleware.go new file mode 100644 index 0000000..9bb8e53 --- /dev/null +++ b/internal/http/middleware.go @@ -0,0 +1,33 @@ +package http + +import ( + "encoding/json" + "net/http" +) + +type RequestError struct { + Msg string `json:"message"` +} + +func (server *Server) logMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server.Logger.Request("", r) + next.ServeHTTP(w, r) + }) +} + +func (server *Server) notFound(w http.ResponseWriter, r *http.Request) { + server.Logger.Request("404 Not Found", r) + w.WriteHeader(http.StatusNotFound) + w.Header().Add("content-type", "application/activity+json") + content, _ := json.Marshal(RequestError{Msg: "not found"}) + w.Write(content) +} + +func (server *Server) notAllowedMethod(w http.ResponseWriter, r *http.Request) { + server.Logger.Request("405 Method Not Allowed", r) + w.WriteHeader(http.StatusNotFound) + w.Header().Add("content-type", "application/activity+json") + content, _ := json.Marshal(RequestError{Msg: "method not allowed"}) + w.Write(content) +} diff --git a/internal/http/routes/post.go b/internal/http/post.go similarity index 69% rename from internal/http/routes/post.go rename to internal/http/post.go index 635cc25..859b871 100644 --- a/internal/http/routes/post.go +++ b/internal/http/post.go @@ -1,27 +1,25 @@ -package routes +package http import ( "context" "encoding/json" + "github.com/gorilla/mux" "net/http" "sublinks/sublinks-federation/internal/activitypub" "sublinks/sublinks-federation/internal/lemmy" - "sublinks/sublinks-federation/internal/log" - - "github.com/gorilla/mux" ) -func SetupPostRoutes(r *mux.Router) { - r.HandleFunc("/post/{postId}", getPostHandler).Methods("GET") +func (server *Server) SetupPostRoutes() { + server.Router.HandleFunc("/post/{postId}", server.getPostHandler).Methods("GET") } -func getPostHandler(w http.ResponseWriter, r *http.Request) { +func (server *Server) getPostHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) ctx := context.Background() c := lemmy.GetLemmyClient(ctx) post, err := c.GetPost(ctx, vars["postId"]) if err != nil { - log.Error("Error reading post", err) + server.Logger.Error("Error reading post", err) return } postLd := activitypub.ConvertPostToApub(post) diff --git a/internal/http/routes/apub.go b/internal/http/routes/apub.go deleted file mode 100644 index b1d3016..0000000 --- a/internal/http/routes/apub.go +++ /dev/null @@ -1,34 +0,0 @@ -package routes - -import ( - "net/http" - - "github.com/gorilla/mux" -) - -func SetupApubRoutes(r *mux.Router) { - r.HandleFunc("/users/{user}/inbox", getInboxHandler).Methods("GET") - r.HandleFunc("/users/{user}/inbox", postInboxHandler).Methods("POST") - r.HandleFunc("/users/{user}/outbox", getOutboxHandler).Methods("GET") - r.HandleFunc("/users/{user}/outbox", postOutboxHandler).Methods("POST") -} - -func getInboxHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Header().Add("content-type", "application/activity+json") -} - -func postInboxHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Header().Add("content-type", "application/activity+json") -} - -func getOutboxHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Header().Add("content-type", "application/activity+json") -} - -func postOutboxHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Header().Add("content-type", "application/activity+json") -} diff --git a/internal/http/routes/routes.go b/internal/http/routes/routes.go deleted file mode 100644 index a82c5e1..0000000 --- a/internal/http/routes/routes.go +++ /dev/null @@ -1,48 +0,0 @@ -package routes - -import ( - "encoding/json" - "net/http" - "sublinks/sublinks-federation/internal/log" - - "github.com/gorilla/mux" -) - -func SetupRoutes() *mux.Router { - r := mux.NewRouter() - SetupUserRoutes(r) - SetupPostRoutes(r) - SetupApubRoutes(r) - SetupActivityRoutes(r) - r.NotFoundHandler = http.HandlerFunc(notFound) - r.MethodNotAllowedHandler = http.HandlerFunc(notAllowedMethod) - r.Use(logMiddleware) - return r -} - -func logMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Request("", r) - next.ServeHTTP(w, r) - }) -} - -type RequestError struct { - Msg string `json:"message"` -} - -func notFound(w http.ResponseWriter, r *http.Request) { - log.Request("404 Not Found", r) - w.WriteHeader(http.StatusNotFound) - w.Header().Add("content-type", "application/activity+json") - content, _ := json.Marshal(RequestError{Msg: "not found"}) - w.Write(content) -} - -func notAllowedMethod(w http.ResponseWriter, r *http.Request) { - log.Request("405 Method Not Allowed", r) - w.WriteHeader(http.StatusNotFound) - w.Header().Add("content-type", "application/activity+json") - content, _ := json.Marshal(RequestError{Msg: "method not allowed"}) - w.Write(content) -} diff --git a/internal/http/server.go b/internal/http/server.go index 2e76ce5..086239d 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -3,20 +3,40 @@ package http import ( "context" "flag" + "github.com/gorilla/mux" "net/http" "os" "os/signal" - "sublinks/sublinks-federation/internal/http/routes" "sublinks/sublinks-federation/internal/log" "time" ) -func RunServer() { +type Server struct { + *mux.Router + log.Logger +} + +func NewServer(logger log.Logger) *Server { + r := mux.NewRouter() + + return &Server{ + Router: r, + Logger: logger, + } +} + +func (server *Server) RunServer() { var wait time.Duration flag.DurationVar(&wait, "graceful-timeout", time.Second*15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m") flag.Parse() - r := routes.SetupRoutes() + server.SetupUserRoutes() + server.SetupPostRoutes() + server.SetupApubRoutes() + server.SetupActivityRoutes() + server.NotFoundHandler = http.HandlerFunc(server.notFound) + server.MethodNotAllowedHandler = http.HandlerFunc(server.notAllowedMethod) + server.Use(server.logMiddleware) srv := &http.Server{ Addr: "0.0.0.0:8080", @@ -24,14 +44,15 @@ func RunServer() { WriteTimeout: time.Second * 15, ReadTimeout: time.Second * 15, IdleTimeout: time.Second * 60, - Handler: r, // Pass our instance of gorilla/mux in. + // pass embed of Server for *mux + Handler: server, } // Run our server in a goroutine so that it doesn't block. go func() { - log.Info("Starting server") + server.Logger.Info("Starting server") if err := srv.ListenAndServe(); err != nil { - log.Error("Error starting server", err) + server.Logger.Error("Error starting server", err) } }() @@ -52,5 +73,5 @@ func RunServer() { // Optionally, you could run srv.Shutdown in a goroutine and block on // <-ctx.Done() if your application should wait for other services // to finalize based on context cancellation. - log.Info("shutting down") + server.Logger.Info("shutting down") } diff --git a/internal/http/routes/user.go b/internal/http/user.go similarity index 62% rename from internal/http/routes/user.go rename to internal/http/user.go index 10abdda..5224bc1 100644 --- a/internal/http/routes/user.go +++ b/internal/http/user.go @@ -1,29 +1,27 @@ -package routes +package http import ( "context" "encoding/json" "fmt" + "github.com/gorilla/mux" "net/http" "sublinks/sublinks-federation/internal/activitypub" "sublinks/sublinks-federation/internal/lemmy" - "sublinks/sublinks-federation/internal/log" - - "github.com/gorilla/mux" ) -func SetupUserRoutes(r *mux.Router) { - r.HandleFunc("/u/{user}", getUserInfoHandler).Methods("GET") +func (server *Server) SetupUserRoutes() { + server.Router.HandleFunc("/u/{user}", server.getUserInfoHandler).Methods("GET") } -func getUserInfoHandler(w http.ResponseWriter, r *http.Request) { +func (server *Server) getUserInfoHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) ctx := context.Background() c := lemmy.GetLemmyClient(ctx) - log.Info(fmt.Sprintf("Looking up user %s", vars["user"])) + server.Logger.Info(fmt.Sprintf("Looking up user %s", vars["user"])) user, err := c.GetUser(ctx, vars["user"]) if err != nil { - log.Error("Error reading user", err) + server.Logger.Error("Error reading user", err) return } diff --git a/internal/log/log.go b/internal/log/log.go index ea5fc61..1aed252 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -2,54 +2,75 @@ package log import ( "encoding/json" + "fmt" + "github.com/rs/zerolog" "io" "net/http" - - "github.com/rs/zerolog/log" + "os" ) -func init() { - log.Debug().Msg("Logger started") +type Logger interface { + Info(msg string) + Debug(msg string) + Error(msg string, err error) + Fatal(msg string, err error) + Warn(msg string) + Request(msg string, r *http.Request) +} + +type Log struct { + *zerolog.Logger } -func Info(msg string) { - log.Info().Msg(msg) +func NewLogger(name string) *Log { + logger := zerolog.New(os.Stdout) + logger.Debug().Msg(fmt.Sprintf("%s logger started", name)) + return &Log{&logger} } -func Debug(msg string) { - log.Debug().Msg(msg) +func (logger *Log) Info(msg string) { + logger.Logger.Info().Msg(msg) } -func Error(msg string, err error) { - log.Error().Err(err).Msg(msg) +func (logger *Log) Debug(msg string) { + logger.Logger.Debug().Msg(msg) } -func Fatal(msg string, err error) { - log.Fatal().Err(err).Msg(msg) +func (logger *Log) Error(msg string, err error) { + logger.Logger.Error().Err(err).Msg(msg) } -func Warn(msg string) { - log.Warn().Msg(msg) +func (logger *Log) Fatal(msg string, err error) { + logger.Logger.Fatal().Err(err).Msg(msg) } -func Request(msg string, r *http.Request) { - defer r.Body.Close() +func (logger *Log) Warn(msg string) { + logger.Logger.Warn().Msg(msg) +} + +func (logger *Log) Request(msg string, r *http.Request) { + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + panic("error closing http io reader") + } + }(r.Body) var body interface{} rawbody, err := io.ReadAll(r.Body) if err != nil { - Error("Error reading request body", err) + logger.Error("Error reading request body", err) body = nil } if r.ContentLength > 0 && r.Header.Get("Content-Type") == "application/json" { err = json.Unmarshal(rawbody, &body) if err != nil { - Error("Error parsing request body into json", err) + logger.Error("Error parsing request body into json", err) body = nil } } else { body = rawbody } - log.Debug(). + logger.Logger.Debug(). Str("method", r.Method). Str("url", r.URL.String()). Str("user-agent", r.UserAgent()).