blob: c31b6b109ed59c13a711cb3ca25ebb6c80908505 [file] [log] [blame]
Akron49ceeb42025-05-23 17:46:01 +02001package main
2
3import (
4 "flag"
5 "fmt"
6 "os"
7 "os/signal"
8 "strings"
9 "syscall"
10
Akronfa55bb22025-05-26 15:10:42 +020011 "github.com/KorAP/KoralPipe-TermMapper/mapper"
Akron49ceeb42025-05-23 17:46:01 +020012 "github.com/gofiber/fiber/v2"
13 "github.com/rs/zerolog"
14 "github.com/rs/zerolog/log"
15)
16
Akron74e1c072025-05-26 14:38:25 +020017const (
18 maxInputLength = 1024 * 1024 // 1MB
19 maxParamLength = 1024 // 1KB
20)
21
Akron49ceeb42025-05-23 17:46:01 +020022type config struct {
23 port int
24 config string
25 logLevel string
26}
27
28func parseFlags() *config {
29 cfg := &config{}
30
31 flag.IntVar(&cfg.port, "port", 8080, "Port to listen on")
32 flag.IntVar(&cfg.port, "p", 8080, "Port to listen on (shorthand)")
33
34 flag.StringVar(&cfg.config, "config", "", "YAML configuration file containing mapping directives")
35 flag.StringVar(&cfg.config, "c", "", "YAML configuration file containing mapping directives (shorthand)")
36
37 flag.StringVar(&cfg.logLevel, "log-level", "info", "Log level (debug, info, warn, error)")
38 flag.StringVar(&cfg.logLevel, "l", "info", "Log level (shorthand)")
39
40 flag.Usage = func() {
41 fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
42 fmt.Fprintf(os.Stderr, "\nA web service for transforming JSON objects using term mapping rules.\n\n")
43 fmt.Fprintf(os.Stderr, "Options:\n")
44 flag.PrintDefaults()
45 }
46
47 flag.Parse()
48
49 if cfg.config == "" {
50 fmt.Fprintln(os.Stderr, "Error: config file is required")
51 flag.Usage()
52 os.Exit(1)
53 }
54
55 return cfg
56}
57
58func setupLogger(level string) {
59 // Parse log level
60 lvl, err := zerolog.ParseLevel(strings.ToLower(level))
61 if err != nil {
62 log.Error().Err(err).Str("level", level).Msg("Invalid log level, defaulting to info")
63 lvl = zerolog.InfoLevel
64 }
65
66 // Configure zerolog
67 zerolog.SetGlobalLevel(lvl)
68 log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
69}
70
71func main() {
72 // Parse command line flags
73 cfg := parseFlags()
74
75 // Set up logging
76 setupLogger(cfg.logLevel)
77
78 // Create a new mapper instance
79 m, err := mapper.NewMapper(cfg.config)
80 if err != nil {
81 log.Fatal().Err(err).Msg("Failed to create mapper")
82 }
83
84 // Create fiber app
85 app := fiber.New(fiber.Config{
86 DisableStartupMessage: true,
Akron74e1c072025-05-26 14:38:25 +020087 BodyLimit: maxInputLength,
Akron49ceeb42025-05-23 17:46:01 +020088 })
89
90 // Set up routes
91 setupRoutes(app, m)
92
93 // Start server
94 go func() {
95 log.Info().Int("port", cfg.port).Msg("Starting server")
96 if err := app.Listen(fmt.Sprintf(":%d", cfg.port)); err != nil {
97 log.Fatal().Err(err).Msg("Server error")
98 }
99 }()
100
101 // Wait for interrupt signal
102 sigChan := make(chan os.Signal, 1)
103 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
104 <-sigChan
105
106 // Graceful shutdown
107 log.Info().Msg("Shutting down server")
108 if err := app.Shutdown(); err != nil {
109 log.Error().Err(err).Msg("Error during shutdown")
110 }
111}
112
113func setupRoutes(app *fiber.App, m *mapper.Mapper) {
114 // Health check endpoint
115 app.Get("/health", func(c *fiber.Ctx) error {
116 return c.SendString("OK")
117 })
118
119 // Transformation endpoint
120 app.Post("/:map/query", handleTransform(m))
121}
122
123func handleTransform(m *mapper.Mapper) fiber.Handler {
124 return func(c *fiber.Ctx) error {
125 // Get parameters
126 mapID := c.Params("map")
127 dir := c.Query("dir", "atob")
128 foundryA := c.Query("foundryA", "")
129 foundryB := c.Query("foundryB", "")
130 layerA := c.Query("layerA", "")
131 layerB := c.Query("layerB", "")
132
Akron74e1c072025-05-26 14:38:25 +0200133 // Validate input parameters
134 if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, c.Body()); err != nil {
135 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
136 "error": err.Error(),
137 })
138 }
139
Akron49ceeb42025-05-23 17:46:01 +0200140 // Validate direction
141 if dir != "atob" && dir != "btoa" {
142 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
143 "error": "invalid direction, must be 'atob' or 'btoa'",
144 })
145 }
146
147 // Parse request body
Akron2cbdab52025-05-23 17:57:10 +0200148 var jsonData any
Akron49ceeb42025-05-23 17:46:01 +0200149 if err := c.BodyParser(&jsonData); err != nil {
150 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
151 "error": "invalid JSON in request body",
152 })
153 }
154
155 // Apply mappings
156 result, err := m.ApplyMappings(mapID, mapper.MappingOptions{
157 Direction: mapper.Direction(dir),
158 FoundryA: foundryA,
159 FoundryB: foundryB,
160 LayerA: layerA,
161 LayerB: layerB,
162 }, jsonData)
163
164 if err != nil {
165 log.Error().Err(err).
166 Str("mapID", mapID).
167 Str("direction", dir).
168 Msg("Failed to apply mappings")
169
170 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
171 "error": err.Error(),
172 })
173 }
174
175 return c.JSON(result)
176 }
177}
Akron74e1c072025-05-26 14:38:25 +0200178
179// validateInput checks if the input parameters are valid
180func validateInput(mapID, dir, foundryA, foundryB, layerA, layerB string, body []byte) error {
181 // Check input lengths
182 if len(mapID) > maxParamLength {
183 return fmt.Errorf("map ID too long (max %d bytes)", maxParamLength)
184 }
185 if len(dir) > maxParamLength {
186 return fmt.Errorf("direction too long (max %d bytes)", maxParamLength)
187 }
188 if len(foundryA) > maxParamLength {
189 return fmt.Errorf("foundryA too long (max %d bytes)", maxParamLength)
190 }
191 if len(foundryB) > maxParamLength {
192 return fmt.Errorf("foundryB too long (max %d bytes)", maxParamLength)
193 }
194 if len(layerA) > maxParamLength {
195 return fmt.Errorf("layerA too long (max %d bytes)", maxParamLength)
196 }
197 if len(layerB) > maxParamLength {
198 return fmt.Errorf("layerB too long (max %d bytes)", maxParamLength)
199 }
200 if len(body) > maxInputLength {
201 return fmt.Errorf("request body too large (max %d bytes)", maxInputLength)
202 }
203
204 // Check for invalid characters in parameters
205 for _, param := range []struct {
206 name string
207 value string
208 }{
209 {"mapID", mapID},
210 {"dir", dir},
211 {"foundryA", foundryA},
212 {"foundryB", foundryB},
213 {"layerA", layerA},
214 {"layerB", layerB},
215 } {
216 if strings.ContainsAny(param.value, "<>{}[]\\") {
217 return fmt.Errorf("%s contains invalid characters", param.name)
218 }
219 }
220
221 return nil
222}