Merge branch 'load-balance' into 'master'

Load balance

See merge request !2
This commit is contained in:
Henry 2017-04-01 17:32:50 +00:00
commit e637277bb5
67 changed files with 2871 additions and 794 deletions

322
README.md
View File

@ -1,16 +1,17 @@
# RVPN Server
## branch: passing-traffic
## branch: load-balancing
- code now passes traffic using just daplie tools
- this will require serve-https and node-tunnel-client to work
- the system supports round-robin load balancing
### Build RVPN
```bash
hcamacho@Hanks-MBP:go-rvpn-server $ go get
hcamacho@Hanks-MBP:go-rvpn-server $ go build
go-rvpn-server $ go get
go-rvpn-server $ go build
```
### Setup Some Entries
@ -20,20 +21,20 @@ hcamacho@Hanks-MBP:go-rvpn-server $ go build
### Start Up Webserver
```bash
hcamacho@Hanks-MBP:tmp $ cd /tmp
hcamacho@Hanks-MBP:tmp $ vi index.html --- Place some index content
hcamacho@Hanks-MBP:tmp $ serve-https -p 8080 -d /tmp --servername hfc.daplie.me --agree-tos --email henry.f.camacho@gmail.com
tmp $ cd /tmp
tmp $ vi index.html --- Place some index content
tmp $ serve-https -p 8080 -d /tmp --servername hfc.daplie.me --agree-tos --email henry.f.camacho@gmail.com
```
### Start Tunnel Client
```bash
hcamacho@Hanks-MBP:node-tunnel-client $ bin/stunnel.js --locals http://hfc.daplie.me:8080,http://test1.hfc.daplie.me:8080 --stunneld wss://localhost.daplie.me:8443 --secret abc123
node-tunnel-client $ bin/stunnel.js --locals http://hfc.daplie.me:8080,http://test1.hfc.daplie.me:8080 --stunneld wss://localhost.daplie.me:8443 --secret abc123
```
### Execute RVPN
```bash
hcamacho@Hanks-MBP:go-rvpn-server $ ./go-rvpn-server
go-rvpn-server $ ./go-rvpn-server
INFO: packer: 2017/03/02 19:16:52.652109 run.go:47: startup
-=-=-=-=-=-=-=-=-=-=
INFO: genericlistener: 2017/03/02 19:16:52.652777 manager.go:77: ConnectionTable starting
@ -50,242 +51,83 @@ INFO: genericlistener: 2017/03/02 19:16:52.652869 conn_tracking.go:25: Tracking
https://hfc.daplie.me:8443
- You'll notice that the browser is redirected to 8080 after accepting the cert. I see a meta-refresh coming back from the serve-https
- The traffic is getting back to the client.
### Test Load Balancing
In a new terminal
```bash
INFO: genericlistener: 2017/03/02 21:24:48.472312 connection.go:207: 00000000 fe 1d 49 50 76 34 2c 31 32 37 2e 30 2e 30 2e 31 |..IPv4,127.0.0.1|
00000010 2c 35 33 35 35 39 2c 33 36 38 2c 68 74 74 70 48 |,53559,368,httpH|
00000020 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d 0a |TTP/1.1 200 OK..|
00000030 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 74 65 |Content-Type: te|
00000040 78 74 2f 68 74 6d 6c 3b 20 63 68 61 72 73 65 74 |xt/html; charset|
00000050 3d 75 74 66 2d 38 0d 0a 44 61 74 65 3a 20 46 72 |=utf-8..Date: Fr|
00000060 69 2c 20 30 33 20 4d 61 72 20 32 30 31 37 20 30 |i, 03 Mar 2017 0|
00000070 33 3a 32 34 3a 34 38 20 47 4d 54 0d 0a 43 6f 6e |3:24:48 GMT..Con|
00000080 6e 65 63 74 69 6f 6e 3a 20 6b 65 65 70 2d 61 6c |nection: keep-al|
00000090 69 76 65 0d 0a 43 6f 6e 74 65 6e 74 2d 4c 65 6e |ive..Content-Len|
000000a0 67 74 68 3a 20 32 32 37 0d 0a 0d 0a 3c 68 74 6d |gth: 227....<htm|
000000b0 6c 3e 0a 3c 68 65 61 64 3e 0a 20 20 3c 4d 45 54 |l>.<head>. <MET|
000000c0 41 20 68 74 74 70 2d 65 71 75 69 76 3d 22 72 65 |A http-equiv="re|
000000d0 66 72 65 73 68 22 20 63 6f 6e 74 65 6e 74 3d 22 |fresh" content="|
000000e0 30 3b 55 52 4c 3d 27 68 74 74 70 73 3a 2f 2f 68 |0;URL='https://h|
000000f0 66 63 2e 64 61 70 6c 69 65 2e 6d 65 3a 38 30 38 |fc.daplie.me:808|
00000100 30 2f 27 22 3e 0a 3c 2f 68 65 61 64 3e 0a 3c 62 |0/'">.</head>.<b|
00000110 6f 64 79 3e 0a 3c 21 2d 2d 20 48 65 6c 6c 6f 20 |ody>.<!-- Hello |
00000120 4d 72 20 44 65 76 65 6c 6f 70 65 72 21 20 57 65 |Mr Developer! We|
00000130 20 64 6f 6e 27 74 20 73 65 72 76 65 20 69 6e 73 | don't serve ins|
00000140 65 63 75 72 65 20 72 65 73 6f 75 72 63 65 73 20 |ecure resources |
00000150 61 72 6f 75 6e 64 20 68 65 72 65 2e 0a 20 20 20 |around here.. |
00000160 20 50 6c 65 61 73 65 20 75 73 65 20 48 54 54 50 | Please use HTTP|
00000170 53 20 69 6e 73 74 65 61 64 2e 20 2d 2d 3e 0a 3c |S instead. -->.<|
00000180 2f 62 6f 64 79 3e 0a 3c 2f 68 74 6d 6c 3e 0a |/body>.</html>.|
node-tunnel-client $ bin/stunnel.js --locals http://hfc.daplie.me:8080,http://test1.hfc.daplie.me:8080 --stunneld wss://localhost.daplie.me:8443 --secret abc123
```
- this set of code works great if I am running the node-tunnel-client on a different machine with apache as a web server.
- need to work through why serve-https thinks the traffic is inecure.
### Check Results
- you should see traffic going to both node-clients hitting the single webserver on the back end.
- Browse: https://rvpn.daplie.invalid:8443/api/com.daplie.rvpn/servers
## restructured-http
- connection handling has been totally rewritten.
- on a specific port RVPN can determine the following:
- if a connection is encrypted or not encrypted
- if a request is a wss_client
- if a request is an admin/api request
- if a request is a plain (to be forwarded) http request
- or if a request is a different protocol (perhaps SSH)
To accomplish the above RVPN uses raw TCP sockets, buffered readers, and a custom Listener. This allows protocol detection (multiple services on one port)
If we expose 443 and 80 to maximize the ability for tunnel clients and south bound traffic, the RVPN is able to deal with this traffic on a limited number of ports, and the most popular ports.
It is possible now to meter any point of the connection (not Interface Level, rather TCP)
There is now a connection manager that dynamically allows new GenericListeners to start on different ports when needed....
```go
newListener := NewListenerRegistration(initialPort)
gl.register <- newListener
```
A new listener is created by sending a NewListenerRegistration on the channel.
```go
ln, err := net.ListenTCP("tcp", listenAddr)
if err != nil {
loginfo.Println("unable to bind", err)
listenerRegistration.status = listenerFault
listenerRegistration.err = err
listenerRegistration.commCh <- listenerRegistration
return
```javascript
{
"type": "servers/GET",
"schema": "",
"txts": 1490473843,
"txid": 8,
"error": "ok",
"error_description": "",
"error_uri": "",
"result": {
"servers": [{
"server_name": "0xc42014a0c0",
"server_id": 1,
"domains": [{
"domain_name": "hfc.daplie.me",
"server_id": 1,
"bytes_in": 4055,
"bytes_out": 8119,
"requests": 12,
"responses": 12,
"source_addr": "127.0.0.1:55875"
}, {
"domain_name": "test1.hfc.daplie.me",
"server_id": 1,
"bytes_in": 0,
"bytes_out": 0,
"requests": 0,
"responses": 0,
"source_addr": "127.0.0.1:55875"
}],
"duration": 182.561747754,
"idle": 21.445976033,
"bytes_in": 8119,
"bytes_out": 4055,
"requests": 12,
"responses": 12,
"source_address": "127.0.0.1:55875"
}, {
"server_name": "0xc4200ea3c0",
"server_id": 2,
"domains": [{
"domain_name": "hfc.daplie.me",
"server_id": 2,
"bytes_in": 1098,
"bytes_out": 62,
"requests": 2,
"responses": 2,
"source_addr": "127.0.0.1:56318"
}, {
"domain_name": "test1.hfc.daplie.me",
"server_id": 2,
"bytes_in": 0,
"bytes_out": 0,
"requests": 0,
"responses": 0,
"source_addr": "127.0.0.1:56318"
}],
"duration": 65.481814913,
"idle": 23.589609269,
"bytes_in": 62,
"bytes_out": 1098,
"requests": 2,
"responses": 2,
"source_address": "127.0.0.1:56318"
}]
}
listenerRegistration.status = listenerAdded
listenerRegistration.commCh <- listenerRegistration
}
```
Once the lister is fired up, I sends back a regisration status to the manager along with the new Listener and status.
### Build
```bash
hcamacho@Hanks-MBP:go-rvpn-server $ go get
hcamacho@Hanks-MBP:go-rvpn-server $ go build
```
### Execute RVPN
```bash
hcamacho@Hanks-MBP:go-rvpn-server $ ./go-rvpn-server
INFO: packer: 2017/02/26 12:43:53.978133 run.go:48: startup
-=-=-=-=-=-=-=-=-=-=
INFO: connection: 2017/02/26 12:43:53.978959 connection_table.go:67: ConnectionTable starting
INFO: connection: 2017/02/26 12:43:53.979000 connection_table.go:50: Reaper waiting for 300 seconds
```
### Connect Tunnel client
```bash
hcamacho@Hanks-MBP:node-tunnel-client $ bin/stunnel.js --locals http://hfc.daplie.me:8443,http://test.hfc.daplie.me:3001,http://127.0.0.1:8080 --stunneld wss://localhost.daplie.me:8443 --secret abc123
[local proxy] http://hfc.daplie.me:8443
[local proxy] http://test.hfc.daplie.me:3001
[local proxy] http://127.0.0.1:8080
[connect] 'wss://localhost.daplie.me:8443'
[open] connected to 'wss://localhost.daplie.me:8443'
```
### Connect Admin
- add a host entry
```
127.0.0.1 tunnel.example.com rvpn.daplie.invalid
```
```bash
browse https://rvpn.daplie.invalid:8443
```
### Send some traffic
- run the RVPN (as above)
- run the tunnel client (as above)
- browse http://127.0.0.1:8443 && https://127.0.0.1:8443
- observe
```bash
hcamacho@Hanks-MBP:node-tunnel-client $ bin/stunnel.js --locals http://hfc.daplie.me:8443,http://test.hfc.daplie.me:3001,http://127.0.0.1:8080 --stunneld wss://localhost.daplie.me:8443 --secret abc123
[local proxy] http://hfc.daplie.me:8443
[local proxy] http://test.hfc.daplie.me:3001
[local proxy] http://127.0.0.1:8080
[connect] 'wss://localhost.daplie.me:8443'
[open] connected to 'wss://localhost.daplie.me:8443'
hello
fe1c495076342c3132372e302e302e312c383038302c3431332c68747470474554202f20485454502f312e310d0a486f73743a203132372e302e302e313a383434330d0a436f6e6e656374696f6e3a206b6565702d616c6976650d0a43616368652d436f6e74726f6c3a206d61782d6167653d300d0a557067726164652d496e7365637572652d52657175657374733a20310d0a557365722d4167656e743a204d6f7a696c6c612f352e3020284d6163696e746f73683b20496e74656c204d6163204f5320582031305f31325f3329204170706c655765624b69742f3533372e333620284b48544d4c2c206c696b65204765636b6f29204368726f6d652f35362e302e323932342e3837205361666172692f3533372e33360d0a4163636570743a20746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c696d6167652f776562702c2a2f2a3b713d302e380d0a4163636570742d456e636f64696e673a20677a69702c206465666c6174652c20736463682c2062720d0a4163636570742d4c616e67756167653a20656e2d55532c656e3b713d302e380d0a0d0a
<EFBFBD>IPv4,127.0.0.1,8080,413,httpGET / HTTP/1.1
Host: 127.0.0.1:8443
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
[exit] loop closed 0
```
Looks like it aborts for some reaon. I have this problem on on a new installation as well.
-=-=-=-=-=-=
A Poor Man's Reverse VPN written in Go
Context
-------
Even in the worst of conditions the fanciest of firewalls can't stop a WebSocket
running over https from creating a secure tunnel.
Whether at home behind a router that lacks UPnP compliance, at school, work,
the library - or even on an airplane, we want any device (or even a browser or
app) to be able to serve from anywhere.
Motivation
----------
We originally wrote this in node.js as
[node-tunnel-server](https://git.daplie.com/Daplie/node-tunnel-server),
but there are a few problems:
* metering
* resource utilization
* binary transfer
### metering
We want to be able to meter all traffic on a socket.
In node.js it wasn't feasible to be able to track the original socket handle
all the way back from the web socket authentication through the various
wrappers.
A user connects via a websocket to the tunnel server
and an authentication token is presented.
If the connection is established the socket should then be metered and reported
including total bytes sent and received and size of payload bytes sent and
received (because the tunnelling adds some overhead).
### resource utilization
node.js does not support usage of multiple cores in-process.
The overhead of passing socket connections between processes seemed non-trivial
at best and likely much less efficient, and impossible at worst.
### binary transfer
node.js doesn't handle binary data very well. People will be transferring
gigabytes of data.
Short Term Goal
----
Build a server compatible with the node.js client (JWT authentication)
that can meter authenticated connections, utilize multiple cores efficiently,
and efficienty garbage collect gigabytes upon gigabytes of transfer.

View File

@ -1 +0,0 @@
Hank

View File

@ -1 +0,0 @@
Test.html

View File

@ -1,9 +1,10 @@
rvpn:
serverName: rvpn1
wssdomain: localhost.daplie.me
admindomain: rvpn.daplie.invalid
genericlistener: 9999
genericlistener: 8443
deadtime:
dwell: 600
dwell: 120
idle: 60
cancelcheck: 10
domains:
@ -13,4 +14,7 @@ rvpn:
secret: abc123
test3.daplie.me:
secret: abc123
loadbalancing:
defaultmethod: round-robin

60
html/admin/404.html Normal file
View File

@ -0,0 +1,60 @@
<!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.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,12 @@
<?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>

View File

@ -0,0 +1,15 @@
<?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>

282
html/admin/css/main.css Normal file
View File

@ -0,0 +1,282 @@
/*! 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;
}
}

424
html/admin/css/normalize.css vendored Normal file
View File

@ -0,0 +1,424 @@
/*! 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;
}

BIN
html/admin/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

15
html/admin/humans.txt Normal file
View File

@ -0,0 +1,15 @@
# humanstxt.org/
# The humans responsible & technology colophon
# TEAM
<name> -- <role> -- <twitter>
# THANKS
<name>
# TECHNOLOGY COLOPHON
CSS3, HTML5
Apache Server Configs, jQuery, Modernizr, Normalize.css

0
html/admin/img/.gitignore vendored Normal file
View File

76
html/admin/index.html Normal file
View File

@ -0,0 +1,76 @@
<!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>

132
html/admin/js/app.js Normal file
View File

@ -0,0 +1,132 @@
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/com.daplie.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/com.daplie.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()
});

0
html/admin/js/main.js Normal file
View File

24
html/admin/js/plugins.js Normal file
View File

@ -0,0 +1,24 @@
// 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.

148
html/admin/js/vendor/filter.js vendored Normal file
View File

@ -0,0 +1,148 @@
// ### 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

View File

@ -0,0 +1,89 @@
<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'">
&nbsp&nbsp&nbsp{{ d.domain_name }}
</div>
</div>
</td>
<td>
{{ s.bytes_in | bytes }}/{{ s.bytes_out | bytes }}
<div ng-hide="checkDetail(s.server_id)">
&nbsp
<div ng-repeat="d in s.domains | orderBy:'domain_name'">
&nbsp&nbsp&nbsp{{ d.bytes_in | bytes }}/{{ d.bytes_out | bytes }}
</div>
</div>
</td>
<td>
{{ s.requests }}/{{ s.responses }}
<div ng-hide="checkDetail(s.server_id)">
&nbsp
<div ng-repeat="d in s.domains | orderBy:'domain_name'">
&nbsp&nbsp&nbsp{{ 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)">
&nbsp
<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>

View File

@ -0,0 +1,46 @@
<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>

5
html/admin/robots.txt Normal file
View File

@ -0,0 +1,5 @@
# www.robotstxt.org/
# Allow crawling of all content
User-agent: *
Disallow:

BIN
html/admin/tile-wide.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
html/admin/tile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

89
main.go
View File

@ -1,21 +1,27 @@
package main
import (
"context"
"crypto/tls"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"time"
"github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"context"
"git.daplie.com/Daplie/go-rvpn-server/rvpn/genericlistener"
"git.daplie.com/Daplie/go-rvpn-server/rvpn/server"
)
var (
logfile = "stdout"
configPath = "./"
configFile = "go-rvpn-server"
loginfo *log.Logger
logdebug *log.Logger
logFlags = log.Ldate | log.Lmicroseconds | log.Lshortfile
@ -25,29 +31,57 @@ var (
argServerAdminBinding string
argServerExternalBinding string
argDeadTime int
connectionTable *genericlistener.Table
connectionTable *server.Table
secretKey = "abc123"
wssHostName = "localhost.daplie.me"
adminHostName = "rvpn.daplie.invalid"
idle int
dwell int
cancelcheck int
lbDefaultMethod string
serverName string
)
func init() {
flag.StringVar(&logfile, "log", logfile, "Log file (or stdout/stderr; empty for none)")
flag.StringVar(&configPath, "config-path", configPath, "Configuration File Path")
flag.StringVar(&configFile, "config-file", configFile, "Configuration File Name")
}
var logoutput io.Writer
//Main -- main entry point
func main() {
flag.Parse()
loginfo = log.New(os.Stdout, "INFO: main: ", logFlags)
logdebug = log.New(os.Stdout, "DEBUG: main:", logFlags)
viper.SetConfigName("go-rvpn-server")
switch logfile {
case "stdout":
logoutput = os.Stdout
case "stderr":
logoutput = os.Stderr
case "":
logoutput = ioutil.Discard
default:
logoutput = &lumberjack.Logger{
Filename: logfile,
MaxSize: 100,
MaxAge: 120,
MaxBackups: 10,
}
}
// send the output io.Writing to the other packages
server.InitLogging(logoutput)
loginfo = log.New(logoutput, "INFO: main: ", logFlags)
logdebug = log.New(logoutput, "DEBUG: main:", logFlags)
viper.SetConfigName(configFile)
viper.AddConfigPath(configPath)
viper.AddConfigPath("./")
err := viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
panic(fmt.Errorf("fatal error config file: %s", err))
}
flag.IntVar(&argDeadTime, "dead-time-counter", 5, "deadtime counter in seconds")
@ -55,18 +89,15 @@ func main() {
wssHostName = viper.Get("rvpn.wssdomain").(string)
adminHostName = viper.Get("rvpn.admindomain").(string)
argGenericBinding = viper.GetInt("rvpn.genericlistener")
deadtime := viper.Get("rvpn.deadtime")
idle = deadtime.(map[string]interface{})["idle"].(int)
dwell = deadtime.(map[string]interface{})["dwell"].(int)
cancelcheck = deadtime.(map[string]interface{})["cancelcheck"].(int)
deadtime := viper.Get("rvpn.deadtime").(map[string]interface{})
idle = deadtime["idle"].(int)
dwell = deadtime["dwell"].(int)
cancelcheck = deadtime["cancelcheck"].(int)
lbDefaultMethod = viper.Get("rvpn.loadbalancing.defaultmethod").(string)
serverName = viper.Get("rvpn.serverName").(string)
loginfo.Println("startup")
loginfo.Println(viper.Get("rvpn.genericlisteners"))
loginfo.Println(viper.Get("rvpn.domains"))
fmt.Println("-=-=-=-=-=-=-=-=-=-=")
certbundle, err := tls.LoadX509KeyPair("certs/fullchain.pem", "certs/privkey.pem")
if err != nil {
loginfo.Println(err)
@ -76,6 +107,14 @@ func main() {
ctx, cancelContext := context.WithCancel(context.Background())
defer cancelContext()
serverStatus := server.NewStatus(ctx)
serverStatus.AdminDomain = adminHostName
serverStatus.WssDomain = wssHostName
serverStatus.Name = serverName
serverStatus.StartTime = time.Now()
serverStatus.DeadTime = server.NewStatusDeadTime(dwell, idle, cancelcheck)
serverStatus.LoadbalanceDefaultMethod = lbDefaultMethod
// Setup for GenericListenServe.
// - establish context for the generic listener
// - startup listener
@ -85,16 +124,18 @@ func main() {
// - if tls, establish, protocol peek buffer, else decrypted
// - match protocol
connectionTracking := genericlistener.NewTracking()
connectionTracking := server.NewTracking()
serverStatus.ConnectionTracking = connectionTracking
go connectionTracking.Run(ctx)
connectionTable = genericlistener.NewTable(dwell, idle)
go connectionTable.Run(ctx)
connectionTable = server.NewTable(dwell, idle)
serverStatus.ConnectionTable = connectionTable
go connectionTable.Run(ctx, lbDefaultMethod)
genericListeners := server.NewGenerListeners(ctx, secretKey, certbundle, serverStatus)
//serverStatus.GenericListeners = genericListeners
genericListeners := genericlistener.NewGenerListeners(ctx, connectionTable, connectionTracking, secretKey, certbundle, wssHostName, adminHostName, cancelcheck)
go genericListeners.Run(ctx, argGenericBinding)
//Run for 10 minutes and then shutdown cleanly
time.Sleep(600 * time.Second)
cancelContext()
select {}
}

45
rvpn/envelope/envelope.go Normal file
View File

@ -0,0 +1,45 @@
package envelope
import (
"bytes"
"encoding/json"
"io"
"time"
)
//Envelope -- Standard daplie response structure
type Envelope struct {
TransactionType string `json:"type"`
Schema string `json:"schema"`
TransactionTimeStamp int64 `json:"txts"`
TransactionID int64 `json:"txid"`
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
ErrorURI string `json:"error_uri"`
Result interface{} `json:"result"`
}
//NewEnvelope -- Constructor
func NewEnvelope(transactionType string) (p *Envelope) {
transactionID++
p = new(Envelope)
p.TransactionType = transactionType
p.TransactionID = transactionID
p.TransactionTimeStamp = time.Now().Unix()
p.Error = "ok"
return
}
//Generate -- encode into JSON and return string
func (e *Envelope) Generate() string {
buf := new(bytes.Buffer)
json.NewEncoder(buf).Encode(e)
return buf.String()
}
//GenerateWriter --
func (e *Envelope) GenerateWriter(w io.Writer) {
json.NewEncoder(w).Encode(e)
}

19
rvpn/envelope/setup.go Normal file
View File

@ -0,0 +1,19 @@
package envelope
import (
"log"
"os"
)
var (
loginfo *log.Logger
logdebug *log.Logger
logFlags = log.Ldate | log.Lmicroseconds | log.Lshortfile
transactionID int64
)
func init() {
loginfo = log.New(os.Stdout, "INFO: envelope: ", logFlags)
logdebug = log.New(os.Stdout, "DEBUG: envelope:", logFlags)
transactionID = 1
}

View File

@ -1,29 +0,0 @@
package genericlistener
//DomainAPI -- Structure to hold the domain tracking for JSON
type DomainAPI struct {
Domain string `json:"domain"`
BytesIn int64 `json:"bytes_in"`
BytesOut int64 `json:"bytes_out"`
}
//NewDomainAPI - Constructor
func NewDomainAPI(domain string, bytesin int64, bytesout int64) (d *DomainAPI) {
d = new(DomainAPI)
d.Domain = domain
d.BytesIn = bytesin
d.BytesOut = bytesout
return
}
// //DomainAPIContainer --
// type DomainAPIContainer struct {
// Domains []*DomainAPI
// }
// //NewDomainAPIContainer -- Constructor
// func NewDomainAPIContainer() (p *DomainAPIContainer) {
// p = new(DomainAPIContainer)
// p.Domains = make([]*DomainAPI, 0)
// return p
// }

View File

@ -1,39 +0,0 @@
package genericlistener
//DomainTrack -- Tracking specifics for domains
type DomainTrack struct {
DomainName string
bytesIn int64
bytesOut int64
}
//NewDomainTrack -- Constructor
func NewDomainTrack(domainName string) (p *DomainTrack) {
p = new(DomainTrack)
p.DomainName = domainName
p.bytesIn = 0
p.bytesOut = 0
return
}
//BytesIn -- Property
func (c *DomainTrack) BytesIn() (b int64) {
b = c.bytesIn
return
}
//BytesOut -- Property
func (c *DomainTrack) BytesOut() (b int64) {
b = c.bytesOut
return
}
//AddIn - Porperty
func (c *DomainTrack) AddIn(num int64) {
c.bytesIn = c.bytesIn + num
}
//AddOut -- Property
func (c *DomainTrack) AddOut(num int64) {
c.bytesOut = c.bytesOut + num
}

View File

@ -1,44 +0,0 @@
package genericlistener
import (
"fmt"
"time"
)
//ServerAPI -- Structure to support the server API
type ServerAPI struct {
ServerName string `json:"server_name"`
Domains []*DomainAPI `json:"domains"`
Duration float64 `json:"duration"`
BytesIn int64 `json:"bytes_in"`
BytesOut int64 `json:"bytes_out"`
}
//NewServerAPI - Constructor
func NewServerAPI(c *Connection) (s *ServerAPI) {
s = new(ServerAPI)
s.ServerName = fmt.Sprintf("%p", c)
s.Domains = make([]*DomainAPI, 0)
s.Duration = time.Since(c.ConnectTime()).Seconds()
s.BytesIn = c.BytesIn()
s.BytesOut = c.BytesOut()
for d := range c.DomainTrack {
dt := c.DomainTrack[d]
domainAPI := NewDomainAPI(dt.DomainName, dt.BytesIn(), dt.BytesOut())
s.Domains = append(s.Domains, domainAPI)
}
return
}
//ServerAPIContainer -- Holder for all the Servers
type ServerAPIContainer struct {
Servers []*ServerAPI `json:"servers"`
}
//NewServerAPIContainer -- Constructor
func NewServerAPIContainer() (p *ServerAPIContainer) {
p = new(ServerAPIContainer)
p.Servers = make([]*ServerAPI, 0)
return p
}

View File

@ -1,17 +0,0 @@
package genericlistener
import (
"log"
"os"
)
var (
loginfo *log.Logger
logdebug *log.Logger
logFlags = log.Ldate | log.Lmicroseconds | log.Lshortfile
)
func init() {
loginfo = log.New(os.Stdout, "INFO: genericlistener: ", logFlags)
logdebug = log.New(os.Stdout, "DEBUG: genericlistener:", logFlags)
}

View File

@ -5,11 +5,13 @@ import (
"fmt"
"net"
"strconv"
"strings"
)
const (
packerV1 byte = 255 - 1
packerV2 byte = 255 - 2
_ = iota // skip the iota value of 0
packerV1 byte = 255 - iota
packerV2
)
//Packer -- contains both header and data
@ -26,121 +28,93 @@ func NewPacker() (p *Packer) {
return
}
func splitHeader(header []byte, names []string) (map[string]string, error) {
parts := strings.Split(string(header), ",")
if p, n := len(parts), len(names); p > n {
return nil, fmt.Errorf("Header contains %d extra fields", p-n)
} else if p < n {
return nil, fmt.Errorf("Header missing fields %q", names[p:])
}
result := make(map[string]string, len(names))
for ind, key := range names {
result[key] = parts[ind]
}
return result, nil
}
//ReadMessage -
func ReadMessage(b []byte) (p *Packer, err error) {
fmt.Println("ReadMessage")
var pos int
err = nil
// detect protocol in use
func ReadMessage(b []byte) (*Packer, error) {
// Detect protocol in use
if b[0] == packerV1 {
p = NewPacker()
// Separate the header and body using the header length in the second byte.
p := NewPacker()
header := b[2 : b[1]+2]
data := b[b[1]+2:]
// Handle Header Length
pos = pos + 1
p.Header.HeaderLen = b[pos]
//handle address family
pos = pos + 1
end := bytes.IndexAny(b[pos:], ",")
if end == -1 {
err = fmt.Errorf("missing , while parsing address family")
// Handle the different parts of the header.
parts, err := splitHeader(header, []string{"address family", "address", "port", "data length", "service"})
if err != nil {
return nil, err
}
bAddrFamily := b[pos : pos+end]
if bytes.ContainsAny(bAddrFamily, addressFamilyText[FamilyIPv4]) {
if familyText := parts["address family"]; familyText == addressFamilyText[FamilyIPv4] {
p.Header.family = FamilyIPv4
} else if bytes.ContainsAny(bAddrFamily, addressFamilyText[FamilyIPv6]) {
} else if familyText == addressFamilyText[FamilyIPv6] {
p.Header.family = FamilyIPv6
} else {
err = fmt.Errorf("Address family not supported %d", bAddrFamily)
return nil, fmt.Errorf("Address family %q not supported", familyText)
}
//handle address
pos = pos + end + 1
end = bytes.IndexAny(b[pos:], ",")
if end == -1 {
err = fmt.Errorf("missing , while parsing address")
return nil, err
}
p.Header.address = net.ParseIP(string(b[pos : pos+end]))
//handle import
pos = pos + end + 1
end = bytes.IndexAny(b[pos:], ",")
if end == -1 {
err = fmt.Errorf("missing , while parsing address")
return nil, err
p.Header.address = net.ParseIP(parts["address"])
if p.Header.address == nil {
return nil, fmt.Errorf("Invalid network address %q", parts["address"])
} else if p.Header.Family() == FamilyIPv4 && p.Header.address.To4() == nil {
return nil, fmt.Errorf("Address %q is not in address family %s", parts["address"], p.Header.FamilyText())
}
p.Header.Port, err = strconv.Atoi(string(b[pos : pos+end]))
if err != nil {
err = fmt.Errorf("error converting port %s", err)
//handle port
if port, err := strconv.Atoi(parts["port"]); err != nil {
return nil, fmt.Errorf("Error converting port %q: %v", parts["port"], err)
} else if port <= 0 || port > 65535 {
return nil, fmt.Errorf("Port %d out of range", port)
} else {
p.Header.Port = port
}
//handle data length
pos = pos + end + 1
end = bytes.IndexAny(b[pos:], ",")
if end == -1 {
err = fmt.Errorf("missing , while parsing address")
return nil, err
}
p.Data.DataLen, err = strconv.Atoi(string(b[pos : pos+end]))
if err != nil {
err = fmt.Errorf("error converting data length %s", err)
if dataLen, err := strconv.Atoi(parts["data length"]); err != nil {
return nil, fmt.Errorf("Error converting data length %q: %v", parts["data length"], err)
} else if dataLen != len(data) {
return nil, fmt.Errorf("Data length %d doesn't match received length %d", dataLen, len(data))
}
//handle Service
pos = pos + end + 1
end = pos + int(p.Header.HeaderLen)
p.Header.Service = string(b[pos : p.Header.HeaderLen+2])
p.Header.Service = parts["service"]
//handle payload
pos = int(p.Header.HeaderLen + 2)
p.Data.AppendBytes(b[pos:])
} else {
err = fmt.Errorf("Version %d not supported", b[0:0])
p.Data.AppendBytes(data)
return p, nil
}
return
return nil, fmt.Errorf("Version %d not supported", 255-b[0])
}
//PackV1 -- Outputs version 1 of packer
func (p *Packer) PackV1() (b bytes.Buffer) {
version := packerV1
var headerBuf bytes.Buffer
headerBuf.WriteString(p.Header.FamilyText())
headerBuf.WriteString(",")
headerBuf.Write([]byte(p.Header.Address().String()))
headerBuf.WriteString(",")
headerBuf.WriteString(fmt.Sprintf("%d", p.Header.Port))
headerBuf.WriteString(",")
headerBuf.WriteString(fmt.Sprintf("%d", p.Data.buffer.Len()))
headerBuf.WriteString(",")
headerBuf.WriteString(p.Header.Service)
var metaBuf bytes.Buffer
metaBuf.WriteByte(version)
metaBuf.WriteByte(byte(headerBuf.Len()))
func (p *Packer) PackV1() bytes.Buffer {
header := strings.Join([]string{
p.Header.FamilyText(),
p.Header.AddressString(),
strconv.Itoa(p.Header.Port),
strconv.Itoa(p.Data.DataLen()),
p.Header.Service,
}, ",")
var buf bytes.Buffer
buf.Write(metaBuf.Bytes())
buf.Write(headerBuf.Bytes())
buf.Write(p.Data.buffer.Bytes())
buf.WriteByte(packerV1)
buf.WriteByte(byte(len(header)))
buf.WriteString(header)
buf.Write(p.Data.Data())
//fmt.Println("header: ", headerBuf.String())
//fmt.Println("meta: ", metaBuf)
//fmt.Println("Data: ", p.Data.buffer)
//fmt.Println("Buffer: ", buf.Bytes())
//fmt.Println("Buffer: ", hex.Dump(buf.Bytes()))
//fmt.Printf("Buffer %s", buf.Bytes())
b = buf
return
return buf
}

View File

@ -1,31 +1,31 @@
package packer
import "bytes"
import (
"bytes"
)
//packerData -- Contains packer data
type packerData struct {
buffer *bytes.Buffer
DataLen int
buffer bytes.Buffer
}
func newPackerData() (p *packerData) {
p = new(packerData)
p.buffer = new(bytes.Buffer)
return
func newPackerData() *packerData {
return new(packerData)
}
func (p *packerData) AppendString(dataString string) (n int, err error) {
n, err = p.buffer.WriteString(dataString)
return
func (p *packerData) AppendString(dataString string) (int, error) {
return p.buffer.WriteString(dataString)
}
func (p *packerData) AppendBytes(dataBytes []byte) (n int, err error) {
n, err = p.buffer.Write(dataBytes)
return
func (p *packerData) AppendBytes(dataBytes []byte) (int, error) {
return p.buffer.Write(dataBytes)
}
//Data --
func (p *packerData) Data() (b []byte) {
b = p.buffer.Bytes()
return
func (p *packerData) Data() []byte {
return p.buffer.Bytes()
}
func (p *packerData) DataLen() int {
return p.buffer.Len()
}

View File

@ -1,17 +1,18 @@
package packer
import "net"
import "fmt"
import (
"fmt"
"net"
)
type addressFamily int
// packerHeader structure to hold our header information.
type packerHeader struct {
family addressFamily
address net.IP
Port int
Service string
HeaderLen byte
family addressFamily
address net.IP
Port int
Service string
}
//Family -- ENUM for Address Family
@ -30,52 +31,42 @@ func newPackerHeader() (p *packerHeader) {
p.SetAddress("127.0.0.1")
p.Port = 65535
p.Service = "na"
p.HeaderLen = 0
return
}
//SetAddress -- Set Address. which sets address family automatically
func (p *packerHeader) SetAddress(addr string) {
p.address = net.ParseIP(addr)
err := p.address.To4()
if err != nil {
if p.address.To4() != nil {
p.family = FamilyIPv4
} else if p.address.To16() != nil {
p.family = FamilyIPv6
} else {
err := p.address.To16()
if err != nil {
p.family = FamilyIPv6
} else {
panic(fmt.Sprintf("setAddress does not support %s", addr))
}
panic(fmt.Sprintf("setAddress does not support %q", addr))
}
}
func (p *packerHeader) AddressBytes() (b []byte) {
b = make([]byte, 16)
switch {
case p.address.To4() != nil:
b = make([]byte, 4)
for pos := range b {
b[pos] = p.address[pos+12]
}
return
func (p *packerHeader) AddressBytes() []byte {
if ip4 := p.address.To4(); ip4 != nil {
p.address = ip4
}
return
return []byte(p.address)
}
func (p *packerHeader) Address() (address net.IP) {
address = p.address
return
func (p *packerHeader) AddressString() string {
return p.address.String()
}
func (p *packerHeader) Family() (family addressFamily) {
family = p.family
return
func (p *packerHeader) Address() net.IP {
return p.address
}
func (p *packerHeader) FamilyText() (familyText string) {
familyText = addressFamilyText[p.family]
return
func (p *packerHeader) Family() addressFamily {
return p.family
}
func (p *packerHeader) FamilyText() string {
return addressFamilyText[p.family]
}

View File

@ -1,7 +1,9 @@
package packer
import "log"
import "os"
import (
"log"
"os"
)
var (
loginfo *log.Logger

View File

@ -0,0 +1,15 @@
package server
//ConnectionStatsAPI --
type ConnectionStatsAPI struct {
Connections int64 `json:"current_connections"`
TotalConnections int64 `json:"total_connections"`
}
//NewConnectionStatsAPI -- Consttuctor
func NewConnectionStatsAPI(connections int64, totalConnections int64) (p *ConnectionStatsAPI) {
p = new(ConnectionStatsAPI)
p.Connections = connections
p.TotalConnections = totalConnections
return
}

View File

@ -0,0 +1,71 @@
package server
//DomainsAPI -- A collections of all the domains
//List of Domains -> DomainAPI
//DomainsAPI -> DomainServerAPI
//
//DomainServerAPI -- Container for Server Stats related to a domain
type DomainServerAPI struct {
ServerName string `json:"server_name"`
Traffic TrafficAPI `json:"traffic"`
}
//NewDomainServerAPI -- Constructor
func NewDomainServerAPI(domain string, conn *Connection) (p *DomainServerAPI) {
p = new(DomainServerAPI)
dt := conn.DomainTrack[domain]
p.Traffic.BytesIn = dt.BytesIn()
p.Traffic.BytesOut = dt.BytesOut()
p.Traffic.Requests = dt.Requests()
p.Traffic.Responses = dt.Responses()
p.ServerName = conn.ServerName()
return
}
//DomainAPI -- Container for domain and related servers
type DomainAPI struct {
DomainName string `json:"domain_name"`
TotalServers int `json:"server_total"`
Servers []*DomainServerAPI `json:"servers"`
Traffic TrafficAPI `json:"traffic"`
}
//NewDomainAPI -- Constructor
func NewDomainAPI(domain string, domainLoadBalance *DomainLoadBalance) (p *DomainAPI) {
p = new(DomainAPI)
p.DomainName = domain
for pos := range domainLoadBalance.connections {
ds := NewDomainServerAPI(domain, domainLoadBalance.connections[pos])
p.Servers = append(p.Servers, ds)
p.TotalServers++
p.Traffic.BytesIn += domainLoadBalance.connections[pos].BytesIn()
p.Traffic.BytesOut += domainLoadBalance.connections[pos].BytesOut()
p.Traffic.Requests += domainLoadBalance.connections[pos].requests
p.Traffic.Responses += domainLoadBalance.connections[pos].responses
}
return
}
//DomainsAPI -- Container for Domains
type DomainsAPI struct {
TotalDomains int `json:"domain_total"`
Domains []*DomainAPI `json:"domains"`
Traffic TrafficAPI `json:"traffic"`
}
//NewDomainsAPI -- Constructor
func NewDomainsAPI(domains map[string]*DomainLoadBalance) (p *DomainsAPI) {
p = new(DomainsAPI)
for domain := range domains {
d := NewDomainAPI(domain, domains[domain])
p.Domains = append(p.Domains, d)
p.Traffic.BytesIn += d.Traffic.BytesIn
p.Traffic.BytesOut += d.Traffic.BytesOut
p.Traffic.Requests += d.Traffic.Requests
p.Traffic.Responses += d.Traffic.Responses
}
return
}

View File

@ -0,0 +1,36 @@
package server
import "time"
//ServerAPI -- Structure to support the server API
type ServerAPI struct {
ServerName string `json:"server_name"`
ServerID int64 `json:"server_id"`
Domains []*ServerDomainAPI `json:"domains"`
Duration float64 `json:"duration"`
Idle float64 `json:"idle"`
BytesIn int64 `json:"bytes_in"`
BytesOut int64 `json:"bytes_out"`
Source string `json:"source_address"`
State bool `json:"server_state"`
}
//NewServerAPI - Constructor
func NewServerAPI(c *Connection) (s *ServerAPI) {
s = new(ServerAPI)
s.ServerName = c.ServerName()
s.ServerID = c.ConnectionID()
s.Domains = make([]*ServerDomainAPI, 0)
s.Duration = time.Since(c.ConnectTime()).Seconds()
s.Idle = time.Since(c.LastUpdate()).Seconds()
s.BytesIn = c.BytesIn()
s.BytesOut = c.BytesOut()
s.Source = c.source
for domainName := range c.DomainTrack {
domainAPI := NewServerDomainAPI(c, c.DomainTrack[domainName])
s.Domains = append(s.Domains, domainAPI)
}
return
}

View File

@ -0,0 +1,60 @@
package server
//ServerDomainsAPI -- Structure to support the server API
type ServerDomainsAPI struct {
DomainName string `json:"domain_name"`
ServerID int64 `json:"server_id"`
BytesIn int64 `json:"bytes_in"`
BytesOut int64 `json:"bytes_out"`
Requests int64 `json:"requests"`
Responses int64 `json:"responses"`
}
//NewServerDomainsAPI - Constructor
func NewServerDomainsAPI(c *Connection, d *DomainTrack) (s *ServerDomainsAPI) {
s = new(ServerDomainsAPI)
s.DomainName = d.DomainName
s.ServerID = c.ConnectionID()
s.BytesIn = d.BytesIn()
s.BytesOut = d.BytesOut()
s.Requests = d.requests
s.Responses = d.responses
return
}
//ServerDomainsAPIContainer -- Holder for all the Servers
type ServerDomainsAPIContainer struct {
Domains []*ServerDomainsAPI `json:"domains"`
}
//NewServerDomainsAPIContainer -- Constructor
func NewServerDomainsAPIContainer() (p *ServerDomainsAPIContainer) {
p = new(ServerDomainsAPIContainer)
p.Domains = make([]*ServerDomainsAPI, 0)
return p
}
//ServerDomainAPI -- Structure to support the server API
type ServerDomainAPI struct {
DomainName string `json:"domain_name"`
ServerID int64 `json:"server_id"`
BytesIn int64 `json:"bytes_in"`
BytesOut int64 `json:"bytes_out"`
Requests int64 `json:"requests"`
Responses int64 `json:"responses"`
Source string `json:"source_addr"`
}
//NewServerDomainAPI - Constructor
func NewServerDomainAPI(c *Connection, d *DomainTrack) (s *ServerDomainAPI) {
s = new(ServerDomainAPI)
s.DomainName = d.DomainName
s.ServerID = c.ConnectionID()
s.BytesIn = d.BytesIn()
s.BytesOut = d.BytesOut()
s.Requests = d.requests
s.Responses = d.responses
s.Source = c.Source()
return
}

View File

@ -0,0 +1,53 @@
package server
import "time"
//ServersAPI -- Structure to support the server API
type ServersAPI struct {
ServerName string `json:"server_name"`
ServerID int64 `json:"server_id"`
Domains []*ServerDomainAPI `json:"domains"`
Duration float64 `json:"duration"`
Idle float64 `json:"idle"`
BytesIn int64 `json:"bytes_in"`
BytesOut int64 `json:"bytes_out"`
Requests int64 `json:"requests"`
Responses int64 `json:"responses"`
Source string `json:"source_address"`
State bool `json:"server_state"`
}
//NewServersAPI - Constructor
func NewServersAPI(c *Connection) (s *ServersAPI) {
s = new(ServersAPI)
s.ServerName = c.ServerName()
s.ServerID = c.ConnectionID()
s.Domains = make([]*ServerDomainAPI, 0)
s.Duration = time.Since(c.ConnectTime()).Seconds()
s.Idle = time.Since(c.LastUpdate()).Seconds()
s.BytesIn = c.BytesIn()
s.BytesOut = c.BytesOut()
s.Requests = c.requests
s.Responses = c.responses
s.Source = c.Source()
s.State = c.State()
for d := range c.DomainTrack {
dt := c.DomainTrack[d]
domainAPI := NewServerDomainAPI(c, dt)
s.Domains = append(s.Domains, domainAPI)
}
return
}
//ServerAPIContainer -- Holder for all the Servers
type ServerAPIContainer struct {
Servers []*ServersAPI `json:"servers"`
}
//NewServerAPIContainer -- Constructor
func NewServerAPIContainer() (p *ServerAPIContainer) {
p = new(ServerAPIContainer)
p.Servers = make([]*ServersAPI, 0)
return p
}

View File

@ -0,0 +1,36 @@
package server
import (
"time"
)
//StatusAPI -- Structure to support the server API
type StatusAPI struct {
Name string `json:"name"`
Uptime float64 `json:"uptime"`
WssDomain string `json:"wss_domain"`
AdminDomain string `json:"admin_domain"`
LoadbalanceDefaultMethod string `json:"loadbalance_default_method"`
DeadTime *StatusDeadTimeAPI `json:"dead_time"`
AdminStats *TrafficAPI `json:"admin_traffic"`
TrafficStats *TrafficAPI `json:"traffic"`
ExtConnections *ConnectionStatsAPI `json:"ext_connections"`
WSSConnections *ConnectionStatsAPI `json:"wss_connections"`
}
//NewStatusAPI - Constructor
func NewStatusAPI(c *Status) (s *StatusAPI) {
s = new(StatusAPI)
s.Name = c.Name
s.Uptime = time.Since(c.StartTime).Seconds()
s.WssDomain = c.WssDomain
s.AdminDomain = c.AdminDomain
s.LoadbalanceDefaultMethod = c.LoadbalanceDefaultMethod
s.DeadTime = NewStatusDeadTimeAPI(c.DeadTime.dwell, c.DeadTime.idle, c.DeadTime.cancelcheck)
s.AdminStats = NewTrafficAPI(c.AdminStats.Requests, c.AdminStats.Responses, c.AdminStats.BytesIn, c.AdminStats.BytesOut)
s.TrafficStats = NewTrafficAPI(c.TrafficStats.Requests, c.TrafficStats.Responses, c.TrafficStats.BytesIn, c.TrafficStats.BytesOut)
s.ExtConnections = NewConnectionStatsAPI(c.ExtConnections.Connections, c.ExtConnections.TotalConnections)
s.WSSConnections = NewConnectionStatsAPI(c.WSSConnections.Connections, c.ExtConnections.TotalConnections)
return
}

View File

@ -0,0 +1,17 @@
package server
//StatusDeadTimeAPI -- structure for deadtime configuration
type StatusDeadTimeAPI struct {
Dwell int `json:"dwell"`
Idle int `json:"idle"`
Cancelcheck int `json:"cancel_check"`
}
//NewStatusDeadTimeAPI -- constructor
func NewStatusDeadTimeAPI(dwell int, idle int, cancelcheck int) (p *StatusDeadTimeAPI) {
p = new(StatusDeadTimeAPI)
p.Dwell = dwell
p.Idle = idle
p.Cancelcheck = cancelcheck
return
}

View File

@ -0,0 +1,20 @@
package server
//TrafficStats --
type TrafficAPI struct {
Requests int64
Responses int64
BytesIn int64
BytesOut int64
}
//NewTrafficStats -- Consttuctor
func NewTrafficAPI(requests, responses, bytesIn, bytesOut int64) (p *TrafficAPI) {
p = new(TrafficAPI)
p.Requests = requests
p.Responses = responses
p.BytesIn = bytesIn
p.BytesOut = bytesOut
return
}

View File

@ -0,0 +1,205 @@
package server
import (
"context"
"net/http"
"runtime"
"strconv"
"strings"
"github.com/gorilla/mux"
"git.daplie.com/Daplie/go-rvpn-server/rvpn/envelope"
)
const (
endPointPrefix = "/api/com.daplie.tunnel/"
)
var connectionTable *Table
var serverStatus *Status
var serverStatusAPI *Status
//handleAdminClient -
// - expecting an existing oneConnListener with a qualified wss client connected.
// - auth will happen again since we were just peeking at the token.
func handleAdminClient(ctx context.Context, oneConn *oneConnListener) {
serverStatus = ctx.Value(ctxServerStatus).(*Status)
connectionTable = serverStatus.ConnectionTable
serverStatusAPI = serverStatus
router := mux.NewRouter().StrictSlash(true)
router.PathPrefix("/admin/").Handler(http.StripPrefix("/admin/", http.FileServer(http.Dir("html/admin"))))
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
loginfo.Println("HandleFunc /")
serverStatus.AdminStats.IncRequests()
switch url := r.URL.Path; url {
case "/":
// check to see if we are using the administrative Host
if strings.Contains(r.Host, "rvpn.daplie.invalid") {
http.Redirect(w, r, "/admin", 301)
serverStatus.AdminStats.IncResponses()
}
default:
http.Error(w, "Not Found", 404)
}
})
router.HandleFunc(endPointPrefix+"domains", getDomainsEndpoint).Methods("GET")
router.HandleFunc(endPointPrefix+"domain/", getDomainEndpoint).Methods("GET")
router.HandleFunc(endPointPrefix+"domain/{domain-name}", getDomainEndpoint).Methods("GET")
router.HandleFunc(endPointPrefix+"servers", getServersEndpoint).Methods("GET")
router.HandleFunc(endPointPrefix+"server/", getServerEndpoint).Methods("GET")
router.HandleFunc(endPointPrefix+"server/{server-id}", getServerEndpoint).Methods("GET")
router.HandleFunc(endPointPrefix+"status/", getStatusEndpoint).Methods("GET")
s := &http.Server{
Addr: ":80",
Handler: router,
}
err := s.Serve(oneConn)
if err != nil {
loginfo.Println("Serve error: ", err)
}
select {
case <-ctx.Done():
loginfo.Println("Cancel signal hit")
return
}
}
func getStatusEndpoint(w http.ResponseWriter, r *http.Request) {
pc, _, _, _ := runtime.Caller(0)
loginfo.Println(runtime.FuncForPC(pc).Name())
serverStatus.AdminStats.IncRequests()
statusContainer := NewStatusAPI(serverStatusAPI)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
env := envelope.NewEnvelope("domains/GET")
env.Result = statusContainer
env.GenerateWriter(w)
serverStatus.AdminStats.IncResponses()
}
func getDomainsEndpoint(w http.ResponseWriter, r *http.Request) {
pc, _, _, _ := runtime.Caller(0)
loginfo.Println(runtime.FuncForPC(pc).Name())
serverStatus.AdminStats.IncRequests()
domainsContainer := NewDomainsAPI(connectionTable.domains)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
env := envelope.NewEnvelope("domains/GET")
env.Result = domainsContainer
env.GenerateWriter(w)
serverStatus.AdminStats.IncResponses()
}
func getDomainEndpoint(w http.ResponseWriter, r *http.Request) {
pc, _, _, _ := runtime.Caller(0)
loginfo.Println(runtime.FuncForPC(pc).Name())
serverStatus.AdminStats.IncRequests()
env := envelope.NewEnvelope("domain/GET")
params := mux.Vars(r)
if id, ok := params["domain-name"]; !ok {
env.Error = "domain-name is missing"
env.ErrorURI = r.RequestURI
env.ErrorDescription = "domain API requires a domain-name"
} else {
domainName := id
if domainLB, ok := connectionTable.domains[domainName]; !ok {
env.Error = "domain-name was not found"
env.ErrorURI = r.RequestURI
env.ErrorDescription = "domain-name not found"
} else {
var domainAPIContainer []*ServerDomainAPI
conns := domainLB.Connections()
for pos := range conns {
conn := conns[pos]
domainAPI := NewServerDomainAPI(conn, conn.DomainTrack[domainName])
domainAPIContainer = append(domainAPIContainer, domainAPI)
}
env.Result = domainAPIContainer
}
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
env.GenerateWriter(w)
serverStatus.AdminStats.IncResponses()
}
func getServersEndpoint(w http.ResponseWriter, r *http.Request) {
pc, _, _, _ := runtime.Caller(0)
loginfo.Println(runtime.FuncForPC(pc).Name())
serverStatus.AdminStats.IncRequests()
serverContainer := NewServerAPIContainer()
for c := range connectionTable.Connections() {
serverAPI := NewServersAPI(c)
serverContainer.Servers = append(serverContainer.Servers, serverAPI)
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
env := envelope.NewEnvelope("servers/GET")
env.Result = serverContainer
env.GenerateWriter(w)
serverStatus.AdminStats.IncResponses()
}
func getServerEndpoint(w http.ResponseWriter, r *http.Request) {
pc, _, _, _ := runtime.Caller(0)
loginfo.Println(runtime.FuncForPC(pc).Name())
serverStatus.AdminStats.IncRequests()
env := envelope.NewEnvelope("server/GET")
params := mux.Vars(r)
if id, ok := params["server-id"]; !ok {
env.Error = "server-id is missing"
env.ErrorURI = r.RequestURI
env.ErrorDescription = "server API requires a server-id"
} else {
serverID, err := strconv.Atoi(id)
if err != nil {
env.Error = "server-id is not an integer"
env.ErrorURI = r.RequestURI
env.ErrorDescription = "server API requires a server-id"
} else {
conn, err := connectionTable.GetConnection(int64(serverID))
if err != nil {
env.Error = "server-id was not found"
env.ErrorURI = r.RequestURI
env.ErrorDescription = "missing server-id, make sure desired service-id is in servers"
} else {
loginfo.Println("test")
serverAPI := NewServerAPI(conn)
env.Result = serverAPI
}
}
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
env.GenerateWriter(w)
serverStatus.AdminStats.IncResponses()
}

View File

@ -1,8 +1,11 @@
package genericlistener
package server
import "net"
import "context"
import "fmt"
import (
"context"
"fmt"
"net"
"sync"
)
//Track -- used to track connection + domain
type Track struct {
@ -20,6 +23,7 @@ func NewTrack(conn net.Conn, domain string) (p *Track) {
//Tracking --
type Tracking struct {
mutex sync.Mutex
connections map[string]*Track
register chan *Track
unregister chan net.Conn
@ -46,18 +50,22 @@ func (p *Tracking) Run(ctx context.Context) {
return
case connection := <-p.register:
p.mutex.Lock()
key := connection.conn.RemoteAddr().String()
loginfo.Println("register fired", key)
p.connections[key] = connection
p.list()
p.mutex.Unlock()
case connection := <-p.unregister:
p.mutex.Lock()
key := connection.RemoteAddr().String()
loginfo.Println("unregister fired", key)
if _, ok := p.connections[key]; ok {
delete(p.connections, key)
}
p.list()
p.mutex.Unlock()
}
}
}
@ -70,12 +78,12 @@ func (p *Tracking) list() {
//Lookup --
// - get connection from key
func (p *Tracking) Lookup(key string) (c *Track, err error) {
func (p *Tracking) Lookup(key string) (*Track, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
if _, ok := p.connections[key]; ok {
c = p.connections[key]
} else {
err = fmt.Errorf("Lookup failed for %s", key)
c = nil
return p.connections[key], nil
}
return
return nil, fmt.Errorf("Lookup failed for %s", key)
}

View File

@ -1,4 +1,4 @@
package genericlistener
package server
import (
"bufio"
@ -28,7 +28,7 @@ func NewWedgeConnSize(c net.Conn, size int) (p *WedgeConn) {
}
//Discard - discard a number of bytes, perhaps after peeking at the
func (w *WedgeConn) Discard(n int) (discarded int, err error) {
func (w *WedgeConn) Discard(n int) (int, error) {
return w.reader.Discard(n)
}
@ -44,8 +44,7 @@ func (w *WedgeConn) ReadByte() (byte, error) {
//Read -- A normal reader.
func (w *WedgeConn) Read(p []byte) (int, error) {
cnt, err := w.reader.Read(p)
return cnt, err
return w.reader.Read(p)
}
//Buffered --
@ -56,13 +55,12 @@ func (w *WedgeConn) Buffered() int {
//PeekAll --
// - get all the chars available
// - pass then back
func (w *WedgeConn) PeekAll() (buf []byte, err error) {
_, err = w.Peek(1)
if err != nil {
func (w *WedgeConn) PeekAll() ([]byte, error) {
// We first peek with 1 so that if there is no buffered data the reader will
// fill the buffer before we read how much data is buffered.
if _, err := w.Peek(1); err != nil {
return nil, err
}
buf, err = w.Peek(w.Buffered())
return
return w.Peek(w.Buffered())
}

View File

@ -1,30 +1,20 @@
package genericlistener
package server
import (
"context"
"io"
"strconv"
"sync"
"time"
"git.daplie.com/Daplie/go-rvpn-server/rvpn/packer"
"sync"
"io"
"context"
"encoding/hex"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
}
"git.daplie.com/Daplie/go-rvpn-server/rvpn/packer"
)
// Connection track websocket and faciliates in and out data
type Connection struct {
mutex *sync.Mutex
mutex sync.Mutex
// The main connection table (should be just one of these created at startup)
connectionTable *Table
@ -44,12 +34,21 @@ type Connection struct {
// Address of the Remote End Point
source string
// serverName -- Name of the server, at this point 1st domain registered. Will likely change with JWT
serverName string
// bytes in
bytesIn int64
// bytes out
bytesOut int64
// requests
requests int64
// response
responses int64
// Connect Time
connectTime time.Time
@ -63,28 +62,38 @@ type Connection struct {
///wssState tracks a highlevel status of the connection, false means do nothing.
wssState bool
//connectionID
connectionID int64
}
//NewConnection -- Constructor
func NewConnection(connectionTable *Table, conn *websocket.Conn, remoteAddress string, initialDomains []interface{}, connectionTrack *Tracking) (p *Connection) {
func NewConnection(connectionTable *Table, conn *websocket.Conn, remoteAddress string,
initialDomains []interface{}, connectionTrack *Tracking, serverName string) (p *Connection) {
connectionID = connectionID + 1
p = new(Connection)
p.mutex = &sync.Mutex{}
p.connectionTable = connectionTable
p.conn = conn
p.source = remoteAddress
p.serverName = serverName
p.bytesIn = 0
p.bytesOut = 0
p.requests = 0
p.responses = 0
p.send = make(chan *SendTrack)
p.connectTime = time.Now()
p.initialDomains = initialDomains
p.connectionTrack = connectionTrack
p.DomainTrack = make(map[string]*DomainTrack)
p.lastUpdate = time.Now()
for _, domain := range initialDomains {
p.AddTrackedDomain(string(domain.(string)))
}
p.State(true)
p.SetState(true)
p.connectionID = connectionID
return
}
@ -95,28 +104,34 @@ func (c *Connection) AddTrackedDomain(domain string) {
c.DomainTrack[domain] = p
}
//ServerName -- Property
func (c *Connection) ServerName() string {
return c.serverName
}
//SetServerName -- Setter
func (c *Connection) SetServerName(serverName string) {
c.serverName = serverName
}
//InitialDomains -- Property
func (c *Connection) InitialDomains() (i []interface{}) {
i = c.initialDomains
return
func (c *Connection) InitialDomains() []interface{} {
return c.initialDomains
}
//ConnectTime -- Property
func (c *Connection) ConnectTime() (t time.Time) {
t = c.connectTime
return
func (c *Connection) ConnectTime() time.Time {
return c.connectTime
}
//BytesIn -- Property
func (c *Connection) BytesIn() (b int64) {
b = c.bytesIn
return
func (c *Connection) BytesIn() int64 {
return c.bytesIn
}
//BytesOut -- Property
func (c *Connection) BytesOut() (b int64) {
b = c.bytesOut
return
func (c *Connection) BytesOut() int64 {
return c.bytesOut
}
//SendCh -- property to sending channel
@ -124,6 +139,11 @@ func (c *Connection) SendCh() chan *SendTrack {
return c.send
}
//Source --
func (c *Connection) Source() string {
return c.source
}
func (c *Connection) addIn(num int64) {
c.bytesIn = c.bytesIn + num
}
@ -132,59 +152,74 @@ func (c *Connection) addOut(num int64) {
c.bytesOut = c.bytesOut + num
}
//ConnectionTable -- property
func (c *Connection) ConnectionTable() (table *Table) {
table = c.connectionTable
return
func (c *Connection) addRequests() {
c.requests = c.requests + 1
}
//GetState -- Get state of Socket...this is a high level state.
func (c *Connection) GetState() bool {
defer func() {
c.mutex.Unlock()
}()
func (c *Connection) addResponse() {
c.responses = c.responses + 1
}
//ConnectionTable -- property
func (c *Connection) ConnectionTable() *Table {
return c.connectionTable
}
//State -- Get state of Socket...this is a high level state.
func (c *Connection) State() bool {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.wssState
}
//State -- Set the set of the high level connection
func (c *Connection) State(state bool) {
defer func() {
c.mutex.Unlock()
}()
//SetState -- Set the set of the high level connection
func (c *Connection) SetState(state bool) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.wssState = state
}
//Update -- updates the lastUpdate property tracking idle time
func (c *Connection) Update() {
defer func() {
c.mutex.Unlock()
}()
c.mutex.Lock()
defer c.mutex.Unlock()
c.lastUpdate = time.Now()
}
//LastUpdate -- retrieve last update
func (c *Connection) LastUpdate() time.Time {
return c.lastUpdate
}
//ConnectionID - Get
func (c *Connection) ConnectionID() int64 {
return c.connectionID
}
//NextWriter -- Wrapper to allow a high level state check before offering NextWriter
//The libary failes if client abends during write-cycle. a fast moving write is not caught before socket state bubbles up
//A synchronised state is maintained
func (c Connection) NextWriter(wssMessageType int) (w io.WriteCloser, err error) {
if c.GetState() == true {
w, err = c.conn.NextWriter(wssMessageType)
} else {
loginfo.Println("NextWriter aborted, state is not true")
func (c *Connection) NextWriter(wssMessageType int) (io.WriteCloser, error) {
if c.State() {
return c.conn.NextWriter(wssMessageType)
}
return
// Is returning a nil error actually the proper thing to do here?
loginfo.Println("NextWriter aborted, state is not true")
return nil, nil
}
//Write -- Wrapper to allow a high level state check before allowing a write to the socket.
func (c *Connection) Write(w io.WriteCloser, message []byte) (cnt int, err error) {
if c.GetState() == true {
cnt, err = w.Write(message)
func (c *Connection) Write(w io.WriteCloser, message []byte) (int, error) {
if c.State() {
return w.Write(message)
}
return
// Is returning a nil error actually the proper thing to do here?
return 0, nil
}
//Reader -- export the reader function
@ -199,17 +234,17 @@ func (c *Connection) Reader(ctx context.Context) {
loginfo.Println("Reader Start ", c)
c.conn.SetReadLimit(65535)
//c.conn.SetReadLimit(65535)
for {
msgType, message, err := c.conn.ReadMessage()
_, message, err := c.conn.ReadMessage()
loginfo.Println("ReadMessage", msgType, err)
//loginfo.Println("ReadMessage", msgType, err)
c.Update()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
c.State(false)
c.SetState(false)
loginfo.Printf("error: %v", err)
}
break
@ -220,31 +255,31 @@ func (c *Connection) Reader(ctx context.Context) {
key := p.Header.Address().String() + ":" + strconv.Itoa(p.Header.Port)
track, err := connectionTrack.Lookup(key)
loginfo.Println(hex.Dump(p.Data.Data()))
//loginfo.Println(hex.Dump(p.Data.Data()))
if err != nil {
loginfo.Println("Unable to locate Tracking for ", key)
//loginfo.Println("Unable to locate Tracking for ", key)
continue
}
//Support for tracking outbound traffic based on domain.
if domainTrack, ok := c.DomainTrack[track.domain]; ok {
//if ok then add to structure, else warn there is something wrong
domainTrack.AddIn(int64(len(message)))
domainTrack.AddOut(int64(len(message)))
domainTrack.AddResponses()
}
track.conn.Write(p.Data.Data())
c.addIn(int64(len(message)))
loginfo.Println("end of read")
c.addResponse()
//loginfo.Println("end of read")
}
}
//Writer -- expoer the writer function
func (c *Connection) Writer() {
defer func() {
c.conn.Close()
}()
defer c.conn.Close()
loginfo.Println("Writer Start ", c)
@ -255,6 +290,7 @@ func (c *Connection) Writer() {
w, err := c.NextWriter(websocket.BinaryMessage)
loginfo.Println("next writer ", w)
if err != nil {
c.SetState(false)
return
}
@ -269,11 +305,13 @@ func (c *Connection) Writer() {
messageLen := int64(len(message.data))
c.addOut(messageLen)
c.addRequests()
//Support for tracking outbound traffic based on domain.
if domainTrack, ok := c.DomainTrack[message.domain]; ok {
//if ok then add to structure, else warn there is something wrong
domainTrack.AddOut(messageLen)
domainTrack.AddIn(messageLen)
domainTrack.AddRequests()
loginfo.Println("adding ", messageLen, " to ", message.domain)
} else {
logdebug.Println("attempting to add bytes to ", message.domain, "it does not exist")

View File

@ -1,6 +1,8 @@
package genericlistener
package server
import "github.com/gorilla/websocket"
import (
"github.com/gorilla/websocket"
)
//Registration -- A connection registration structure used to bring up a connection
//connection table will then handle additing and sdtarting up the various readers
@ -12,6 +14,9 @@ type Registration struct {
// Address of the Remote End Point
source string
// serverName
serverName string
// communications channel between go routines
commCh chan bool
@ -22,10 +27,11 @@ type Registration struct {
}
//NewRegistration -- Constructor
func NewRegistration(conn *websocket.Conn, remoteAddress string, initialDomains []interface{}, connectionTrack *Tracking) (p *Registration) {
func NewRegistration(conn *websocket.Conn, remoteAddress string, initialDomains []interface{}, connectionTrack *Tracking, serverName string) (p *Registration) {
p = new(Registration)
p.conn = conn
p.source = remoteAddress
p.serverName = serverName
p.commCh = make(chan bool)
p.initialDomains = initialDomains
p.connectionTrack = connectionTrack

View File

@ -1,8 +1,10 @@
package genericlistener
package server
import "fmt"
import "time"
import "context"
import (
"context"
"fmt"
"time"
)
const (
initialDomains = 0
@ -12,7 +14,7 @@ const (
//Table maintains the set of connections
type Table struct {
connections map[*Connection][]string
domains map[string]*Connection
domains map[string]*DomainLoadBalance
register chan *Registration
unregister chan *Connection
domainAnnounce chan *DomainMapping
@ -22,10 +24,10 @@ type Table struct {
}
//NewTable -- consructor
func NewTable(dwell int, idle int) (p *Table) {
func NewTable(dwell, idle int) (p *Table) {
p = new(Table)
p.connections = make(map[*Connection][]string)
p.domains = make(map[string]*Connection)
p.domains = make(map[string]*DomainLoadBalance)
p.register = make(chan *Registration)
p.unregister = make(chan *Connection)
p.domainAnnounce = make(chan *DomainMapping)
@ -36,15 +38,23 @@ func NewTable(dwell int, idle int) (p *Table) {
}
//Connections Property
func (c *Table) Connections() (table map[*Connection][]string) {
table = c.connections
return
func (c *Table) Connections() map[*Connection][]string {
return c.connections
}
//ConnByDomain -- Obtains a connection from a domain announcement.
func (c *Table) ConnByDomain(domain string) (conn *Connection, ok bool) {
conn, ok = c.domains[domain]
return
//ConnByDomain -- Obtains a connection from a domain announcement. A domain may be announced more than once
//if that is the case the system stores these connections and then sends traffic back round-robin
//back to the WSS connections
func (c *Table) ConnByDomain(domain string) (*Connection, bool) {
for dn := range c.domains {
loginfo.Println(dn, domain)
}
if domainsLB, ok := c.domains[domain]; ok {
loginfo.Println("found")
conn := domainsLB.NextMember()
return conn, ok
}
return nil, false
}
//reaper --
@ -56,7 +66,7 @@ func (c *Table) reaper(delay int, idle int) {
loginfo.Println("Running scanning ", len(c.connections))
for d := range c.connections {
if d.GetState() == false {
if !d.State() {
if time.Since(d.lastUpdate).Seconds() > float64(idle) {
loginfo.Println("reaper removing ", d.lastUpdate, time.Since(d.lastUpdate).Seconds())
delete(c.connections, d)
@ -66,8 +76,19 @@ func (c *Table) reaper(delay int, idle int) {
}
}
//GetConnection -- find connection by server-id
func (c *Table) GetConnection(serverID int64) (*Connection, error) {
for conn := range c.connections {
if conn.ConnectionID() == serverID {
return conn, nil
}
}
return nil, fmt.Errorf("Server-id %d not found", serverID)
}
//Run -- Execute
func (c *Table) Run(ctx context.Context) {
func (c *Table) Run(ctx context.Context, defaultMethod string) {
loginfo.Println("ConnectionTable starting")
go c.reaper(c.dwell, c.idle)
@ -82,7 +103,8 @@ func (c *Table) Run(ctx context.Context) {
case registration := <-c.register:
loginfo.Println("register fired")
connection := NewConnection(c, registration.conn, registration.source, registration.initialDomains, registration.connectionTrack)
connection := NewConnection(c, registration.conn, registration.source, registration.initialDomains,
registration.connectionTrack, registration.serverName)
c.connections[connection] = make([]string, initialDomains)
registration.commCh <- true
@ -92,7 +114,17 @@ func (c *Table) Run(ctx context.Context) {
newDomain := string(domain.(string))
loginfo.Println("adding domain ", newDomain, " to connection ", connection.conn.RemoteAddr().String())
c.domains[newDomain] = connection
//check to see if domain is already present.
if _, ok := c.domains[newDomain]; ok {
//append to a list of connections for that domain
c.domains[newDomain].AddConnection(connection)
} else {
//if not, then add as the 1st to the list of connections
c.domains[newDomain] = NewDomainLoadBalance(defaultMethod)
c.domains[newDomain].AddConnection(connection)
}
// add to the connection domain list
s := c.connections[connection]
@ -103,11 +135,26 @@ func (c *Table) Run(ctx context.Context) {
case connection := <-c.unregister:
loginfo.Println("closing connection ", connection.conn.RemoteAddr().String())
//does connection exist in the connection table -- should never be an issue
if _, ok := c.connections[connection]; ok {
//iterate over the connections for the domain
for _, domain := range c.connections[connection] {
fmt.Println("removing domain ", domain)
loginfo.Println("remove domain", domain)
//removing domain, make sure it is present (should never be a problem)
if _, ok := c.domains[domain]; ok {
delete(c.domains, domain)
domainLB := c.domains[domain]
domainLB.RemoveConnection(connection)
//check to see if domain is free of connections, if yes, delete map entry
if domainLB.count > 0 {
//ignore...perhaps we will do something here dealing wtih the lb method
} else {
delete(c.domains, domain)
}
}
}
@ -129,7 +176,6 @@ func (c *Table) Run(ctx context.Context) {
}
//Register -- Property
func (c *Table) Register() (r chan *Registration) {
r = c.register
return
func (c *Table) Register() chan *Registration {
return c.register
}

View File

@ -0,0 +1,107 @@
package server
import (
"fmt"
"sync"
)
const (
lbmUnSupported string = "unsuported"
lbmRoundRobin string = "round-robin"
lbmLeastConnections string = "least-connections"
)
//DomainLoadBalance -- Use as a structure for domain connections
//and load balancing those connections. Initial modes are round-robin
//but suspect we will need least-connections, and sticky
type DomainLoadBalance struct {
mutex sync.Mutex
//lb method, supported round robin.
method string
//the last connection based on calculation
lastmember int
// a list of connections in this load balancing context
connections []*Connection
//a counter to track total connections, so we aren't calling len all the time
count int
//true if the system belives a recalcuation is required
recalc bool
}
//NewDomainLoadBalance -- Constructor
func NewDomainLoadBalance(defaultMethod string) (p *DomainLoadBalance) {
p = new(DomainLoadBalance)
p.method = defaultMethod
p.lastmember = 0
p.count = 0
return
}
//Connections -- Access connections
func (p *DomainLoadBalance) Connections() []*Connection {
return p.connections
}
//NextMember -- increments the lastmember, and then checks if >= to count, if true
//the last is reset to 0
func (p *DomainLoadBalance) NextMember() (conn *Connection) {
p.mutex.Lock()
defer p.mutex.Unlock()
//check for round robin, if not RR then drop out and call calculate
loginfo.Println("NextMember:", p)
if p.method == lbmRoundRobin {
p.lastmember++
if p.lastmember >= p.count {
p.lastmember = 0
}
nextConn := p.connections[p.lastmember]
return nextConn
}
// Not round robin
switch method := p.method; method {
default:
panic(fmt.Errorf("fatal unsupported loadbalance method %s", method))
}
}
//AddConnection -- Add an additional connection to the list of connections for this domain
//this should not affect the next member calculation in RR. However it many in other
//methods
func (p *DomainLoadBalance) AddConnection(conn *Connection) []*Connection {
loginfo.Println("AddConnection", fmt.Sprintf("%p", conn))
p.mutex.Lock()
defer p.mutex.Unlock()
p.connections = append(p.connections, conn)
p.count++
loginfo.Println("AddConnection", p)
return p.connections
}
//RemoveConnection -- removes a matching connection from the list. This may
//affect the nextmember calculation if found so the recalc flag is set.
func (p *DomainLoadBalance) RemoveConnection(conn *Connection) {
loginfo.Println("RemoveConnection", fmt.Sprintf("%p", conn))
p.mutex.Lock()
defer p.mutex.Unlock()
//scan all the connections
for pos := range p.connections {
loginfo.Println("RemoveConnection", pos, len(p.connections), p.count)
if p.connections[pos] == conn {
//found connection remove it
loginfo.Printf("found connection %p", conn)
p.connections[pos], p.connections = p.connections[len(p.connections)-1], p.connections[:len(p.connections)-1]
p.count--
break
}
}
loginfo.Println("RemoveConnection:", p)
}

View File

@ -1,4 +1,4 @@
package genericlistener
package server
//DomainMapping --
type DomainMapping struct {

View File

@ -0,0 +1,61 @@
package server
//DomainTrack -- Tracking specifics for domains
type DomainTrack struct {
DomainName string
bytesIn int64
bytesOut int64
requests int64
responses int64
}
//NewDomainTrack -- Constructor
func NewDomainTrack(domainName string) (p *DomainTrack) {
p = new(DomainTrack)
p.DomainName = domainName
p.bytesIn = 0
p.bytesOut = 0
p.requests = 0
p.responses = 0
return
}
//BytesIn -- Property
func (c *DomainTrack) BytesIn() int64 {
return c.bytesIn
}
//BytesOut -- Property
func (c *DomainTrack) BytesOut() int64 {
return c.bytesOut
}
//AddIn - Property
func (c *DomainTrack) AddIn(num int64) {
c.bytesIn = c.bytesIn + num
}
//AddOut -- Property
func (c *DomainTrack) AddOut(num int64) {
c.bytesOut = c.bytesOut + num
}
//AddRequests - Property
func (c *DomainTrack) AddRequests() {
c.requests = c.requests + 1
}
//AddResponses - Property
func (c *DomainTrack) AddResponses() {
c.responses = c.responses + 1
}
//Requests -- Property
func (c *DomainTrack) Requests() int64 {
return c.requests
}
//Responses -- Property
func (c *DomainTrack) Responses() int64 {
return c.responses
}

View File

@ -1,26 +1,22 @@
package genericlistener
package server
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"strconv"
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"net/http"
"bufio"
"git.daplie.com/Daplie/go-rvpn-server/rvpn/packer"
)
@ -28,14 +24,18 @@ type contextKey string
//CtxConnectionTrack
const (
ctxSecretKey contextKey = "secretKey"
ctxConnectionTable contextKey = "connectionTable"
ctxConfig contextKey = "config"
ctxListenerRegistration contextKey = "listenerRegistration"
ctxConnectionTrack contextKey = "connectionTrack"
ctxWssHostName contextKey = "wsshostname"
ctxAdminHostName contextKey = "adminHostName"
ctxCancelCheck contextKey = "cancelcheck"
ctxSecretKey contextKey = "secretKey"
ctxServerStatus contextKey = "serverstatus"
//ctxConnectionTable contextKey = "connectionTable"
ctxConfig contextKey = "config"
ctxListenerRegistration contextKey = "listenerRegistration"
ctxConnectionTrack contextKey = "connectionTrack"
ctxWssHostName contextKey = "wsshostname"
ctxAdminHostName contextKey = "adminHostName"
ctxCancelCheck contextKey = "cancelcheck"
ctxLoadbalanceDefaultMethod contextKey = "lbdefaultmethod"
)
const (
@ -271,17 +271,17 @@ func handleStream(ctx context.Context, wConn *WedgeConn) {
//handleExternalHTTPRequest -
// - get a wConn and start processing requests
func handleExternalHTTPRequest(ctx context.Context, extConn *WedgeConn, hostname string, service string) {
connectionTracking := ctx.Value(ctxConnectionTrack).(*Tracking)
func handleExternalHTTPRequest(ctx context.Context, extConn *WedgeConn, hostname, service string) {
//connectionTracking := ctx.Value(ctxConnectionTrack).(*Tracking)
serverStatus := ctx.Value(ctxServerStatus).(*Status)
defer func() {
connectionTracking.unregister <- extConn
serverStatus.ExtConnectionUnregister(extConn)
extConn.Close()
}()
connectionTable := ctx.Value(ctxConnectionTable).(*Table)
//find the connection by domain name
conn, ok := connectionTable.ConnByDomain(hostname)
conn, ok := serverStatus.ConnectionTable.ConnByDomain(hostname)
if !ok {
//matching connection can not be found based on ConnByDomain
loginfo.Println("unable to match ", hostname, " to an existing connection")
@ -290,7 +290,7 @@ func handleExternalHTTPRequest(ctx context.Context, extConn *WedgeConn, hostname
}
track := NewTrack(extConn, hostname)
connectionTracking.register <- track
serverStatus.ExtConnectionRegister(track)
loginfo.Println("Domain Accepted", hostname, extConn.RemoteAddr().String())
@ -323,10 +323,11 @@ func handleExternalHTTPRequest(ctx context.Context, extConn *WedgeConn, hostname
p.Data.AppendBytes(buffer[0:cnt])
buf := p.PackV1()
loginfo.Println(hex.Dump(buf.Bytes()))
//loginfo.Println(hex.Dump(buf.Bytes()))
//Bundle up the send request and dispatch
sendTrack := NewSendTrack(buf.Bytes(), hostname)
conn.SendCh() <- sendTrack
serverStatus.SendExtRequest(conn, sendTrack)
_, err = extConn.Discard(cnt)
if err != nil {
@ -337,71 +338,14 @@ func handleExternalHTTPRequest(ctx context.Context, extConn *WedgeConn, hostname
}
}
//handleAdminClient -
// - expecting an existing oneConnListener with a qualified wss client connected.
// - auth will happen again since we were just peeking at the token.
func handleAdminClient(ctx context.Context, oneConn *oneConnListener) {
connectionTable := ctx.Value(ctxConnectionTable).(*Table)
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
loginfo.Println("HandleFunc /")
switch url := r.URL.Path; url {
case "/":
// check to see if we are using the administrative Host
if strings.Contains(r.Host, "rvpn.daplie.invalid") {
http.Redirect(w, r, "/admin", 301)
}
default:
http.Error(w, "Not Found", 404)
}
})
router.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintln(w, "<html>Welcome..press <a href=/api/servers>Servers</a> to access stats</html>")
})
router.HandleFunc("/api/servers", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("here")
serverContainer := NewServerAPIContainer()
for c := range connectionTable.Connections() {
serverAPI := NewServerAPI(c)
serverContainer.Servers = append(serverContainer.Servers, serverAPI)
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
json.NewEncoder(w).Encode(serverContainer)
})
s := &http.Server{
Addr: ":80",
Handler: router,
}
err := s.Serve(oneConn)
if err != nil {
loginfo.Println("Serve error: ", err)
}
select {
case <-ctx.Done():
loginfo.Println("Cancel signal hit")
return
}
}
//handleWssClient -
// - expecting an existing oneConnListener with a qualified wss client connected.
// - auth will happen again since we were just peeking at the token.
func handleWssClient(ctx context.Context, oneConn *oneConnListener) {
secretKey := ctx.Value(ctxSecretKey).(string)
connectionTable := ctx.Value(ctxConnectionTable).(*Table)
serverStatus := ctx.Value(ctxServerStatus).(*Status)
//connectionTable := ctx.Value(ctxConnectionTable).(*Table)
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@ -426,8 +370,8 @@ func handleWssClient(ctx context.Context, oneConn *oneConnListener) {
domains, ok := claims["domains"].([]interface{})
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
ReadBufferSize: 65535,
WriteBufferSize: 65535,
}
conn, err := upgrader.Upgrade(w, r, nil)
@ -438,11 +382,11 @@ func handleWssClient(ctx context.Context, oneConn *oneConnListener) {
loginfo.Println("before connection table")
//newConnection := connection.NewConnection(connectionTable, conn, r.RemoteAddr, domains)
serverName := domains[0].(string)
newRegistration := NewRegistration(conn, r.RemoteAddr, domains, serverStatus.ConnectionTracking, serverName)
serverStatus.WSSConnectionRegister(newRegistration)
connectionTrack := ctx.Value(ctxConnectionTrack).(*Tracking)
newRegistration := NewRegistration(conn, r.RemoteAddr, domains, connectionTrack)
connectionTable.Register() <- newRegistration
ok = <-newRegistration.CommCh()
if !ok {
loginfo.Println("connection registration failed ", newRegistration)

View File

@ -1,4 +1,4 @@
package genericlistener
package server
import (
"context"
@ -43,8 +43,8 @@ func NewListenerRegistration(port int) (p *ListenerRegistration) {
return
}
//GenericListeners -
type GenericListeners struct {
//servers -
type servers struct {
listeners map[*net.Listener]int
ctx context.Context
connnectionTable *Table
@ -52,38 +52,41 @@ type GenericListeners struct {
secretKey string
certbundle tls.Certificate
register chan *ListenerRegistration
genericListeners *GenericListeners
servers *servers
wssHostName string
adminHostName string
cancelCheck int
lbDefaultMethod string
serverStatus *Status
}
//NewGenerListeners --
func NewGenerListeners(ctx context.Context, connectionTable *Table, connectionTrack *Tracking, secretKey string, certbundle tls.Certificate, wssHostName string, adminHostName string, cancelCheck int) (p *GenericListeners) {
p = new(GenericListeners)
func NewGenerListeners(ctx context.Context, secretKey string, certbundle tls.Certificate, serverStatus *Status) (p *servers) {
p = new(servers)
p.listeners = make(map[*net.Listener]int)
p.ctx = ctx
p.connnectionTable = connectionTable
p.connectionTracking = connectionTrack
p.connnectionTable = serverStatus.ConnectionTable
p.connectionTracking = serverStatus.ConnectionTracking
p.secretKey = secretKey
p.certbundle = certbundle
p.register = make(chan *ListenerRegistration)
p.wssHostName = wssHostName
p.adminHostName = adminHostName
p.cancelCheck = cancelCheck
p.wssHostName = serverStatus.WssDomain
p.adminHostName = serverStatus.AdminDomain
p.cancelCheck = serverStatus.DeadTime.cancelcheck
p.lbDefaultMethod = serverStatus.LoadbalanceDefaultMethod
p.serverStatus = serverStatus
return
}
//Run -- Execute
// - execute the GenericLister
// - pass initial port, we'll announce that
func (gl *GenericListeners) Run(ctx context.Context, initialPort int) {
func (gl *servers) Run(ctx context.Context, initialPort int) {
loginfo.Println("ConnectionTable starting")
config := &tls.Config{Certificates: []tls.Certificate{gl.certbundle}}
ctx = context.WithValue(ctx, ctxSecretKey, gl.secretKey)
ctx = context.WithValue(ctx, ctxConnectionTable, gl.connnectionTable)
loginfo.Println(gl.connectionTracking)
@ -93,6 +96,8 @@ func (gl *GenericListeners) Run(ctx context.Context, initialPort int) {
ctx = context.WithValue(ctx, ctxWssHostName, gl.wssHostName)
ctx = context.WithValue(ctx, ctxAdminHostName, gl.adminHostName)
ctx = context.WithValue(ctx, ctxCancelCheck, gl.cancelCheck)
ctx = context.WithValue(ctx, ctxLoadbalanceDefaultMethod, gl.lbDefaultMethod)
ctx = context.WithValue(ctx, ctxServerStatus, gl.serverStatus)
go func(ctx context.Context) {
for {

View File

@ -1,4 +1,4 @@
package genericlistener
package server
import (
"io"
@ -9,18 +9,16 @@ type oneConnListener struct {
conn net.Conn
}
func (l *oneConnListener) Accept() (c net.Conn, err error) {
c = l.conn
if c == nil {
err = io.EOF
loginfo.Println("Accept")
return
func (l *oneConnListener) Accept() (net.Conn, error) {
if l.conn == nil {
loginfo.Println("Accept EOF")
return nil, io.EOF
}
err = nil
c := l.conn
l.conn = nil
loginfo.Println("Accept", c.LocalAddr().String(), c.RemoteAddr().String())
return
return c, nil
}
func (l *oneConnListener) Close() error {

View File

@ -1,4 +1,4 @@
package genericlistener
package server
//SendTrack -- Used as a channel communication to id domain asssociated to domain for outbound WSS
type SendTrack struct {

29
rvpn/server/setup.go Normal file
View File

@ -0,0 +1,29 @@
package server
import (
"io"
"log"
"os"
)
var (
//Logoutput -- passing the output writer from main
loginfo *log.Logger
logdebug *log.Logger
logFlags = log.Ldate | log.Lmicroseconds | log.Lshortfile
connectionID int64
)
func init() {
loginfo = log.New(os.Stdout, "INFO: server: ", logFlags)
logdebug = log.New(os.Stdout, "DEBUG: server:", logFlags)
connectionID = 0
}
//InitLogging -- after main sets up output, it will init all packages InitLogging
//I am sure I am doing this wrong, but I could not find a way to have package level
//logging with the flags I wanted and the ability to run lumberjack file management
func InitLogging(logoutput io.Writer) {
loginfo.SetOutput(logoutput)
}

78
rvpn/server/status.go Normal file
View File

@ -0,0 +1,78 @@
package server
import (
"context"
"time"
)
//Status --
type Status struct {
ctx context.Context
Name string
StartTime time.Time
WssDomain string
AdminDomain string
DeadTime *StatusDeadTime
ConnectionTracking *Tracking
ConnectionTable *Table
servers *servers
LoadbalanceDefaultMethod string
AdminStats *TrafficStats
AdminReqTyoe *AdminReqType
TrafficStats *TrafficStats
ExtConnections *ConnectionStats
WSSConnections *ConnectionStats
}
//NewStatus --
func NewStatus(ctx context.Context) (p *Status) {
p = new(Status)
p.ctx = ctx
p.AdminStats = new(TrafficStats)
p.TrafficStats = new(TrafficStats)
p.ExtConnections = new(ConnectionStats)
p.WSSConnections = new(ConnectionStats)
return
}
// South Facing Functions
//WSSConnectionRegister --
func (p *Status) WSSConnectionRegister(newRegistration *Registration) {
p.ConnectionTable.Register() <- newRegistration
p.WSSConnections.IncConnections()
}
//WSSConnectionUnregister --
//unregisters a south facing connection
//intercept and update global statistics
func (p *Status) WSSConnectionUnregister() {
}
// External Facing Functions
//ExtConnectionRegister --
//registers an ext facing connection
//intercept and update global statistics
func (p *Status) ExtConnectionRegister(newTrack *Track) {
p.ConnectionTracking.register <- newTrack
p.ExtConnections.IncConnections()
}
//ExtConnectionUnregister --
//unregisters an ext facing connection
//intercept and update global statistics
func (p *Status) ExtConnectionUnregister(extConn *WedgeConn) {
p.ConnectionTracking.unregister <- extConn
p.ExtConnections.DecConnections()
}
//SendExtRequest --
//sends a request to a south facing connection
//intercept the send, update our global stats
func (p *Status) SendExtRequest(conn *Connection, sendTrack *SendTrack) {
p.TrafficStats.IncRequests()
p.TrafficStats.AddBytesOut(int64(len(sendTrack.data)))
conn.SendCh() <- sendTrack
}

View File

@ -0,0 +1,17 @@
package server
//StatusDeadTime -- structure for deadtime configuration
type StatusDeadTime struct {
dwell int
idle int
cancelcheck int
}
//NewStatusDeadTime -- constructor
func NewStatusDeadTime(dwell, idle, cancelcheck int) (p *StatusDeadTime) {
p = new(StatusDeadTime)
p.dwell = dwell
p.idle = idle
p.cancelcheck = cancelcheck
return
}

View File

@ -0,0 +1,52 @@
package server
import "sync"
type adminReqType string
const (
adminStatus adminReqType = "admin_status"
adminDomain adminReqType = "admin_domain"
adminDomains adminReqType = "admin_domains"
adminServer adminReqType = "admin_server"
adminServers adminReqType = "admin_servers"
)
//AdminReqType --
type AdminReqType struct {
mutex *sync.Mutex
RequestType map[adminReqType]int64
}
//NewAdminReqType -- Constructor
func NewAdminReqType() (p *AdminReqType) {
p = new(AdminReqType)
p.mutex = &sync.Mutex{}
p.RequestType = make(map[adminReqType]int64)
return
}
func (p *AdminReqType) add(reqType adminReqType) {
p.mutex.Lock()
defer p.mutex.Unlock()
if _, ok := p.RequestType[reqType]; ok {
p.RequestType[reqType]++
} else {
p.RequestType[reqType] = int64(1)
}
}
func (p *AdminReqType) get(reqType adminReqType) (total int64) {
p.mutex.Lock()
defer p.mutex.Unlock()
if _, ok := p.RequestType[reqType]; ok {
total = p.RequestType[reqType]
} else {
total = 0
}
return
}

View File

@ -0,0 +1,28 @@
package server
//ConnectionStats --
type ConnectionStats struct {
Connections int64
TotalConnections int64
}
//NewConnectionStats -- Consttuctor
func NewConnectionStats() (p *ConnectionStats) {
p = new(ConnectionStats)
p.Connections = 0
p.TotalConnections = 0
return
}
//IncConnections --
func (p *ConnectionStats) IncConnections() {
p.Connections++
p.TotalConnections++
}
//DecConnections --
func (p *ConnectionStats) DecConnections() {
if p.Connections > 0 {
p.Connections--
}
}

View File

@ -0,0 +1,40 @@
package server
//TrafficStats --
type TrafficStats struct {
Requests int64
Responses int64
BytesIn int64
BytesOut int64
}
//NewTrafficStats -- Consttuctor
func NewTrafficStats() (p *TrafficStats) {
p = new(TrafficStats)
p.Requests = 0
p.Responses = 0
p.BytesIn = 0
p.BytesOut = 0
return
}
//IncRequests --
func (p *TrafficStats) IncRequests() {
p.Requests++
}
//IncResponses --
func (p *TrafficStats) IncResponses() {
p.Responses++
}
//AddBytesIn --
func (p *TrafficStats) AddBytesIn(count int64) {
p.BytesIn = p.BytesIn + count
}
//AddBytesOut --
func (p *TrafficStats) AddBytesOut(count int64) {
p.BytesOut = p.BytesOut + count
}

View File

@ -1,6 +1,8 @@
package genericlistener
package server
import "errors"
import (
"errors"
)
func getHello(b []byte) (string, error) {
rest := b[5:]

View File

@ -1,17 +0,0 @@
package xlate
import (
"log"
"os"
)
var (
loginfo *log.Logger
logdebug *log.Logger
logFlags = log.Ldate | log.Lmicroseconds | log.Lshortfile
)
func init() {
loginfo = log.New(os.Stdout, "INFO: xlate: ", logFlags)
logdebug = log.New(os.Stdout, "DEBUG: xlate:", logFlags)
}