Merge pull request #94 from creekorful/93-create-process-pkg

Create process package
pull/95/head
Aloïs Micard 3 years ago committed by GitHub
commit c21783d436
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,11 +2,12 @@ package main
import (
"github.com/creekorful/trandoshan/internal/api"
"github.com/creekorful/trandoshan/internal/process"
"os"
)
func main() {
app := api.GetApp()
app := process.MakeApp(&api.State{})
if err := app.Run(os.Args); err != nil {
os.Exit(1)
}

@ -2,11 +2,12 @@ package main
import (
"github.com/creekorful/trandoshan/internal/archiver"
"github.com/creekorful/trandoshan/internal/process"
"os"
)
func main() {
app := archiver.GetApp()
app := process.MakeApp(&archiver.State{})
if err := app.Run(os.Args); err != nil {
os.Exit(1)
}

@ -2,11 +2,12 @@ package main
import (
"github.com/creekorful/trandoshan/internal/configapi"
"github.com/creekorful/trandoshan/internal/process"
"os"
)
func main() {
app := configapi.GetApp()
app := process.MakeApp(&configapi.State{})
if err := app.Run(os.Args); err != nil {
os.Exit(1)
}

@ -2,11 +2,12 @@ package main
import (
"github.com/creekorful/trandoshan/internal/crawler"
"github.com/creekorful/trandoshan/internal/process"
"os"
)
func main() {
app := crawler.GetApp()
app := process.MakeApp(&crawler.State{})
if err := app.Run(os.Args); err != nil {
os.Exit(1)
}

@ -2,11 +2,12 @@ package main
import (
"github.com/creekorful/trandoshan/internal/extractor"
"github.com/creekorful/trandoshan/internal/process"
"os"
)
func main() {
app := extractor.GetApp()
app := process.MakeApp(&extractor.State{})
if err := app.Run(os.Args); err != nil {
os.Exit(1)
}

@ -1,12 +1,13 @@
package main
import (
"github.com/creekorful/trandoshan/internal/process"
"github.com/creekorful/trandoshan/internal/scheduler"
"os"
)
func main() {
app := scheduler.GetApp()
app := process.MakeApp(&scheduler.State{})
if err := app.Run(os.Args); err != nil {
os.Exit(1)
}

@ -80,10 +80,12 @@ services:
--hub-uri amqp://guest:guest@rabbitmq:5672
--elasticsearch-uri http://elasticsearch:9200
--signing-key K==M5RsU_DQa4_XSbkX?L27s^xWmde25
--config-api-uri http://configapi:8080
restart: always
depends_on:
- rabbitmq
- elasticsearch
- configapi
ports:
- 15005:8080
configapi:
@ -93,7 +95,7 @@ services:
--hub-uri amqp://guest:guest@rabbitmq:5672
--db-uri redis:6379
--default-value forbidden-hostnames="[{\"hostname\": \"facebookcorewwwi.onion\"}]"
--default-value forbidden-mime-types="[{\"content-type\":\"image/png\",\"extensions\":[\"png\"]},{\"content-type\":\"image/gif\",\"extensions\":[\"gif\"]},{\"content-type\":\"image/jpeg\",\"extensions\":[\"jpg\",\"jpeg\"]},{\"content-type\":\"image/bmp\",\"extensions\":[\"bmp\"]},{\"content-type\":\"text/css\",\"extensions\":[\"css\"]},{\"content-type\":\"application/javascript\",\"extensions\":[\"js\"]}]"
--default-value forbidden-mime-types="[{\"content-type\":\"image/png\",\"extensions\":[\"png\"]},{\"content-type\":\"image/gif\",\"extensions\":[\"gif\"]},{\"content-type\":\"image/jpeg\",\"extensions\":[\"jpg\",\"jpeg\"]},{\"content-type\":\"image/bmp\",\"extensions\":[\"bmp\"]},{\"content-type\":\"image/svg+xml\",\"extensions\":[\"svg\"]},{\"content-type\":\"text/css\",\"extensions\":[\"css\"]},{\"content-type\":\"application/javascript\",\"extensions\":[\"js\"]}]"
--default-value allowed-mime-types="[{\"content-type\":\"text/\",\"extensions\":[]}]"
--default-value refresh-delay="{\"delay\": -1}"
restart: always

@ -11,7 +11,6 @@ require (
github.com/go-resty/resty/v2 v2.3.0
github.com/golang/mock v1.4.4
github.com/gorilla/mux v1.8.0
github.com/labstack/echo/v4 v4.1.16
github.com/olekukonko/tablewriter v0.0.4
github.com/olivere/elastic/v7 v7.0.20
github.com/rs/zerolog v1.20.0

@ -24,8 +24,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis/v8 v8.4.4 h1:fGqgxCTR1sydaKI00oQf3OmkU/DIe/I/fYXvGklCIuc=
github.com/go-redis/redis/v8 v8.4.4/go.mod h1:nA0bQuF0i5JFx4Ta9RZxGKXFrQ8cRWntra97f0196iY=
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
@ -45,12 +45,14 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
@ -62,21 +64,11 @@ github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2K
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o=
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
@ -84,9 +76,11 @@ github.com/olivere/elastic/v7 v7.0.20 h1:5FFpGPVJlBSlWBOdict406Y3yNTIpVpAiUvdFZe
github.com/olivere/elastic/v7 v7.0.20/go.mod h1:Kh7iIsXIBl5qRQOBFoylCsXVTtye3keQU2Y/YbR7HD8=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U=
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -112,6 +106,7 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
@ -119,9 +114,6 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.9.0 h1:hNpmUdy/+ZXYpGy0OBfm7K0UQTzb73W0T0U4iJIVrMw=
github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xhit/go-str2duration/v2 v2.0.0 h1:uFtk6FWB375bP7ewQl+/1wBcn840GPhnySOdcz/okPE=
github.com/xhit/go-str2duration/v2 v2.0.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
@ -129,9 +121,6 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v0.15.0 h1:CZFy2lPhxd4HlhZnYK8gRyDotksO3Ip9rBweY1vVYJw=
go.opentelemetry.io/otel v0.15.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -149,8 +138,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@ -164,16 +151,11 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -206,15 +188,19 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mvdan.cc/xurls/v2 v2.1.0 h1:KaMb5GLhlcSX+e+qhbRJODnUUBvlw01jt4yrjFIHAuA=

@ -1,83 +1,332 @@
package api
import (
"encoding/base64"
"encoding/json"
"github.com/creekorful/trandoshan/api"
"github.com/creekorful/trandoshan/internal/api/auth"
"github.com/creekorful/trandoshan/internal/api/rest"
"github.com/creekorful/trandoshan/internal/api/service"
"github.com/creekorful/trandoshan/internal/logging"
"github.com/creekorful/trandoshan/internal/util"
"github.com/labstack/echo/v4"
"github.com/creekorful/trandoshan/internal/api/database"
configapi "github.com/creekorful/trandoshan/internal/configapi/client"
"github.com/creekorful/trandoshan/internal/event"
"github.com/creekorful/trandoshan/internal/process"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"net/http"
"strconv"
"strings"
"time"
)
// GetApp return the api app
func GetApp() *cli.App {
return &cli.App{
Name: "tdsh-api",
Version: "0.7.0",
Usage: "Trandoshan API component",
Flags: []cli.Flag{
logging.GetLogFlag(),
util.GetHubURI(),
&cli.StringFlag{
Name: "elasticsearch-uri",
Usage: "URI to the Elasticsearch server",
Required: true,
},
&cli.StringFlag{
Name: "signing-key",
Usage: "Signing key for the JWT token",
Required: true,
},
&cli.StringSliceFlag{
Name: "users",
Usage: "List of API users. (Format user:password)",
Required: false,
},
&cli.StringFlag{
Name: "refresh-delay",
Usage: "Duration before allowing indexation of existing resource (none = never)",
},
},
Action: execute,
}
var (
defaultPaginationSize = 50
maxPaginationSize = 100
)
// State represent the application state
type State struct {
db database.Database
pub event.Publisher
configClient configapi.Client
}
// Name return the process name
func (state *State) Name() string {
return "api"
}
func execute(c *cli.Context) error {
logging.ConfigureLogger(c)
// CommonFlags return process common flags
func (state *State) CommonFlags() []string {
return []string{process.HubURIFlag, process.ConfigAPIURIFlag}
}
e := echo.New()
e.HTTPErrorHandler = func(err error, c echo.Context) {
log.Err(err).Msg("error while processing API call")
e.DefaultHTTPErrorHandler(err, c)
// CustomFlags return process custom flags
func (state *State) CustomFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "elasticsearch-uri",
Usage: "URI to the Elasticsearch server",
Required: true,
},
&cli.StringFlag{
Name: "signing-key",
Usage: "Signing key for the JWT token",
Required: true,
},
&cli.StringSliceFlag{
Name: "users",
Usage: "List of API users. (Format user:password)",
Required: false,
},
}
e.HideBanner = true
}
log.Info().Str("ver", c.App.Version).
Str("elasticsearch-uri", c.String("elasticsearch-uri")).
Str("hub-uri", c.String("hub-uri")).
Msg("Starting tdsh-api")
// Initialize the process
func (state *State) Initialize(provider process.Provider) error {
db, err := database.NewElasticDB(provider.GetValue("elasticsearch-uri"))
if err != nil {
return err
}
state.db = db
signingKey := []byte(c.String("signing-key"))
pub, err := provider.Subscriber()
if err != nil {
return err
}
state.pub = pub
// Create the service
svc, err := service.New(c)
configClient, err := provider.ConfigClient([]string{configapi.RefreshDelayKey})
if err != nil {
log.Err(err).Msg("error while creating API service")
return err
}
state.configClient = configClient
return nil
}
// Subscribers return the process subscribers
func (state *State) Subscribers() []process.SubscriberDef {
return []process.SubscriberDef{}
}
// Setup middlewares
// HTTPHandler returns the HTTP API the process expose
func (state *State) HTTPHandler(provider process.Provider) http.Handler {
r := mux.NewRouter()
signingKey := []byte(provider.GetValue("signing-key"))
authMiddleware := auth.NewMiddleware(signingKey)
e.Use(authMiddleware.Middleware())
r.Use(authMiddleware.Middleware())
r.HandleFunc("/v1/resources", state.searchResources).Methods(http.MethodGet)
r.HandleFunc("/v1/resources", state.addResource).Methods(http.MethodPost)
r.HandleFunc("/v1/urls", state.scheduleURL).Methods(http.MethodPost)
return r
}
func (state *State) searchResources(w http.ResponseWriter, r *http.Request) {
searchParams, err := getSearchParams(r)
if err != nil {
log.Err(err).Msg("error while getting search params")
w.WriteHeader(http.StatusBadRequest)
return
}
totalCount, err := state.db.CountResources(searchParams)
if err != nil {
log.Err(err).Msg("error while counting on ES")
w.WriteHeader(http.StatusInternalServerError)
return
}
res, err := state.db.SearchResources(searchParams)
if err != nil {
log.Err(err).Msg("error while searching on ES")
w.WriteHeader(http.StatusInternalServerError)
return
}
var resources []api.ResourceDto
for _, r := range res {
resources = append(resources, api.ResourceDto{
URL: r.URL,
Body: r.Body,
Title: r.Title,
Time: r.Time,
})
}
// Add endpoints
e.GET("/v1/resources", rest.SearchResources(svc))
e.POST("/v1/resources", rest.AddResource(svc))
e.POST("/v1/urls", rest.ScheduleURL(svc))
// Write pagination headers
writePagination(w, searchParams, totalCount)
log.Info().Msg("Successfully initialized tdsh-api. Waiting for requests")
// Write body
writeJSON(w, resources)
}
func (state *State) addResource(w http.ResponseWriter, r *http.Request) {
var res api.ResourceDto
if err := json.NewDecoder(r.Body).Decode(&res); err != nil {
log.Warn().Str("err", err.Error()).Msg("error while decoding request body")
w.WriteHeader(http.StatusUnprocessableEntity)
return
}
// Hacky stuff to prevent from adding 'duplicate resource'
// the thing is: even with the scheduler preventing from crawling 'duplicates' URL by adding a refresh period
// and checking if the resource is not already indexed, this implementation may not work if the URLs was published
// before the resource is saved. And this happen a LOT of time.
// therefore the best thing to do is to make the API check if the resource should **really** be added by checking if
// it isn't present on the database. This may sounds hacky, but it's the best solution i've come up at this time.
endDate := time.Time{}
if refreshDelay, err := state.configClient.GetRefreshDelay(); err == nil {
if refreshDelay.Delay != -1 {
endDate = time.Now().Add(-refreshDelay.Delay)
}
}
count, err := state.db.CountResources(&api.ResSearchParams{
URL: res.URL,
EndDate: endDate,
PageSize: 1,
PageNumber: 1,
})
if err != nil {
log.Err(err).Msg("error while searching for resource")
w.WriteHeader(http.StatusInternalServerError)
return
}
if count > 0 {
// Not an error
log.Debug().Str("url", res.URL).Msg("Skipping duplicate resource")
w.WriteHeader(http.StatusOK)
return
}
// Create Elasticsearch document
doc := database.ResourceIdx{
URL: res.URL,
Body: res.Body,
Time: res.Time,
Title: res.Title,
Meta: res.Meta,
Description: res.Description,
Headers: res.Headers,
}
if err := state.db.AddResource(doc); err != nil {
log.Err(err).Msg("Error while adding resource")
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Info().Str("url", res.URL).Msg("Successfully saved resource")
writeJSON(w, res)
}
func (state *State) scheduleURL(w http.ResponseWriter, r *http.Request) {
var url string
if err := json.NewDecoder(r.Body).Decode(&url); err != nil {
log.Warn().Str("err", err.Error()).Msg("error while decoding request body")
w.WriteHeader(http.StatusUnprocessableEntity)
return
}
if err := state.pub.PublishEvent(&event.FoundURLEvent{URL: url}); err != nil {
log.Err(err).Msg("unable to schedule URL")
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Info().Str("url", url).Msg("successfully scheduled URL")
}
func getSearchParams(r *http.Request) (*api.ResSearchParams, error) {
params := &api.ResSearchParams{}
if param := r.URL.Query()["keyword"]; len(param) == 1 {
params.Keyword = param[0]
}
if param := r.URL.Query()["with-body"]; len(param) == 1 {
params.WithBody = param[0] == "true"
}
// extract raw query params (unescaped to keep + sign when parsing date)
rawQueryParams := getRawQueryParam(r.URL.RawQuery)
if val, exist := rawQueryParams["start-date"]; exist {
d, err := time.Parse(time.RFC3339, val)
if err == nil {
params.StartDate = d
} else {
return nil, err
}
}
if val, exist := rawQueryParams["end-date"]; exist {
d, err := time.Parse(time.RFC3339, val)
if err == nil {
params.EndDate = d
} else {
return nil, err
}
}
// base64decode the URL
if param := r.URL.Query()["url"]; len(param) == 1 {
b, err := base64.URLEncoding.DecodeString(param[0])
if err != nil {
return nil, err
}
params.URL = string(b)
}
// Acquire pagination
page, size := getPagination(r)
params.PageNumber = page
params.PageSize = size
return params, nil
}
func writePagination(w http.ResponseWriter, searchParams *api.ResSearchParams, total int64) {
w.Header().Set(api.PaginationPageHeader, strconv.Itoa(searchParams.PageNumber))
w.Header().Set(api.PaginationSizeHeader, strconv.Itoa(searchParams.PageSize))
w.Header().Set(api.PaginationCountHeader, strconv.FormatInt(total, 10))
}
func getPagination(r *http.Request) (page int, size int) {
page = 1
size = defaultPaginationSize
// Get pagination page
if param := r.URL.Query()[api.PaginationPageQueryParam]; len(param) == 1 {
if val, err := strconv.Atoi(param[0]); err == nil {
page = val
}
}
// Get pagination size
if param := r.URL.Query()[api.PaginationSizeQueryParam]; len(param) == 1 {
if val, err := strconv.Atoi(param[0]); err == nil {
size = val
}
}
// Prevent too much results from being returned
if size > maxPaginationSize {
size = maxPaginationSize
}
return page, size
}
func getRawQueryParam(url string) map[string]string {
if url == "" {
return map[string]string{}
}
val := map[string]string{}
parts := strings.Split(url, "&")
for _, part := range parts {
p := strings.Split(part, "=")
val[p[0]] = p[1]
}
return val
}
func writeJSON(w http.ResponseWriter, body interface{}) {
b, err := json.Marshal(body)
if err != nil {
log.Err(err).Msg("error while serializing body")
w.WriteHeader(http.StatusInternalServerError)
return
}
return e.Start(":8080")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(b)
}

@ -0,0 +1,369 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"github.com/creekorful/trandoshan/api"
"github.com/creekorful/trandoshan/internal/api/database"
"github.com/creekorful/trandoshan/internal/api/database_mock"
"github.com/creekorful/trandoshan/internal/configapi/client"
"github.com/creekorful/trandoshan/internal/configapi/client_mock"
"github.com/creekorful/trandoshan/internal/event"
"github.com/creekorful/trandoshan/internal/event_mock"
"github.com/golang/mock/gomock"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestWritePagination(t *testing.T) {
rec := httptest.NewRecorder()
searchParams := &api.ResSearchParams{
PageSize: 15,
PageNumber: 7,
}
total := int64(1200)
writePagination(rec, searchParams, total)
if rec.Header().Get(api.PaginationPageHeader) != "7" {
t.Fail()
}
if rec.Header().Get(api.PaginationSizeHeader) != "15" {
t.Fail()
}
if rec.Header().Get(api.PaginationCountHeader) != "1200" {
t.Fail()
}
}
func TestReadPagination(t *testing.T) {
// valid params
req := httptest.NewRequest(http.MethodGet, "/index.php?pagination-page=1&pagination-size=10", nil)
if page, size := getPagination(req); page != 1 || size != 10 {
t.Errorf("wanted page: 1, size: 10 (got %d, %d)", page, size)
}
// make sure invalid parameter are set as wanted
req = httptest.NewRequest(http.MethodGet, "/index.php?pagination-page=abcd&pagination-size=lol", nil)
if page, size := getPagination(req); page != 1 || size != defaultPaginationSize {
t.Errorf("wanted page: 1, size: %d (got %d, %d)", defaultPaginationSize, page, size)
}
// make sure we prevent too much results from being returned
target := fmt.Sprintf("/index.php?pagination-page=10&pagination-size=%d", maxPaginationSize+1)
req = httptest.NewRequest(http.MethodGet, target, nil)
if page, size := getPagination(req); page != 10 || size != maxPaginationSize {
t.Errorf("wanted page: 10, size: %d (got %d, %d)", maxPaginationSize, page, size)
}
// make sure no parameter we set to default
req = httptest.NewRequest(http.MethodGet, "/index.php", nil)
if page, size := getPagination(req); page != 1 || size != defaultPaginationSize {
t.Errorf("wanted page: 1, size: %d (got %d, %d)", defaultPaginationSize, page, size)
}
}
func TestGetSearchParams(t *testing.T) {
startDate := time.Now()
target := fmt.Sprintf("/resources?with-body=true&pagination-page=1&keyword=keyword&url=dXJs&start-date=%s", startDate.Format(time.RFC3339))
req := httptest.NewRequest(http.MethodPost, target, nil)
params, err := getSearchParams(req)
if err != nil {
t.Errorf("error while parsing search params: %s", err)
t.FailNow()
}
if !params.WithBody {
t.Errorf("wrong withBody: %v", params.WithBody)
}
if params.PageSize != 50 {
t.Errorf("wrong pagination-size: %d", params.PageSize)
}
if params.PageNumber != 1 {
t.Errorf("wrong pagination-page: %d", params.PageNumber)
}
if params.Keyword != "keyword" {
t.Errorf("wrong keyword: %s", params.Keyword)
}
if params.StartDate.Year() != startDate.Year() {
t.Errorf("wrong start-date (year)")
}
if params.StartDate.Month() != startDate.Month() {
t.Errorf("wrong start-date (month)")
}
if params.StartDate.Day() != startDate.Day() {
t.Errorf("wrong start-date (day)")
}
if params.StartDate.Hour() != startDate.Hour() {
t.Errorf("wrong start-date (hour)")
}
if params.StartDate.Minute() != startDate.Minute() {
t.Errorf("wrong start-date (minute)")
}
if params.StartDate.Second() != startDate.Second() {
t.Errorf("wrong start-date (second)")
}
if params.URL != "url" {
t.Errorf("wrong url: %s", params.URL)
}
}
func TestScheduleURL(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
// The requests
req := httptest.NewRequest(http.MethodPost, "/v1/urls", strings.NewReader("\"https://google.onion\""))
rec := httptest.NewRecorder()
// Mocking status
pubMock := event_mock.NewMockPublisher(mockCtrl)
s := State{pub: pubMock}
pubMock.EXPECT().PublishEvent(&event.FoundURLEvent{URL: "https://google.onion"}).Return(nil)
s.scheduleURL(rec, req)
if rec.Code != http.StatusOK {
t.Fail()
}
}
func TestAddResource(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
body := api.ResourceDto{
URL: "https://example.onion",
Body: "TheBody",
Title: "Example",
Time: time.Time{},
Meta: map[string]string{"content": "content-meta"},
Description: "the description",
Headers: map[string]string{"Content-Type": "application/html", "Server": "Traefik"},
}
bodyBytes, err := json.Marshal(body)
if err != nil {
t.FailNow()
}
// The requests
req := httptest.NewRequest(http.MethodPost, "/v1/resources", bytes.NewReader(bodyBytes))
rec := httptest.NewRecorder()
dbMock := database_mock.NewMockDatabase(mockCtrl)
configClientMock := client_mock.NewMockClient(mockCtrl)
dbMock.EXPECT().CountResources(&searchParamsMatcher{target: api.ResSearchParams{
URL: "https://example.onion",
PageSize: 1,
PageNumber: 1,
}}).Return(int64(0), nil)
dbMock.EXPECT().AddResource(database.ResourceIdx{
URL: "https://example.onion",
Body: "TheBody",
Title: "Example",
Time: time.Time{},
Meta: map[string]string{"content": "content-meta"},
Description: "the description",
Headers: map[string]string{"Content-Type": "application/html", "Server": "Traefik"},
})
configClientMock.EXPECT().GetRefreshDelay().Return(client.RefreshDelay{Delay: 5 * time.Hour}, nil)
s := State{db: dbMock, configClient: configClientMock}
s.addResource(rec, req)
if rec.Code != http.StatusOK {
t.FailNow()
}
if rec.Header().Get("Content-Type") != "application/json" {
t.Fail()
}
var res api.ResourceDto
if err := json.NewDecoder(rec.Body).Decode(&res); err != nil {
t.FailNow()
}
if res.URL != "https://example.onion" {
t.FailNow()
}
if res.Body != "TheBody" {
t.FailNow()
}
if res.Title != "Example" {
t.FailNow()
}
if !res.Time.IsZero() {
t.FailNow()
}
if res.Meta["content"] != "content-meta" {
t.FailNow()
}
if res.Description != "the description" {
t.FailNow()
}
if res.Headers["Content-Type"] != "application/html" {
t.FailNow()
}
if res.Headers["Server"] != "Traefik" {
t.FailNow()
}
}
func TestAddResourceDuplicateNotAllowed(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
body := api.ResourceDto{
URL: "https://example.onion",
Body: "TheBody",
Title: "Example",
Time: time.Time{},
Meta: map[string]string{"content": "content-meta"},
Description: "the description",
Headers: map[string]string{"Content-Type": "application/html", "Server": "Traefik"},
}
bodyBytes, err := json.Marshal(body)
if err != nil {
t.FailNow()
}
// The requests
req := httptest.NewRequest(http.MethodPost, "/v1/resources", bytes.NewReader(bodyBytes))
rec := httptest.NewRecorder()
dbMock := database_mock.NewMockDatabase(mockCtrl)
configClientMock := client_mock.NewMockClient(mockCtrl)
dbMock.EXPECT().CountResources(&searchParamsMatcher{target: api.ResSearchParams{
URL: "https://example.onion",
PageSize: 1,
PageNumber: 1,
}, endDateZero: true}).Return(int64(1), nil)
configClientMock.EXPECT().GetRefreshDelay().Return(client.RefreshDelay{Delay: -1}, nil)
s := State{db: dbMock, configClient: configClientMock}
s.addResource(rec, req)
if rec.Code != http.StatusOK {
t.FailNow()
}
}
func TestAddResourceTooYoung(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
body := api.ResourceDto{
URL: "https://example.onion",
Body: "TheBody",
Title: "Example",
Time: time.Time{},
Meta: map[string]string{"content": "content-meta"},
Description: "the description",
Headers: map[string]string{"Content-Type": "application/html", "Server": "Traefik"},
}
bodyBytes, err := json.Marshal(body)
if err != nil {
t.FailNow()
}
// The requests
req := httptest.NewRequest(http.MethodPost, "/v1/resources", bytes.NewReader(bodyBytes))
rec := httptest.NewRecorder()
dbMock := database_mock.NewMockDatabase(mockCtrl)
configClientMock := client_mock.NewMockClient(mockCtrl)
dbMock.EXPECT().CountResources(&searchParamsMatcher{target: api.ResSearchParams{
URL: "https://example.onion",
EndDate: time.Now().Add(-10 * time.Minute),
PageSize: 1,
PageNumber: 1,
}}).Return(int64(1), nil)
configClientMock.EXPECT().GetRefreshDelay().Return(client.RefreshDelay{Delay: 10 * time.Minute}, nil)
s := State{db: dbMock, configClient: configClientMock}
s.addResource(rec, req)
if rec.Code != http.StatusOK {
t.FailNow()
}
}
func TestSearchResources(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
// The requests
req := httptest.NewRequest(http.MethodPost, "/v1/resources?keyword=example", nil)
rec := httptest.NewRecorder()
dbMock := database_mock.NewMockDatabase(mockCtrl)
dbMock.EXPECT().CountResources(gomock.Any()).Return(int64(150), nil)
dbMock.EXPECT().SearchResources(gomock.Any()).Return([]database.ResourceIdx{
{
URL: "example-1.onion",
Body: "Example 1",
Title: "Example 1",
Time: time.Time{},
},
{
URL: "example-2.onion",
Body: "Example 2",
Title: "Example 2",
Time: time.Time{},
},
}, nil)
s := State{db: dbMock}
s.searchResources(rec, req)
if rec.Code != http.StatusOK {
t.Fail()
}
if rec.Header().Get("Content-Type") != "application/json" {
t.Fail()
}
if rec.Header().Get(api.PaginationCountHeader) != "150" {
t.Fail()
}
var resources []api.ResourceDto
if err := json.NewDecoder(rec.Body).Decode(&resources); err != nil {
t.Fatalf("error while decoding body: %s", err)
}
if len(resources) != 2 {
t.Errorf("got %d resources want 2", len(resources))
}
}
// custom matcher to ignore time field when doing comparison ;(
// todo: do less crappy?
type searchParamsMatcher struct {
target api.ResSearchParams
endDateZero bool
}
func (sm *searchParamsMatcher) Matches(x interface{}) bool {
arg := x.(*api.ResSearchParams)
return arg.URL == sm.target.URL && arg.PageSize == sm.target.PageSize && arg.PageNumber == sm.target.PageNumber &&
sm.endDateZero == arg.EndDate.IsZero()
}
func (sm *searchParamsMatcher) String() string {
return "is valid search params"
}

@ -1,24 +1,20 @@
package auth
import (
"context"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo/v4"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"net/http"
"strings"
)
// ErrInvalidOrMissingAuth is returned if the authorization header is absent or invalid
var ErrInvalidOrMissingAuth = &echo.HTTPError{
Code: http.StatusUnauthorized,
Message: "Invalid or missing `Authorization` header",
}
type key int
// ErrAccessUnauthorized is returned if the token doesn't grant access to the current resource
var ErrAccessUnauthorized = &echo.HTTPError{
Code: http.StatusUnauthorized,
Message: "Access to the resource is not authorized",
}
const (
usernameKey key = iota
)
// Token is the authentication token used by processes when dialing with the API
type Token struct {
@ -40,14 +36,16 @@ func NewMiddleware(signingKey []byte) *Middleware {
return &Middleware{signingKey: signingKey}
}
// Middleware return an echo compatible middleware func to use
func (m *Middleware) Middleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Middleware return an net/http compatible middleware func to use
func (m *Middleware) Middleware() mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract authorization header
tokenStr := c.Request().Header.Get(echo.HeaderAuthorization)
tokenStr := r.Header.Get("Authorization")
if tokenStr == "" {
return ErrInvalidOrMissingAuth
log.Warn().Msg("missing token")
w.WriteHeader(http.StatusUnauthorized)
return
}
tokenStr = strings.TrimPrefix(tokenStr, "Bearer ")
@ -63,13 +61,17 @@ func (m *Middleware) Middleware() echo.MiddlewareFunc {
return m.signingKey, nil
})
if err != nil {
return ErrInvalidOrMissingAuth
log.Err(err).Msg("error while decoding JWT token")
w.WriteHeader(http.StatusUnauthorized)
return
}
// From here we have a valid JWT token, extract claims
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return fmt.Errorf("error while parsing token claims")
log.Err(err).Msg("error while decoding token claims")
w.WriteHeader(http.StatusInternalServerError)
return
}
rights := map[string][]string{}
@ -85,28 +87,38 @@ func (m *Middleware) Middleware() echo.MiddlewareFunc {
}
// Validate rights
paths, contains := t.Rights[c.Request().Method]
paths, contains := t.Rights[r.Method]
if !contains {
return ErrAccessUnauthorized
log.Warn().
Str("username", t.Username).
Str("method", r.Method).
Str("resource", r.URL.Path).
Msg("Access to resources is unauthorized")
w.WriteHeader(http.StatusUnauthorized)
return
}
authorized := false
for _, path := range paths {
if path == c.Request().URL.Path {
if path == r.URL.Path {
authorized = true
break
}
}
if !authorized {
return ErrAccessUnauthorized
log.Warn().
Str("username", t.Username).
Str("method", r.Method).
Str("resource", r.URL.Path).
Msg("Access to resources is unauthorized")
w.WriteHeader(http.StatusUnauthorized)
return
}
// Set user context
c.Set("username", t.Username)
// Everything's fine, call next handler ;D
return next(c)
}
ctx := context.WithValue(r.Context(), usernameKey, t.Username)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

@ -2,7 +2,6 @@ package auth
import (
"fmt"
"github.com/labstack/echo/v4"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -10,60 +9,60 @@ import (
)
func TestMiddleware_NoTokenShouldReturnUnauthorized(t *testing.T) {
e := echo.New()
m := (&Middleware{signingKey: []byte("test")}).Middleware()(okHandler())
// no token shouldn't be able to access
req := httptest.NewRequest(http.MethodGet, "/users", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if err := m(c); err != ErrInvalidOrMissingAuth {
t.Errorf("ErrInvalidOrMissingAuth was expected")
m.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("StatusUnauthorized was expected")
}
}
func TestMiddleware_InvalidTokenShouldReturnUnauthorized(t *testing.T) {
e := echo.New()
m := (&Middleware{signingKey: []byte("test")}).Middleware()
m := (&Middleware{signingKey: []byte("test")}).Middleware()(okHandler())
req := httptest.NewRequest(http.MethodGet, "/users", nil)
req.Header.Add(echo.HeaderAuthorization, "zarBR")
req.Header.Add("Authorization", "zarBR")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if err := m(okHandler())(c); err != ErrInvalidOrMissingAuth {
t.Errorf("ErrInvalidOrMissingAuth was expected")
m.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("StatusUnauthorized was expected")
}
}
func TestMiddleware_BadRightsShouldReturnUnauthorized(t *testing.T) {
e := echo.New()
m := (&Middleware{signingKey: []byte("test")}).Middleware()
m := (&Middleware{signingKey: []byte("test")}).Middleware()(okHandler())
req := httptest.NewRequest(http.MethodPost, "/users", nil)
req.Header.Add(echo.HeaderAuthorization, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gRG9lIiwicmlnaHRzIjp7IkdFVCI6WyIvdXNlcnMiXSwiUE9TVCI6WyIvc2VhcmNoIl19fQ.fRx0Q66ZgnY_rKCf-9Vaz6gzGKH_tKSgkVHhoQMtKfM")
req.Header.Add("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gRG9lIiwicmlnaHRzIjp7IkdFVCI6WyIvdXNlcnMiXSwiUE9TVCI6WyIvc2VhcmNoIl19fQ.fRx0Q66ZgnY_rKCf-9Vaz6gzGKH_tKSgkVHhoQMtKfM")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if err := m(okHandler())(c); err != ErrAccessUnauthorized {
t.Errorf("ErrAccessUnauthorized was expected")
m.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("StatusUnauthorized was expected")
}
}
func TestMiddleware(t *testing.T) {
e := echo.New()
m := (&Middleware{signingKey: []byte("test")}).Middleware()
m := (&Middleware{signingKey: []byte("test")}).Middleware()(okHandler())
req := httptest.NewRequest(http.MethodGet, "/users?id=10", nil)
req.Header.Add(echo.HeaderAuthorization, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gRG9lIiwicmlnaHRzIjp7IkdFVCI6WyIvdXNlcnMiXSwiUE9TVCI6WyIvc2VhcmNoIl19fQ.fRx0Q66ZgnY_rKCf-9Vaz6gzGKH_tKSgkVHhoQMtKfM")
req.Header.Add("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gRG9lIiwicmlnaHRzIjp7IkdFVCI6WyIvdXNlcnMiXSwiUE9TVCI6WyIvc2VhcmNoIl19fQ.fRx0Q66ZgnY_rKCf-9Vaz6gzGKH_tKSgkVHhoQMtKfM")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
_ = m(okHandler())(c)
m.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fail()
t.Errorf("StatusUnauthorized was expected")
}
b, err := ioutil.ReadAll(rec.Body)
if err != nil {
t.Fail()
@ -71,18 +70,16 @@ func TestMiddleware(t *testing.T) {
if string(b) != "Hello, John Doe" {
t.Fail()
}
if token := c.Get("username").(string); token != "John Doe" {
t.Fail()
}
}
func okHandler() echo.HandlerFunc {
return func(c echo.Context) error {
if username := c.Get("username").(string); username != "" {
return c.String(http.StatusOK, fmt.Sprintf("Hello, %s", username))
func okHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if username := r.Context().Value(usernameKey).(string); username != "" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(fmt.Sprintf("Hello, %s", username)))
return
}
return c.NoContent(http.StatusNoContent)
w.WriteHeader(http.StatusNoContent)
}
}

@ -1,18 +1,12 @@
package database
import (
"context"
"encoding/json"
"github.com/creekorful/trandoshan/api"
"github.com/olivere/elastic/v7"
"github.com/rs/zerolog/log"
"time"
)
//go:generate mockgen -destination=../database_mock/database_mock.go -package=database_mock . Database
var resourcesIndex = "resources"
// ResourceIdx represent a resource as stored in elasticsearch
type ResourceIdx struct {
URL string `json:"url"`
@ -31,139 +25,3 @@ type Database interface {
CountResources(params *api.ResSearchParams) (int64, error)
AddResource(res ResourceIdx) error
}
type elasticSearchDB struct {
client *elastic.Client
}
// NewElasticDB create a new Database based on ES instance
func NewElasticDB(uri string) (Database, error) {
// Create Elasticsearch client
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
ec, err := elastic.DialContext(ctx,
elastic.SetURL(uri),
elastic.SetSniff(false),
elastic.SetHealthcheck(false),
)
if err != nil {
return nil, err
}
if err := setupElasticSearch(ctx, ec); err != nil {
return nil, err
}
return &elasticSearchDB{
client: ec,
}, nil
}
func (e *elasticSearchDB) SearchResources(params *api.ResSearchParams) ([]ResourceIdx, error) {
q := buildSearchQuery(params)
from := (params.PageNumber - 1) * params.PageSize
res, err := e.client.Search().
Index(resourcesIndex).
Query(q).
From(from).
Size(params.PageSize).
Do(context.Background())
if err != nil {
return nil, err
}
var resources []ResourceIdx
for _, hit := range res.Hits.Hits {
var resource ResourceIdx
if err := json.Unmarshal(hit.Source, &resource); err != nil {
log.Warn().Str("err", err.Error()).Msg("Error while un-marshaling resource")
continue
}
// Remove body if not wanted
if !params.WithBody {
resource.Body = ""
}
resources = append(resources, resource)
}
return resources, nil
}
func (e *elasticSearchDB) CountResources(params *api.ResSearchParams) (int64, error) {
q := buildSearchQuery(params)
count, err := e.client.Count(resourcesIndex).Query(q).Do(context.Background())
if err != nil {
return 0, err
}
return count, nil
}
func (e *elasticSearchDB) AddResource(res ResourceIdx) error {
_, err := e.client.Index().
Index(resourcesIndex).
BodyJson(res).
Do(context.Background())
return err
}
func buildSearchQuery(params *api.ResSearchParams) elastic.Query {
var queries []elastic.Query
if params.URL != "" {
log.Trace().Str("url", params.URL).Msg("SearchQuery: Setting url")
queries = append(queries, elastic.NewTermQuery("url.keyword", params.URL))
}
if params.Keyword != "" {
log.Trace().Str("body", params.Keyword).Msg("SearchQuery: Setting body")
queries = append(queries, elastic.NewMatchQuery("body", params.Keyword))
}
if !params.StartDate.IsZero() || !params.EndDate.IsZero() {
timeQuery := elastic.NewRangeQuery("time")
if !params.StartDate.IsZero() {
log.Trace().
Str("startDate", params.StartDate.Format(time.RFC3339)).
Msg("SearchQuery: Setting startDate")
timeQuery.Gte(params.StartDate.Format(time.RFC3339))
}
if !params.EndDate.IsZero() {
log.Trace().
Str("endDate", params.EndDate.Format(time.RFC3339)).
Msg("SearchQuery: Setting endDate")
timeQuery.Lte(params.EndDate.Format(time.RFC3339))
}
queries = append(queries, timeQuery)
}
// Handle specific case
if len(queries) == 0 {
return elastic.NewMatchAllQuery()
}
if len(queries) == 1 {
return queries[0]
}
// otherwise AND combine them
return elastic.NewBoolQuery().Must(queries...)
}
func setupElasticSearch(ctx context.Context, es *elastic.Client) error {
// Setup index if doesn't exist
exist, err := es.IndexExists(resourcesIndex).Do(ctx)
if err != nil {
return err
}
if !exist {
log.Debug().Str("index", resourcesIndex).Msg("Creating missing index")
if _, err := es.CreateIndex(resourcesIndex).Do(ctx); err != nil {
return err
}
}
return nil
}

@ -0,0 +1,148 @@
package database
import (
"context"
"encoding/json"
"github.com/creekorful/trandoshan/api"
"github.com/olivere/elastic/v7"
"github.com/rs/zerolog/log"
"time"
)
var resourcesIndex = "resources"
type elasticSearchDB struct {
client *elastic.Client
}
// NewElasticDB create a new Database based on ES instance
func NewElasticDB(uri string) (Database, error) {
// Create Elasticsearch client
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
ec, err := elastic.DialContext(ctx,
elastic.SetURL(uri),
elastic.SetSniff(false),
elastic.SetHealthcheck(false),
)
if err != nil {
return nil, err
}
if err := setupElasticSearch(ctx, ec); err != nil {
return nil, err
}
return &elasticSearchDB{
client: ec,
}, nil
}
func (e *elasticSearchDB) SearchResources(params *api.ResSearchParams) ([]ResourceIdx, error) {
q := buildSearchQuery(params)
from := (params.PageNumber - 1) * params.PageSize
res, err := e.client.Search().
Index(resourcesIndex).
Query(q).
From(from).
Size(params.PageSize).
Do(context.Background())
if err != nil {
return nil, err
}
var resources []ResourceIdx
for _, hit := range res.Hits.Hits {
var resource ResourceIdx
if err := json.Unmarshal(hit.Source, &resource); err != nil {
log.Warn().Str("err", err.Error()).Msg("Error while un-marshaling resource")
continue
}
// Remove body if not wanted
if !params.WithBody {
resource.Body = ""
}
resources = append(resources, resource)
}
return resources, nil
}
func (e *elasticSearchDB) CountResources(params *api.ResSearchParams) (int64, error) {
q := buildSearchQuery(params)
count, err := e.client.Count(resourcesIndex).Query(q).Do(context.Background())
if err != nil {
return 0, err
}
return count, nil
}
func (e *elasticSearchDB) AddResource(res ResourceIdx) error {
_, err := e.client.Index().
Index(resourcesIndex).
BodyJson(res).
Do(context.Background())
return err
}
func buildSearchQuery(params *api.ResSearchParams) elastic.Query {
var queries []elastic.Query
if params.URL != "" {
log.Trace().Str("url", params.URL).Msg("SearchQuery: Setting url")
queries = append(queries, elastic.NewTermQuery("url.keyword", params.URL))
}
if params.Keyword != "" {
log.Trace().Str("body", params.Keyword).Msg("SearchQuery: Setting body")
queries = append(queries, elastic.NewMatchQuery("body", params.Keyword))
}
if !params.StartDate.IsZero() || !params.EndDate.IsZero() {
timeQuery := elastic.NewRangeQuery("time")
if !params.StartDate.IsZero() {
log.Trace().
Str("startDate", params.StartDate.Format(time.RFC3339)).
Msg("SearchQuery: Setting startDate")
timeQuery.Gte(params.StartDate.Format(time.RFC3339))
}
if !params.EndDate.IsZero() {
log.Trace().
Str("endDate", params.EndDate.Format(time.RFC3339)).
Msg("SearchQuery: Setting endDate")
timeQuery.Lte(params.EndDate.Format(time.RFC3339))
}
queries = append(queries, timeQuery)
}
// Handle specific case
if len(queries) == 0 {
return elastic.NewMatchAllQuery()
}
if len(queries) == 1 {
return queries[0]
}
// otherwise AND combine them
return elastic.NewBoolQuery().Must(queries...)
}
func setupElasticSearch(ctx context.Context, es *elastic.Client) error {
// Setup index if doesn't exist
exist, err := es.IndexExists(resourcesIndex).Do(ctx)
if err != nil {
return err
}
if !exist {
log.Debug().Str("index", resourcesIndex).Msg("Creating missing index")
if _, err := es.CreateIndex(resourcesIndex).Do(ctx); err != nil {
return err
}
}
return nil
}

@ -1,150 +0,0 @@
package rest
import (
"encoding/base64"
"github.com/creekorful/trandoshan/api"
"github.com/creekorful/trandoshan/internal/api/service"
"github.com/labstack/echo/v4"
"net/http"
"strconv"
"strings"
"time"
)
var (
defaultPaginationSize = 50
maxPaginationSize = 100
)
// SearchResources allows to search resources
func SearchResources(s *service.Service) echo.HandlerFunc {
return func(c echo.Context) error {
searchParams, err := newSearchParams(c)
if err != nil {
return err
}
resources, total, err := s.SearchResources(searchParams)
if err != nil {
return err
}
writePagination(c, searchParams, total)
return c.JSON(http.StatusOK, resources)
}
}
// AddResource persist a new resource
func AddResource(s *service.Service) echo.HandlerFunc {
return func(c echo.Context) error {
var res api.ResourceDto
if err := c.Bind(&res); err != nil {
return err
}
res, err := s.AddResource(res)
if err != nil {
return err
}
return c.JSON(http.StatusCreated, res)
}
}
// ScheduleURL schedule given URL for crawling
func ScheduleURL(s *service.Service) echo.HandlerFunc {
return func(c echo.Context) error {
var url string
if err := c.Bind(&url); err != nil {
return err
}
return s.ScheduleURL(url)
}
}
func readPagination(c echo.Context) (int, int) {
paginationPage, err := strconv.Atoi(c.QueryParam(api.PaginationPageQueryParam))
if err != nil {
paginationPage = 1
}
paginationSize, err := strconv.Atoi(c.QueryParam(api.PaginationSizeQueryParam))
if err != nil {
paginationSize = defaultPaginationSize
}
// Prevent too much results from being returned
if paginationSize > maxPaginationSize {
paginationSize = maxPaginationSize
}
return paginationPage, paginationSize
}
func writePagination(c echo.Context, s *api.ResSearchParams, totalCount int64) {
c.Response().Header().Set(api.PaginationPageHeader, strconv.Itoa(s.PageNumber))
c.Response().Header().Set(api.PaginationSizeHeader, strconv.Itoa(s.PageSize))
c.Response().Header().Set(api.PaginationCountHeader, strconv.FormatInt(totalCount, 10))
}
func newSearchParams(c echo.Context) (*api.ResSearchParams, error) {
params := &api.ResSearchParams{}
params.Keyword = c.QueryParam("keyword")
if c.QueryParam("with-body") == "true" {
params.WithBody = true
}
// extract raw query params (unescaped to keep + sign when parsing date)
rawQueryParams := getRawQueryParam(c.QueryString())
if val, exist := rawQueryParams["start-date"]; exist {
d, err := time.Parse(time.RFC3339, val)
if err == nil {
params.StartDate = d
} else {
return nil, err
}
}
if val, exist := rawQueryParams["end-date"]; exist {
d, err := time.Parse(time.RFC3339, val)
if err == nil {
params.EndDate = d
} else {
return nil, err
}
}
// First of all base64decode the URL
b64URL := c.QueryParam("url")
b, err := base64.URLEncoding.DecodeString(b64URL)
if err != nil {
return nil, err
}
params.URL = string(b)
// Acquire pagination
page, size := readPagination(c)
params.PageNumber = page
params.PageSize = size
return params, nil
}
func getRawQueryParam(url string) map[string]string {
if url == "" {
return map[string]string{}
}
val := map[string]string{}
parts := strings.Split(url, "&")
for _, part := range parts {
p := strings.Split(part, "=")
val[p[0]] = p[1]
}
return val
}

@ -1,62 +0,0 @@
package rest
import (
"fmt"
"github.com/labstack/echo/v4"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestNewSearchParams(t *testing.T) {
e := echo.New()
startDate := time.Now()
target := fmt.Sprintf("/resources?with-body=true&pagination-page=1&keyword=keyword&url=dXJs&start-date=%s", startDate.Format(time.RFC3339))
req := httptest.NewRequest(http.MethodPost, target, nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
params, err := newSearchParams(c)
if err != nil {
t.Errorf("error while parsing search params: %s", err)
t.FailNow()
}
if !params.WithBody {
t.Errorf("wrong withBody: %v", params.WithBody)
}
if params.PageSize != 50 {
t.Errorf("wrong pagination-size: %d", params.PageSize)
}
if params.PageNumber != 1 {
t.Errorf("wrong pagination-page: %d", params.PageNumber)
}
if params.Keyword != "keyword" {
t.Errorf("wrong keyword: %s", params.Keyword)
}
if params.StartDate.Year() != startDate.Year() {
t.Errorf("wrong start-date (year)")
}
if params.StartDate.Month() != startDate.Month() {
t.Errorf("wrong start-date (month)")
}
if params.StartDate.Day() != startDate.Day() {
t.Errorf("wrong start-date (day)")
}
if params.StartDate.Hour() != startDate.Hour() {
t.Errorf("wrong start-date (hour)")
}
if params.StartDate.Minute() != startDate.Minute() {
t.Errorf("wrong start-date (minute)")
}
if params.StartDate.Second() != startDate.Second() {
t.Errorf("wrong start-date (second)")
}
if params.URL != "url" {
t.Errorf("wrong url: %s", params.URL)
}
}

@ -1,136 +0,0 @@
package service
import (
"fmt"
"github.com/creekorful/trandoshan/api"
"github.com/creekorful/trandoshan/internal/api/database"
"github.com/creekorful/trandoshan/internal/duration"
"github.com/creekorful/trandoshan/internal/event"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"time"
)
// Service represent the functionality the API expose
type Service struct {
db database.Database
pub event.Publisher
refreshDelay time.Duration
}
// New create a new Service instance
func New(c *cli.Context) (*Service, error) {
// Connect to the messaging server
pub, err := event.NewPublisher(c.String("hub-uri"))
if err != nil {
return nil, fmt.Errorf("error while connecting to hub server: %s", err)
}
// Create Elasticsearch client
db, err := database.NewElasticDB(c.String("elasticsearch-uri"))
if err != nil {
return nil, fmt.Errorf("error while connecting to the database: %s", err)
}
refreshDelay := duration.ParseDuration(c.String("refresh-delay"))
return &Service{
db: db,
pub: pub,
refreshDelay: refreshDelay,
}, nil
}
// SearchResources allows to search resources using given params
func (s *Service) SearchResources(params *api.ResSearchParams) ([]api.ResourceDto, int64, error) {
totalCount, err := s.db.CountResources(params)
if err != nil {
log.Err(err).Msg("Error while counting on ES")
return nil, 0, err
}
res, err := s.db.SearchResources(params)
if err != nil {
log.Err(err).Msg("Error while searching on ES")
return nil, 0, err
}
var resources []api.ResourceDto
for _, r := range res {
resources = append(resources, api.ResourceDto{
URL: r.URL,
Body: r.Body,
Title: r.Title,
Time: r.Time,
})
}
return resources, totalCount, nil
}
// AddResource allows to add given resource
func (s *Service) AddResource(res api.ResourceDto) (api.ResourceDto, error) {
// Hacky stuff to prevent from adding 'duplicate resource'
// the thing is: even with the scheduler preventing from crawling 'duplicates' URL by adding a refresh period
// and checking if the resource is not already indexed, this implementation may not work if the URLs was published
// before the resource is saved. And this happen a LOT of time.
// therefore the best thing to do is to make the API check if the resource should **really** be added by checking if
// it isn't present on the database. This may sounds hacky, but it's the best solution i've come up at this time.
endDate := time.Time{}
if s.refreshDelay != -1 {
endDate = time.Now().Add(-s.refreshDelay)
}
count, err := s.db.CountResources(&api.ResSearchParams{
URL: res.URL,
EndDate: endDate,
PageSize: 1,
PageNumber: 1,
})
if err != nil {
log.Err(err).Msg("error while searching for resource")
return api.ResourceDto{}, nil
}
if count > 0 {
// Not an error
log.Debug().Str("url", res.URL).Msg("Skipping duplicate resource")
return res, nil
}
// Create Elasticsearch document
doc := database.ResourceIdx{
URL: res.URL,
Body: res.Body,
Time: res.Time,
Title: res.Title,
Meta: res.Meta,
Description: res.Description,
Headers: res.Headers,
}
if err := s.db.AddResource(doc); err != nil {
log.Err(err).Msg("Error while adding resource")
return api.ResourceDto{}, err
}
log.Debug().Str("url", res.URL).Msg("Successfully saved resource")
return res, nil
}
// ScheduleURL schedule given url for crawling
func (s *Service) ScheduleURL(url string) error {
// Publish the URL
if err := s.pub.PublishEvent(&event.FoundURLEvent{URL: url}); err != nil {
log.Err(err).Msg("Unable to publish URL")
return err
}
log.Debug().Str("url", url).Msg("Successfully published URL")
return nil
}
// Close disconnect the service consumer
func (s *Service) Close() {
s.pub.Close()
}

@ -1,202 +0,0 @@
package service
import (
"github.com/creekorful/trandoshan/api"
"github.com/creekorful/trandoshan/internal/api/database"
"github.com/creekorful/trandoshan/internal/api/database_mock"
"github.com/creekorful/trandoshan/internal/event"
"github.com/creekorful/trandoshan/internal/event_mock"
"github.com/golang/mock/gomock"
"testing"
"time"
)
func TestSearchResources(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
params := &api.ResSearchParams{Keyword: "example"}
dbMock := database_mock.NewMockDatabase(mockCtrl)
dbMock.EXPECT().CountResources(params).Return(int64(150), nil)
dbMock.EXPECT().SearchResources(params).Return([]database.ResourceIdx{
{
URL: "example-1.onion",
Body: "Example 1",
Title: "Example 1",
Time: time.Time{},
},
{
URL: "example-2.onion",
Body: "Example 2",
Title: "Example 2",
Time: time.Time{},
},
}, nil)
s := Service{db: dbMock}
res, count, err := s.SearchResources(params)
if err != nil {
t.FailNow()
}
if count != 150 {
t.Error()
}
if len(res) != 2 {
t.Error()
}
}
func TestAddResource(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
dbMock := database_mock.NewMockDatabase(mockCtrl)
dbMock.EXPECT().CountResources(&searchParamsMatcher{target: api.ResSearchParams{
URL: "https://example.onion",
PageSize: 1,
PageNumber: 1,
}}).Return(int64(0), nil)
dbMock.EXPECT().AddResource(database.ResourceIdx{
URL: "https://example.onion",
Body: "TheBody",
Title: "Example",
Time: time.Time{},
Meta: map[string]string{"content": "content-meta"},
Description: "the description",
Headers: map[string]string{"Content-Type": "application/html", "Server": "Traefik"},
})
s := Service{db: dbMock, refreshDelay: 5 * time.Hour}
res, err := s.AddResource(api.ResourceDto{
URL: "https://example.onion",
Body: "TheBody",
Title: "Example",
Time: time.Time{},
Meta: map[string]string{"content": "content-meta"},
Description: "the description",
Headers: map[string]string{"Content-Type": "application/html", "Server": "Traefik"},
})
if err != nil {
t.FailNow()
}
if res.URL != "https://example.onion" {
t.FailNow()
}
if res.Body != "TheBody" {
t.FailNow()
}
if res.Title != "Example" {
t.FailNow()
}
if !res.Time.IsZero() {
t.FailNow()
}
if res.Meta["content"] != "content-meta" {
t.FailNow()
}
if res.Description != "the description" {
t.FailNow()
}
if res.Headers["Content-Type"] != "application/html" {
t.FailNow()
}
if res.Headers["Server"] != "Traefik" {
t.FailNow()
}
}
func TestAddResourceDuplicateNotAllowed(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
dbMock := database_mock.NewMockDatabase(mockCtrl)
dbMock.EXPECT().CountResources(&searchParamsMatcher{target: api.ResSearchParams{
URL: "https://example.onion",
PageSize: 1,
PageNumber: 1,
}, endDateZero: true}).Return(int64(1), nil)
s := Service{db: dbMock, refreshDelay: -1}
_, err := s.AddResource(api.ResourceDto{
URL: "https://example.onion",
Body: "TheBody",
Title: "Example",
Time: time.Time{},
Meta: map[string]string{"content": "content-meta"},
Description: "the description",
Headers: map[string]string{"Content-Type": "application/html", "Server": "Traefik"},
})
if err != nil {
t.FailNow()
}
}
func TestAddResourceTooYoung(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
dbMock := database_mock.NewMockDatabase(mockCtrl)
dbMock.EXPECT().CountResources(&searchParamsMatcher{target: api.ResSearchParams{
URL: "https://example.onion",
EndDate: time.Now().Add(-10 * time.Minute),
PageSize: 1,
PageNumber: 1,
}}).Return(int64(1), nil)
s := Service{db: dbMock, refreshDelay: -10 * time.Minute}
_, err := s.AddResource(api.ResourceDto{
URL: "https://example.onion",
Body: "TheBody",
Title: "Example",
Time: time.Time{},
Meta: map[string]string{"content": "content-meta"},
Description: "the description",
Headers: map[string]string{"Content-Type": "application/html", "Server": "Traefik"},
})
if err != nil {
t.FailNow()
}
}
func TestScheduleURL(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
pubMock := event_mock.NewMockPublisher(mockCtrl)
s := Service{pub: pubMock}
pubMock.EXPECT().PublishEvent(&event.FoundURLEvent{URL: "https://example.onion"})
if err := s.ScheduleURL("https://example.onion"); err != nil {
t.FailNow()
}
}
// custom matcher to ignore time field when doing comparison ;(
// todo: do less crappy?
type searchParamsMatcher struct {
target api.ResSearchParams
endDateZero bool
}
func (sm *searchParamsMatcher) Matches(x interface{}) bool {
arg := x.(*api.ResSearchParams)
return arg.URL == sm.target.URL && arg.PageSize == sm.target.PageSize && arg.PageNumber == sm.target.PageNumber &&
sm.endDateZero == arg.EndDate.IsZero()
}
func (sm *searchParamsMatcher) String() string {
return "is valid search params"
}

@ -4,82 +4,63 @@ import (
"fmt"
"github.com/creekorful/trandoshan/internal/archiver/storage"
"github.com/creekorful/trandoshan/internal/event"
"github.com/creekorful/trandoshan/internal/logging"
"github.com/creekorful/trandoshan/internal/util"
"github.com/creekorful/trandoshan/internal/process"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"os"
"os/signal"
"net/http"
"strings"
"syscall"
)
// GetApp return the crawler app
func GetApp() *cli.App {
return &cli.App{
Name: "tdsh-archiver",
Version: "0.7.0",
Usage: "Trandoshan archiver component",
Flags: []cli.Flag{
logging.GetLogFlag(),
util.GetHubURI(),
&cli.StringFlag{
Name: "storage-dir",
Usage: "Path to the storage directory",
Required: true,
},
},
Action: execute,
}
// State represent the application state
type State struct {
storage storage.Storage
}
func execute(ctx *cli.Context) error {
logging.ConfigureLogger(ctx)
// Name return the process name
func (state *State) Name() string {
return "archiver"
}
log.Info().
Str("ver", ctx.App.Version).
Str("hub-uri", ctx.String("hub-uri")).
Str("storage-dir", ctx.String("storage-dir")).
Msg("Starting tdsh-archiver")
// CommonFlags return process common flags
func (state *State) CommonFlags() []string {
return []string{process.HubURIFlag}
}
// Create the subscriber
sub, err := event.NewSubscriber(ctx.String("hub-uri"))
if err != nil {
return err
// CustomFlags return process custom flags
func (state *State) CustomFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "storage-dir",
Usage: "Path to the storage directory",
Required: true,
},
}
defer sub.Close()
}
// Create local storage
st, err := storage.NewLocalStorage(ctx.String("storage-dir"))
// Initialize the process
func (state *State) Initialize(provider process.Provider) error {
st, err := storage.NewLocalStorage(provider.GetValue("storage-dir"))
if err != nil {
return err
}
state.storage = st
state := state{
storage: st,
}
return nil
}
if err := sub.Subscribe(event.NewResourceExchange, "archivingQueue", state.handleNewResourceEvent); err != nil {
return err
// Subscribers return the process subscribers
func (state *State) Subscribers() []process.SubscriberDef {
return []process.SubscriberDef{
{Exchange: event.NewResourceExchange, Queue: "archivingQueue", Handler: state.handleNewResourceEvent},
}
log.Info().Msg("Successfully initialized tdsh-archiver. Waiting for resources")
// Handle graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
// Block until we receive our signal.
<-c
return nil
}
type state struct {
storage storage.Storage
// HTTPHandler returns the HTTP API the process expose
func (state *State) HTTPHandler(provider process.Provider) http.Handler {
return nil
}
func (state *state) handleNewResourceEvent(subscriber event.Subscriber, msg event.RawMessage) error {
func (state *State) handleNewResourceEvent(subscriber event.Subscriber, msg event.RawMessage) error {
var evt event.NewResourceEvent
if err := subscriber.Read(&msg, &evt); err != nil {
return err

@ -30,7 +30,7 @@ func TestHandleNewResourceEvent(t *testing.T) {
storageMock.EXPECT().Store("https://example.onion", tn, []byte("Server: Traefik\r\nContent-Type: application/html\r\n\r\nHello, world")).Return(nil)
s := state{storage: storageMock}
s := State{storage: storageMock}
if err := s.handleNewResourceEvent(subscriberMock, msg); err != nil {
t.Fail()
}

@ -4,8 +4,7 @@ import (
"fmt"
"github.com/creekorful/trandoshan/internal/configapi/database"
"github.com/creekorful/trandoshan/internal/event"
"github.com/creekorful/trandoshan/internal/logging"
"github.com/creekorful/trandoshan/internal/util"
"github.com/creekorful/trandoshan/internal/process"
"github.com/go-redis/redis/v8"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
@ -13,98 +12,85 @@ import (
"io/ioutil"
"net/http"
"strings"
"time"
)
// GetApp return the config api app
func GetApp() *cli.App {
return &cli.App{
Name: "tdsh-configapi",
Version: "0.7.0",
Usage: "Trandoshan ConfigAPI component",
Flags: []cli.Flag{
logging.GetLogFlag(),
util.GetHubURI(),
&cli.StringFlag{
Name: "db-uri",
Usage: "URI to the database server",
Required: true,
},
&cli.StringSliceFlag{
Name: "default-value",
Usage: "Set default value of key. (format key=value)",
},
},
Action: execute,
}
// State represent the application state
type State struct {
db database.Database
pub event.Publisher
}
func execute(ctx *cli.Context) error {
logging.ConfigureLogger(ctx)
// Name return the process name
func (state *State) Name() string {
return "configapi"
}
log.Info().
Str("ver", ctx.App.Version).
Str("hub-uri", ctx.String("hub-uri")).
Str("db-uri", ctx.String("db-uri")).
Msg("Starting tdsh-configapi")
// CommonFlags return process common flags
func (state *State) CommonFlags() []string {
return []string{process.HubURIFlag}
}
// CustomFlags return process custom flags
func (state *State) CustomFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "db-uri",
Usage: "URI to the database server",
Required: true,
},
&cli.StringSliceFlag{
Name: "default-value",
Usage: "Set default value of key. (format key=value)",
},
}
}
// Create publisher
pub, err := event.NewPublisher(ctx.String("hub-uri"))
// Initialize the process
func (state *State) Initialize(provider process.Provider) error {
db, err := database.NewRedisDatabase(provider.GetValue("db-uri"))
if err != nil {
return err
}
state.db = db
// Create database connection
db, err := database.NewRedisDatabase(ctx.String("db-uri"))
pub, err := provider.Publisher()
if err != nil {
return err
}
state.pub = pub
// Parse default values
defaultValues := map[string]string{}
for _, value := range ctx.StringSlice("default-value") {
for _, value := range provider.GetValues("default-value") {
parts := strings.Split(value, "=")
if len(parts) == 2 {
defaultValues[parts[0]] = parts[1]
}
}
// Set default values if needed
if len(defaultValues) > 0 {
if err := setDefaultValues(db, defaultValues); err != nil {
log.Err(err).Msg("error while setting default values")
return err
}
}
state := state{
db: db,
pub: pub,
}
return nil // TODO
}
// Subscribers return the process subscribers
func (state *State) Subscribers() []process.SubscriberDef {
return []process.SubscriberDef{}
}
// HTTPHandler returns the HTTP API the process expose
func (state *State) HTTPHandler(provider process.Provider) http.Handler {
r := mux.NewRouter()
r.HandleFunc("/config/{key}", state.getConfiguration).Methods(http.MethodGet)
r.HandleFunc("/config/{key}", state.setConfiguration).Methods(http.MethodPut)
srv := &http.Server{
Addr: "0.0.0.0:8080",
// Good practice to set timeouts to avoid Slowloris attacks.
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
Handler: r, // Pass our instance of gorilla/mux in.
}
return srv.ListenAndServe()
}
type state struct {
db database.Database
pub event.Publisher
return r
}
func (state *state) getConfiguration(w http.ResponseWriter, r *http.Request) {
func (state *State) getConfiguration(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
key := vars["key"]
@ -121,7 +107,7 @@ func (state *state) getConfiguration(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(b)
}
func (state *state) setConfiguration(w http.ResponseWriter, r *http.Request) {
func (state *State) setConfiguration(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
key := vars["key"]

@ -25,7 +25,7 @@ func TestGetConfiguration(t *testing.T) {
rec := httptest.NewRecorder()
s := state{db: dbMock}
s := State{db: dbMock}
s.getConfiguration(rec, req)
if rec.Code != http.StatusOK {
@ -63,7 +63,7 @@ func TestSetConfiguration(t *testing.T) {
rec := httptest.NewRecorder()
s := state{db: dbMock, pub: pubMock}
s := State{db: dbMock, pub: pubMock}
s.setConfiguration(rec, req)
if rec.Code != http.StatusOK {

@ -4,152 +4,112 @@ import (
"crypto/tls"
"fmt"
"github.com/creekorful/trandoshan/internal/clock"
"github.com/creekorful/trandoshan/internal/configapi/client"
"github.com/creekorful/trandoshan/internal/crawler/http"
configapi "github.com/creekorful/trandoshan/internal/configapi/client"
chttp "github.com/creekorful/trandoshan/internal/crawler/http"
"github.com/creekorful/trandoshan/internal/event"
"github.com/creekorful/trandoshan/internal/logging"
"github.com/creekorful/trandoshan/internal/util"
"github.com/creekorful/trandoshan/internal/process"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpproxy"
"io/ioutil"
"os"
"os/signal"
"net/http"
"strings"
"syscall"
"time"
)
const defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0"
// GetApp return the crawler app
func GetApp() *cli.App {
return &cli.App{
Name: "tdsh-crawler",
Version: "0.7.0",
Usage: "Trandoshan crawler component",
Flags: []cli.Flag{
logging.GetLogFlag(),
util.GetHubURI(),
util.GetConfigAPIURIFlag(),
&cli.StringFlag{
Name: "tor-uri",
Usage: "URI to the TOR SOCKS proxy",
Required: true,
},
&cli.StringFlag{
Name: "user-agent",
Usage: "User agent to use",
Value: defaultUserAgent,
},
},
Action: execute,
}
var errContentTypeNotAllowed = fmt.Errorf("content type is not allowed")
// State represent the application state
type State struct {
httpClient chttp.Client
clock clock.Clock
configClient configapi.Client
}
func execute(ctx *cli.Context) error {
logging.ConfigureLogger(ctx)
// Name return the process name
func (state *State) Name() string {
return "crawler"
}
log.Info().
Str("ver", ctx.App.Version).
Str("hub-uri", ctx.String("hub-uri")).
Str("tor-uri", ctx.String("tor-uri")).
Str("config-api-uri", ctx.String("config-api-uri")).
Msg("Starting tdsh-crawler")
// CommonFlags return process common flags
func (state *State) CommonFlags() []string {
return []string{process.HubURIFlag, process.ConfigAPIURIFlag}
}
// Create the HTTP client
httpClient := http.NewFastHTTPClient(&fasthttp.Client{
// CustomFlags return process custom flags
func (state *State) CustomFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "tor-uri",
Usage: "URI to the TOR SOCKS proxy",
Required: true,
},
&cli.StringFlag{
Name: "user-agent",
Usage: "User agent to use",
Value: "Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0",
},
}
}
// Initialize the process
func (state *State) Initialize(provider process.Provider) error {
state.httpClient = chttp.NewFastHTTPClient(&fasthttp.Client{
// Use given TOR proxy to reach the hidden services
Dial: fasthttpproxy.FasthttpSocksDialer(ctx.String("tor-uri")),
Dial: fasthttpproxy.FasthttpSocksDialer(provider.GetValue("tor-uri")),
// Disable SSL verification since we do not really care about this
TLSConfig: &tls.Config{InsecureSkipVerify: true},
ReadTimeout: time.Second * 5,
WriteTimeout: time.Second * 5,
Name: ctx.String("user-agent"),
Name: provider.GetValue("user-agent"),
})
// Create the subscriber
sub, err := event.NewSubscriber(ctx.String("hub-uri"))
cl, err := provider.Clock()
if err != nil {
return err
}
defer sub.Close()
state.clock = cl
// Create the ConfigAPI client
keys := []string{client.AllowedMimeTypesKey}
configClient, err := client.NewConfigClient(ctx.String("config-api-uri"), sub, keys)
configClient, err := provider.ConfigClient([]string{configapi.AllowedMimeTypesKey})
if err != nil {
log.Err(err).Msg("error while creating config client")
return err
}
state.configClient = configClient
state := state{
httpClient: httpClient,
clock: &clock.SystemClock{},
configClient: configClient,
}
return nil
}
if err := sub.Subscribe(event.NewURLExchange, "crawlingQueue", state.handleNewURLEvent); err != nil {
return err
// Subscribers return the process subscribers
func (state *State) Subscribers() []process.SubscriberDef {
return []process.SubscriberDef{
{Exchange: event.NewURLExchange, Queue: "crawlingQueue", Handler: state.handleNewURLEvent},
}
log.Info().Msg("Successfully initialized tdsh-crawler. Waiting for URLs")
// Handle graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
// Block until we receive our signal.
<-c
return nil
}
type state struct {
httpClient http.Client
clock clock.Clock
configClient client.Client
// HTTPHandler returns the HTTP API the process expose
func (state *State) HTTPHandler(provider process.Provider) http.Handler {
return nil
}
func (state *state) handleNewURLEvent(subscriber event.Subscriber, msg event.RawMessage) error {
func (state *State) handleNewURLEvent(subscriber event.Subscriber, msg event.RawMessage) error {
var evt event.NewURLEvent
if err := subscriber.Read(&msg, &evt); err != nil {
return err
}
b, headers, err := crawURL(state.httpClient, evt.URL, state.configClient)
if err != nil {
return err
}
log.Debug().Str("url", evt.URL).Msg("Processing URL")
res := event.NewResourceEvent{
URL: evt.URL,
Body: b,
Headers: headers,
Time: state.clock.Now(),
}
if err := subscriber.PublishEvent(&res); err != nil {
return err
}
return nil
}
func crawURL(httpClient http.Client, url string, configClient client.Client) (string, map[string]string, error) {
log.Debug().Str("url", url).Msg("Processing URL")
r, err := httpClient.Get(url)
r, err := state.httpClient.Get(evt.URL)
if err != nil {
return "", nil, err
return err
}
// Determinate if content type is allowed
allowed := false
contentType := r.Headers()["Content-Type"]
if allowedMimeTypes, err := configClient.GetAllowedMimeTypes(); err == nil {
if allowedMimeTypes, err := state.configClient.GetAllowedMimeTypes(); err == nil {
if len(allowedMimeTypes) == 0 {
allowed = true
}
@ -163,14 +123,25 @@ func crawURL(httpClient http.Client, url string, configClient client.Client) (st
}
if !allowed {
err := fmt.Errorf("forbidden content type : %s", contentType)
return "", nil, err
return fmt.Errorf("%s (%s): %w", evt.URL, contentType, errContentTypeNotAllowed)
}
// Ready body
b, err := ioutil.ReadAll(r.Body())
if err != nil {
return "", nil, err
return err
}
res := event.NewResourceEvent{
URL: evt.URL,
Body: string(b),
Headers: r.Headers(),
Time: state.clock.Now(),
}
return string(b), r.Headers(), nil
if err := subscriber.PublishEvent(&res); err != nil {
return err
}
return nil
}

@ -1,6 +1,7 @@
package crawler
import (
"errors"
"github.com/creekorful/trandoshan/internal/clock_mock"
"github.com/creekorful/trandoshan/internal/configapi/client"
"github.com/creekorful/trandoshan/internal/configapi/client_mock"
@ -13,151 +14,115 @@ import (
"time"
)
func TestCrawlURLForbiddenContentType(t *testing.T) {
func TestHandleNewURLEvent(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
subscriberMock := event_mock.NewMockSubscriber(mockCtrl)
httpClientMock := http_mock.NewMockClient(mockCtrl)
url := "https://example.onion"
configClientMock := client_mock.NewMockClient(mockCtrl)
configClientMock.EXPECT().GetAllowedMimeTypes().Return([]client.MimeType{{ContentType: "text/plain", Extensions: nil}}, nil)
httpResponseMock := http_mock.NewMockResponse(mockCtrl)
httpResponseMock.EXPECT().Headers().Return(map[string]string{"Content-Type": "image/png"})
httpClientMock.EXPECT().Get(url).Return(httpResponseMock, nil)
body, headers, err := crawURL(httpClientMock, url, configClientMock)
if body != "" || headers != nil || err == nil {
t.Fail()
}
}
func TestCrawlURLSameContentType(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
httpClientMock := http_mock.NewMockClient(mockCtrl)
clockMock := clock_mock.NewMockClock(mockCtrl)
configClientMock := client_mock.NewMockClient(mockCtrl)
httpResponseMock := http_mock.NewMockResponse(mockCtrl)
url := "https://example.onion"
s := State{
httpClient: httpClientMock,
configClient: configClientMock,
clock: clockMock,
}
type test struct {
// the incoming url
url string
// the response headers
responseHeaders map[string]string
// the response body
responseBody string
// internal state: allowed mime types
allowedMimeTypes []client.MimeType
contentType string
// is the test expected to pass?
pass bool
}
tests := []test{
{
allowedMimeTypes: []client.MimeType{{ContentType: "text/plain", Extensions: nil}},
contentType: "text/plain",
url: "https://example.onion/image.png?id=12&test=2",
responseHeaders: map[string]string{"Content-Type": "text/plain", "Server": "Debian"},
responseBody: "Hello",
allowedMimeTypes: []client.MimeType{
{ContentType: "text/plain", Extensions: nil},
{ContentType: "text/css", Extensions: nil},
},
pass: true,
},
{
url: "https://example.onion",
responseHeaders: map[string]string{"Content-Type": "text/plain"},
responseBody: "Hello",
allowedMimeTypes: []client.MimeType{},
pass: true,
},
{
allowedMimeTypes: []client.MimeType{{ContentType: "text/", Extensions: nil}},
contentType: "text/plain",
url: "https://example.onion",
responseHeaders: map[string]string{"Content-Type": "text/plain"},
responseBody: "Hello",
allowedMimeTypes: []client.MimeType{
{
ContentType: "text/",
Extensions: nil,
},
},
pass: true,
},
{
url: "https://example.onion/image.png",
responseHeaders: map[string]string{"Content-Type": "image/png"},
responseBody: "Hello",
allowedMimeTypes: []client.MimeType{
{
ContentType: "text/plain",
Extensions: nil,
},
},
pass: false,
},
}
for _, test := range tests {
msg := event.RawMessage{}
subscriberMock.EXPECT().
Read(&msg, &event.NewURLEvent{}).
SetArg(1, event.NewURLEvent{URL: test.url}).
Return(nil)
// mock crawling
httpResponseMock.EXPECT().Headers().Return(test.responseHeaders)
httpClientMock.EXPECT().Get(test.url).Return(httpResponseMock, nil)
// mock config retrieval
configClientMock.EXPECT().GetAllowedMimeTypes().Return(test.allowedMimeTypes, nil)
httpResponseMock.EXPECT().Headers().Times(2).Return(map[string]string{"Content-Type": test.contentType})
httpResponseMock.EXPECT().Body().Return(strings.NewReader("Hello"))
if test.pass {
httpResponseMock.EXPECT().Headers().Return(test.responseHeaders)
httpResponseMock.EXPECT().Body().Return(strings.NewReader(test.responseBody))
httpClientMock.EXPECT().Get(url).Return(httpResponseMock, nil)
tn := time.Now()
clockMock.EXPECT().Now().Return(tn)
body, headers, err := crawURL(httpClientMock, url, configClientMock)
if err != nil {
t.Fail()
// if test should pass expect event publishing
subscriberMock.EXPECT().PublishEvent(&event.NewResourceEvent{
URL: test.url,
Body: test.responseBody,
Headers: test.responseHeaders,
Time: tn,
}).Return(nil)
}
if body != "Hello" {
t.Fail()
}
if len(headers) != 1 {
t.Fail()
err := s.handleNewURLEvent(subscriberMock, msg)
if test.pass && err != nil {
t.Errorf("test should have passed but has failed with: %s", err)
}
if headers["Content-Type"] != test.contentType {
t.Fail()
if !test.pass && !errors.Is(err, errContentTypeNotAllowed) {
t.Errorf("test shouldn't have passed but hasn't returned expected error: %s", err)
}
}
}
func TestCrawlURLNoContentTypeFiltering(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
httpClientMock := http_mock.NewMockClient(mockCtrl)
url := "https://example.onion"
configClientMock := client_mock.NewMockClient(mockCtrl)
configClientMock.EXPECT().GetAllowedMimeTypes().Return([]client.MimeType{}, nil)
httpResponseMock := http_mock.NewMockResponse(mockCtrl)
httpResponseMock.EXPECT().Headers().Times(2).Return(map[string]string{"Content-Type": "text/plain"})
httpResponseMock.EXPECT().Body().Return(strings.NewReader("Hello"))
httpClientMock.EXPECT().Get(url).Return(httpResponseMock, nil)
body, headers, err := crawURL(httpClientMock, url, configClientMock)
if err != nil {
t.Fail()
}
if body != "Hello" {
t.Fail()
}
if len(headers) != 1 {
t.Fail()
}
if headers["Content-Type"] != "text/plain" {
t.Fail()
}
}
func TestHandleNewURLEvent(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
subscriberMock := event_mock.NewMockSubscriber(mockCtrl)
httpClientMock := http_mock.NewMockClient(mockCtrl)
httpResponseMock := http_mock.NewMockResponse(mockCtrl)
clockMock := clock_mock.NewMockClock(mockCtrl)
configClientMock := client_mock.NewMockClient(mockCtrl)
msg := event.RawMessage{}
subscriberMock.EXPECT().
Read(&msg, &event.NewURLEvent{}).
SetArg(1, event.NewURLEvent{URL: "https://example.onion/image.png?id=12&test=2"}).
Return(nil)
httpResponseMock.EXPECT().Headers().Times(2).Return(map[string]string{"Content-Type": "text/plain", "Server": "Debian"})
httpResponseMock.EXPECT().Body().Return(strings.NewReader("Hello"))
httpClientMock.EXPECT().Get("https://example.onion/image.png?id=12&test=2").Return(httpResponseMock, nil)
tn := time.Now()
clockMock.EXPECT().Now().Return(tn)
configClientMock.EXPECT().GetAllowedMimeTypes().
Return([]client.MimeType{
{ContentType: "text/plain", Extensions: nil},
{ContentType: "text/css", Extensions: nil},
}, nil)
subscriberMock.EXPECT().PublishEvent(&event.NewResourceEvent{
URL: "https://example.onion/image.png?id=12&test=2",
Body: "Hello",
Headers: map[string]string{"Content-Type": "text/plain", "Server": "Debian"},
Time: tn,
}).Return(nil)
s := state{
httpClient: httpClientMock,
configClient: configClientMock,
clock: clockMock,
}
if err := s.handleNewURLEvent(subscriberMock, msg); err != nil {
t.Fail()
}
}

@ -6,74 +6,58 @@ import (
"github.com/PuerkitoBio/purell"
"github.com/creekorful/trandoshan/api"
"github.com/creekorful/trandoshan/internal/event"
"github.com/creekorful/trandoshan/internal/logging"
"github.com/creekorful/trandoshan/internal/util"
"github.com/creekorful/trandoshan/internal/process"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"mvdan.cc/xurls/v2"
"os"
"os/signal"
"net/http"
"strings"
"syscall"
)
// GetApp return the extractor app
func GetApp() *cli.App {
return &cli.App{
Name: "tdsh-extractor",
Version: "0.7.0",
Usage: "Trandoshan extractor component",
Flags: []cli.Flag{
logging.GetLogFlag(),
util.GetHubURI(),
util.GetAPIURIFlag(),
util.GetAPITokenFlag(),
},
Action: execute,
}
// State represent the application state
type State struct {
apiClient api.API
}
func execute(ctx *cli.Context) error {
logging.ConfigureLogger(ctx)
// Name return the process name
func (state *State) Name() string {
return "extractor"
}
log.Info().
Str("ver", ctx.App.Version).
Str("hub-uri", ctx.String("hub-uri")).
Str("api-uri", ctx.String("api-uri")).
Msg("Starting tdsh-extractor")
// CommonFlags return process common flags
func (state *State) CommonFlags() []string {
return []string{process.HubURIFlag, process.APIURIFlag, process.APITokenFlag}
}
apiClient := util.GetAPIClient(ctx)
// CustomFlags return process custom flags
func (state *State) CustomFlags() []cli.Flag {
return []cli.Flag{}
}
// Create the event subscriber
sub, err := event.NewSubscriber(ctx.String("hub-uri"))
// Initialize the process
func (state *State) Initialize(provider process.Provider) error {
apiClient, err := provider.APIClient()
if err != nil {
return err
}
defer sub.Close()
state.apiClient = apiClient
state := state{apiClient: apiClient}
return nil
}
if err := sub.Subscribe(event.NewResourceExchange, "extractingQueue", state.handleNewResourceEvent); err != nil {
return err
// Subscribers return the process subscribers
func (state *State) Subscribers() []process.SubscriberDef {
return []process.SubscriberDef{
{Exchange: event.NewResourceExchange, Queue: "extractingQueue", Handler: state.handleNewResourceEvent},
}
log.Info().Msg("Successfully initialized tdsh-extractor. Waiting for resources")
// Handle graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
// Block until we receive our signal.
<-c
return nil
}
type state struct {
apiClient api.API
// HTTPHandler returns the HTTP API the process expose
func (state *State) HTTPHandler(provider process.Provider) http.Handler {
return nil
}
func (state *state) handleNewResourceEvent(subscriber event.Subscriber, msg event.RawMessage) error {
func (state *State) handleNewResourceEvent(subscriber event.Subscriber, msg event.RawMessage) error {
var evt event.NewResourceEvent
if err := subscriber.Read(&msg, &evt); err != nil {
return err

@ -126,7 +126,7 @@ This is sparta (hosted on https://example.org)
PublishEvent(&event.FoundURLEvent{URL: "https://google.com/test?test=test"}).
Return(nil)
s := state{apiClient: apiClientMock}
s := State{apiClient: apiClientMock}
if err := s.handleNewResourceEvent(subscriberMock, msg); err != nil {
t.FailNow()
}

@ -0,0 +1,229 @@
package process
import (
"context"
"fmt"
"github.com/creekorful/trandoshan/api"
"github.com/creekorful/trandoshan/internal/clock"
configapi "github.com/creekorful/trandoshan/internal/configapi/client"
"github.com/creekorful/trandoshan/internal/event"
"github.com/creekorful/trandoshan/internal/logging"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
const (
version = "0.7.0"
// APIURIFlag is the api-uri flag
APIURIFlag = "api-uri"
// APITokenFlag is the api-token flag
APITokenFlag = "api-token"
// HubURIFlag is the hub-uri flag
HubURIFlag = "hub-uri"
// ConfigAPIURIFlag is the config-api-uri flag
ConfigAPIURIFlag = "config-api-uri"
)
// Provider is the implementation provider
type Provider interface {
// Clock return a clock implementation
Clock() (clock.Clock, error)
// ConfigClient return a new configured configapi.Client
ConfigClient(keys []string) (configapi.Client, error)
// APIClient return a new configured api.API (client)
APIClient() (api.API, error)
// Subscriber return a new configured subscriber
Subscriber() (event.Subscriber, error)
// Publisher return a new configured publisher
Publisher() (event.Publisher, error)
// GetValue return value for given key
GetValue(key string) string
// GetValue return values for given key
GetValues(key string) []string
}
type defaultProvider struct {
ctx *cli.Context
}
// NewDefaultProvider create a brand new default provider using given cli.Context
func NewDefaultProvider(ctx *cli.Context) Provider {
return &defaultProvider{ctx: ctx}
}
func (p *defaultProvider) Clock() (clock.Clock, error) {
return &clock.SystemClock{}, nil
}
func (p *defaultProvider) ConfigClient(keys []string) (configapi.Client, error) {
sub, err := p.Subscriber()
if err != nil {
return nil, err
}
return configapi.NewConfigClient(p.ctx.String(ConfigAPIURIFlag), sub, keys)
}
func (p *defaultProvider) APIClient() (api.API, error) {
return api.NewClient(p.ctx.String(APIURIFlag), p.ctx.String(APITokenFlag)), nil
}
func (p *defaultProvider) Subscriber() (event.Subscriber, error) {
return event.NewSubscriber(p.ctx.String(HubURIFlag))
}
func (p *defaultProvider) Publisher() (event.Publisher, error) {
return event.NewPublisher(p.ctx.String(HubURIFlag))
}
func (p *defaultProvider) GetValue(key string) string {
return p.ctx.String(key)
}
func (p *defaultProvider) GetValues(key string) []string {
return p.ctx.StringSlice(key)
}
// SubscriberDef is the subscriber definition
type SubscriberDef struct {
Exchange string
Queue string
Handler event.Handler
}
// Process is a component of Trandoshan
type Process interface {
Name() string
CommonFlags() []string
CustomFlags() []cli.Flag
Initialize(provider Provider) error
Subscribers() []SubscriberDef
HTTPHandler(provider Provider) http.Handler
}
// MakeApp return cli.App corresponding for given Process
func MakeApp(process Process) *cli.App {
app := &cli.App{
Name: fmt.Sprintf("tdsh-%s", process.Name()),
Version: version,
Usage: fmt.Sprintf("Trandoshan %s component", process.Name()),
Flags: []cli.Flag{
logging.GetLogFlag(),
},
Action: execute(process),
}
// Add common flags
flags := getCustomFlags()
for _, flag := range process.CommonFlags() {
if customFlag, contains := flags[flag]; contains {
app.Flags = append(app.Flags, customFlag)
}
}
// Add custom flags
for _, flag := range process.CustomFlags() {
app.Flags = append(app.Flags, flag)
}
return app
}
func execute(process Process) cli.ActionFunc {
return func(c *cli.Context) error {
provider := NewDefaultProvider(c)
// Common setup
logging.ConfigureLogger(c)
// Custom setup
if err := process.Initialize(provider); err != nil {
return err
}
// Create subscribers if any
if len(process.Subscribers()) > 0 {
sub, err := provider.Subscriber()
if err != nil {
return err
}
// TODO sub.Close()
for _, subscriberDef := range process.Subscribers() {
if err := sub.Subscribe(subscriberDef.Exchange, subscriberDef.Queue, subscriberDef.Handler); err != nil {
return err
}
}
}
var srv *http.Server
// Expose HTTP API if any
if h := process.HTTPHandler(provider); h != nil {
srv = &http.Server{
Addr: "0.0.0.0:8080",
// Good practice to set timeouts to avoid Slowloris attacks.
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
Handler: h, // Pass our instance of gorilla/mux in.
}
go func() {
_ = srv.ListenAndServe()
}()
}
log.Info().
Str("ver", c.App.Version).
Msg(fmt.Sprintf("Started %s", c.App.Name))
// Handle graceful shutdown
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
// Block until we receive our signal.
<-ch
// Close HTTP API if any
if srv != nil {
_ = srv.Shutdown(context.Background())
}
// Connections are deferred here
return nil
}
}
func getCustomFlags() map[string]cli.Flag {
flags := map[string]cli.Flag{}
flags[HubURIFlag] = &cli.StringFlag{
Name: HubURIFlag,
Usage: "URI to the hub (event) server",
Required: true,
}
flags[APIURIFlag] = &cli.StringFlag{
Name: APIURIFlag,
Usage: "URI to the API server",
Required: true,
}
flags[APITokenFlag] = &cli.StringFlag{
Name: APITokenFlag,
Usage: "Token to use to authenticate against the API",
Required: true,
}
flags[ConfigAPIURIFlag] = &cli.StringFlag{
Name: ConfigAPIURIFlag,
Usage: "URI to the ConfigAPI server",
Required: true,
}
return flags
}

@ -4,17 +4,14 @@ import (
"errors"
"fmt"
"github.com/creekorful/trandoshan/api"
"github.com/creekorful/trandoshan/internal/configapi/client"
configapi "github.com/creekorful/trandoshan/internal/configapi/client"
"github.com/creekorful/trandoshan/internal/event"
"github.com/creekorful/trandoshan/internal/logging"
"github.com/creekorful/trandoshan/internal/util"
"github.com/creekorful/trandoshan/internal/process"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
@ -26,78 +23,58 @@ var (
errHostnameNotAllowed = errors.New("hostname is not allowed")
)
// GetApp return the scheduler app
func GetApp() *cli.App {
return &cli.App{
Name: "tdsh-scheduler",
Version: "0.7.0",
Usage: "Trandoshan scheduler component",
Flags: []cli.Flag{
logging.GetLogFlag(),
util.GetHubURI(),
util.GetAPIURIFlag(),
util.GetAPITokenFlag(),
util.GetConfigAPIURIFlag(),
},
Action: execute,
}
// State represent the application state
type State struct {
apiClient api.API
configClient configapi.Client
}
func execute(ctx *cli.Context) error {
logging.ConfigureLogger(ctx)
// Name return the process name
func (state *State) Name() string {
return "scheduler"
}
log.Info().
Str("ver", ctx.App.Version).
Str("hub-uri", ctx.String("hub-uri")).
Str("api-uri", ctx.String("api-uri")).
Str("config-api-uri", ctx.String("config-api-uri")).
Msg("Starting tdsh-scheduler")
// CommonFlags return process common flags
func (state *State) CommonFlags() []string {
return []string{process.HubURIFlag, process.APIURIFlag, process.APITokenFlag, process.ConfigAPIURIFlag}
}
// Create the API client
apiClient := util.GetAPIClient(ctx)
// CustomFlags return process custom flags
func (state *State) CustomFlags() []cli.Flag {
return []cli.Flag{}
}
// Create the subscriber
sub, err := event.NewSubscriber(ctx.String("hub-uri"))
// Initialize the process
func (state *State) Initialize(provider process.Provider) error {
apiClient, err := provider.APIClient()
if err != nil {
return err
}
defer sub.Close()
state.apiClient = apiClient
// Create the ConfigAPI client
keys := []string{client.ForbiddenMimeTypesKey, client.ForbiddenHostnamesKey, client.RefreshDelayKey}
configClient, err := client.NewConfigClient(ctx.String("config-api-uri"), sub, keys)
keys := []string{configapi.ForbiddenMimeTypesKey, configapi.ForbiddenHostnamesKey, configapi.RefreshDelayKey}
configClient, err := provider.ConfigClient(keys)
if err != nil {
log.Err(err).Msg("error while creating config client")
return err
}
state.configClient = configClient
state := state{
apiClient: apiClient,
configClient: configClient,
}
return nil
}
if err := sub.Subscribe(event.FoundURLExchange, "schedulingQueue", state.handleURLFoundEvent); err != nil {
return err
// Subscribers return the process subscribers
func (state *State) Subscribers() []process.SubscriberDef {
return []process.SubscriberDef{
{Exchange: event.FoundURLExchange, Queue: "schedulingQueue", Handler: state.handleURLFoundEvent},
}
log.Info().Msg("Successfully initialized tdsh-scheduler. Waiting for URLs")
// Handle graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
// Block until we receive our signal.
<-c
return nil
}
type state struct {
apiClient api.API
configClient client.Client
// HTTPHandler returns the HTTP API the process expose
func (state *State) HTTPHandler(provider process.Provider) http.Handler {
return nil
}
func (state *state) handleURLFoundEvent(subscriber event.Subscriber, msg event.RawMessage) error {
func (state *State) handleURLFoundEvent(subscriber event.Subscriber, msg event.RawMessage) error {
var evt event.FoundURLEvent
if err := subscriber.Read(&msg, &evt); err != nil {
return err

@ -30,7 +30,7 @@ func TestHandleMessageNotOnion(t *testing.T) {
SetArg(1, event.FoundURLEvent{URL: url}).
Return(nil)
s := state{
s := State{
apiClient: apiClientMock,
configClient: configClientMock,
}
@ -51,7 +51,7 @@ func TestHandleMessageWrongProtocol(t *testing.T) {
msg := event.RawMessage{}
s := state{
s := State{
apiClient: apiClientMock,
configClient: configClientMock,
}
@ -95,7 +95,7 @@ func TestHandleMessageAlreadyCrawled(t *testing.T) {
configClientMock.EXPECT().GetForbiddenHostnames().Return([]client.ForbiddenHostname{}, nil)
configClientMock.EXPECT().GetRefreshDelay().Return(client.RefreshDelay{Delay: -1}, nil)
s := state{
s := State{
apiClient: apiClientMock,
configClient: configClientMock,
}
@ -124,7 +124,7 @@ func TestHandleMessageForbiddenExtensions(t *testing.T) {
configClientMock.EXPECT().GetForbiddenMimeTypes().Return([]client.MimeType{{Extensions: []string{"png"}}}, nil)
s := state{
s := State{
apiClient: apiClientMock,
configClient: configClientMock,
}
@ -177,7 +177,7 @@ func TestHandleMessageHostnameForbidden(t *testing.T) {
configClientMock.EXPECT().GetForbiddenMimeTypes().Return([]client.MimeType{}, nil)
configClientMock.EXPECT().GetForbiddenHostnames().Return(test.forbiddenHostnames, nil)
s := state{
s := State{
apiClient: apiClientMock,
configClient: configClientMock,
}
@ -219,7 +219,7 @@ func TestHandleMessage(t *testing.T) {
configClientMock.EXPECT().GetForbiddenHostnames().Return([]client.ForbiddenHostname{}, nil)
configClientMock.EXPECT().GetRefreshDelay().Return(client.RefreshDelay{Delay: -1}, nil)
s := state{
s := State{
apiClient: apiClientMock,
configClient: configClientMock,
}

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/creekorful/trandoshan/api"
"github.com/creekorful/trandoshan/internal/logging"
"github.com/creekorful/trandoshan/internal/util"
"github.com/olekukonko/tablewriter"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
@ -14,18 +13,23 @@ import (
// GetApp returns the Trandoshan CLI app
func GetApp() *cli.App {
apiFlag := util.GetAPIURIFlag()
apiFlag.Value = "http://localhost:15005"
apiFlag.Required = false
return &cli.App{
Name: "trandoshanctl",
Version: "0.7.0",
Usage: "Trandoshan CLI",
Flags: []cli.Flag{
logging.GetLogFlag(),
apiFlag,
util.GetAPITokenFlag(),
&cli.StringFlag{
Name: "api-uri",
Usage: "URI to the API server",
Value: "http://localhost:15005",
Required: false,
},
&cli.StringFlag{
Name: "api-token",
Usage: "Token to use to authenticate against the API",
Required: true,
},
},
Commands: []*cli.Command{
{
@ -58,7 +62,7 @@ func schedule(c *cli.Context) error {
url := c.Args().First()
// Create the API client
apiClient := util.GetAPIClient(c)
apiClient := api.NewClient(c.String("api-uri"), c.String("api-token"))
if err := apiClient.ScheduleURL(url); err != nil {
log.Err(err).Str("url", url).Msg("Unable to schedule crawling for URL")
@ -74,7 +78,7 @@ func search(c *cli.Context) error {
keyword := c.Args().First()
// Create the API client
apiClient := util.GetAPIClient(c)
apiClient := api.NewClient(c.String("api-uri"), c.String("api-token"))
params := api.ResSearchParams{
Keyword: keyword,

@ -1,12 +0,0 @@
package util
import "github.com/urfave/cli/v2"
// GetHubURI return the URI of the hub (event) server
func GetHubURI() *cli.StringFlag {
return &cli.StringFlag{
Name: "hub-uri",
Usage: "URI to the hub (event) server",
Required: true,
}
}

@ -1,29 +0,0 @@
package util
import (
"github.com/creekorful/trandoshan/api"
"github.com/urfave/cli/v2"
)
// GetAPITokenFlag return the cli flag to provide API token
func GetAPITokenFlag() *cli.StringFlag {
return &cli.StringFlag{
Name: "api-token",
Usage: "Token to use to authenticate against the API",
Required: true,
}
}
// GetAPIURIFlag return the cli flag to set api uri
func GetAPIURIFlag() *cli.StringFlag {
return &cli.StringFlag{
Name: "api-uri",
Usage: "URI to the API server",
Required: true,
}
}
// GetAPIClient return a new configured API client
func GetAPIClient(c *cli.Context) api.API {
return api.NewClient(c.String("api-uri"), c.String("api-token"))
}

@ -1,12 +0,0 @@
package util
import "github.com/urfave/cli/v2"
// GetConfigAPIURIFlag return the cli flag to set config api uri
func GetConfigAPIURIFlag() *cli.StringFlag {
return &cli.StringFlag{
Name: "config-api-uri",
Usage: "URI to the ConfigAPI server",
Required: true,
}
}
Loading…
Cancel
Save