🧪 Add SSE test tools
This commit is contained in:
parent
97df54a315
commit
450250c419
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
/config
|
|
||||||
/letsencrypt
|
/letsencrypt
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
@ -1,9 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="WEB_MODULE" version="4">
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="FacetManager">
|
||||||
|
<facet type="Python" name="Python facet">
|
||||||
|
<configuration sdkName="Python 3.9" />
|
||||||
|
</facet>
|
||||||
|
</component>
|
||||||
<component name="Go" enabled="true" />
|
<component name="Go" enabled="true" />
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$" />
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="Python 3.9 interpreter library" level="application" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
6
.idea/misc.xml
Normal file
6
.idea/misc.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.9" />
|
||||||
|
</component>
|
||||||
|
</project>
|
28
config/example.toml
Normal file
28
config/example.toml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
id = "example-region"
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "example-sse"
|
||||||
|
host = ["localhost:8000"]
|
||||||
|
path = ["/sse"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "example-sse-destination"
|
||||||
|
uri = "http://localhost:5000?sse=enable"
|
||||||
|
[[locations.transformers]]
|
||||||
|
type = "replacePath"
|
||||||
|
options = { pattern = "/sse", value = "" }
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "example-websocket"
|
||||||
|
host = ["localhost:8000"]
|
||||||
|
path = ["/ws"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "example-websocket-destination"
|
||||||
|
uri = "http://localhost:8765"
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "example-location"
|
||||||
|
host = ["localhost:8000"]
|
||||||
|
path = ["/"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "example-destination"
|
||||||
|
uri = "files://test/data"
|
@ -3,35 +3,45 @@ package navi
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/fasthttp/websocket"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/proxy"
|
||||||
|
"github.com/gofiber/fiber/v2/utils"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fasthttp/websocket"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/proxy"
|
|
||||||
"github.com/gofiber/fiber/v2/utils"
|
|
||||||
"github.com/samber/lo"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeHypertextResponse(c *fiber.Ctx, dest *Destination) error {
|
func makeUnifiedResponse(c *fiber.Ctx, dest *Destination) error {
|
||||||
if websocket.FastHTTPIsWebSocketUpgrade(c.Context()) {
|
if websocket.FastHTTPIsWebSocketUpgrade(c.Context()) {
|
||||||
// Handle websocket
|
// Handle websocket
|
||||||
return makeWebsocketResponse(c, dest)
|
return makeWebsocketResponse(c, dest)
|
||||||
} else {
|
} else {
|
||||||
// Handle normal request
|
_, queries := dest.GetRawUri()
|
||||||
|
if len(queries.Get("sse")) > 0 {
|
||||||
|
// Handle server-side event
|
||||||
|
return makeSeverSideEventResponse(c, dest)
|
||||||
|
} else {
|
||||||
|
// Handle normal http request
|
||||||
|
return makeHypertextResponse(c, dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHypertextResponse(c *fiber.Ctx, dest *Destination) error {
|
||||||
timeout := time.Duration(viper.GetInt64("performance.network_timeout")) * time.Millisecond
|
timeout := time.Duration(viper.GetInt64("performance.network_timeout")) * time.Millisecond
|
||||||
return proxy.Do(c, dest.MakeUri(c), &fasthttp.Client{
|
return proxy.Do(c, dest.MakeUri(c), &fasthttp.Client{
|
||||||
ReadTimeout: timeout,
|
ReadTimeout: timeout,
|
||||||
WriteTimeout: timeout,
|
WriteTimeout: timeout,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var wsUpgrader = websocket.FastHTTPUpgrader{}
|
var wsUpgrader = websocket.FastHTTPUpgrader{}
|
||||||
|
|
||||||
@ -58,7 +68,7 @@ func makeWebsocketResponse(c *fiber.Ctx, dest *Destination) error {
|
|||||||
for {
|
for {
|
||||||
mode, message, err := remote.ReadMessage()
|
mode, message, err := remote.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
log.Warn().Err(err).Msg("An error occurred during the websocket proxying...")
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
signal <- struct {
|
signal <- struct {
|
||||||
@ -88,6 +98,11 @@ func makeWebsocketResponse(c *fiber.Ctx, dest *Destination) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeSeverSideEventResponse(c *fiber.Ctx, dest *Destination) error {
|
||||||
|
// TODO Impl SSE with https://github.com/gofiber/recipes/blob/master/sse/main.go
|
||||||
|
return fiber.NewError(fiber.StatusNotImplemented, "Server-side-events was not available now.")
|
||||||
|
}
|
||||||
|
|
||||||
func makeFileResponse(c *fiber.Ctx, dest *Destination) error {
|
func makeFileResponse(c *fiber.Ctx, dest *Destination) error {
|
||||||
uri, queries := dest.GetRawUri()
|
uri, queries := dest.GetRawUri()
|
||||||
root := http.Dir(uri)
|
root := http.Dir(uri)
|
||||||
|
@ -14,7 +14,7 @@ type RoadApp struct {
|
|||||||
func (v *RoadApp) Forward(ctx *fiber.Ctx, dest *Destination) error {
|
func (v *RoadApp) Forward(ctx *fiber.Ctx, dest *Destination) error {
|
||||||
switch dest.GetType() {
|
switch dest.GetType() {
|
||||||
case DestinationHypertext:
|
case DestinationHypertext:
|
||||||
return makeHypertextResponse(ctx, dest)
|
return makeUnifiedResponse(ctx, dest)
|
||||||
case DestinationStaticFile:
|
case DestinationStaticFile:
|
||||||
return makeFileResponse(ctx, dest)
|
return makeFileResponse(ctx, dest)
|
||||||
default:
|
default:
|
||||||
|
@ -41,8 +41,12 @@ type Destination struct {
|
|||||||
Transformers []transformers.TransformerConfig `json:"transformers" toml:"transformers"`
|
Transformers []transformers.TransformerConfig `json:"transformers" toml:"transformers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Destination) GetProtocol() string {
|
||||||
|
return strings.SplitN(v.Uri, "://", 2)[0]
|
||||||
|
}
|
||||||
|
|
||||||
func (v *Destination) GetType() DestinationType {
|
func (v *Destination) GetType() DestinationType {
|
||||||
protocol := strings.SplitN(v.Uri, "://", 2)[0]
|
protocol := v.GetProtocol()
|
||||||
switch protocol {
|
switch protocol {
|
||||||
case "http", "https":
|
case "http", "https":
|
||||||
return DestinationHypertext
|
return DestinationHypertext
|
||||||
@ -56,7 +60,7 @@ func (v *Destination) GetRawUri() (string, url.Values) {
|
|||||||
uri := strings.SplitN(v.Uri, "://", 2)[1]
|
uri := strings.SplitN(v.Uri, "://", 2)[1]
|
||||||
data := strings.SplitN(uri, "?", 2)
|
data := strings.SplitN(uri, "?", 2)
|
||||||
data = append(data, " ") // Make data array least have two element
|
data = append(data, " ") // Make data array least have two element
|
||||||
qs, _ := url.ParseQuery(data[0])
|
qs, _ := url.ParseQuery(data[1])
|
||||||
|
|
||||||
return data[0], qs
|
return data[0], qs
|
||||||
}
|
}
|
||||||
@ -71,8 +75,9 @@ func (v *Destination) MakeUri(ctx *fiber.Ctx) string {
|
|||||||
|
|
||||||
path := string(ctx.Request().URI().Path())
|
path := string(ctx.Request().URI().Path())
|
||||||
hash := string(ctx.Request().URI().Hash())
|
hash := string(ctx.Request().URI().Hash())
|
||||||
|
uri, _ := v.GetRawUri()
|
||||||
|
|
||||||
return v.Uri + path +
|
return uri + path +
|
||||||
lo.Ternary(len(queries) > 0, "?"+strings.Join(queries, "&"), "") +
|
lo.Ternary(len(queries) > 0, "?"+strings.Join(queries, "&"), "") +
|
||||||
lo.Ternary(len(hash) > 0, "#"+hash, "")
|
lo.Ternary(len(hash) > 0, "#"+hash, "")
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + Solid + TS</title>
|
<title>RoadSign Sideload</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
27
test/data/sse/index.html
Normal file
27
test/data/sse/index.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SSE Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Server-Sent Events Example</h1>
|
||||||
|
<div id="sse-data"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const eventSource = new EventSource("/sse")
|
||||||
|
const sseDataElement = document.getElementById("sse-data")
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
sseDataElement.innerText = `Data from server: ${event.data}`
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error("EventSource failed:", error)
|
||||||
|
eventSource.close()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
28
test/data/sse/server.py
Normal file
28
test/data/sse/server.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from flask import Flask, render_template, Response
|
||||||
|
|
||||||
|
app = Flask(__name__, template_folder=".")
|
||||||
|
|
||||||
|
|
||||||
|
# Generator function to simulate real-time updates
|
||||||
|
def event_stream():
|
||||||
|
count = 0
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
count += 1
|
||||||
|
yield f"data: {count}\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/sse')
|
||||||
|
def sse():
|
||||||
|
return Response(event_stream(), content_type='text/event-stream')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, threaded=True)
|
Loading…
Reference in New Issue
Block a user