Initial commit

This commit is contained in:
Tyler 2018-07-31 21:22:38 -04:00
commit c337be1ce0
18 changed files with 14441 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
joker-server

83
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,83 @@
stages:
- build
- build-docker
build-static:
image: mhart/alpine-node:latest
script:
- npm install -g @vue/cli
build-windows-amd64:
image: golang:alpine
stage: build
script:
- apk --no-cache add git
- go get -d
- GOOS=windows go build -o joker.exe
artifacts:
paths:
- joker.exe
build-linux-amd64:
image: golang:alpine
stage: build
script:
- apk --no-cache add git
- go get -d
- GOOS=linux go build -o joker
artifacts:
paths:
- joker
build-linux-arm:
image: golang:alpine
stage: build
script:
- apk --no-cache add git
- go get -d
- GOARCH=arm GOOS=linux go build -o joker-arm
artifacts:
paths:
- joker-arm
build-linux-arm64:
image: golang:alpine
stage: build
script:
- apk --no-cache add git
- go get -d
- GOARCH=arm64 GOOS=linux go build -o joker-arm64
artifacts:
paths:
- joker-arm64
build-docker-amd64:
image: docker:latest
stage: build-docker
script:
- apk --no-cache add git
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker image build -t $CI_REGISTRY_IMAGE:amd64-latest -f docker/Dockerfile .
- docker push $CI_REGISTRY_IMAGE:amd64-latest
build-docker-arm:
image: docker:latest
stage: build-docker
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- mkdir tmp
- wget -O tmp/qemu-arm-static https://github.com/multiarch/qemu-user-static/releases/download/v2.12.0/qemu-arm-static
- chmod +x tmp/qemu-arm-static
- docker image build -t $CI_REGISTRY_IMAGE:arm-latest -f docker/Dockerfile.arm .
- docker push $CI_REGISTRY_IMAGE:arm-latest
build-docker-arm64:
image: docker:latest
stage: build-docker
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- mkdir tmp
- wget -O tmp/qemu-aarch64-static https://github.com/multiarch/qemu-user-static/releases/download/v2.12.0/qemu-aarch64-static
- chmod +x tmp/qemu-aarch64-static
- docker image build -t $CI_REGISTRY_IMAGE:arm64-latest -f docker/Dockerfile.arm64 .
- docker push $CI_REGISTRY_IMAGE:arm64-latest

12
docker/Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM alpine:3.7
MAINTAINER Tyler Stuyfzand <tyler@tystuyfzand.com>
EXPOSE 8080
RUN apk --no-cache add tini
ENTRYPOINT ["/sbin/tini", "-g", "--"]
CMD ["joker"]
COPY joker /usr/local/bin/joker
RUN chmod +x /usr/local/bin/joker

14
docker/Dockerfile.arm Normal file
View File

@ -0,0 +1,14 @@
FROM arm32v6/alpine
COPY tmp/qemu-arm-static /usr/bin/qemu-arm-static
MAINTAINER Tyler Stuyfzand <tyler@tystuyfzand.com>
EXPOSE 8080
RUN apk --no-cache add tini
ENTRYPOINT ["/sbin/tini", "-g", "--"]
CMD ["joker"]
COPY joker-arm /usr/local/bin/joker
RUN chmod +x /usr/local/bin/joker

14
docker/Dockerfile.arm64 Normal file
View File

@ -0,0 +1,14 @@
FROM arm64v8/alpine
COPY tmp/qemu-aarch64-static /usr/bin/qemu-aarch64-static
MAINTAINER Tyler Stuyfzand <tyler@tystuyfzand.com>
EXPOSE 8080
RUN apk --no-cache add tini
ENTRYPOINT ["/sbin/tini", "-g", "--"]
CMD ["joker"]
COPY joker-arm64 /usr/local/bin/joker
RUN chmod +x /usr/local/bin/joker

21
joker/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

5
joker/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

13820
joker/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
joker/package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "joker",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.0",
"@fortawesome/free-solid-svg-icons": "^5.1.0",
"@fortawesome/vue-fontawesome": "^0.1.0",
"axios": "^0.18.0",
"bootstrap-vue": "^2.0.0-rc.11",
"jquery": "^3.3.1",
"vue": "^2.5.16",
"vue-router": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.0.0-beta.15",
"@vue/cli-plugin-eslint": "^3.0.0-beta.15",
"@vue/cli-service": "^3.0.0-beta.15",
"vue-template-compiler": "^2.5.16"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

BIN
joker/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

16
joker/public/index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Joker - Another web console for godns</title>
</head>
<body>
<noscript>
<strong>We're sorry but Joker doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

34
joker/src/App.vue Normal file
View File

@ -0,0 +1,34 @@
<template>
<div id="app">
<Header />
<div class="container">
<router-view :key="$route.fullPath"></router-view>
</div>
<Footer :info="info" />
</div>
</template>
<script>
import Header from './components/Header.vue'
import Footer from './components/Footer.vue'
import axios from 'axios';
let data = {
info: {}
};
axios.get('/info')
.then((response) => {
data.info = response.data;
});
export default {
name: 'app',
components: {
Header, Footer
},
data: function() {
return data;
}
}
</script>

View File

@ -0,0 +1,9 @@
<template>
</template>
<script>
export default {
name: "AddModal",
}
</script>

View File

@ -0,0 +1,120 @@
<template>
<div class="row">
<div class="col-12">
<h3 v-if="this.$route.params.domain">{{ this.$route.params.domain }}</h3>
<p>
<span v-if="this.$route.params.domain">
<button @click.stop="$router.go(-1)" class="btn btn-info">Back</button>
&nbsp;
</span>
<button class="btn btn-danger">Add</button>
</p>
</div>
<div class="col-12">
<b-table id="records" striped bordered hover fixed :items="items" :fields="fields" v-on:row-clicked="rowClicked" v-on:row-dblclicked="edit" head-variant="dark" sort-by="domain">
<template slot="ip" slot-scope="row">
<span v-if="row.item.editing">
<input ref="ip" :value="row.item.ip"/>
</span>
<span v-else>
{{ row.item.ip }}
</span>
</template>
<template slot="actions" slot-scope="row">
<span v-if="row.item.editing">
<b-btn size="sm" variant="success" @click.stop="save(row.item)"><fa icon="save"/> Save</b-btn>&nbsp;<b-btn size="sm" variant="danger" @click.stop="cancel(row.item)"><fa icon="times"/> Cancel</b-btn>
</span>
<span v-else>
<b-btn size="sm" variant="success" @click.stop="edit(row.item)"><fa icon="edit"/> Edit</b-btn>&nbsp;<b-btn size="sm" variant="danger" @click.stop="remove(row.item)"><fa icon="trash"/> Delete</b-btn>
</span>
</template>
</b-table>
</div>
</div>
</template>
<script>
import axios from 'axios';
let data = {
domain: null,
items: [],
fields: [
{key: "domain", label: 'Domain', sortable: true},
{key: 'ip', label: 'IP', sortable: true},
{key: 'actions', label: 'Actions'}
]
};
export default {
name: 'DNS',
props: ['base'],
methods: {
domainProvider: function () {
},
rowClicked: function (item) {
if (item.itemCount > 1) {
this.$router.push({name: 'dns', params: {domain: item.domain}});
}
},
edit: function (item) {
// Nothing
if (item.ip === '-') {
this.$router.push({name: 'dns', params: {domain: item.domain}});
return;
}
item.editing = true;
},
cancel: function (item) {
item.editing = false;
},
save: function (item) {
item.ip = this.$refs.ip.value;
axios.post('/update', {
domain: item.domain,
ip: item.ip
}).then(() => {
item.editing = false;
});
},
remove: function(item) {
let term = 'record';
if (item.itemCount > 1) {
term = 'group';
}
if (!confirm('Are you sure you wish to remove this ' + term + '?')) {
return;
}
axios.post('/remove', {
domain: item.domain,
group: item.itemCount > 1
}).then(() => {
for (var i = 0; i < data.items.length; i++) {
if (data.items[i].domain == item.domain) {
data.items.splice(i, 1);
break;
}
}
if (data.domain && data.items.length == 0) {
this.$router.push({name: 'dns', params: {}});
}
});
}
},
data() {
return data;
}
}
</script>
<style>
.table {
font-size: large;
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<footer class="text-muted">
<div class="container">
<p class="float-right" v-if="info">
Server: <b-badge>{{ info.redis }}</b-badge>
</p>
<p>&copy; 2018 Meow.tf</p>
</div>
</footer>
</template>
<script>
export default {
name: 'Footer',
props: [ 'info' ]
}
</script>

View File

@ -0,0 +1,15 @@
<template>
<div class="row">
<div class="col">
<div class="text-center">
<h1>Joker - Yet another web console for godns</h1>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Header'
}
</script>

36
joker/src/main.js Normal file
View File

@ -0,0 +1,36 @@
import Vue from 'vue'
import Router from 'vue-router'
import App from './App.vue'
import DNS from './components/DNS.vue';
Vue.use(Router);
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
library.add(fas);
Vue.component('fa', FontAwesomeIcon);
Vue.use(BootstrapVue);
Vue.config.productionTip = false;
const router = new Router({
mode: 'hash',
base: __dirname,
routes: [
{ path : '/', name : 'home', component : DNS },
{ path : '/domain/:domain', name : 'dns', component : DNS }
]
});
new Vue({
router,
render: h => h(App)
}).$mount('#app');

174
main.go Normal file
View File

@ -0,0 +1,174 @@
package main
import (
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/hoisie/redis"
"encoding/json"
"github.com/weppos/publicsuffix-go/publicsuffix"
"strings"
"github.com/asaskevich/govalidator"
"flag"
"os"
)
var c *redis.Client
type infoResponse struct {
RedisServer string `json:"redis"`
}
var (
flagKey = flag.String("key", "godns:hosts", "Redis key for hash set")
flagServer = flag.String("server", "192.168.1.9:6379", "redis host")
filePath = flag.String("filepath", "/var/lib/joker/dist", "file path")
)
var (
key string
server string
)
func main() {
flag.Parse()
if key = os.Getenv("REDIS_KEY"); key == "" {
key = *flagKey
}
if server = os.Getenv("REDIS_SERVER"); server == "" {
server = *flagServer
}
c = &redis.Client{
Addr: server,
}
router := httprouter.New()
fs := http.FileServer(http.Dir(*filePath))
serveFiles := func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fs.ServeHTTP(w, r)
}
router.GET("/", serveFiles)
router.GET("/js/*filepath", serveFiles)
router.GET("/css/*filepath", serveFiles)
router.GET("/info", getInfo)
router.GET("/records", getRecords)
router.GET("/records/:zone", getRecords)
router.POST("/update", updateRecord)
router.POST("/remove", removeRecord)
http.ListenAndServe(":8080", router)
}
func getInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
json.NewEncoder(w).Encode(&infoResponse{
RedisServer: server,
})
}
func getRecords(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
zone := p.ByName("zone")
hosts := make(map[string]string)
if err := c.Hgetall(key, hosts); err != nil {
return
}
groups := make(map[string]map[string]string)
for host, addr := range hosts {
domain, err := publicsuffix.Domain(host)
if err != nil {
domain = host
}
if zone != "" && !strings.HasSuffix(host, zone) {
continue
}
if group, ok := groups[domain]; ok {
group[host] = addr
} else {
groups[domain] = map[string]string{host:addr}
}
}
json.NewEncoder(w).Encode(groups)
}
type domainRequest struct {
Domain string `json:"domain"`
IP string `json:"ip"`
Group bool `json:"group"`
}
func updateRecord(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var req domainRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return
}
req.Domain = strings.ToLower(req.Domain)
if !govalidator.IsDNSName(req.Domain) || !govalidator.IsIP(req.IP) {
w.WriteHeader(http.StatusBadRequest)
return
}
if _, err := c.Hset(key, req.Domain, []byte(req.IP)); err != nil {
return
}
c.Publish("godns:update_record", []byte(strings.ToLower(req.Domain)))
}
func removeRecord(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var req domainRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return
}
req.Domain = strings.ToLower(req.Domain)
if !govalidator.IsDNSName(strings.Replace(req.Domain, "*", "a", -1)) {
w.WriteHeader(http.StatusBadRequest)
return
}
if req.Group {
hosts := make(map[string]string)
if err := c.Hgetall(key, hosts); err != nil {
return
}
for host, _ := range hosts {
host = strings.ToLower(host)
if !strings.HasSuffix(host, req.Domain) {
continue
}
c.Hdel(key, host)
c.Publish("godns:remove_record", []byte(host))
}
} else {
if _, err := c.Hdel(key, req.Domain); err != nil {
return
}
c.Publish("godns:remove_record", []byte(strings.ToLower(req.Domain)))
}
}