mirror of
https://github.com/gusaul/grpcox.git
synced 2024-12-26 02:40:10 +00:00
first commit
This commit is contained in:
commit
54ab742f5c
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
grpcox
|
93
core/grpcox.go
Normal file
93
core/grpcox.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fullstorydev/grpcurl"
|
||||||
|
"github.com/jhump/protoreflect/grpcreflect"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/keepalive"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GrpCox - main object
|
||||||
|
type GrpCox struct {
|
||||||
|
KeepAlive float64
|
||||||
|
PlainText bool
|
||||||
|
|
||||||
|
// TODO : utilize below args
|
||||||
|
headers []string
|
||||||
|
reflectHeaders []string
|
||||||
|
authority string
|
||||||
|
insecure bool
|
||||||
|
cacert string
|
||||||
|
cert string
|
||||||
|
key string
|
||||||
|
serverName string
|
||||||
|
isUnixSocket func() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResource - open resource to targeted grpc server
|
||||||
|
func (g *GrpCox) GetResource(ctx context.Context, target string) (*Resource, error) {
|
||||||
|
var err error
|
||||||
|
r := new(Resource)
|
||||||
|
h := append(g.headers, g.reflectHeaders...)
|
||||||
|
md := grpcurl.MetadataFromHeaders(h)
|
||||||
|
refCtx := metadata.NewOutgoingContext(ctx, md)
|
||||||
|
r.clientConn, err = g.dial(ctx, target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(r.clientConn))
|
||||||
|
r.descSource = grpcurl.DescriptorSourceFromServer(ctx, r.refClient)
|
||||||
|
r.headers = h
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GrpCox) dial(ctx context.Context, target string) (*grpc.ClientConn, error) {
|
||||||
|
dialTime := 10 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, dialTime)
|
||||||
|
defer cancel()
|
||||||
|
var opts []grpc.DialOption
|
||||||
|
|
||||||
|
// keep alive
|
||||||
|
if g.KeepAlive > 0 {
|
||||||
|
timeout := time.Duration(g.KeepAlive * float64(time.Second))
|
||||||
|
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||||
|
Time: timeout,
|
||||||
|
Timeout: timeout,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.authority != "" {
|
||||||
|
opts = append(opts, grpc.WithAuthority(g.authority))
|
||||||
|
}
|
||||||
|
|
||||||
|
var creds credentials.TransportCredentials
|
||||||
|
if !g.PlainText {
|
||||||
|
var err error
|
||||||
|
creds, err = grpcurl.ClientTransportCredentials(g.insecure, g.cacert, g.cert, g.key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if g.serverName != "" {
|
||||||
|
if err := creds.OverrideServerName(g.serverName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
network := "tcp"
|
||||||
|
if g.isUnixSocket != nil && g.isUnixSocket() {
|
||||||
|
network = "unix"
|
||||||
|
}
|
||||||
|
cc, err := grpcurl.BlockingDial(ctx, network, target, creds, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cc, nil
|
||||||
|
}
|
183
core/resource.go
Normal file
183
core/resource.go
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fullstorydev/grpcurl"
|
||||||
|
"github.com/jhump/protoreflect/desc"
|
||||||
|
"github.com/jhump/protoreflect/grpcreflect"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resource - hold 3 main function (List, Describe, and Invoke)
|
||||||
|
type Resource struct {
|
||||||
|
clientConn *grpc.ClientConn
|
||||||
|
descSource grpcurl.DescriptorSource
|
||||||
|
refClient *grpcreflect.Client
|
||||||
|
|
||||||
|
headers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// List - To list all services exposed by a server
|
||||||
|
// symbol can be "" to list all available services
|
||||||
|
// symbol also can be service name to list all available method
|
||||||
|
func (r *Resource) List(symbol string) ([]string, error) {
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
if symbol == "" {
|
||||||
|
svcs, err := grpcurl.ListServices(r.descSource)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
if len(svcs) == 0 {
|
||||||
|
return result, fmt.Errorf("No Services")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, svc := range svcs {
|
||||||
|
result = append(result, fmt.Sprintf("%s\n", svc))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
methods, err := grpcurl.ListMethods(r.descSource, symbol)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
if len(methods) == 0 {
|
||||||
|
return result, fmt.Errorf("No Function") // probably unlikely
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range methods {
|
||||||
|
result = append(result, fmt.Sprintf("%s\n", m))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Describe - The "describe" verb will print the type of any symbol that the server knows about
|
||||||
|
// or that is found in a given protoset file.
|
||||||
|
// It also prints a description of that symbol, in the form of snippets of proto source.
|
||||||
|
// It won't necessarily be the original source that defined the element, but it will be equivalent.
|
||||||
|
func (r *Resource) Describe(symbol string) (string, string, error) {
|
||||||
|
|
||||||
|
var result, template string
|
||||||
|
|
||||||
|
var symbols []string
|
||||||
|
if symbol != "" {
|
||||||
|
symbols = []string{symbol}
|
||||||
|
} else {
|
||||||
|
// if no symbol given, describe all exposed services
|
||||||
|
svcs, err := r.descSource.ListServices()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if len(svcs) == 0 {
|
||||||
|
log.Println("Server returned an empty list of exposed services")
|
||||||
|
}
|
||||||
|
symbols = svcs
|
||||||
|
}
|
||||||
|
for _, s := range symbols {
|
||||||
|
if s[0] == '.' {
|
||||||
|
s = s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
dsc, err := r.descSource.FindSymbol(s)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
txt, err := grpcurl.GetDescriptorText(dsc, r.descSource)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
result = txt
|
||||||
|
|
||||||
|
if dsc, ok := dsc.(*desc.MessageDescriptor); ok {
|
||||||
|
// for messages, also show a template in JSON, to make it easier to
|
||||||
|
// create a request to invoke an RPC
|
||||||
|
tmpl := grpcurl.MakeTemplate(dsc)
|
||||||
|
_, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format("json"), r.descSource, true, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
str, err := formatter(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
template = str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke - invoking gRPC function
|
||||||
|
func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (string, time.Duration, error) {
|
||||||
|
// because of grpcurl directlu fmt.Printf on their invoke function
|
||||||
|
// so we stub the Stdout using os.Pipe
|
||||||
|
backUpStdout := os.Stdout
|
||||||
|
defer func() {
|
||||||
|
os.Stdout = backUpStdout
|
||||||
|
}()
|
||||||
|
|
||||||
|
f, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
rf, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format("json"), r.descSource, false, true, in)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
h := grpcurl.NewDefaultEventHandler(os.Stdout, r.descSource, formatter, false)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
err = grpcurl.InvokeRPC(ctx, r.descSource, r.clientConn, symbol, r.headers, h, rf.Next)
|
||||||
|
end := time.Now().Sub(start) / time.Millisecond
|
||||||
|
if err != nil {
|
||||||
|
return "", end, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.Status.Code() != codes.OK {
|
||||||
|
log.Printf("ERROR:\n Code: %s\n Message: %s\n", h.Status.Code().String(), h.Status.Message())
|
||||||
|
r.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy the output in a separate goroutine so printing can't block indefinitely
|
||||||
|
outC := make(chan string)
|
||||||
|
go func() {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, f)
|
||||||
|
outC <- buf.String()
|
||||||
|
}()
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
out := <-outC
|
||||||
|
|
||||||
|
return out, end, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - to close all resources that was opened before
|
||||||
|
func (r *Resource) Close() {
|
||||||
|
if r.refClient != nil {
|
||||||
|
r.refClient.Reset()
|
||||||
|
r.refClient = nil
|
||||||
|
}
|
||||||
|
if r.clientConn != nil {
|
||||||
|
r.clientConn.Close()
|
||||||
|
r.clientConn = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) exit(code int) {
|
||||||
|
// to force reset before os exit
|
||||||
|
r.Close()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
26
grpcox.go
Normal file
26
grpcox.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gusaul/grpcox/handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := ":6969"
|
||||||
|
muxRouter := mux.NewRouter()
|
||||||
|
handler.Init(muxRouter)
|
||||||
|
srv := &http.Server{
|
||||||
|
Handler: muxRouter,
|
||||||
|
Addr: "127.0.0.1" + port,
|
||||||
|
WriteTimeout: 15 * time.Second,
|
||||||
|
ReadTimeout: 15 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Service started on", port)
|
||||||
|
log.Fatal(srv.ListenAndServe())
|
||||||
|
}
|
153
handler/handler.go
Normal file
153
handler/handler.go
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gusaul/grpcox/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func index(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body := new(bytes.Buffer)
|
||||||
|
err := indexHTML.Execute(body, make(map[string]string))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Write(body.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLists(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
host := vars["host"]
|
||||||
|
if host == "" {
|
||||||
|
writeError(w, fmt.Errorf("Invalid Host"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service := vars["serv_name"]
|
||||||
|
|
||||||
|
g := new(core.GrpCox)
|
||||||
|
useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls"))
|
||||||
|
g.PlainText = !useTLS
|
||||||
|
|
||||||
|
res, err := g.GetResource(context.Background(), host)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer res.Close()
|
||||||
|
|
||||||
|
result, err := res.List(service)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response(w, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func describeFunction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
host := vars["host"]
|
||||||
|
if host == "" {
|
||||||
|
writeError(w, fmt.Errorf("Invalid Host"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
funcName := vars["func_name"]
|
||||||
|
if host == "" {
|
||||||
|
writeError(w, fmt.Errorf("Invalid Func Name"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g := new(core.GrpCox)
|
||||||
|
useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls"))
|
||||||
|
g.PlainText = !useTLS
|
||||||
|
|
||||||
|
res, err := g.GetResource(context.Background(), host)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer res.Close()
|
||||||
|
|
||||||
|
// get param
|
||||||
|
result, _, err := res.Describe(funcName)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
match := reGetFuncArg.FindStringSubmatch(result)
|
||||||
|
if len(match) < 2 {
|
||||||
|
writeError(w, fmt.Errorf("Invalid Func Type"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// describe func
|
||||||
|
result, template, err := res.Describe(match[1])
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type desc struct {
|
||||||
|
Schema string `json:"schema"`
|
||||||
|
Template string `json:"template"`
|
||||||
|
}
|
||||||
|
|
||||||
|
response(w, desc{
|
||||||
|
Schema: result,
|
||||||
|
Template: template,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func invokeFunction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
host := vars["host"]
|
||||||
|
if host == "" {
|
||||||
|
writeError(w, fmt.Errorf("Invalid Host"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
funcName := vars["func_name"]
|
||||||
|
if host == "" {
|
||||||
|
writeError(w, fmt.Errorf("Invalid Func Name"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g := new(core.GrpCox)
|
||||||
|
useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls"))
|
||||||
|
g.PlainText = !useTLS
|
||||||
|
|
||||||
|
res, err := g.GetResource(context.Background(), host)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer res.Close()
|
||||||
|
|
||||||
|
// get param
|
||||||
|
result, timer, err := res.Invoke(context.Background(), funcName, r.Body)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type invRes struct {
|
||||||
|
Time string `json:"timer"`
|
||||||
|
Result string `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
response(w, invRes{
|
||||||
|
Time: timer.String(),
|
||||||
|
Result: result,
|
||||||
|
})
|
||||||
|
}
|
36
handler/routes.go
Normal file
36
handler/routes.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init - routes initialization
|
||||||
|
func Init(router *mux.Router) {
|
||||||
|
router.HandleFunc("/", index)
|
||||||
|
|
||||||
|
ajaxRoute := router.PathPrefix("/server/{host}").Subrouter()
|
||||||
|
ajaxRoute.HandleFunc("/services", corsHandler(getLists)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
ajaxRoute.HandleFunc("/service/{serv_name}/functions", corsHandler(getLists)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
ajaxRoute.HandleFunc("/function/{func_name}/describe", corsHandler(describeFunction)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
ajaxRoute.HandleFunc("/function/{func_name}/invoke", corsHandler(invokeFunction)).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
assetsPath := "index"
|
||||||
|
router.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir(assetsPath+"/css/"))))
|
||||||
|
router.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(http.Dir(assetsPath+"/js/"))))
|
||||||
|
router.PathPrefix("/font/").Handler(http.StripPrefix("/font/", http.FileServer(http.Dir(assetsPath+"/font/"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func corsHandler(h http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "use_tls")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
44
handler/types.go
Normal file
44
handler/types.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reGetFuncArg *regexp.Regexp
|
||||||
|
indexHTML *template.Template
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
reGetFuncArg = regexp.MustCompile("\\( (.*) \\) returns")
|
||||||
|
indexHTML = template.Must(template.New("index.html").Delims("{[", "]}").ParseFiles("index/index.html"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response - Standar ajax Response
|
||||||
|
type Response struct {
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(w http.ResponseWriter, err error) {
|
||||||
|
e, _ := json.Marshal(Response{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func response(w http.ResponseWriter, data interface{}) {
|
||||||
|
e, _ := json.Marshal(Response{
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(e)
|
||||||
|
}
|
6
index/css/bootstrap.min.css
vendored
Executable file
6
index/css/bootstrap.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
44
index/css/mdb.min.css
vendored
Executable file
44
index/css/mdb.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
63
index/css/style.css
Normal file
63
index/css/style.css
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
body {
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
.mt-10 {
|
||||||
|
margin-top:10px;
|
||||||
|
}
|
||||||
|
.pt-50 {
|
||||||
|
padding-top:50px;
|
||||||
|
}
|
||||||
|
.custom-pretty {
|
||||||
|
border:none!important;
|
||||||
|
}
|
||||||
|
.custom-control-label:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top:0;
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0,0,0,.5);
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
.loading-overlay div {
|
||||||
|
margin:auto;
|
||||||
|
}
|
||||||
|
#choose-service, #choose-function, #body-request {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
.schema-body {
|
||||||
|
height: 250px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
#response {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* github corner */
|
||||||
|
.github-corner:hover .octo-arm{
|
||||||
|
animation:octocat-wave 560ms ease-in-out
|
||||||
|
}
|
||||||
|
@keyframes octocat-wave{
|
||||||
|
0%,100%{
|
||||||
|
transform:rotate(0)
|
||||||
|
}
|
||||||
|
20%,60%{
|
||||||
|
transform:rotate(-25deg)
|
||||||
|
}
|
||||||
|
40%,80%{
|
||||||
|
transform:rotate(10deg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:500px){
|
||||||
|
.github-corner:hover .octo-arm{
|
||||||
|
animation:none
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-corner .octo-arm{
|
||||||
|
animation:octocat-wave 560ms ease-in-out
|
||||||
|
}
|
||||||
|
}
|
BIN
index/font/roboto/Roboto-Bold.eot
Normal file
BIN
index/font/roboto/Roboto-Bold.eot
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Bold.ttf
Normal file
BIN
index/font/roboto/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Bold.woff
Normal file
BIN
index/font/roboto/Roboto-Bold.woff
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Bold.woff2
Normal file
BIN
index/font/roboto/Roboto-Bold.woff2
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Light.eot
Normal file
BIN
index/font/roboto/Roboto-Light.eot
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Light.ttf
Normal file
BIN
index/font/roboto/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Light.woff
Normal file
BIN
index/font/roboto/Roboto-Light.woff
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Light.woff2
Normal file
BIN
index/font/roboto/Roboto-Light.woff2
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Medium.eot
Normal file
BIN
index/font/roboto/Roboto-Medium.eot
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Medium.ttf
Normal file
BIN
index/font/roboto/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Medium.woff
Normal file
BIN
index/font/roboto/Roboto-Medium.woff
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Medium.woff2
Normal file
BIN
index/font/roboto/Roboto-Medium.woff2
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Regular.eot
Normal file
BIN
index/font/roboto/Roboto-Regular.eot
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Regular.ttf
Normal file
BIN
index/font/roboto/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Regular.woff
Normal file
BIN
index/font/roboto/Roboto-Regular.woff
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Regular.woff2
Normal file
BIN
index/font/roboto/Roboto-Regular.woff2
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Thin.eot
Normal file
BIN
index/font/roboto/Roboto-Thin.eot
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Thin.ttf
Normal file
BIN
index/font/roboto/Roboto-Thin.ttf
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Thin.woff
Normal file
BIN
index/font/roboto/Roboto-Thin.woff
Normal file
Binary file not shown.
BIN
index/font/roboto/Roboto-Thin.woff2
Normal file
BIN
index/font/roboto/Roboto-Thin.woff2
Normal file
Binary file not shown.
124
index/index.html
Normal file
124
index/index.html
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>gRPCox - gRPC Testing Environment</title>
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
|
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="css/mdb.min.css" rel="stylesheet">
|
||||||
|
<link href="css/style.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container pt-50">
|
||||||
|
<div class="row animated fadeIn">
|
||||||
|
<div class="col">
|
||||||
|
<div class="md-form input-group">
|
||||||
|
<input type="text" class="form-control" id="server-target">
|
||||||
|
<label for="server-target">gRPC Server Target</label>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button id="get-services" class="btn btn-mdb-color waves-effect m-0" type="button"><i class="fa fa-plug"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="use-tls">
|
||||||
|
<label class="custom-control-label" for="use-tls">Use TLS</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="other-elem" id="choose-service" style="display: none">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text btn-dark" for="select-service"><i class="fa fa-television"></i> Services</span>
|
||||||
|
</div>
|
||||||
|
<select class="browser-default custom-select" id="select-service"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="other-elem" id="choose-function" style="display: none">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text btn-dark" for="select-function"><i class="fa fa-rocket"></i> Methods</span>
|
||||||
|
</div>
|
||||||
|
<select class="browser-default custom-select" id="select-function"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row other-elem" id="body-request" style="display: none">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body schema-body">
|
||||||
|
<pre id="editor"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary waves-effect mt-10" id="invoke-func" type="button"><i class="fa fa-play"></i> Submit</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body schema-body">
|
||||||
|
<h4 class="card-title"><a>Schema Input</a></h4>
|
||||||
|
<pre class="prettyprint custom-pretty" id="schema-proto"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row other-elem" id="response" style="display: none">
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<small class="pull-right" id="timer-resp">Time : <span></span></small>
|
||||||
|
<h4 class="card-title"><a>Response:</a></h4>
|
||||||
|
<p class="card-text">
|
||||||
|
<pre class="prettyprint custom-pretty" id="json-response"></pre>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a target="_blank" href="https://github.com/gusaul" class="github-corner" aria-label="View source on GitHub">
|
||||||
|
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;"
|
||||||
|
aria-hidden="true">
|
||||||
|
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||||
|
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
|
||||||
|
fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
|
||||||
|
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
|
||||||
|
fill="currentColor" class="octo-body"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="loading-overlay" style="display: none">
|
||||||
|
<div><svg width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"
|
||||||
|
class="lds-flat-ball" style="background: none;">
|
||||||
|
<defs>
|
||||||
|
<mask ng-attr-id="{{config.mid}}" id="lds-flat-ball-mask-e49120e4825">
|
||||||
|
<circle cx="50" cy="50" r="45" fill="#fff"></circle>
|
||||||
|
</mask>
|
||||||
|
</defs>
|
||||||
|
<circle cx="50" cy="50" r="45" ng-attr-fill="{{config.base}}" fill="#85a2b6"></circle>
|
||||||
|
<path ng-attr-fill="{{config.dark}}" mask="url(#lds-flat-ball-mask-e49120e4825)" fill="rgb(107, 131, 147)" d="M 37.2721 55.3778 L 62.7279 29.922 L 162.728 129.922 L 137.272 155.378 Z">
|
||||||
|
<animate attributeName="d" calcMode="spline" values="M 37.27207793864214 40.72792206135786 L 62.72792206135786 15.272077938642143 L 162.72792206135784 115.27207793864214 L 137.27207793864216 140.72792206135784 Z;M 37.27207793864214 84.72792206135786 L 62.72792206135786 59.27207793864214 L 162.72792206135784 159.27207793864216 L 137.27207793864216 184.72792206135784 Z;M 37.27207793864214 40.72792206135786 L 62.72792206135786 15.272077938642143 L 162.72792206135784 115.27207793864214 L 137.27207793864216 140.72792206135784 Z"
|
||||||
|
keyTimes="0;0.5;1" dur="1" keySplines="0.45 0 0.9 0.55;0 0.45 0.55 0.9" begin="0s" repeatCount="indefinite"></animate>
|
||||||
|
</path>
|
||||||
|
<circle cx="50" ng-attr-cy="{{config.cy}}" ng-attr-r="{{config.radius}}" ng-attr-fill="{{config.color}}" cy="42.6499"
|
||||||
|
r="18" fill="#fdfdfd">
|
||||||
|
<animate attributeName="cy" calcMode="spline" values="28;72;28" keyTimes="0;0.5;1" dur="1" keySplines="0.45 0 0.9 0.55;0 0.45 0.55 0.9"
|
||||||
|
begin="0s" repeatCount="indefinite"></animate>
|
||||||
|
</circle>
|
||||||
|
</svg></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="js/jquery-3.3.1.min.js"></script>
|
||||||
|
<script type="text/javascript" src="js/bootstrap.min.js"></script>
|
||||||
|
<script type="text/javascript" src="js/mdb.min.js"></script>
|
||||||
|
<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.1/ace.js"></script>
|
||||||
|
<script type="text/javascript" src="js/style.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
7
index/js/bootstrap.min.js
vendored
Executable file
7
index/js/bootstrap.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
2
index/js/jquery-3.3.1.min.js
vendored
Normal file
2
index/js/jquery-3.3.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
index/js/mdb.min.js
vendored
Normal file
1
index/js/mdb.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
181
index/js/style.js
Normal file
181
index/js/style.js
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
var target, use_tls, editor;
|
||||||
|
|
||||||
|
$('#get-services').click(function(){
|
||||||
|
var t = get_valid_target();
|
||||||
|
if (target != t) {
|
||||||
|
target = t;
|
||||||
|
use_tls = $('#use-tls').is(":checked");
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.other-elem').hide();
|
||||||
|
var button = $(this).html();
|
||||||
|
$.ajax({
|
||||||
|
url: "server/"+target+"/services",
|
||||||
|
global: true,
|
||||||
|
method: "GET",
|
||||||
|
success: function(res){
|
||||||
|
if (res.error) {
|
||||||
|
target = "";
|
||||||
|
use_tls = "";
|
||||||
|
alert(res.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$("#select-service").html(new Option("Choose Service", ""));
|
||||||
|
$.each(res.data, (_, item) => $("#select-service").append(new Option(item, item)));
|
||||||
|
$('#choose-service').show();
|
||||||
|
},
|
||||||
|
error: function(_, _, errorThrown) {
|
||||||
|
target = "";
|
||||||
|
use_tls = "";
|
||||||
|
alert(errorThrown);
|
||||||
|
},
|
||||||
|
beforeSend: function(xhr){
|
||||||
|
xhr.setRequestHeader('use_tls', use_tls);
|
||||||
|
$(this).html("Loading...");
|
||||||
|
},
|
||||||
|
complete: function(){
|
||||||
|
$(this).html(button);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#select-service').change(function(){
|
||||||
|
var selected = $(this).val();
|
||||||
|
if (selected == "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#body-request').hide();
|
||||||
|
$('#response').hide();
|
||||||
|
$.ajax({
|
||||||
|
url: "server/"+target+"/service/"+selected+"/functions",
|
||||||
|
global: true,
|
||||||
|
method: "GET",
|
||||||
|
success: function(res){
|
||||||
|
if (res.error) {
|
||||||
|
alert(res.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$("#select-function").html(new Option("Choose Methods", ""));
|
||||||
|
$.each(res.data, (_, item) => $("#select-function").append(new Option(item.substr(selected.length) , item)));
|
||||||
|
$('#choose-function').show();
|
||||||
|
},
|
||||||
|
error: err,
|
||||||
|
beforeSend: function(xhr){
|
||||||
|
xhr.setRequestHeader('use_tls', use_tls);
|
||||||
|
show_loading();
|
||||||
|
},
|
||||||
|
complete: function(){
|
||||||
|
hide_loading();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#select-function').change(function(){
|
||||||
|
var selected = $(this).val();
|
||||||
|
if (selected == "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#response').hide();
|
||||||
|
$.ajax({
|
||||||
|
url: "server/"+target+"/function/"+selected+"/describe",
|
||||||
|
global: true,
|
||||||
|
method: "GET",
|
||||||
|
success: function(res){
|
||||||
|
if (res.error) {
|
||||||
|
alert(res.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_editor(res.data.template);
|
||||||
|
$("#schema-proto").html(PR.prettyPrintOne(res.data.schema));
|
||||||
|
$('#body-request').show();
|
||||||
|
},
|
||||||
|
error: err,
|
||||||
|
beforeSend: function(xhr){
|
||||||
|
xhr.setRequestHeader('use_tls', use_tls);
|
||||||
|
show_loading();
|
||||||
|
},
|
||||||
|
complete: function(){
|
||||||
|
hide_loading();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#invoke-func').click(function(){
|
||||||
|
var func = $('#select-function').val();
|
||||||
|
if (func == "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var body = editor.getValue();
|
||||||
|
var button = $(this).html();
|
||||||
|
$.ajax({
|
||||||
|
url: "server/"+target+"/function/"+func+"/invoke",
|
||||||
|
global: true,
|
||||||
|
method: "POST",
|
||||||
|
data: body,
|
||||||
|
dataType: "json",
|
||||||
|
success: function(res){
|
||||||
|
if (res.error) {
|
||||||
|
alert(res.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$("#json-response").html(PR.prettyPrintOne(res.data.result));
|
||||||
|
$("#timer-resp span").html(res.data.timer);
|
||||||
|
$('#response').show();
|
||||||
|
},
|
||||||
|
error: err,
|
||||||
|
beforeSend: function(xhr){
|
||||||
|
xhr.setRequestHeader('use_tls', use_tls);
|
||||||
|
$(this).html("Loading...");
|
||||||
|
},
|
||||||
|
complete: function(){
|
||||||
|
$(this).html(button);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function generate_editor(content) {
|
||||||
|
if(editor) {
|
||||||
|
editor.setValue(content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$("#editor").html(content);
|
||||||
|
editor = ace.edit("editor");
|
||||||
|
editor.setOptions({
|
||||||
|
maxLines: Infinity
|
||||||
|
});
|
||||||
|
editor.renderer.setScrollMargin(10, 10, 10, 10);
|
||||||
|
editor.setTheme("ace/theme/github");
|
||||||
|
editor.session.setMode("ace/mode/json");
|
||||||
|
editor.renderer.setShowGutter(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_valid_target() {
|
||||||
|
t = $('#server-target').val().trim();
|
||||||
|
if (t == "") {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
ts = t.split("://");
|
||||||
|
if (ts.length > 1) {
|
||||||
|
$('#server-target').val(ts[1]);
|
||||||
|
return ts[1];
|
||||||
|
}
|
||||||
|
return ts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function err(_, _, errorThrown) {
|
||||||
|
alert(errorThrown);
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_loading() {
|
||||||
|
$('.loading-overlay').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide_loading() {
|
||||||
|
$('.loading-overlay').hide();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user