• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

go-pkgz / jrpc / 12736946946

12 Jan 2025 08:45PM UTC coverage: 89.868% (-0.8%) from 90.667%
12736946946

Pull #14

github

paskal
Switch to tollbooth v8
Pull Request #14: Switch to tollbooth v8

0 of 3 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

204 of 227 relevant lines covered (89.87%)

8.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.41
/server.go
1
package jrpc
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "fmt"
7
        "net/http"
8
        "sync"
9
        "time"
10

11
        "github.com/didip/tollbooth/v8"
12
        "github.com/didip/tollbooth/v8/limiter"
13
        "github.com/go-chi/chi/v5"
14
        "github.com/go-chi/chi/v5/middleware"
15
        "github.com/go-chi/render"
16
        "github.com/go-pkgz/rest"
17
        "github.com/go-pkgz/rest/logger"
18
)
19

20
// Server is json-rpc server with an optional basic auth
21
type Server struct {
22
        api string // url path, i.e. "/command" or "/rpc" etc., required
23

24
        authUser          string      // basic auth user name, should match Client.AuthUser, optional
25
        authPasswd        string      // basic auth password, should match Client.AuthPasswd, optional
26
        customMiddlewares middlewares // list of custom middlewares, should match array of http.Handler func, optional
27

28
        signature signaturePayload // add server signature to server response headers appName, author, version), disable by default
29

30
        timeouts Timeouts // values and timeouts for the server
31
        limits   limits   // values and limits for the server
32
        logger   L        // logger, if nil will default to NoOpLogger
33

34
        funcs struct {
35
                m    map[string]ServerFn
36
                once sync.Once
37
        }
38

39
        httpServer struct {
40
                *http.Server
41
                sync.Mutex
42
        }
43
}
44

45
// Timeouts includes values and timeouts for the server
46
type Timeouts struct {
47
        ReadHeaderTimeout time.Duration // amount of time allowed to read request headers
48
        WriteTimeout      time.Duration // max duration before timing out writes of the response
49
        IdleTimeout       time.Duration // max amount of time to wait for the next request when keep-alive enabled
50
        CallTimeout       time.Duration // max time allowed to finish the call, optional
51
}
52

53
// limits includes limits values for a server
54
type limits struct {
55
        serverThrottle int     // max number of parallel calls for the server
56
        clientLimit    float64 // max number of call/sec per client
57
}
58

59
// signaturePayload is the server application info which add to server response headers
60
type signaturePayload struct {
61
        appName string // server version, injected from main and used for informational headers only
62
        author  string // plugin name, injected from main and used for informational headers only
63
        version string // custom application server number
64
}
65

66
// ServerFn handler registered for each method with Add or Group.
67
// Implementations provided by consumer and defines response logic.
68
type ServerFn func(id uint64, params json.RawMessage) Response
69

70
// middlewares contains list of custom middlewares which user can attach to server
71
type middlewares []func(http.Handler) http.Handler
72

73
// NewServer the main constructor of server instance which pass API url and another options values
74
func NewServer(api string, options ...Option) *Server {
19✔
75

19✔
76
        srv := &Server{
19✔
77
                api:      api,
19✔
78
                timeouts: getDefaultTimeouts(),
19✔
79
                logger:   NoOpLogger,
19✔
80
        }
19✔
81

19✔
82
        for _, opt := range options {
30✔
83
                opt(srv)
11✔
84
        }
11✔
85
        return srv
19✔
86
}
87

88
// Run http server on given port
89
func (s *Server) Run(port int) error {
10✔
90

10✔
91
        if s.authUser == "" || s.authPasswd == "" {
17✔
92
                s.logger.Logf("[WARN] extension server runs without auth")
7✔
93
        }
7✔
94

95
        if s.funcs.m == nil && len(s.funcs.m) == 0 {
11✔
96
                return fmt.Errorf("nothing mapped for dispatch, Add has to be called prior to Run")
1✔
97
        }
1✔
98

99
        router := chi.NewRouter()
9✔
100

9✔
101
        if s.limits.serverThrottle > 0 {
9✔
102
                router.Use(middleware.Throttle(s.limits.serverThrottle))
×
103
        }
×
104

105
        router.Use(middleware.RealIP, rest.Ping, rest.Recoverer(s.logger))
9✔
106

9✔
107
        if s.signature.version != "" || s.signature.author != "" || s.signature.appName != "" {
10✔
108
                router.Use(rest.AppInfo(s.signature.appName, s.signature.author, s.signature.version))
1✔
109
        }
1✔
110

111
        if s.timeouts.CallTimeout > 0 {
9✔
112
                router.Use(middleware.Timeout(s.timeouts.CallTimeout))
×
113
        }
×
114

115
        logInfoWithBody := logger.New(logger.Log(s.logger), logger.WithBody, logger.Prefix("[DEBUG]")).Handler
9✔
116
        router.Use(logInfoWithBody)
9✔
117

9✔
118
        if s.limits.clientLimit > 0 {
9✔
NEW
119
                lmt := tollbooth.NewLimiter(s.limits.clientLimit, nil)
×
NEW
120
                lmt.SetIPLookup(limiter.IPLookup{Name: "X-Real-IP"})
×
NEW
121
                router.Use(tollbooth.HTTPMiddleware(lmt))
×
UNCOV
122
        }
×
123

124
        router.Use(middleware.NoCache)
9✔
125
        router.Use(s.basicAuth)
9✔
126
        router.Use(s.customMiddlewares...)
9✔
127
        router.Post(s.api, s.handler)
9✔
128

9✔
129
        s.httpServer.Lock()
9✔
130
        s.httpServer.Server = &http.Server{
9✔
131
                Addr:              fmt.Sprintf(":%d", port),
9✔
132
                Handler:           router,
9✔
133
                ReadHeaderTimeout: s.timeouts.ReadHeaderTimeout,
9✔
134
                WriteTimeout:      s.timeouts.WriteTimeout,
9✔
135
                IdleTimeout:       s.timeouts.IdleTimeout,
9✔
136
        }
9✔
137
        s.httpServer.Unlock()
9✔
138

9✔
139
        s.logger.Logf("[INFO] listen on %d", port)
9✔
140
        return s.httpServer.ListenAndServe()
9✔
141
}
142

143
// Shutdown http server
144
func (s *Server) Shutdown() error {
10✔
145
        s.httpServer.Lock()
10✔
146
        defer s.httpServer.Unlock()
10✔
147
        if s.httpServer.Server == nil {
11✔
148
                return fmt.Errorf("http server is not running")
1✔
149
        }
1✔
150
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
9✔
151
        defer cancel()
9✔
152
        return s.httpServer.Shutdown(ctx)
9✔
153
}
154

155
// Add method handler. Handler will be called on matching method (Request.Method)
156
func (s *Server) Add(method string, fn ServerFn) {
12✔
157
        s.httpServer.Lock()
12✔
158
        defer s.httpServer.Unlock()
12✔
159
        if s.httpServer.Server != nil {
13✔
160
                s.logger.Logf("[WARN] ignored method %s, can't be added to activated server", method)
1✔
161
                return
1✔
162
        }
1✔
163

164
        s.funcs.once.Do(func() {
21✔
165
                s.funcs.m = map[string]ServerFn{}
10✔
166
        })
10✔
167

168
        s.funcs.m[method] = fn
11✔
169
        s.logger.Logf("[INFO] add handler for %s", method)
11✔
170
}
171

172
// HandlersGroup alias for map of handlers
173
type HandlersGroup map[string]ServerFn
174

175
// Group of handlers with common prefix, match on group.method
176
func (s *Server) Group(prefix string, m HandlersGroup) {
1✔
177
        for k, v := range m {
3✔
178
                s.Add(prefix+"."+k, v)
2✔
179
        }
2✔
180
}
181

182
// handler is http handler multiplexing calls by req.Method
183
func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
14✔
184
        req := struct {
14✔
185
                ID     uint64           `json:"id"`
14✔
186
                Method string           `json:"method"`
14✔
187
                Params *json.RawMessage `json:"params"`
14✔
188
        }{}
14✔
189

14✔
190
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
14✔
191
                rest.SendErrorJSON(w, r, s.logger, http.StatusBadRequest, err, req.Method)
×
192
                return
×
193
        }
×
194
        fn, ok := s.funcs.m[req.Method]
14✔
195
        if !ok {
17✔
196
                rest.SendErrorJSON(w, r, s.logger, http.StatusNotImplemented, fmt.Errorf("unsupported method"), req.Method)
3✔
197
                return
3✔
198

3✔
199
        }
3✔
200

201
        params := json.RawMessage{}
11✔
202
        if req.Params != nil {
16✔
203
                params = *req.Params
5✔
204
        }
5✔
205

206
        render.JSON(w, r, fn(req.ID, params))
11✔
207
}
208

209
// basicAuth middleware. enabled only if both AuthUser and AuthPasswd defined.
210
func (s *Server) basicAuth(h http.Handler) http.Handler {
9✔
211
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23✔
212

14✔
213
                if s.authUser == "" || s.authPasswd == "" {
25✔
214
                        h.ServeHTTP(w, r)
11✔
215
                        return
11✔
216
                }
11✔
217

218
                user, pass, ok := r.BasicAuth()
3✔
219
                if user != s.authUser || pass != s.authPasswd || !ok {
4✔
220
                        w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
1✔
221
                        http.Error(w, "Unauthorized", http.StatusUnauthorized)
1✔
222
                        return
1✔
223
                }
1✔
224
                h.ServeHTTP(w, r)
2✔
225
        })
226
}
227

228
func getDefaultTimeouts() Timeouts {
20✔
229
        return Timeouts{
20✔
230
                ReadHeaderTimeout: 5 * time.Second,
20✔
231
                WriteTimeout:      10 * time.Second,
20✔
232
                IdleTimeout:       5 * time.Second,
20✔
233
        }
20✔
234
}
20✔
235

236
// L defined logger interface used for an optional rest logging
237
type L interface {
238
        Logf(format string, args ...interface{})
239
}
240

241
// LoggerFunc type is an adapter to allow the use of ordinary functions as Logger.
242
type LoggerFunc func(format string, args ...interface{})
243

244
// Logf calls f(id)
245
func (f LoggerFunc) Logf(format string, args ...interface{}) { f(format, args...) }
45✔
246

247
// NoOpLogger logger does nothing
248
var NoOpLogger = LoggerFunc(func(format string, args ...interface{}) {}) //nolint
45✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc