Initial commit
This commit is contained in:
commit
c337be1ce0
|
@ -0,0 +1 @@
|
|||
joker-server
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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*
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AddModal",
|
||||
}
|
||||
</script>
|
|
@ -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>
|
||||
|
||||
</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> <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> <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>
|
|
@ -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>© 2018 Meow.tf</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Footer',
|
||||
props: [ 'info' ]
|
||||
}
|
||||
</script>
|
|
@ -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>
|
|
@ -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');
|
|
@ -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)))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue