1 package server
2
3 import (
4 "flag"
5 "fmt"
6 "html/template"
7 "io/fs"
8 "log"
9 "net/http"
10 "os"
11 "path/filepath"
12 "plugin"
13 "reflect"
14 "strings"
15
16 "codeberg.org/tslocum/sriracha"
17 . "codeberg.org/tslocum/sriracha/model"
18 )
19
20 type attachHandler func(db sriracha.DB, post *Post, file []byte) (handled bool, err error)
21
22 type attachHandlerInfo struct {
23 Name string
24 Handler attachHandler
25 }
26
27 type postHandler func(db sriracha.DB, post *Post) error
28
29 type postHandlerInfo struct {
30 Name string
31 Handler postHandler
32 }
33
34 type insertHandler func(db sriracha.DB, post *Post) error
35
36 type insertHandlerInfo struct {
37 Name string
38 Handler insertHandler
39 }
40
41 type createHandler func(db sriracha.DB, post *Post) error
42
43 type createHandlerInfo struct {
44 Name string
45 Handler createHandler
46 }
47
48 type reportHandler func(db sriracha.DB, post *Post) error
49
50 type reportHandlerInfo struct {
51 Name string
52 Handler reportHandler
53 }
54
55 type auditHandler func(db sriracha.DB, user string, action string, info string) error
56
57 type auditHandlerInfo struct {
58 Name string
59 Handler auditHandler
60 }
61
62 type serveHandler func(db sriracha.DB, a *Account, w http.ResponseWriter, r *http.Request) (template.HTML, error)
63
64 type serveHandlerInfo struct {
65 Name string
66 Handler serveHandler
67 }
68
69 type pluginInfo struct {
70 ID int
71 Name string
72 FullName string
73 About string
74 Help template.HTML
75 Config []sriracha.PluginConfig
76 Events []string
77 Serve serveHandler
78 }
79
80 var (
81 allPlugins []any
82 allPluginInfo []*pluginInfo
83 allPluginAttachHandlers []attachHandlerInfo
84 allPluginPostHandlers []postHandlerInfo
85 allPluginInsertHandlers []insertHandlerInfo
86 allPluginCreateHandlers []createHandlerInfo
87 allPluginReportHandlers []reportHandlerInfo
88 allPluginAuditHandlers []auditHandlerInfo
89 allPluginServeHandlers []serveHandlerInfo
90 )
91
92
93 func (s *Server) registerPlugin(plugin any) {
94 info := &pluginInfo{
95 ID: len(allPlugins) + 1,
96 }
97
98 v := reflect.ValueOf(plugin)
99 if v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {
100 v = v.Elem()
101 }
102 info.FullName = v.Type().Name()
103 info.Name = strings.ToLower(info.FullName)
104
105 if pAbout, ok := plugin.(sriracha.Plugin); ok {
106 info.About = pAbout.About()
107 } else {
108 log.Fatalf("%s does not implement required methods", info.Name)
109 }
110
111 if pHelp, ok := plugin.(sriracha.PluginWithHelp); ok {
112 info.Help = pHelp.Help()
113 }
114
115 if pConfig, ok := plugin.(sriracha.PluginWithConfig); ok {
116 config := pConfig.Config()
117 for i := range config {
118 err := config[i].Validate()
119 if err != nil {
120 optionName := config[i].Name
121 if strings.TrimSpace(optionName) == "" {
122 optionName = fmt.Sprintf("#%d", i)
123 } else {
124 optionName = fmt.Sprintf(`"%s"`, optionName)
125 }
126 log.Fatalf("%s configuration option %s is invalid: %s", info.Name, optionName, err)
127 } else if config[i].Type == sriracha.TypeBoolean && config[i].Default == "" {
128 config[i].Default = "0"
129 }
130
131 if config[i].Type == sriracha.TypeEnum {
132 config[i].Value = ""
133 } else {
134 config[i].Value = config[i].Default
135 }
136 }
137 info.Config = config
138 }
139
140 if _, ok := plugin.(sriracha.PluginWithUpdate); ok {
141 info.Events = append(info.Events, "Update")
142 }
143
144 if pAttach, ok := plugin.(sriracha.PluginWithAttach); ok {
145 info.Events = append(info.Events, "Attach")
146 allPluginAttachHandlers = append(allPluginAttachHandlers, attachHandlerInfo{strings.ToLower(info.Name), pAttach.Attach})
147 }
148
149 if pPost, ok := plugin.(sriracha.PluginWithPost); ok {
150 info.Events = append(info.Events, "Post")
151 allPluginPostHandlers = append(allPluginPostHandlers, postHandlerInfo{strings.ToLower(info.Name), pPost.Post})
152 }
153
154 if pInsert, ok := plugin.(sriracha.PluginWithInsert); ok {
155 info.Events = append(info.Events, "Insert")
156 allPluginInsertHandlers = append(allPluginInsertHandlers, insertHandlerInfo{strings.ToLower(info.Name), pInsert.Insert})
157 }
158
159 if pCreate, ok := plugin.(sriracha.PluginWithCreate); ok {
160 info.Events = append(info.Events, "Create")
161 allPluginCreateHandlers = append(allPluginCreateHandlers, createHandlerInfo{strings.ToLower(info.Name), pCreate.Create})
162 }
163
164 if pReport, ok := plugin.(sriracha.PluginWithReport); ok {
165 info.Events = append(info.Events, "Report")
166 allPluginReportHandlers = append(allPluginReportHandlers, reportHandlerInfo{strings.ToLower(info.Name), pReport.Report})
167 }
168
169 if pAudit, ok := plugin.(sriracha.PluginWithAudit); ok {
170 info.Events = append(info.Events, "Audit")
171 allPluginAuditHandlers = append(allPluginAuditHandlers, auditHandlerInfo{strings.ToLower(info.Name), pAudit.Audit})
172 }
173
174 if pServe, ok := plugin.(sriracha.PluginWithServe); ok {
175 info.Events = append(info.Events, "Serve")
176 info.Serve = pServe.Serve
177 allPluginServeHandlers = append(allPluginServeHandlers, serveHandlerInfo{strings.ToLower(info.Name), pServe.Serve})
178 }
179
180 if len(info.Events) == 0 {
181 info.Events = append(info.Events, "None")
182 }
183
184 allPlugins = append(allPlugins, plugin)
185 allPluginInfo = append(allPluginInfo, info)
186 }
187
188 func (s *Server) loadPlugin(pluginPath string) error {
189 wrapErr := func(err error) error {
190 return fmt.Errorf("failed to load plugin %s: %s", pluginPath, err)
191 }
192
193 info, err := os.Stat(pluginPath)
194 if err != nil {
195 return wrapErr(err)
196 } else if info.IsDir() {
197 return filepath.WalkDir(pluginPath, func(path string, d fs.DirEntry, err error) error {
198 if err != nil {
199 return err
200 } else if d.IsDir() || path == pluginPath {
201 return nil
202 }
203 return s.loadPlugin(path)
204 })
205 } else if !strings.HasSuffix(pluginPath, ".so") {
206 return nil
207 }
208
209 const pluginExample = "plugins must declare a function named \"Plugin\" which returns a new instance:\n func Plugin() any {\n return &MyPlugin{}\n }"
210 plugin, err := plugin.Open(pluginPath)
211 if err != nil {
212 return wrapErr(err)
213 }
214 pluginSymbol, err := plugin.Lookup("Plugin")
215 if err != nil {
216 return wrapErr(fmt.Errorf("expected function \"Plugin\" was not found: " + pluginExample))
217 }
218 pluginFunc, ok := pluginSymbol.(func() any)
219 if !ok {
220 return wrapErr(fmt.Errorf("symbol \"Plugin\" was found but does not match the expected function signature: " + pluginExample))
221 }
222 s.registerPlugin(pluginFunc())
223 return nil
224 }
225
226 func (s *Server) loadPlugins() error {
227 for _, pluginPath := range flag.Args() {
228 err := s.loadPlugin(pluginPath)
229 if err != nil {
230 return err
231 }
232 }
233 if len(allPluginInfo) != 0 {
234 var plural string
235 if len(allPluginInfo) != 1 {
236 plural = "s"
237 }
238 var names []string
239 for _, info := range allPluginInfo {
240 names = append(names, info.FullName)
241 }
242 fmt.Printf("Loaded %d plugin%s (%s)\n", len(allPluginInfo), plural, strings.Join(names, ", "))
243 }
244 return nil
245 }
246
View as plain text