forked from fullstorydev/grpcui
-
Notifications
You must be signed in to change notification settings - Fork 0
/
webform.go
207 lines (193 loc) · 7.12 KB
/
webform.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package grpcui
import (
"bytes"
"fmt"
"github.com/jhump/protoreflect/desc/builder"
"github.com/jhump/protoreflect/desc/protoprint"
"html/template"
"os"
"sort"
"strings"
"unicode"
"github.com/jhump/protoreflect/desc"
"github.com/fullstorydev/grpcui/internal/resources/webform"
)
var (
webFormTemplate = template.Must(template.New("grpc web form").Parse(string(webform.Template())))
protoPrinter = protoprint.Printer{
Compact: true,
Indent: " ",
}
)
// WebFormContents returns an HTML form that can be embedded into a web UI to
// provide an interactive form for issuing RPCs.
//
// For a fully self-contained handler that provides both an HTML UI and the
// needed server handlers, see grpcui.UIHandler instead.
//
// The given invokeURI and metadataURI indicate the URI paths where server
// handlers are registered for invoking RPCs and querying RPC metadata,
// respectively. Handlers for these endpoints are provided via the
// RPCInvokeHandler and RPCMetadataHandler functions:
//
// // This example uses "/rpcs" as the base URI.
// pageHandler := func(w http.ResponseWriter, r *http.Request) {
// webForm := grpcui.WebFormContents("/rpcs/invoke/", "/rpcs/metadata", descs)
// webFormJs := grpcui.WebFormScript()
// generateHTMLPage(w, r, webForm, webFormJs)
// }
//
// // Make sure the RPC handlers are registered at the same URI paths
// // that were used in the call to WebFormContents:
// rpcInvokeHandler := http.StripPrefix("/rpcs/invoke", grpcui.RPCInvokeHandler(conn, descs))
// mux.Handle("/rpcs/invoke/", rpcInvokeHandler)
// mux.Handle("/rpcs/metadata", grpcui.RPCMetadataHandler(descs))
// mux.HandleFunc("/rpcs/index.html", pageHandler)
//
// The given descs is a slice of methods which are exposed through the web form.
// You can use AllMethodsForServices, AllMethodsForServer, and
// AllMethodsViaReflection helper functions to build this list.
//
// The returned HTML form requires that the contents of WebFormScript() have
// already been loaded as a script in the page.
func WebFormContents(invokeURI, metadataURI string, descs []*desc.MethodDescriptor) []byte {
return WebFormContentsWithOptions(invokeURI, metadataURI, descs, WebFormOptions{})
}
// WebFormOptions contains optional arguments when creating a gRPCui web form.
type WebFormOptions struct {
// The set of metadata to show in the web form by default. Each value in
// the slice should be in the form "name: value"
DefaultMetadata []string
// If non-nil and true, the web form JS code will log debug information
// to the JS console. If nil, whether debug is enabled or not depends on
// an environment variable: GRPC_WEBFORM_DEBUG (if it's not blank, then
// debug is enabled).
Debug *bool
}
// WebFormContentsWithOptions is the same as WebFormContents except that it
// accepts an additional argument, options. This can be used to toggle the JS
// code into debug logging and can also be used to define the set of metadata to
// show in the web form by default (empty if unspecified).
func WebFormContentsWithOptions(invokeURI, metadataURI string, descs []*desc.MethodDescriptor, opts WebFormOptions) []byte {
type metadataEntry struct {
Name, Value string
}
params := struct {
InvokeURI string
MetadataURI string
Services []string
SvcDescs map[string]string
Methods map[string][]string
MtdDescs map[string]string
DefaultMetadata []metadataEntry
Debug bool
}{
InvokeURI: invokeURI,
MetadataURI: metadataURI,
SvcDescs: map[string]string{},
Methods: map[string][]string{},
MtdDescs: map[string]string{},
Debug: os.Getenv("GRPC_WEBFORM_DEBUG") != "",
}
if opts.Debug != nil {
params.Debug = *opts.Debug
}
for _, md := range opts.DefaultMetadata {
parts := strings.SplitN(md, ":", 2)
key := strings.TrimSpace(parts[0])
var val string
if len(parts) > 1 {
val = strings.TrimLeftFunc(parts[1], unicode.IsSpace)
}
params.DefaultMetadata = append(params.DefaultMetadata, metadataEntry{Name: key, Value: val})
}
// build list of distinct service and method names and sort them
uniqueServices := map[string]*desc.ServiceDescriptor{}
for _, md := range descs {
sd := md.GetService()
svcName := sd.GetFullyQualifiedName()
uniqueServices[svcName] = sd
params.Methods[svcName] = append(params.Methods[svcName], md.GetName())
desc, err := protoPrinter.PrintProtoToString(md)
if err != nil {
// generate simple description with no comments or options
var reqStr, respStr string
if md.IsClientStreaming() {
reqStr = "stream "
}
if md.IsServerStreaming() {
respStr = "stream "
}
desc = fmt.Sprintf(" rpc %s (%s%s) returns (%s%s);", md.GetName(), reqStr, md.GetInputType().GetFullyQualifiedName(), respStr, md.GetOutputType().GetFullyQualifiedName())
} else {
// indent and remove trailing newline
desc = strings.TrimSuffix(desc, "\n")
parts := strings.Split(desc, "\n")
for i := range parts {
parts[i] = " " + parts[i]
}
desc = strings.Join(parts, "\n")
}
params.MtdDescs[md.GetFullyQualifiedName()] = desc
}
for svcName, sd := range uniqueServices {
params.Services = append(params.Services, svcName)
// for the service description, omit the methods (we just want the
// service's comments and options)
sb, err := builder.FromService(sd)
var desc string
if err == nil {
for _, md := range sd.GetMethods() {
sb.RemoveMethod(md.GetName())
}
if s, e := sb.Build(); e == nil {
desc, err = protoPrinter.PrintProtoToString(s)
}
}
if err != nil {
desc = fmt.Sprintf("service %s {", sd.GetName())
} else {
// strip last line, trailing close brace
tr := "\n}"
if strings.HasSuffix(desc, "\n") {
tr += "\n"
}
desc = strings.TrimSuffix(desc, tr)
}
params.SvcDescs[sd.GetFullyQualifiedName()] = desc
}
sort.Strings(params.Services)
for _, methods := range params.Methods {
sort.Strings(methods)
}
// render the template
var formBuf bytes.Buffer
if err := webFormTemplate.Execute(&formBuf, params); err != nil {
panic(err)
}
return formBuf.Bytes()
}
// WebFormScript returns the JavaScript that powers the web form returned by
// WebFormContents.
//
// The returned JavaScript requires that jQuery and jQuery UI libraries already
// be loaded in the container HTML page. It includes JavaScript code that relies
// on the "$" symbol.
//
// Note that the script, by default, does not handle CSRF protection. To add
// that, the enclosing page, in which the script is embedded, can use jQuery to
// configure this. For example, you can use the $.ajaxSend() jQuery function to
// intercept RPC invocations and automatically add a CSRF token header. To then
// check the token on the server-side, you will need to create a wrapper handler
// that first verifies the CSRF token header before delegating to a
// RPCInvokeHandler.
func WebFormScript() []byte {
return webform.Script()
}
// WebFormSampleCSS returns a CSS stylesheet for styling the HTML web form
// returned by WebFormContents. It is possible for uses of the web form to
// supply their own stylesheet, but this makes it simple to use default
// styling.
func WebFormSampleCSS() []byte {
return webform.SampleCSS()
}