wrap_it_ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Rakefile +11 -0
- data/app/assets/javascripts/wrap_it_ruby/interception.js +172 -0
- data/app/assets/stylesheets/wrap_it_ruby/application.css +52 -0
- data/app/controllers/wrap_it_ruby/application_controller.rb +9 -0
- data/app/controllers/wrap_it_ruby/home_controller.rb +10 -0
- data/app/controllers/wrap_it_ruby/proxy_controller.rb +24 -0
- data/app/helpers/wrap_it_ruby/iframe_helper.rb +11 -0
- data/app/helpers/wrap_it_ruby/menu_helper.rb +9 -0
- data/app/javascript/wrap_it_ruby/controllers/iframe_proxy_controller.js +87 -0
- data/app/views/wrap_it_ruby/home/index.html.erb +1 -0
- data/app/views/wrap_it_ruby/layouts/application.html.erb +30 -0
- data/app/views/wrap_it_ruby/proxy/show.html.ruby +5 -0
- data/app/views/wrap_it_ruby/shared/_navbar.html.ruby +11 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +9 -0
- data/lib/generators/wrap_it_ruby/install/install_generator.rb +29 -0
- data/lib/generators/wrap_it_ruby/install/templates/menu.yml +16 -0
- data/lib/wrap_it_ruby/engine.rb +39 -0
- data/lib/wrap_it_ruby/menu.rb +40 -0
- data/lib/wrap_it_ruby/middleware/proxy_middleware.rb +154 -0
- data/lib/wrap_it_ruby/middleware/root_relative_proxy_middleware.rb +52 -0
- data/lib/wrap_it_ruby/middleware/script_injection_middleware.rb +80 -0
- data/lib/wrap_it_ruby/version.rb +5 -0
- data/lib/wrap_it_ruby.rb +7 -0
- metadata +150 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bcdd8a5fa91589bc96243a96ae809d92cbe0f585ab9a58175213644fe1cd798a
|
|
4
|
+
data.tar.gz: 2199f6305cba48c81d659cedeeafc31d1a95c73380a94601dcd182bc8520deea
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2818a78eab5310065a3b4b90bc712778db8c5c5b19db5cfd2e175a12761bc2233efa0a194bf20575695030c837e263a241861d13cb489efb87e15ed503d58b3e
|
|
7
|
+
data.tar.gz: 9f9cf00ff131d17f691599527468b9686b8751a6c9748d849c25ff5ef70d8894fe1e9d2d790d39a9d5d3bb4424e9f9d9a5abc938333e985582d820c052f3e379
|
data/Rakefile
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// interception.js — Injected into proxied HTML by ScriptInjectionMiddleware.
|
|
2
|
+
//
|
|
3
|
+
// Rewrites cross-origin URLs through /_proxy/{host} and stamps
|
|
4
|
+
// X-Proxy-Host on fetch/XHR so RootRelativeProxyMiddleware can route
|
|
5
|
+
// root-relative requests.
|
|
6
|
+
//
|
|
7
|
+
// Intercepts: fetch, XMLHttpRequest, WebSocket, EventSource,
|
|
8
|
+
// <a> clicks, <form> submissions.
|
|
9
|
+
//
|
|
10
|
+
(function (window) {
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
var PROXY_BASE = '/_proxy';
|
|
14
|
+
|
|
15
|
+
console.log('[interception]', 'loaded');
|
|
16
|
+
|
|
17
|
+
var _fetch = window.fetch;
|
|
18
|
+
var _XHR = window.XMLHttpRequest;
|
|
19
|
+
var _xhrOpen = _XHR.prototype.open;
|
|
20
|
+
var _xhrSend = _XHR.prototype.send;
|
|
21
|
+
var _xhrSetHeader = _XHR.prototype.setRequestHeader;
|
|
22
|
+
var _WebSocket = window.WebSocket;
|
|
23
|
+
var _EventSource = window.EventSource;
|
|
24
|
+
|
|
25
|
+
const PROXY_HOST = window.__proxyHost;
|
|
26
|
+
|
|
27
|
+
const rewriteUrl = (url) => {
|
|
28
|
+
if (typeof url !== 'string') return url
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(url)
|
|
31
|
+
if (parsed.host === window.__hostingSite) return url
|
|
32
|
+
return `/_proxy/${parsed.host}${parsed.pathname}${parsed.search}${parsed.hash}`
|
|
33
|
+
} catch {
|
|
34
|
+
return url
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// =========================== Navigation ====================================
|
|
39
|
+
//
|
|
40
|
+
document.addEventListener('click', (event) => {
|
|
41
|
+
const link = event.target.closest('a')
|
|
42
|
+
if (!link) return
|
|
43
|
+
|
|
44
|
+
const raw = link.getAttribute('href')
|
|
45
|
+
const url = raw.startsWith('/') ? `/_proxy/${window.__proxyHost}${raw}` : rewriteUrl(raw)
|
|
46
|
+
|
|
47
|
+
event.preventDefault()
|
|
48
|
+
window.location.href = url
|
|
49
|
+
}, true)
|
|
50
|
+
|
|
51
|
+
document.addEventListener('submit', (event) => {
|
|
52
|
+
event.preventDefault()
|
|
53
|
+
const form = event.target
|
|
54
|
+
const raw = form.getAttribute('action') || ''
|
|
55
|
+
const url = raw.startsWith('/') ? `/_proxy/${window.__proxyHost}${raw}` : rewriteUrl(raw)
|
|
56
|
+
|
|
57
|
+
form.setAttribute('action', url)
|
|
58
|
+
event.target.submit()
|
|
59
|
+
}, true)
|
|
60
|
+
|
|
61
|
+
//document.addEventListener('submit', (event) => {
|
|
62
|
+
// event.preventDefault()
|
|
63
|
+
// const form = event.target
|
|
64
|
+
// const raw = form.getAttribute('action') || ''
|
|
65
|
+
// const url = raw.startsWith('/') ? `/_proxy/${window.__proxyHost}${raw}` : rewriteUrl(raw)
|
|
66
|
+
|
|
67
|
+
// fetch(url, { method: form.method || 'POST', body: new FormData(form) })
|
|
68
|
+
// .then(res => res.text())
|
|
69
|
+
// .then(html => document.documentElement.innerHTML = html)
|
|
70
|
+
//}, true)
|
|
71
|
+
|
|
72
|
+
//document.addEventListener('click', (event) => {
|
|
73
|
+
// const link = event.target.closest('a')
|
|
74
|
+
// if (!link) return
|
|
75
|
+
|
|
76
|
+
// event.preventDefault()
|
|
77
|
+
// const raw = link.getAttribute('href')
|
|
78
|
+
// console.log('[interception] click', { raw })
|
|
79
|
+
|
|
80
|
+
// // Relative URLs need the proxy prefix added
|
|
81
|
+
// const url = raw.startsWith('/') ? `/_proxy/${window.__proxyHost}${raw}` : rewriteUrl(raw)
|
|
82
|
+
|
|
83
|
+
// fetch(url)
|
|
84
|
+
// .then(res => res.text())
|
|
85
|
+
// .then(html => document.documentElement.innerHTML = html)
|
|
86
|
+
//}, true)
|
|
87
|
+
|
|
88
|
+
// ============================== fetch ======================================
|
|
89
|
+
|
|
90
|
+
window.fetch = (input, init = {}) => {
|
|
91
|
+
const isReq = input instanceof Request
|
|
92
|
+
const url = isReq ? input.url : String(input instanceof URL ? input.href : input)
|
|
93
|
+
|
|
94
|
+
const headers = new Headers(init.headers || (isReq ? input.headers : {}))
|
|
95
|
+
headers.set('x-proxy-host', '_proxy')
|
|
96
|
+
|
|
97
|
+
const newInit = {
|
|
98
|
+
method: init.method || (isReq ? input.method : 'GET'),
|
|
99
|
+
headers,
|
|
100
|
+
body: 'body' in init ? init.body : (isReq ? input.body : null),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const passthrough = ['credentials', 'mode', 'cache', 'redirect', 'referrer',
|
|
104
|
+
'referrerPolicy', 'integrity', 'keepalive', 'signal']
|
|
105
|
+
for (const key of passthrough) {
|
|
106
|
+
if (key in init) newInit[key] = init[key]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (newInit.body instanceof ReadableStream) newInit.duplex = 'half'
|
|
110
|
+
|
|
111
|
+
return _fetch.call(window, rewriteUrl(url), newInit).catch((err) => {
|
|
112
|
+
console.warn('Proxied fetch failed, retrying without modifications:', err)
|
|
113
|
+
return _fetch.call(window, input, init)
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// =========================== XMLHttpRequest ================================
|
|
118
|
+
|
|
119
|
+
// Patch open() to stash the original args — we need them to re-open
|
|
120
|
+
// with a rewritten URL since XHR won't let you change it after open()
|
|
121
|
+
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
|
122
|
+
this._proxyMeta = { method, url: String(url), openArgs: rest }
|
|
123
|
+
return _xhrOpen.call(this, method, url, ...rest)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
127
|
+
const meta = this._proxyMeta
|
|
128
|
+
if (!meta) return _xhrSend.call(this, body)
|
|
129
|
+
|
|
130
|
+
const rewritten = rewriteUrl(meta.url)
|
|
131
|
+
|
|
132
|
+
// If the URL changed, we have to re-call open() with the new URL
|
|
133
|
+
if (rewritten !== meta.url) {
|
|
134
|
+
_xhrOpen.call(this, meta.method, rewritten, ...meta.openArgs)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_xhrSetHeader.call(this, 'x-proxy-host', '_proxy')
|
|
138
|
+
return _xhrSend.call(this, body)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================= WebSocket ===================================
|
|
142
|
+
|
|
143
|
+
//window.WebSocket = function (url, protocols) {
|
|
144
|
+
// var req = { type: 'websocket', url: rewriteUrl(url), originalUrl: url,
|
|
145
|
+
// method: 'GET', headers: {}, body: null };
|
|
146
|
+
// runRequestHooks(req);
|
|
147
|
+
// if (protocols !== undefined) return new _WebSocket(req.url, protocols);
|
|
148
|
+
// return new _WebSocket(req.url);
|
|
149
|
+
//};
|
|
150
|
+
//window.WebSocket.prototype = _WebSocket.prototype;
|
|
151
|
+
//window.WebSocket.CONNECTING = _WebSocket.CONNECTING;
|
|
152
|
+
//window.WebSocket.OPEN = _WebSocket.OPEN;
|
|
153
|
+
//window.WebSocket.CLOSING = _WebSocket.CLOSING;
|
|
154
|
+
//window.WebSocket.CLOSED = _WebSocket.CLOSED;
|
|
155
|
+
|
|
156
|
+
//// ============================ EventSource ==================================
|
|
157
|
+
|
|
158
|
+
//if (_EventSource) {
|
|
159
|
+
// window.EventSource = function (url, dict) {
|
|
160
|
+
// var raw = typeof url === 'string' ? url : url.href;
|
|
161
|
+
// var req = { type: 'eventsource', url: rewriteUrl(raw), originalUrl: raw,
|
|
162
|
+
// method: 'GET', headers: {}, body: null };
|
|
163
|
+
// runRequestHooks(req);
|
|
164
|
+
// return new _EventSource(req.url, dict);
|
|
165
|
+
// };
|
|
166
|
+
// window.EventSource.prototype = _EventSource.prototype;
|
|
167
|
+
// window.EventSource.CONNECTING = _EventSource.CONNECTING;
|
|
168
|
+
// window.EventSource.OPEN = _EventSource.OPEN;
|
|
169
|
+
// window.EventSource.CLOSED = _EventSource.CLOSED;
|
|
170
|
+
//}
|
|
171
|
+
|
|
172
|
+
})(window);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WrapItRuby engine styles — full layout + iframe proxy.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
html, body {
|
|
6
|
+
height: 100%;
|
|
7
|
+
margin: 0;
|
|
8
|
+
background-color: white;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#site-wrapper {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
height: 100%;
|
|
15
|
+
width: 100%;
|
|
16
|
+
|
|
17
|
+
#site-menu {
|
|
18
|
+
.ui.menu {
|
|
19
|
+
border: none;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#site-content {
|
|
24
|
+
flex-grow: 1;
|
|
25
|
+
margin: 7px;
|
|
26
|
+
margin-top: 0px;
|
|
27
|
+
border-radius: 15px;
|
|
28
|
+
background-color: grey;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#proxy-content {
|
|
34
|
+
height: 100%;
|
|
35
|
+
|
|
36
|
+
.iframe-wrapper {
|
|
37
|
+
height: 100%;
|
|
38
|
+
position: relative;
|
|
39
|
+
|
|
40
|
+
iframe {
|
|
41
|
+
border: 0;
|
|
42
|
+
display: block;
|
|
43
|
+
position: absolute;
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: 100%;
|
|
46
|
+
top: 0;
|
|
47
|
+
bottom: 0;
|
|
48
|
+
left: 0;
|
|
49
|
+
right: 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WrapItRuby
|
|
4
|
+
class ProxyController < ApplicationController
|
|
5
|
+
before_action :authenticate_user!
|
|
6
|
+
|
|
7
|
+
def show
|
|
8
|
+
get_menu_item.then do |menu_item|
|
|
9
|
+
target_path = request.path.delete_prefix(menu_item["route"])
|
|
10
|
+
target_domain = menu_item["url"]
|
|
11
|
+
|
|
12
|
+
target_url = "#{target_domain}/#{target_path}"
|
|
13
|
+
@iframe_src = "/_proxy/#{target_url}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def get_menu_item
|
|
20
|
+
path = request.path
|
|
21
|
+
all_proxy_menu_items.find { |item| path.start_with?(item["route"]) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// wrap_it_ruby/controllers/iframe_proxy_controller.js
|
|
2
|
+
//
|
|
3
|
+
// Manages browser history <-> iframe URL synchronisation.
|
|
4
|
+
//
|
|
5
|
+
// The proxy host and route segment differ:
|
|
6
|
+
// iframe: /_proxy/github.com/n-at-han-k
|
|
7
|
+
// browser: /github/n-at-han-k
|
|
8
|
+
//
|
|
9
|
+
// `host` is used when working with iframe paths (github.com)
|
|
10
|
+
// `current` is used when building browser paths (github)
|
|
11
|
+
//
|
|
12
|
+
// - History sync: on iframe load, translates the /_proxy prefixed
|
|
13
|
+
// iframe path back to the browser path and pushes state.
|
|
14
|
+
// - Breakout: if navigation inside the iframe lands on a path
|
|
15
|
+
// belonging to a DIFFERENT menu route, Turbo.visit
|
|
16
|
+
// swaps the frame to the new route.
|
|
17
|
+
//
|
|
18
|
+
import { Controller } from "@hotwired/stimulus"
|
|
19
|
+
|
|
20
|
+
export default class extends Controller {
|
|
21
|
+
static values = {
|
|
22
|
+
paths: Array, // iframe menu paths, e.g. ["/grafana", "/argocd"]
|
|
23
|
+
current: String, // route segment, e.g. "github"
|
|
24
|
+
host: String, // proxy host, e.g. "github.com"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
connect() {
|
|
28
|
+
this.element.addEventListener("load", this.onLoad)
|
|
29
|
+
window.addEventListener("popstate", this.onPopstate)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
disconnect() {
|
|
33
|
+
this.element.removeEventListener("load", this.onLoad)
|
|
34
|
+
window.removeEventListener("popstate", this.onPopstate)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onLoad = () => {
|
|
38
|
+
const { pathname, search } = this.element.contentWindow.location
|
|
39
|
+
if (pathname === "about:blank") return
|
|
40
|
+
|
|
41
|
+
const iframePath = pathname + search
|
|
42
|
+
const breakout = this.#detectBreakout(iframePath)
|
|
43
|
+
|
|
44
|
+
if (breakout) {
|
|
45
|
+
Turbo.visit(breakout, { action: "advance" })
|
|
46
|
+
} else {
|
|
47
|
+
this.#syncHistory(iframePath)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
onPopstate = (event) => {
|
|
52
|
+
const src = event.state?.iframeSrc
|
|
53
|
+
if (src) this.element.contentWindow.location.replace(src)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- private ----
|
|
57
|
+
|
|
58
|
+
// iframe path: /_proxy/github.com/n-at-han-k/repo
|
|
59
|
+
// Extract the host from the proxy path and check if it belongs to
|
|
60
|
+
// a different menu route.
|
|
61
|
+
#detectBreakout(iframePath) {
|
|
62
|
+
if (!iframePath.startsWith("/_proxy/")) return null
|
|
63
|
+
|
|
64
|
+
// Pull the host out: "github.com"
|
|
65
|
+
const afterProxy = iframePath.slice("/_proxy/".length)
|
|
66
|
+
const iframeHost = afterProxy.split("/")[0]
|
|
67
|
+
|
|
68
|
+
if (iframeHost === this.hostValue) return null
|
|
69
|
+
|
|
70
|
+
// Check if this host belongs to another menu route
|
|
71
|
+
// (would need host->route mapping; skip for now)
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#syncHistory(iframePath) {
|
|
76
|
+
const proxyBase = `/_proxy/${this.hostValue}`
|
|
77
|
+
const subPath = iframePath.startsWith(proxyBase)
|
|
78
|
+
? iframePath.slice(proxyBase.length)
|
|
79
|
+
: iframePath
|
|
80
|
+
|
|
81
|
+
const browserPath = `/${this.currentValue}${subPath}`
|
|
82
|
+
|
|
83
|
+
if (browserPath !== window.location.pathname + window.location.search) {
|
|
84
|
+
history.pushState({ iframeSrc: iframePath }, "", browserPath)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<p>Welcome, <%= current_user&.display_name || current_user&.uid %>.</p>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= content_for(:title) || site_title %></title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="application-name" content="<%= site_title %>">
|
|
8
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
9
|
+
<%= csrf_meta_tags %>
|
|
10
|
+
<%= csp_meta_tag %>
|
|
11
|
+
|
|
12
|
+
<%= yield :head %>
|
|
13
|
+
|
|
14
|
+
<link rel="icon" href="/icon.png" type="image/png">
|
|
15
|
+
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
|
16
|
+
<link rel="apple-touch-icon" href="/icon.png">
|
|
17
|
+
|
|
18
|
+
<%= stylesheet_link_tag "semantic.min.css", "data-turbo-track": "reload" %>
|
|
19
|
+
<%= stylesheet_link_tag "wrap_it_ruby/application", "data-turbo-track": "reload" %>
|
|
20
|
+
<%= fui_javascript_tags %>
|
|
21
|
+
<%= javascript_importmap_tags %>
|
|
22
|
+
</head>
|
|
23
|
+
|
|
24
|
+
<body>
|
|
25
|
+
<div id="site-wrapper">
|
|
26
|
+
<div id="site-menu"><%= render "wrap_it_ruby/shared/navbar" %></div>
|
|
27
|
+
<div id="site-content"><%= yield %></div>
|
|
28
|
+
</div>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Menu(attached: true) {
|
|
2
|
+
WrapItRuby::Menu.menu_config.each do |group|
|
|
3
|
+
if group["items"]
|
|
4
|
+
group["items"].each do |item|
|
|
5
|
+
MenuItem(href: item["route"]) { text item["label"] }
|
|
6
|
+
end
|
|
7
|
+
else
|
|
8
|
+
MenuItem(href: group["route"]) { text group["label"] }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
}
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Importmap pins for the WrapItRuby engine.
|
|
2
|
+
# These get merged into the host app's importmap via the engine initializer.
|
|
3
|
+
|
|
4
|
+
pin_all_from WrapItRuby::Engine.root.join("app/javascript/wrap_it_ruby/controllers"),
|
|
5
|
+
under: "controllers/wrap_it_ruby",
|
|
6
|
+
to: "wrap_it_ruby/controllers"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WrapItRuby
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Install WrapItRuby into the host application"
|
|
9
|
+
|
|
10
|
+
def copy_menu_config
|
|
11
|
+
template "menu.yml", "config/menu.yml"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def mount_engine_routes
|
|
15
|
+
route 'mount WrapItRuby::Engine, at: "/"'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def show_post_install_message
|
|
19
|
+
say ""
|
|
20
|
+
say "WrapItRuby installed!", :green
|
|
21
|
+
say ""
|
|
22
|
+
say " 1. Edit config/menu.yml to configure your proxy routes"
|
|
23
|
+
say " 2. Make sure your ApplicationController defines authenticate_user!"
|
|
24
|
+
say " 3. Include wrap_it_ruby/application.css in your stylesheet"
|
|
25
|
+
say ""
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# config/menu.yml
|
|
2
|
+
#
|
|
3
|
+
# Menu configuration for WrapItRuby.
|
|
4
|
+
#
|
|
5
|
+
# route: local path in this app (e.g. /grafana)
|
|
6
|
+
# url: upstream host to proxy (e.g. grafana.example.com)
|
|
7
|
+
#
|
|
8
|
+
# type:
|
|
9
|
+
# proxy - proxied service rendered inside an iframe
|
|
10
|
+
# link - standard navigation within the app
|
|
11
|
+
|
|
12
|
+
- label: Example
|
|
13
|
+
route: /example
|
|
14
|
+
url: example.com
|
|
15
|
+
icon: globe
|
|
16
|
+
type: proxy
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "wrap_it_ruby/middleware/proxy_middleware"
|
|
4
|
+
require "wrap_it_ruby/middleware/root_relative_proxy_middleware"
|
|
5
|
+
require "wrap_it_ruby/middleware/script_injection_middleware"
|
|
6
|
+
|
|
7
|
+
module WrapItRuby
|
|
8
|
+
class Engine < ::Rails::Engine
|
|
9
|
+
isolate_namespace WrapItRuby
|
|
10
|
+
|
|
11
|
+
# Insert proxy middleware early — before Rails routing — so /_proxy/*
|
|
12
|
+
# requests never hit ActionDispatch at all.
|
|
13
|
+
#
|
|
14
|
+
# Order: RootRelativeProxy -> ScriptInjection -> Proxy
|
|
15
|
+
# 1. RootRelativeProxy rewrites root-relative paths to /_proxy/{host}
|
|
16
|
+
# 2. ScriptInjection strips Accept-Encoding, passes to Proxy,
|
|
17
|
+
# then injects <base> + interception.js into HTML responses
|
|
18
|
+
# 3. Proxy does the actual upstream proxying
|
|
19
|
+
initializer "wrap_it_ruby.middleware" do |app|
|
|
20
|
+
app.middleware.insert_before ActionDispatch::Static, WrapItRuby::Middleware::RootRelativeProxyMiddleware
|
|
21
|
+
app.middleware.insert_after WrapItRuby::Middleware::RootRelativeProxyMiddleware, WrapItRuby::Middleware::ScriptInjectionMiddleware
|
|
22
|
+
app.middleware.insert_after WrapItRuby::Middleware::ScriptInjectionMiddleware, WrapItRuby::Middleware::ProxyMiddleware
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Register importmap pins for the Stimulus controller
|
|
26
|
+
initializer "wrap_it_ruby.importmap", before: "importmap" do |app|
|
|
27
|
+
if app.config.respond_to?(:importmap)
|
|
28
|
+
app.config.importmap.paths << Engine.root.join("config/importmap.rb")
|
|
29
|
+
app.config.importmap.cache_sweepers << Engine.root.join("app/javascript")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Add engine assets to the asset load path (for propshaft/sprockets)
|
|
34
|
+
initializer "wrap_it_ruby.assets" do |app|
|
|
35
|
+
app.config.assets.paths << Engine.root.join("app/assets/javascripts")
|
|
36
|
+
app.config.assets.paths << Engine.root.join("app/assets/stylesheets")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module WrapItRuby
|
|
6
|
+
# Loads and queries the menu configuration from the host app's
|
|
7
|
+
# config/menu.yml file.
|
|
8
|
+
#
|
|
9
|
+
# Can be used as a module (extend self) or included in controllers/helpers.
|
|
10
|
+
#
|
|
11
|
+
module Menu
|
|
12
|
+
def menu_config = load_menu
|
|
13
|
+
|
|
14
|
+
def all_menu_items
|
|
15
|
+
menu_config.flat_map { |item| [item, *item.fetch("items", [])] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def all_proxy_menu_items
|
|
19
|
+
all_menu_items.select { |item| item["type"] == "proxy" }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def proxy_paths
|
|
23
|
+
all_menu_items
|
|
24
|
+
.select { |item| item["type"] == "proxy" }
|
|
25
|
+
.map { |item| item["route"] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
extend self
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def menu_file
|
|
33
|
+
Rails.root.join("config/menu.yml")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def load_menu
|
|
37
|
+
@menu_config ||= YAML.load_file(menu_file)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/http/client"
|
|
4
|
+
require "async/http/endpoint"
|
|
5
|
+
require "async/websocket/adapters/rack"
|
|
6
|
+
|
|
7
|
+
module WrapItRuby
|
|
8
|
+
module Middleware
|
|
9
|
+
# Rack middleware that proxies /_proxy/{host}/{path} to the upstream host.
|
|
10
|
+
#
|
|
11
|
+
# Strips frame-blocking headers so responses render inside an iframe.
|
|
12
|
+
# Rewrites Location headers so redirects stay within the proxy.
|
|
13
|
+
# Detects WebSocket upgrades and pipes frames bidirectionally.
|
|
14
|
+
#
|
|
15
|
+
class ProxyMiddleware
|
|
16
|
+
PATTERN = %r{\A/_proxy/(?<host>[^/]+)(?<path>/.*)?\z}
|
|
17
|
+
|
|
18
|
+
HOP_HEADERS = %w[
|
|
19
|
+
connection
|
|
20
|
+
keep-alive
|
|
21
|
+
proxy-authenticate
|
|
22
|
+
proxy-authorization
|
|
23
|
+
te
|
|
24
|
+
trailers
|
|
25
|
+
transfer-encoding
|
|
26
|
+
upgrade
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
def initialize(app)
|
|
30
|
+
@app = app
|
|
31
|
+
@clients = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call(env)
|
|
35
|
+
PATTERN.match(env["PATH_INFO"].to_s).then do |match|
|
|
36
|
+
if match
|
|
37
|
+
host = match[:host]
|
|
38
|
+
path = match[:path] || "/"
|
|
39
|
+
|
|
40
|
+
if websocket?(env)
|
|
41
|
+
proxy_websocket(env, host, path)
|
|
42
|
+
else
|
|
43
|
+
proxy_http(env, host, path)
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
@app.call(env)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# ---- HTTP ----
|
|
54
|
+
|
|
55
|
+
def proxy_http(env, host, path)
|
|
56
|
+
client = client_for(host)
|
|
57
|
+
|
|
58
|
+
query = env["QUERY_STRING"]
|
|
59
|
+
full_path = query && !query.empty? ? "#{path}?#{query}" : path
|
|
60
|
+
headers = forwarded_headers(env, host)
|
|
61
|
+
body = read_body(env)
|
|
62
|
+
|
|
63
|
+
request = Protocol::HTTP::Request.new(
|
|
64
|
+
"https", host, env["REQUEST_METHOD"], full_path, nil, headers, body
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
response = client.call(request)
|
|
68
|
+
|
|
69
|
+
rack_body = Enumerator.new do |y|
|
|
70
|
+
while (chunk = response.body&.read)
|
|
71
|
+
y << chunk
|
|
72
|
+
end
|
|
73
|
+
ensure
|
|
74
|
+
response.body&.close
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
rack_headers = strip_headers(response.headers, host)
|
|
78
|
+
[response.status, rack_headers, rack_body]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# ---- WebSocket ----
|
|
82
|
+
|
|
83
|
+
def websocket?(env)
|
|
84
|
+
Async::WebSocket::Adapters::Rack.websocket?(env)
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# ---- Helpers ----
|
|
89
|
+
|
|
90
|
+
def client_for(host)
|
|
91
|
+
@clients[host] ||= Async::HTTP::Client.new(
|
|
92
|
+
Async::HTTP::Endpoint.parse("https://#{host}"),
|
|
93
|
+
retries: 0
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def forwarded_headers(env, host)
|
|
98
|
+
headers = Protocol::HTTP::Headers.new
|
|
99
|
+
|
|
100
|
+
env.each do |key, value|
|
|
101
|
+
next unless key.start_with?("HTTP_")
|
|
102
|
+
name = key.delete_prefix("HTTP_").downcase.tr("_", "-")
|
|
103
|
+
next if name == "host" || HOP_HEADERS.include?(name)
|
|
104
|
+
headers.add(name, value)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
headers.add("content-type", env["CONTENT_TYPE"]) if env["CONTENT_TYPE"]
|
|
108
|
+
headers.add("content-length", env["CONTENT_LENGTH"]) if env["CONTENT_LENGTH"]
|
|
109
|
+
headers.add("host", host)
|
|
110
|
+
|
|
111
|
+
headers
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def strip_headers(upstream_headers, host)
|
|
115
|
+
result = {}
|
|
116
|
+
upstream_headers.each do |name, value|
|
|
117
|
+
key = name.downcase
|
|
118
|
+
next if key == "x-frame-options"
|
|
119
|
+
next if key == "content-security-policy"
|
|
120
|
+
next if key == "content-security-policy-report-only"
|
|
121
|
+
next if key == "content-encoding"
|
|
122
|
+
next if HOP_HEADERS.include?(key)
|
|
123
|
+
result[name] = value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Rewrite Location headers so redirects stay within the proxy
|
|
127
|
+
if (location = result["location"] || result["Location"])
|
|
128
|
+
result_key = result.key?("location") ? "location" : "Location"
|
|
129
|
+
begin
|
|
130
|
+
uri = URI.parse(location)
|
|
131
|
+
if uri.host == host || uri.host&.end_with?(".#{host}")
|
|
132
|
+
redirect_host = uri.host || host
|
|
133
|
+
result[result_key] = "/_proxy/#{redirect_host}#{uri.path}#{"?#{uri.query}" if uri.query}"
|
|
134
|
+
elsif uri.relative?
|
|
135
|
+
result[result_key] = "/_proxy/#{host}#{location}"
|
|
136
|
+
end
|
|
137
|
+
rescue URI::InvalidURIError
|
|
138
|
+
# leave it alone
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
result
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def read_body(env)
|
|
146
|
+
input = env["rack.input"]
|
|
147
|
+
return nil unless input
|
|
148
|
+
body = input.read
|
|
149
|
+
input.rewind rescue nil
|
|
150
|
+
body && !body.empty? ? Protocol::HTTP::Body::Buffered.wrap(body) : nil
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WrapItRuby
|
|
4
|
+
module Middleware
|
|
5
|
+
# Handles root-relative requests (e.g. /logo.png, /api/data) that originate
|
|
6
|
+
# from within a proxied iframe page.
|
|
7
|
+
#
|
|
8
|
+
# Detects the proxy host from two sources (in priority order):
|
|
9
|
+
#
|
|
10
|
+
# 1. X-Proxy-Host header -- set by the interception script on fetch/XHR.
|
|
11
|
+
# Signals the request came from inside the proxy. The actual upstream
|
|
12
|
+
# host is resolved from the Referer header.
|
|
13
|
+
#
|
|
14
|
+
# 2. Referer header -- contains /_proxy/{host}/... for requests where the
|
|
15
|
+
# browser sends the full path. Works for both scripted requests and
|
|
16
|
+
# asset loads (<img>, <link>, <script>).
|
|
17
|
+
#
|
|
18
|
+
# Rewrites PATH_INFO to /_proxy/{host}{path} so ProxyMiddleware handles it.
|
|
19
|
+
# Must be inserted BEFORE ProxyMiddleware in the Rack stack.
|
|
20
|
+
#
|
|
21
|
+
class RootRelativeProxyMiddleware
|
|
22
|
+
PROXY_PREFIX = "/_proxy"
|
|
23
|
+
PROXY_HOST_HEADER = "HTTP_X_PROXY_HOST"
|
|
24
|
+
REFERER_PATTERN = %r{/_proxy/(?<host>[^/]+)}
|
|
25
|
+
|
|
26
|
+
def initialize(app)
|
|
27
|
+
@app = app
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(env)
|
|
31
|
+
path = env["PATH_INFO"].to_s
|
|
32
|
+
|
|
33
|
+
unless path.start_with?(PROXY_PREFIX)
|
|
34
|
+
host = extract_proxy_host(env)
|
|
35
|
+
if host
|
|
36
|
+
env["PATH_INFO"] = "#{PROXY_PREFIX}/#{host}#{path}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@app.call(env)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def extract_proxy_host(env)
|
|
46
|
+
referer = env["HTTP_REFERER"]
|
|
47
|
+
match = REFERER_PATTERN.match(referer) if referer
|
|
48
|
+
match[:host] if match
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WrapItRuby
|
|
4
|
+
module Middleware
|
|
5
|
+
# Injects interception.js inline into HTML responses from the proxy.
|
|
6
|
+
#
|
|
7
|
+
# - Strips Accept-Encoding from proxy requests so upstream sends
|
|
8
|
+
# uncompressed HTML (avoids decompress/recompress just to inject).
|
|
9
|
+
# - Buffers HTML responses and injects the script as the first child
|
|
10
|
+
# of <head>.
|
|
11
|
+
# - Extracts the proxy host from PATH_INFO and injects it as
|
|
12
|
+
# window.__proxyHost so the script works even when the browser URL
|
|
13
|
+
# doesn't contain /_proxy/ (e.g. after root-relative navigation).
|
|
14
|
+
# - Injects window.__hostingSite so the interception script knows
|
|
15
|
+
# which host is the proxy server itself.
|
|
16
|
+
# - Reads the script file once and caches it.
|
|
17
|
+
#
|
|
18
|
+
class ScriptInjectionMiddleware
|
|
19
|
+
PROXY_PREFIX = "/_proxy/"
|
|
20
|
+
SCRIPT_FILE = File.expand_path("../../../app/assets/javascripts/wrap_it_ruby/interception.js", __dir__).freeze
|
|
21
|
+
|
|
22
|
+
def initialize(app)
|
|
23
|
+
@app = app
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(env)
|
|
27
|
+
path = env["PATH_INFO"].to_s
|
|
28
|
+
|
|
29
|
+
if path.start_with?(PROXY_PREFIX)
|
|
30
|
+
host = path.delete_prefix(PROXY_PREFIX).split("/", 2).first
|
|
31
|
+
hosting_site = env["HTTP_HOST"]
|
|
32
|
+
|
|
33
|
+
env.delete("HTTP_ACCEPT_ENCODING")
|
|
34
|
+
status, headers, body = @app.call(env)
|
|
35
|
+
|
|
36
|
+
if html_response?(headers)
|
|
37
|
+
body = inject_script(body, headers, host, hosting_site)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
[status, headers, body]
|
|
41
|
+
else
|
|
42
|
+
@app.call(env)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def script_source
|
|
49
|
+
@script_source ||= File.read(SCRIPT_FILE)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def html_response?(headers)
|
|
53
|
+
ct = headers["content-type"] || headers["Content-Type"] || ""
|
|
54
|
+
ct.include?("text/html")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def inject_script(body, headers, host, hosting_site)
|
|
58
|
+
html = +""
|
|
59
|
+
body.each { |chunk| html << chunk }
|
|
60
|
+
body.close if body.respond_to?(:close)
|
|
61
|
+
html.force_encoding("UTF-8")
|
|
62
|
+
|
|
63
|
+
tag = "<base href=\"/_proxy/#{host}/\">" \
|
|
64
|
+
"<script>" \
|
|
65
|
+
"window.__proxyHost=#{host.to_json};" \
|
|
66
|
+
"window.__hostingSite=#{hosting_site.to_json};" \
|
|
67
|
+
"#{script_source}" \
|
|
68
|
+
"</script>"
|
|
69
|
+
|
|
70
|
+
unless html.sub!(%r{(<head[^>]*>)}i, "\\1#{tag}")
|
|
71
|
+
html.prepend(tag)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
headers.delete("content-length")
|
|
75
|
+
headers["content-length"] = html.bytesize.to_s
|
|
76
|
+
[html]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/wrap_it_ruby.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wrap_it_ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nathan Kidd
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: importmap-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: stimulus-rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: async-http
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: async-websocket
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rails-active-ui
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
description: Wraps upstream web applications in an iframe via a same-origin reverse
|
|
97
|
+
proxy. Provides Rack middleware for proxying, script injection, and root-relative
|
|
98
|
+
URL rewriting, plus Stimulus controllers for browser history synchronisation.
|
|
99
|
+
email:
|
|
100
|
+
- nathankidd@hey.com
|
|
101
|
+
executables: []
|
|
102
|
+
extensions: []
|
|
103
|
+
extra_rdoc_files: []
|
|
104
|
+
files:
|
|
105
|
+
- Rakefile
|
|
106
|
+
- app/assets/javascripts/wrap_it_ruby/interception.js
|
|
107
|
+
- app/assets/stylesheets/wrap_it_ruby/application.css
|
|
108
|
+
- app/controllers/wrap_it_ruby/application_controller.rb
|
|
109
|
+
- app/controllers/wrap_it_ruby/home_controller.rb
|
|
110
|
+
- app/controllers/wrap_it_ruby/proxy_controller.rb
|
|
111
|
+
- app/helpers/wrap_it_ruby/iframe_helper.rb
|
|
112
|
+
- app/helpers/wrap_it_ruby/menu_helper.rb
|
|
113
|
+
- app/javascript/wrap_it_ruby/controllers/iframe_proxy_controller.js
|
|
114
|
+
- app/views/wrap_it_ruby/home/index.html.erb
|
|
115
|
+
- app/views/wrap_it_ruby/layouts/application.html.erb
|
|
116
|
+
- app/views/wrap_it_ruby/proxy/show.html.ruby
|
|
117
|
+
- app/views/wrap_it_ruby/shared/_navbar.html.ruby
|
|
118
|
+
- config/importmap.rb
|
|
119
|
+
- config/routes.rb
|
|
120
|
+
- lib/generators/wrap_it_ruby/install/install_generator.rb
|
|
121
|
+
- lib/generators/wrap_it_ruby/install/templates/menu.yml
|
|
122
|
+
- lib/wrap_it_ruby.rb
|
|
123
|
+
- lib/wrap_it_ruby/engine.rb
|
|
124
|
+
- lib/wrap_it_ruby/menu.rb
|
|
125
|
+
- lib/wrap_it_ruby/middleware/proxy_middleware.rb
|
|
126
|
+
- lib/wrap_it_ruby/middleware/root_relative_proxy_middleware.rb
|
|
127
|
+
- lib/wrap_it_ruby/middleware/script_injection_middleware.rb
|
|
128
|
+
- lib/wrap_it_ruby/version.rb
|
|
129
|
+
homepage: https://github.com/n-at-han-k/wrap-it-ruby
|
|
130
|
+
licenses:
|
|
131
|
+
- Apache-2.0
|
|
132
|
+
metadata: {}
|
|
133
|
+
rdoc_options: []
|
|
134
|
+
require_paths:
|
|
135
|
+
- lib
|
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
137
|
+
requirements:
|
|
138
|
+
- - ">="
|
|
139
|
+
- !ruby/object:Gem::Version
|
|
140
|
+
version: '3.2'
|
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
requirements: []
|
|
147
|
+
rubygems_version: 3.7.2
|
|
148
|
+
specification_version: 4
|
|
149
|
+
summary: Rails engine for iframe-based reverse proxy portals
|
|
150
|
+
test_files: []
|