cleanup
This commit is contained in:
parent
eef6ca679f
commit
c234077fdc
18
.jshintrc
18
.jshintrc
|
@ -1,18 +0,0 @@
|
|||
{ "node": true
|
||||
, "browser": true
|
||||
, "jquery": true
|
||||
, "globals": { "angular": true, "Promise": true }
|
||||
|
||||
, "indent": 2
|
||||
, "onevar": true
|
||||
, "laxcomma": true
|
||||
, "laxbreak": true
|
||||
, "curly": true
|
||||
, "nonbsp": true
|
||||
|
||||
, "eqeqeq": true
|
||||
, "immed": true
|
||||
, "undef": true
|
||||
, "unused": true
|
||||
, "latedef": true
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// +build !dev
|
||||
//go:generate go run -mod vendor github.com/shurcooL/vfsgen/cmd/vfsgendev -source="git.rootprojects.org/root/telebit/admin".AdminFS
|
||||
//go:generate go run -mod vendor github.com/shurcooL/vfsgen/cmd/vfsgendev -source="git.rootprojects.org/root/telebit/assets/admin".AdminFS
|
||||
|
||||
package admin
|
|
@ -1,4 +1,4 @@
|
|||
// +build !dev
|
||||
//go:generate go run -mod vendor github.com/shurcooL/vfsgen/cmd/vfsgendev -source="git.rootprojects.org/root/telebit/files".Assets
|
||||
//go:generate go run -mod vendor github.com/shurcooL/vfsgen/cmd/vfsgendev -source="git.rootprojects.org/root/telebit/assets/files".Assets
|
||||
|
||||
package files
|
|
@ -1,6 +0,0 @@
|
|||
go generate ./...
|
||||
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod vendor -o mgmt-server-linux ./cmd/mgmt/*.go
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -mod vendor -o mgmt-server-macos ./cmd/mgmt/*.go
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -o mgmt-server-windows-debug.exe ./cmd/mgmt/*.go
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -ldflags "-H windowsgui" -o mgmt-server-windows.exe ./cmd/mgmt/*.go
|
|
@ -1,11 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -u
|
||||
|
||||
go generate ./...
|
||||
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod vendor -o telebit-relay-linux ./cmd/telebit/*.go
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -mod vendor -o telebit-relay-macos ./cmd/telebit/*.go
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -o telebit-relay-windows-debug.exe ./cmd/telebit/*.go
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -ldflags "-H windowsgui" -o telebit-relay-windows.exe ./cmd/telebit/*.go
|
|
@ -5,7 +5,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
|
||||
"git.rootprojects.org/root/telebit/iplist"
|
||||
"git.rootprojects.org/root/telebit/internal/iplist"
|
||||
)
|
||||
|
||||
func help() {
|
||||
|
|
|
@ -11,9 +11,9 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit"
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
||||
"git.rootprojects.org/root/telebit/internal/telebit"
|
||||
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
|
|
|
@ -19,17 +19,15 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit"
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dns01"
|
||||
"git.rootprojects.org/root/telebit/internal/http01"
|
||||
"git.rootprojects.org/root/telebit/internal/iplist"
|
||||
"git.rootprojects.org/root/telebit/internal/mgmt"
|
||||
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
||||
"git.rootprojects.org/root/telebit/internal/service"
|
||||
telebitX "git.rootprojects.org/root/telebit/internal/telebit"
|
||||
"git.rootprojects.org/root/telebit/iplist"
|
||||
"git.rootprojects.org/root/telebit/table"
|
||||
"git.rootprojects.org/root/telebit/tunnel"
|
||||
"git.rootprojects.org/root/telebit/internal/telebit"
|
||||
"git.rootprojects.org/root/telebit/internal/tunnel"
|
||||
|
||||
"github.com/coolaj86/certmagic"
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
|
@ -590,7 +588,7 @@ func muxAll(
|
|||
if "" != *apiHostname {
|
||||
// this is a generic net listener
|
||||
r := chi.NewRouter()
|
||||
telebitX.RouteAdmin(*authURL, r)
|
||||
telebit.RouteAdmin(*authURL, r)
|
||||
apiListener := tunnel.NewListener()
|
||||
go func() {
|
||||
httpsrv := &http.Server{Handler: r}
|
||||
|
@ -716,7 +714,7 @@ func routeSubscribersAndClients(client net.Conn) error {
|
|||
|
||||
// tryToServeName picks the server tunnel with the least connections, if any
|
||||
func tryToServeName(servername string, wconn *telebit.ConnWrap) bool {
|
||||
srv, ok := table.GetServer(servername)
|
||||
srv, ok := telebit.GetServer(servername)
|
||||
if !ok || nil == srv {
|
||||
if ok {
|
||||
// TODO BUG: Sometimes srv=nil & ok=true, which should not be possible
|
||||
|
|
|
@ -11,9 +11,9 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit"
|
||||
"git.rootprojects.org/root/telebit/internal/mgmt"
|
||||
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
||||
"git.rootprojects.org/root/telebit/internal/telebit"
|
||||
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/gorilla/websocket"
|
||||
|
|
|
@ -13,9 +13,8 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
telebit "git.rootprojects.org/root/telebit"
|
||||
tbDns01 "git.rootprojects.org/root/telebit/internal/dns01"
|
||||
"git.rootprojects.org/root/telebit/table"
|
||||
"git.rootprojects.org/root/telebit/internal/telebit"
|
||||
|
||||
"github.com/coolaj86/certmagic"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
|
@ -219,7 +218,7 @@ func upgradeWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
wsTun := telebit.NewWebsocketTunnel(conn)
|
||||
server := &table.SubscriberConn{
|
||||
server := &telebit.SubscriberConn{
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
WSConn: conn,
|
||||
WSTun: wsTun,
|
||||
|
@ -243,7 +242,7 @@ func upgradeWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Printf("a subscriber stream is done: %q\n", err)
|
||||
}()
|
||||
|
||||
table.Add(server)
|
||||
telebit.Add(server)
|
||||
}
|
||||
|
||||
func getACMEProvider(acmeRelay, token *string) (challenge.Provider, error) {
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Page Not Found</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
* {
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
color: #888;
|
||||
display: table;
|
||||
font-family: sans-serif;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
margin: 2em auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #555;
|
||||
font-size: 2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 auto;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 280px) {
|
||||
body,
|
||||
p {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 0 0 0.3em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Page Not Found</h1>
|
||||
<p>Sorry, but the page you were trying to view does not exist.</p>
|
||||
</body>
|
||||
</html>
|
||||
<!-- IE needs 512+ bytes: http://blogs.msdn.com/b/ieinternals/archive/2010/08/19/http-error-pages-in-internet-explorer.aspx -->
|
Binary file not shown.
Before Width: | Height: | Size: 3.9 KiB |
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Please read: https://msdn.microsoft.com/en-us/library/ie/dn455106.aspx -->
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src="tile.png"/>
|
||||
<square150x150logo src="tile.png"/>
|
||||
<wide310x150logo src="tile-wide.png"/>
|
||||
<square310x310logo src="tile.png"/>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
|
||||
<cross-domain-policy>
|
||||
<!-- Read this: https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->
|
||||
|
||||
<!-- Most restrictive policy: -->
|
||||
<site-control permitted-cross-domain-policies="none"/>
|
||||
|
||||
<!-- Least restrictive policy: -->
|
||||
<!--
|
||||
<site-control permitted-cross-domain-policies="all"/>
|
||||
<allow-access-from domain="*" to-ports="*" secure="false"/>
|
||||
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
|
||||
-->
|
||||
</cross-domain-policy>
|
|
@ -1,263 +0,0 @@
|
|||
/*! HTML5 Boilerplate v5.3.0 | MIT License | https://html5boilerplate.com/ */
|
||||
|
||||
/*
|
||||
* What follows is the result of much research on cross-browser styling.
|
||||
* Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,
|
||||
* Kroc Camen, and the H5BP dev community and team.
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
Base styles: opinionated defaults
|
||||
========================================================================== */
|
||||
|
||||
html {
|
||||
color: #222;
|
||||
font-size: 1em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove text-shadow in selection highlight:
|
||||
* https://twitter.com/miketaylr/status/12228805301
|
||||
*
|
||||
* These selection rule sets have to be separate.
|
||||
* Customize the background color to match your design.
|
||||
*/
|
||||
|
||||
::-moz-selection {
|
||||
background: #b3d4fc;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #b3d4fc;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* A better looking default horizontal rule
|
||||
*/
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove the gap between audio, canvas, iframes,
|
||||
* images, videos and the bottom of their containers:
|
||||
* https://github.com/h5bp/html5-boilerplate/issues/440
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
iframe,
|
||||
img,
|
||||
svg,
|
||||
video {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove default fieldset styles.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allow only vertical resizing of textareas.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Browser Upgrade Prompt
|
||||
========================================================================== */
|
||||
|
||||
.browserupgrade {
|
||||
margin: 0.2em 0;
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Author's custom styles
|
||||
========================================================================== */
|
||||
|
||||
/* ==========================================================================
|
||||
Helper classes
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Hide visually and from screen readers
|
||||
*/
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Hide only visually, but have it available for screen readers:
|
||||
* http://snook.ca/archives/html_and_css/hiding-content-for-accessibility
|
||||
*/
|
||||
|
||||
.visuallyhidden {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Extends the .visuallyhidden class to allow the element
|
||||
* to be focusable when navigated to via the keyboard:
|
||||
* https://www.drupal.org/node/897638
|
||||
*/
|
||||
|
||||
.visuallyhidden.focusable:active,
|
||||
.visuallyhidden.focusable:focus {
|
||||
clip: auto;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Hide visually and from screen readers, but maintain layout
|
||||
*/
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/*
|
||||
* Clearfix: contain floats
|
||||
*
|
||||
* For modern browsers
|
||||
* 1. The space content is one way to avoid an Opera bug when the
|
||||
* `contenteditable` attribute is included anywhere else in the document.
|
||||
* Otherwise it causes space to appear at the top and bottom of elements
|
||||
* that receive the `clearfix` class.
|
||||
* 2. The use of `table` rather than `block` is only necessary if using
|
||||
* `:before` to contain the top-margins of child elements.
|
||||
*/
|
||||
|
||||
.clearfix:before,
|
||||
.clearfix:after {
|
||||
content: " "; /* 1 */
|
||||
display: table; /* 2 */
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
EXAMPLE Media Queries for Responsive Design.
|
||||
These examples override the primary ('mobile first') styles.
|
||||
Modify as content requires.
|
||||
========================================================================== */
|
||||
|
||||
@media only screen and (min-width: 35em) {
|
||||
/* Style adjustments for viewports that meet the condition */
|
||||
}
|
||||
|
||||
@media print, (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 1.25dppx), (min-resolution: 120dpi) {
|
||||
/* Style adjustments for high resolution devices */
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Print styles.
|
||||
Inlined to avoid the additional HTTP request:
|
||||
http://www.phpied.com/delay-loading-your-print-css/
|
||||
========================================================================== */
|
||||
|
||||
@media print {
|
||||
*,
|
||||
*:before,
|
||||
*:after,
|
||||
*:first-letter,
|
||||
*:first-line {
|
||||
background: transparent !important;
|
||||
color: #000 !important; /* Black prints faster:
|
||||
http://www.sanbeiji.com/archives/953 */
|
||||
box-shadow: none !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a[href]:after {
|
||||
content: " (" attr(href) ")";
|
||||
}
|
||||
|
||||
abbr[title]:after {
|
||||
content: " (" attr(title) ")";
|
||||
}
|
||||
|
||||
/*
|
||||
* Don't show links that are fragment identifiers,
|
||||
* or use the `javascript:` pseudo protocol
|
||||
*/
|
||||
|
||||
a[href^="#"]:after,
|
||||
a[href^="javascript:"]:after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
pre,
|
||||
blockquote {
|
||||
border: 1px solid #999;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
/*
|
||||
* Printing Tables:
|
||||
* http://css-discuss.incutio.com/wiki/Printing_Tables
|
||||
*/
|
||||
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
tr,
|
||||
img {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
p,
|
||||
h2,
|
||||
h3 {
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
}
|
|
@ -1,424 +0,0 @@
|
|||
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS and IE text size adjust after device orientation change,
|
||||
* without disabling user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||
* and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||
* Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability of focused elements when they are also in an
|
||||
* active/hover state.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
box-sizing: content-box; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 766 B |
|
@ -1,15 +0,0 @@
|
|||
# humanstxt.org/
|
||||
# The humans responsible & technology colophon
|
||||
|
||||
# TEAM
|
||||
|
||||
<name> -- <role> -- <twitter>
|
||||
|
||||
# THANKS
|
||||
|
||||
<name>
|
||||
|
||||
# TECHNOLOGY COLOPHON
|
||||
|
||||
CSS3, HTML5
|
||||
Apache Server Configs, jQuery, Modernizr, Normalize.css
|
|
@ -1,97 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="no-js" lang="">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title></title>
|
||||
<meta name="description" content="" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||
<!-- Place favicon.ico in the root directory -->
|
||||
|
||||
<link rel="stylesheet" href="css/normalize.css" />
|
||||
<link rel="stylesheet" href="css/main.css" />
|
||||
<script src="js/vendor/modernizr-2.8.3.min.js"></script>
|
||||
<base href="/" />
|
||||
</head>
|
||||
<body ng-app="rvpnApp">
|
||||
<!--[if lt IE 8]>
|
||||
<p class="browserupgrade">
|
||||
You are using an <strong>outdated</strong> browser. Please
|
||||
<a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.
|
||||
</p>
|
||||
<![endif]-->
|
||||
|
||||
<!-- Admin GUI Begins Here -->
|
||||
<header class="bs-docs-nav navbar navbar-static-top" id="top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a href="/admin/index.html" target="_parent" class="navbar-brand">RVPN Admin</a>
|
||||
</div>
|
||||
<nav class="collapse navbar-collapse" id="bs-navbar">
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="/admin/status">Status</a></li>
|
||||
<li><a href="/admin/servers">Servers</a></li>
|
||||
<li><a href="/admin/#domains">Domains</a></li>
|
||||
<li><a href="/admin/#connections">Connections</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="#">Help</a></li>
|
||||
<li><a href="#">Login</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-1.12.0.min.js"></script>
|
||||
<script>
|
||||
window.jQuery || document.write('<script src="admin/js/vendor/jquery-1.12.0.min.js"><\/script>');
|
||||
</script>
|
||||
<script src="admin/js/plugins.js"></script>
|
||||
<script src="admin/js/main.js"></script>
|
||||
|
||||
<!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
|
||||
<script>
|
||||
(function (b, o, i, l, e, r) {
|
||||
b.GoogleAnalyticsObject = l;
|
||||
b[l] ||
|
||||
(b[l] = function () {
|
||||
(b[l].q = b[l].q || []).push(arguments);
|
||||
});
|
||||
b[l].l = +new Date();
|
||||
e = o.createElement(i);
|
||||
r = o.getElementsByTagName(i)[0];
|
||||
e.src = "https://www.google-analytics.com/analytics.js";
|
||||
r.parentNode.insertBefore(e, r);
|
||||
})(window, document, "script", "ga");
|
||||
ga("create", "UA-XXXXX-X", "auto");
|
||||
ga("send", "pageview");
|
||||
</script>
|
||||
</body>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular-route.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
|
||||
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<script
|
||||
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
|
||||
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<script src="/admin/js/vendor/filter.js"></script>
|
||||
<script src="/admin/js/app.js"></script>
|
||||
</html>
|
|
@ -1,129 +0,0 @@
|
|||
console.log("app.sh startup");
|
||||
|
||||
var app = angular.module("rvpnApp", ["ngRoute", "angular-duration-format"]);
|
||||
|
||||
app.config(function ($routeProvider, $locationProvider) {
|
||||
$routeProvider
|
||||
|
||||
.when("/admin/status/", {
|
||||
templateUrl: "admin/partials/status.html"
|
||||
})
|
||||
|
||||
.when("/admin/index.html", {
|
||||
templateUrl: "admin/partials/servers.html"
|
||||
})
|
||||
|
||||
.when("/admin/servers/", {
|
||||
templateUrl: "admin/partials/servers.html"
|
||||
})
|
||||
|
||||
.when("/admin/#domains", {
|
||||
templateUrl: "green.htm"
|
||||
})
|
||||
|
||||
.when("/blue", {
|
||||
templateUrl: "blue.htm"
|
||||
});
|
||||
$locationProvider.html5Mode(true);
|
||||
});
|
||||
|
||||
app.filter("bytes", function () {
|
||||
return function (bytes, precision) {
|
||||
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return "-";
|
||||
if (typeof precision === "undefined") precision = 1;
|
||||
var units = ["bytes", "kB", "MB", "GB", "TB", "PB"],
|
||||
number = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + " " + units[number];
|
||||
};
|
||||
});
|
||||
|
||||
app.filter("hfcduration", function () {
|
||||
return function (duration, precision) {
|
||||
remain = duration;
|
||||
duration_day = 24 * 60 * 60;
|
||||
duration_hour = 60 * 60;
|
||||
duration_minute = 60;
|
||||
duration_str = "";
|
||||
|
||||
days = Math.floor(remain / duration_day);
|
||||
if (days > 0) {
|
||||
remain = remain - days * duration_day;
|
||||
duration_str = duration_str + days + "d";
|
||||
}
|
||||
|
||||
hours = Math.floor(remain / duration_hour);
|
||||
if (hours > 0) {
|
||||
remain = remain - hours * duration_hour;
|
||||
duration_str = duration_str + hours + "h";
|
||||
}
|
||||
|
||||
mins = Math.floor(remain / duration_minute);
|
||||
if (mins > 0) {
|
||||
remain = remain - mins * duration_minute;
|
||||
duration_str = duration_str + mins + "m";
|
||||
}
|
||||
|
||||
secs = Math.floor(remain);
|
||||
duration_str = duration_str + secs + "s";
|
||||
|
||||
return duration_str;
|
||||
};
|
||||
});
|
||||
|
||||
app.controller("statusController", function ($scope, $http) {
|
||||
console.log("statusController");
|
||||
$scope.status_search = "";
|
||||
|
||||
var api = "/api/org.rootprojects.tunnel/status";
|
||||
|
||||
$scope.updateView = function () {
|
||||
$http.get(api).then(function (response) {
|
||||
console.log(response);
|
||||
data = response.data;
|
||||
if (data.error == "ok") {
|
||||
$scope.status = data.result;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateView();
|
||||
});
|
||||
|
||||
app.controller("serverController", function ($scope, $http) {
|
||||
$scope.servers = [];
|
||||
$scope.servers_search = "";
|
||||
$scope.servers_trigger_details = [];
|
||||
$scope.filtered;
|
||||
|
||||
var api = "/api/org.rootprojects.tunnel/servers";
|
||||
|
||||
$scope.updateView = function () {
|
||||
$http.get(api).then(function (response) {
|
||||
//console.log(response);
|
||||
data = response.data;
|
||||
if (data.error == "ok") {
|
||||
$scope.servers = data.result.servers;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.triggerDetail = function (id) {
|
||||
//console.log("triggerDetail ", id, $scope.servers_trigger_details[id])
|
||||
if ($scope.servers_trigger_details[id] == true) {
|
||||
$scope.servers_trigger_details[id] = false;
|
||||
} else {
|
||||
$scope.servers_trigger_details[id] = true;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.checkDetail = function (id) {
|
||||
//console.log("checkDetail ", id, $scope.servers_trigger_details[id])
|
||||
if ($scope.servers_trigger_details[id] == true) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updateView();
|
||||
});
|
|
@ -1,44 +0,0 @@
|
|||
// Avoid `console` errors in browsers that lack a console.
|
||||
(function () {
|
||||
var method;
|
||||
var noop = function () {};
|
||||
var methods = [
|
||||
"assert",
|
||||
"clear",
|
||||
"count",
|
||||
"debug",
|
||||
"dir",
|
||||
"dirxml",
|
||||
"error",
|
||||
"exception",
|
||||
"group",
|
||||
"groupCollapsed",
|
||||
"groupEnd",
|
||||
"info",
|
||||
"log",
|
||||
"markTimeline",
|
||||
"profile",
|
||||
"profileEnd",
|
||||
"table",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"timeline",
|
||||
"timelineEnd",
|
||||
"timeStamp",
|
||||
"trace",
|
||||
"warn"
|
||||
];
|
||||
var length = methods.length;
|
||||
var console = (window.console = window.console || {});
|
||||
|
||||
while (length--) {
|
||||
method = methods[length];
|
||||
|
||||
// Only stub undefined methods.
|
||||
if (!console[method]) {
|
||||
console[method] = noop;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Place any jQuery/helper plugins in here.
|
|
@ -1,165 +0,0 @@
|
|||
// ### filter.js >>
|
||||
|
||||
angular.module("angular-duration-format.filter", []).filter("duration", function () {
|
||||
var DURATION_FORMATS_SPLIT = /((?:[^ydhms']+)|(?:'(?:[^']|'')*')|(?:y+|d+|h+|m+|s+))(.*)/;
|
||||
var DURATION_FORMATS = {
|
||||
y: {
|
||||
// years
|
||||
// "longer" years are not supported
|
||||
value: 365 * 24 * 60 * 60 * 1000
|
||||
},
|
||||
yy: {
|
||||
value: "y",
|
||||
pad: 2
|
||||
},
|
||||
d: {
|
||||
// days
|
||||
value: 24 * 60 * 60 * 1000
|
||||
},
|
||||
dd: {
|
||||
value: "d",
|
||||
pad: 2
|
||||
},
|
||||
h: {
|
||||
// hours
|
||||
value: 60 * 60 * 1000
|
||||
},
|
||||
hh: {
|
||||
// padded hours
|
||||
value: "h",
|
||||
pad: 2
|
||||
},
|
||||
m: {
|
||||
// minutes
|
||||
value: 60 * 1000
|
||||
},
|
||||
mm: {
|
||||
// padded minutes
|
||||
value: "m",
|
||||
pad: 2
|
||||
},
|
||||
s: {
|
||||
// seconds
|
||||
value: 1000
|
||||
},
|
||||
ss: {
|
||||
// padded seconds
|
||||
value: "s",
|
||||
pad: 2
|
||||
},
|
||||
sss: {
|
||||
// milliseconds
|
||||
value: 1
|
||||
},
|
||||
ssss: {
|
||||
// padded milliseconds
|
||||
value: "sss",
|
||||
pad: 4
|
||||
}
|
||||
};
|
||||
|
||||
function _parseFormat(string) {
|
||||
// @inspiration AngularJS date filter
|
||||
var parts = [];
|
||||
var format = string ? string.toString() : "";
|
||||
|
||||
while (format) {
|
||||
var match = DURATION_FORMATS_SPLIT.exec(format);
|
||||
|
||||
if (match) {
|
||||
parts = parts.concat(match.slice(1));
|
||||
|
||||
format = parts.pop();
|
||||
} else {
|
||||
parts.push(format);
|
||||
|
||||
format = null;
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
function _formatDuration(timestamp, format) {
|
||||
var text = "";
|
||||
var values = {};
|
||||
|
||||
format
|
||||
.filter(function (format) {
|
||||
// filter only value parts of format
|
||||
return DURATION_FORMATS.hasOwnProperty(format);
|
||||
})
|
||||
.map(function (format) {
|
||||
// get formats with values only
|
||||
var config = DURATION_FORMATS[format];
|
||||
|
||||
if (config.hasOwnProperty("pad")) {
|
||||
return config.value;
|
||||
} else {
|
||||
return format;
|
||||
}
|
||||
})
|
||||
.filter(function (format, index, arr) {
|
||||
// remove duplicates
|
||||
return arr.indexOf(format) === index;
|
||||
})
|
||||
.map(function (format) {
|
||||
// get format configurations with values
|
||||
return angular.extend(
|
||||
{
|
||||
name: format
|
||||
},
|
||||
DURATION_FORMATS[format]
|
||||
);
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
// sort formats descending by value
|
||||
return b.value - a.value;
|
||||
})
|
||||
.forEach(function (format) {
|
||||
// create values for format parts
|
||||
var value = (values[format.name] = Math.floor(timestamp / format.value));
|
||||
|
||||
timestamp = timestamp - value * format.value;
|
||||
});
|
||||
|
||||
format.forEach(function (part) {
|
||||
var format = DURATION_FORMATS[part];
|
||||
|
||||
if (format) {
|
||||
var value = values[format.value];
|
||||
|
||||
text += format.hasOwnProperty("pad")
|
||||
? _padNumber(value, Math.max(format.pad, value.toString().length))
|
||||
: values[part];
|
||||
} else {
|
||||
text += part.replace(/(^'|'$)/g, "").replace(/''/g, "'");
|
||||
}
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function _padNumber(number, len) {
|
||||
return (new Array(len + 1).join("0") + number).slice(-len);
|
||||
}
|
||||
|
||||
return function (value, format) {
|
||||
var parsedValue = parseFloat(value, 10);
|
||||
var parsedFormat = _parseFormat(format);
|
||||
|
||||
if (isNaN(parsedValue) || parsedFormat.length === 0) {
|
||||
return value;
|
||||
} else {
|
||||
return _formatDuration(parsedValue, parsedFormat);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// ### << filter.js
|
||||
|
||||
// ### main.js >>
|
||||
|
||||
angular.module("angular-duration-format", ["angular-duration-format.filter"]);
|
||||
|
||||
// ### << main.js
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,98 +0,0 @@
|
|||
<div class="panel panel-default" data-ng-controller="serverController">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
Servers
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<form class="form-inline pull-right">
|
||||
<div class="form-group">
|
||||
<label for="search">Search:</label>
|
||||
<input type="text" class="form-control" id="search" data-ng-model="servers_search" />
|
||||
</div>
|
||||
<button type="button" title="Refresh" class="btn btn-default" aria-label="Refresh">
|
||||
<span
|
||||
class="glyphicon glyphicon-refresh"
|
||||
title="Refresh"
|
||||
aria-hidden="false"
|
||||
ng-click="updateView()"
|
||||
></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<table class="table table-striped table-bordered">
|
||||
<th width="3%">ID</th>
|
||||
<th width="10%">Name</th>
|
||||
<th width="10%">Address</th>
|
||||
<th width="10%">Xfer (in/out)</th>
|
||||
<th width="10%">Req/Resp</th>
|
||||
<th width="5%">Duration</th>
|
||||
<th width="5%">State</th>
|
||||
<th width="5%">Idle</th>
|
||||
<th width="1%">
|
||||
<center><span class="glyphicon glyphicon-option-vertical" aria-hidden="true"></span></center>
|
||||
</th>
|
||||
|
||||
<tr ng-repeat="s in servers | filter:servers_search | orderBy:'server_id'">
|
||||
<td>{{ s.server_id }}</td>
|
||||
<td>{{ s.server_name }}</td>
|
||||
<td>
|
||||
{{ s.source_address }}
|
||||
<div ng-hide="checkDetail(s.server_id)">
|
||||
domains({{ s.domains.length}})
|
||||
<div ng-repeat="d in s.domains | orderBy:'domain_name'">
|
||||
   {{ d.domain_name }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{ s.bytes_in | bytes }}/{{ s.bytes_out | bytes }}
|
||||
<div ng-hide="checkDetail(s.server_id)">
|
||||
 
|
||||
<div ng-repeat="d in s.domains | orderBy:'domain_name'">
|
||||
   {{ d.bytes_in | bytes }}/{{ d.bytes_out | bytes }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{ s.requests }}/{{ s.responses }}
|
||||
<div ng-hide="checkDetail(s.server_id)">
|
||||
 
|
||||
<div ng-repeat="d in s.domains | orderBy:'domain_name'">
|
||||
   {{ d.requests }}/{{ d.responses }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>{{ s.duration | hfcduration }}</td>
|
||||
<td>{{ s.server_state }}</td>
|
||||
<td>{{ s.idle | hfcduration }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="glyphicon glyphicon-zoom-in"
|
||||
title="Detail"
|
||||
aria-hidden="false"
|
||||
ng-click="triggerDetail(s.server_id)"
|
||||
></span>
|
||||
<div ng-hide="checkDetail(s.server_id)">
|
||||
 
|
||||
<div ng-repeat="d in s.domains | orderBy:'domain_name'">
|
||||
<span
|
||||
class="glyphicon glyphicon-zoom-in"
|
||||
title="Detail"
|
||||
aria-hidden="false"
|
||||
ng-click="triggerDetail(s.server_id+d.domain_name)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
|
@ -1,52 +0,0 @@
|
|||
<div class="panel panel-default" data-ng-controller="statusController">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
Status
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<form class="form-inline pull-right">
|
||||
<div class="form-group">
|
||||
<label for="search">Search:</label>
|
||||
<input type="text" class="form-control" id="search" data-ng-model="servers_search" />
|
||||
</div>
|
||||
<button type="button" title="Refresh" class="btn btn-default" aria-label="Refresh">
|
||||
<span
|
||||
class="glyphicon glyphicon-refresh"
|
||||
title="Refresh"
|
||||
aria-hidden="false"
|
||||
ng-click="updateView()"
|
||||
></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-md-8">Server Name: {{ status.name }} (Uptime: {{ status.uptime | hfcduration }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-md-8">Administrative Domain: {{ status.admin_domain }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-md-8">Server Domain: {{ status.wss_domain }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-md-8">Default LB Method: {{ status.loadbalance_default_method }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-md-8">
|
||||
Deadtime: dwell:{{ status.dead_time.dwell}} idle:{{ status.dead_time.idle}} cancel:{{
|
||||
status.dead_time.cancel_check}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,5 +0,0 @@
|
|||
# www.robotstxt.org/
|
||||
|
||||
# Allow crawling of all content
|
||||
User-agent: *
|
||||
Disallow:
|
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.4 KiB |
|
@ -7,9 +7,9 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
telebit "git.rootprojects.org/root/telebit"
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
||||
"git.rootprojects.org/root/telebit/internal/telebit"
|
||||
)
|
||||
|
||||
type SuccessResponse struct {
|
||||
|
|
|
@ -9,7 +9,8 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit/files"
|
||||
"git.rootprojects.org/root/telebit/assets/files"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
// pq injects itself into sql as 'postgres'
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
|
|
|
@ -13,17 +13,15 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit"
|
||||
"git.rootprojects.org/root/telebit/admin"
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/table"
|
||||
"git.rootprojects.org/root/telebit/assets/admin"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var authorizer telebit.Authorizer
|
||||
var authorizer Authorizer
|
||||
|
||||
// RouteAdmin sets up the API, including the Mgmt proxy and ACME relay
|
||||
func RouteAdmin(authURL string, r chi.Router) {
|
||||
|
@ -134,7 +132,7 @@ type SubscriberStatus struct {
|
|||
|
||||
func getAllSubscribers(w http.ResponseWriter, r *http.Request) {
|
||||
statuses := []*SubscriberStatus{}
|
||||
table.Servers.Range(func(key, value interface{}) bool {
|
||||
Servers.Range(func(key, value interface{}) bool {
|
||||
srvMap := value.(*sync.Map)
|
||||
status := getSubscribersHelper(srvMap)
|
||||
statuses = append(statuses, status)
|
||||
|
@ -160,7 +158,7 @@ func getSubscribers(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
var srvMap *sync.Map
|
||||
srvMapX, ok := table.Servers.Load(subject)
|
||||
srvMapX, ok := Servers.Load(subject)
|
||||
if ok {
|
||||
srvMap = srvMapX.(*sync.Map)
|
||||
statuses.Subscribers = append(statuses.Subscribers, getSubscribersHelper(srvMap))
|
||||
|
@ -179,7 +177,7 @@ func getSubscribersHelper(srvMap *sync.Map) *SubscriberStatus {
|
|||
|
||||
srvMap.Range(func(k, v interface{}) bool {
|
||||
status.Sockets = append(status.Sockets, k.(string))
|
||||
srv := v.(*table.SubscriberConn)
|
||||
srv := v.(*SubscriberConn)
|
||||
if nil == status.Since || srv.Since.Sub(*status.Since) < 0 {
|
||||
copied := srv.Since.Truncate(time.Second)
|
||||
status.Since = &copied
|
||||
|
@ -199,7 +197,7 @@ func getSubscribersHelper(srvMap *sync.Map) *SubscriberStatus {
|
|||
func delSubscribers(w http.ResponseWriter, r *http.Request) {
|
||||
subject := chi.URLParam(r, "subject")
|
||||
|
||||
ok := table.Remove(subject)
|
||||
ok := Remove(subject)
|
||||
if !ok {
|
||||
// TODO should this be an error?
|
||||
_ = json.NewEncoder(w).Encode(&struct {
|
||||
|
@ -243,7 +241,7 @@ func upgradeWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
wsTun := telebit.NewWebsocketTunnel(conn)
|
||||
wsTun := NewWebsocketTunnel(conn)
|
||||
fmt.Printf("New Authenticated WebSocket Remote Server\n")
|
||||
fmt.Printf("\thttp.req.RemoteAddr: %+v\n", r.RemoteAddr)
|
||||
fmt.Printf("\tconn.RemoteAddr(): %+v\n", conn.RemoteAddr())
|
||||
|
@ -255,15 +253,15 @@ func upgradeWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||
// Rather the client's local address (the specific relay server) would be more useful.
|
||||
ctxEncoder, cancelEncoder := context.WithCancel(context.Background())
|
||||
now := time.Now()
|
||||
server := &table.SubscriberConn{
|
||||
server := &SubscriberConn{
|
||||
Since: &now,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
WSConn: conn,
|
||||
WSTun: wsTun,
|
||||
Grants: grants,
|
||||
Clients: &sync.Map{},
|
||||
MultiEncoder: telebit.NewEncoder(ctxEncoder, wsTun),
|
||||
MultiDecoder: telebit.NewDecoder(wsTun),
|
||||
MultiEncoder: NewEncoder(ctxEncoder, wsTun),
|
||||
MultiDecoder: NewDecoder(wsTun),
|
||||
}
|
||||
// TODO should this happen at NewEncoder()?
|
||||
// (or is it even necessary anymore?)
|
||||
|
@ -283,8 +281,8 @@ func upgradeWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Printf("a subscriber stream is done: %q\n", err)
|
||||
// TODO check what happens when we leave a junk connection
|
||||
//fmt.Println("[debug] [warn] removing server turned off")
|
||||
table.RemoveServer(server)
|
||||
RemoveServer(server)
|
||||
}()
|
||||
|
||||
table.Add(server)
|
||||
Add(server)
|
||||
}
|
||||
|
|
|
@ -4,12 +4,10 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.rootprojects.org/root/telebit"
|
||||
)
|
||||
|
||||
func NewAuthorizer(authURL string) telebit.Authorizer {
|
||||
return func(r *http.Request) (*telebit.Grants, error) {
|
||||
func NewAuthorizer(authURL string) Authorizer {
|
||||
return func(r *http.Request) (*Grants, error) {
|
||||
// do we have a valid wss_client?
|
||||
|
||||
fmt.Printf("[authz] Authorization = %s\n", r.Header.Get("Authorization"))
|
||||
|
@ -28,7 +26,7 @@ func NewAuthorizer(authURL string) telebit.Authorizer {
|
|||
|
||||
fmt.Printf("[authz] authURL = %s\n", authURL)
|
||||
fmt.Printf("[authz] token = %s\n", tokenString)
|
||||
grants, err := telebit.Inspect(authURL, tokenString)
|
||||
grants, err := Inspect(authURL, tokenString)
|
||||
|
||||
if nil != err {
|
||||
fmt.Printf("[authorizer] error inspecting %q: %s\ntoken: %s\n", authURL, err, tokenString)
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/sni"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/sni"
|
||||
)
|
||||
|
||||
// ConnWrap is just a cheap way to DRY up some switch conn.(type) statements to handle special features of Conn
|
|
@ -5,7 +5,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
)
|
||||
|
||||
// Decoder handles a Reader stream containing mplexy-encoded clients
|
|
@ -9,7 +9,7 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
)
|
||||
|
||||
// TODO: try to be more like encoding/csv, or more like encoding/pem and encoding/json?
|
|
@ -9,7 +9,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
)
|
||||
|
||||
// A Listener transforms a multiplexed websocket connection into individual net.Conn-like connections.
|
|
@ -5,7 +5,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
)
|
||||
|
||||
type Parser struct {
|
|
@ -9,7 +9,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
)
|
||||
|
||||
// A RouteMux is a net.Conn multiplexer.
|
|
@ -1,18 +1,17 @@
|
|||
package table
|
||||
package telebit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
|
||||
telebit "git.rootprojects.org/root/telebit"
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
|
@ -108,18 +107,18 @@ type SubscriberConn struct {
|
|||
Since *time.Time
|
||||
RemoteAddr string
|
||||
WSConn *websocket.Conn
|
||||
WSTun net.Conn // *telebit.WebsocketTunnel
|
||||
Grants *telebit.Grants
|
||||
WSTun net.Conn // *WebsocketTunnel
|
||||
Grants *Grants
|
||||
Clients *sync.Map
|
||||
|
||||
// TODO is this the right codec type?
|
||||
MultiEncoder *telebit.Encoder
|
||||
MultiDecoder *telebit.Decoder
|
||||
MultiEncoder *Encoder
|
||||
MultiDecoder *Decoder
|
||||
|
||||
// to fulfill Router interface
|
||||
}
|
||||
|
||||
func (s *SubscriberConn) RouteBytes(src, dst telebit.Addr, payload []byte) {
|
||||
func (s *SubscriberConn) RouteBytes(src, dst Addr, payload []byte) {
|
||||
id := fmt.Sprintf("%s:%d", src.Hostname(), src.Port())
|
||||
if dbg.Debug {
|
||||
fmt.Fprintf(
|
||||
|
@ -163,9 +162,9 @@ func (s *SubscriberConn) RouteBytes(src, dst telebit.Addr, payload []byte) {
|
|||
}
|
||||
|
||||
func (s *SubscriberConn) Serve(client net.Conn) error {
|
||||
var wconn *telebit.ConnWrap
|
||||
var wconn *ConnWrap
|
||||
switch conn := client.(type) {
|
||||
case *telebit.ConnWrap:
|
||||
case *ConnWrap:
|
||||
wconn = conn
|
||||
default:
|
||||
// this probably isn't strictly necessary
|
||||
|
@ -201,27 +200,27 @@ func (s *SubscriberConn) Serve(client net.Conn) error {
|
|||
|
||||
servername := wconn.Servername()
|
||||
|
||||
termination := telebit.Unknown
|
||||
scheme := telebit.None
|
||||
termination := Unknown
|
||||
scheme := None
|
||||
if "" != servername {
|
||||
dstAddr = servername
|
||||
//scheme = telebit.TLS
|
||||
scheme = telebit.HTTPS
|
||||
//scheme = TLS
|
||||
scheme = HTTPS
|
||||
}
|
||||
if 80 == dstPort {
|
||||
scheme = telebit.HTTPS
|
||||
scheme = HTTPS
|
||||
} else if 443 == dstPort {
|
||||
// TODO dstAddr = wconn.Servername()
|
||||
scheme = telebit.HTTP
|
||||
scheme = HTTP
|
||||
}
|
||||
|
||||
src := telebit.NewAddr(
|
||||
src := NewAddr(
|
||||
scheme,
|
||||
termination,
|
||||
srcAddr,
|
||||
srcPort,
|
||||
)
|
||||
dst := telebit.NewAddr(
|
||||
dst := NewAddr(
|
||||
scheme,
|
||||
termination,
|
||||
dstAddr,
|
|
@ -16,8 +16,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
httpshim "git.rootprojects.org/root/telebit/tunnel"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
httpshim "git.rootprojects.org/root/telebit/internal/tunnel"
|
||||
|
||||
"github.com/coolaj86/certmagic"
|
||||
"github.com/mholt/acmez"
|
|
@ -7,7 +7,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
)
|
||||
|
||||
const (
|
|
@ -10,7 +10,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
|
@ -1,8 +0,0 @@
|
|||
TOKEN=$(go run cmd/signjwt/*.go)
|
||||
echo "TOKEN: $TOKEN"
|
||||
|
||||
echo "Active:"
|
||||
curl -L http://localhost:3000/api/devices -H "Authorization: Bearer ${TOKEN}"
|
||||
|
||||
echo "Inactive:"
|
||||
curl -L http://localhost:3000/api/devices?inactive=true -H "Authorization: Bearer ${TOKEN}"
|
43
mgmt.sh
43
mgmt.sh
|
@ -1,43 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -u
|
||||
|
||||
# 1. (srv) create a new shared key for a given slug
|
||||
# 2. (dev) try to update via ping
|
||||
# 3. (dev) use key to exchange machine id
|
||||
# 4. (dev) use key to connect to remote
|
||||
# 5. (dev) ping occasionally
|
||||
|
||||
TOKEN=$(go run cmd/signjwt/*.go)
|
||||
echo "TOKEN: $TOKEN"
|
||||
|
||||
my_shared="ZR2rxYmcKJcmtKgmH9D5Qw"
|
||||
my_domain="example.com"
|
||||
my_client="1-client-slug"
|
||||
TOK=$(curl -X POST http://localhost:3000/api/devices -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" -d '{ "slug": "'$my_client'", "shared_key": "'$my_shared'" }')
|
||||
echo Response: $TOK
|
||||
|
||||
SHARED=$(echo "$TOK" | sed 's/.*shared_key":"//g' | sed 's/".*//')
|
||||
echo Shared Key: $SHARED
|
||||
my_parts=$(go run cmd/signjwt/*.go $SHARED machineid)
|
||||
my_ppid=$(echo $my_parts | cut -d' ' -f1)
|
||||
my_keyid=$(echo $my_parts | cut -d' ' -f2)
|
||||
echo "PPID: $my_ppid KeyID: $my_keyid"
|
||||
|
||||
TOKEN=$(go run cmd/signjwt/*.go $my_ppid)
|
||||
echo "PING 1 (should fail)"
|
||||
curl -X POST http://localhost:3000/api/ping -H "Authorization: Bearer ${TOKEN}"
|
||||
echo ""
|
||||
|
||||
curl -X POST http://localhost:3000/api/register-device/$SHARED -H "Content-Type: application/json" -d '{ "machine_ppid": "'$my_ppid'", "public_key": "'$my_keyid'" }'
|
||||
echo ''
|
||||
|
||||
echo "PING 2 (should work)"
|
||||
curl -X POST http://localhost:3000/api/ping -H "Authorization: Bearer ${TOKEN}"
|
||||
echo ""
|
||||
|
||||
curl -X POST http://localhost:3000/api/dns/"${my_client}.${my_domain}" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "token": "xxxx", "key_authorization": "yyyy" }'
|
Loading…
Reference in New Issue