0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Go言語でKibana(Elastic Cloud)へのプロキシを作成する

Last updated at Posted at 2020-04-06

この記事では App Engine Standard Environment(2nd gen) を用いて Kibana へのプロキシを作成する方法について紹介しています。

Elastic Cloud の一番安いプランで一つデプロイを持っているのですが、今回そこで可視化しているダッシュボードを不特定多数に公開したくなりました。
そこで困ったのが、Elastic Cloud には Anonymous ユーザ機能がない・・・。
ダッシュボードにアクセスさせるにはユーザにログインしてもらう必要があり、しかしパスワードとかは勝手に変更されないようにしなければならないので、みなさんどうやってるのか調べた結果、

  • nginx や Apache でリバースプロキシ
  • Bearer basic ヘッダーで自動でログイン
  • https://{ユーザ名}:{パスワード}@yoursubdomain.yourdomain/kibana では自動ログインできないので、ヘッダーで送る必要がある(ただ、ヘッダーの方がユーザパスワードを iframe の url に書かなくていいので安心できそう
  • パスワード変更のリクエストをピンポイントで弾く設定を入れる

として対応しているようでした。
この方針で問題ないのですが、自分の場合以下の問題が発生。

  • nginx 立てたくない
  • https なサイトに iframe でダッシュボードを表示したいので nginx を https 化することが必須
  • Let's encrypt とかで自動化したとしても、念のため定期的に証明書の確認しなきゃいけないのが嫌だった
  • nginx 立てるお金がない
  • f1-micro 等無料枠の仮想マシンに建てるにしても証明書管理したくなかった
  • Google App Engine Flexible なら Docker で http なリバプロ立てておけば Google が勝手に https な URL を作ってくれる(*.appspot.com)けど、Flexible Environment には無料枠がない:sob:

と言うわけで、App Engine Standard Environment で Go で適当なプロキシ作ることにしました。
特定のヘッダーにのみ気をつけて GET/POST プロキシさせるだけできれいに表示できた。
(けど、なんか他にいい方法ないですかね??)

main.go
package main

import (
	"context"
	"time"

	"cloud.google.com/go/profiler"
	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()

	router.AppEngine = true

	// Stackdriver Profiler
	profiler.Start(profiler.Config{})

	// login/logout は触れさせない
	// router.Any("/login", proxy.Proxy)
	// router.Any("/logout", proxy.Proxy)
	router.Any("/bundles/*uri", proxy.Proxy)
	router.Any("/built_assets/*uri", proxy.Proxy)
	router.Any("/node_modules/*uri", proxy.Proxy)
	router.Any("/translations/*uri", proxy.Proxy)
	router.Any("/ui/*uri", proxy.Proxy)
	router.Any("/api/*uri", proxy.Proxy)
	router.Any("/s/yourspace/*uri", proxy.Proxy) // FIXME 「yourspace」と言う名前のスペース以外触れさせない

	router.Run()
}
proxy.go
package proxy

import (
	"encoding/base64"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
)

const KibanaURL = "https://yourkibana.us-west-2.aws.found.io:9243" // FIXME

var Authorization = "Basic " + base64.StdEncoding.EncodeToString([]byte("youruser:yourpassword")) // FIXME

func replaceDomain(url string) string {
	return strings.ReplaceAll(url, "https://yourgaeproject.appspot.com", KibanaURL) // FIXME
}

func Proxy(ctx *gin.Context) {
	method := ctx.Request.Method

	httpClient, err := httput.NewClient(ctx, time.Duration(20)*time.Second)
	if err != nil {
		ctx.Status(http.StatusInternalServerError)
		return
	}

	if strings.EqualFold(method, "GET") {
		ProxyGet(ctx, httpClient)
	} else if strings.EqualFold(method, "POST") {
		ProxyPost(ctx, httpClient)
	} else {
		ctx.Status(http.StatusMethodNotAllowed)
		return
	}
}

func ProxyGet(ctx *gin.Context, httpClient *httput.HTTPClient) {
	uri := ctx.Request.URL.String()

	if strings.Contains(uri, "/api/spaces/space") { // スペース一覧の表示を禁止
		ctx.Status(http.StatusForbidden)
		return
	}

	if strings.HasPrefix(uri, "/s/") && !strings.Contains(uri, "/yourspace/") { // FIXME yourspace スペース以外のスペースの表示を禁止
		ctx.Status(http.StatusForbidden)
		return
	}

	isStatic := strings.HasSuffix(uri, ".js") || strings.HasSuffix(uri, ".css") || strings.HasSuffix(uri, ".svg")
	if isStatic {
		staticContent, err := redisut.Get(ctx, uri).Result()
		if err == nil { // Redis に入ってればそれを使う
			if strings.HasSuffix(uri, ".js") {
				ctx.Writer.Header().Add("Content-Type", "text/javascript")
			} else if strings.HasSuffix(uri, ".css") {
				ctx.Writer.Header().Add("Content-Type", "text/css")
			} else if strings.HasSuffix(uri, ".svg") {
				ctx.Writer.Header().Add("Content-Type", "image/svg+xml")
			}

			ctx.String(http.StatusOK, staticContent)

			return
		}
	}

	headers := make(map[string]string)
	headers["Authorization"] = Authorization
	if ctx.Request.Header.Get("kbn-version") != "" {
		headers["kbn-version"] = ctx.Request.Header.Get("kbn-version")
	}
	if ctx.Request.Header.Get("User-Agent") != "" {
		headers["User-Agent"] = ctx.Request.Header.Get("User-Agent")
	}
	if ctx.Request.Header.Get("Origin") != "" {
		headers["Origin"] = replaceDomain(ctx.Request.Header.Get("Origin"))
	}
	if ctx.Request.Header.Get("Referer") != "" {
		headers["Referer"] = replaceDomain(ctx.Request.Header.Get("Referer"))
	}

	resp, err := httpClient.GetURLResponse(ctx, KibanaURL+uri, "GET", headers)
	if err != nil {
		ctx.Status(http.StatusInternalServerError)
		return
	}

	if resp.Header.Get("Location") != "" {
		ctx.Redirect(resp.StatusCode, resp.Header.Get("Location"))
		return
	}
	if resp.Header.Get("kbn-license-sig") != "" {
		ctx.Writer.Header().Add("kbn-license-sig", resp.Header.Get("kbn-license-sig"))
	}
	if resp.Header.Get("kbn-name") != "" {
		ctx.Writer.Header().Add("kbn-name", resp.Header.Get("kbn-name"))
	}
	if resp.Header.Get("kbn-xpack-sig") != "" {
		ctx.Writer.Header().Add("kbn-xpack-sig", resp.Header.Get("kbn-xpack-sig"))
	}
	if resp.Header.Get("Content-Type") != "" {
		ctx.Writer.Header().Add("Content-Type", resp.Header.Get("Content-Type"))
	}
	if resp.Header.Get("x-cloud-request-id") != "" {
		ctx.Writer.Header().Add("x-cloud-request-id", resp.Header.Get("x-cloud-request-id"))
	}
	if resp.Header.Get("x-found-handling-cluster") != "" {
		ctx.Writer.Header().Add("x-found-handling-cluster", resp.Header.Get("x-found-handling-cluster"))
	}
	if resp.Header.Get("x-found-handling-instance") != "" {
		ctx.Writer.Header().Add("x-found-handling-instance", resp.Header.Get("x-found-handling-instance"))
	}
	if resp.Header.Get("x-found-handling-server") != "" {
		ctx.Writer.Header().Add("x-found-handling-server", resp.Header.Get("x-found-handling-server"))
	}

	body, err := httput.GetBody(ctx, resp.Body, nil)
	if err != nil {
		ctx.Status(http.StatusInternalServerError)
		return
	}

	if isStatic {
		redisut.SetNX(ctx, uri, body, redisut.StaticFilesTTL)
	}

	ctx.String(resp.StatusCode, body)
}

func ProxyPost(ctx *gin.Context, httpClient *httput.HTTPClient) {
	uri := ctx.Request.URL.String()

	if strings.Contains(uri, "/internal/security/") { // パスワード等プロフィールの変更を禁止
		ctx.Status(http.StatusForbidden)
		return
	}

	if strings.HasPrefix(uri, "/s/") && !strings.Contains(uri, "/yourspace/") { // FIXME yourspace スペース以外の表示を禁止
		ctx.Status(http.StatusForbidden)
		return
	}

	headers := make(map[string]string)
	headers["Authorization"] = Authorization
	if ctx.Request.Header.Get("kbn-version") != "" {
		headers["kbn-version"] = ctx.Request.Header.Get("kbn-version")
	}
	if ctx.Request.Header.Get("User-Agent") != "" {
		headers["User-Agent"] = ctx.Request.Header.Get("User-Agent")
	}
	if ctx.Request.Header.Get("Content-Length") != "" {
		headers["Content-Length"] = ctx.Request.Header.Get("Content-Length")
	}
	if ctx.Request.Header.Get("Content-Type") != "" {
		headers["Content-Type"] = ctx.Request.Header.Get("Content-Type")
	}
	if ctx.Request.Header.Get("Origin") != "" {
		headers["Origin"] = replaceDomain(ctx.Request.Header.Get("Origin"))
	}
	if ctx.Request.Header.Get("Referer") != "" {
		headers["Referer"] = replaceDomain(ctx.Request.Header.Get("Referer"))
	}

	resp, err := httpClient.PostBodyURLResponse(ctx, KibanaURL+uri, ctx.Request.Body, headers)
	if err != nil {
		ctx.Status(http.StatusInternalServerError)
		return
	}

	if resp.Header.Get("Location") != "" {
		ctx.Redirect(resp.StatusCode, resp.Header.Get("Location"))
		return
	}
	if resp.Header.Get("kbn-license-sig") != "" {
		ctx.Writer.Header().Add("kbn-license-sig", resp.Header.Get("kbn-license-sig"))
	}
	if resp.Header.Get("kbn-name") != "" {
		ctx.Writer.Header().Add("kbn-name", resp.Header.Get("kbn-name"))
	}
	if resp.Header.Get("kbn-xpack-sig") != "" {
		ctx.Writer.Header().Add("kbn-xpack-sig", resp.Header.Get("kbn-xpack-sig"))
	}
	if resp.Header.Get("Content-Type") != "" {
		ctx.Writer.Header().Add("Content-Type", resp.Header.Get("Content-Type"))
	}
	if resp.Header.Get("x-cloud-request-id") != "" {
		ctx.Writer.Header().Add("x-cloud-request-id", resp.Header.Get("x-cloud-request-id"))
	}
	if resp.Header.Get("x-found-handling-cluster") != "" {
		ctx.Writer.Header().Add("x-found-handling-cluster", resp.Header.Get("x-found-handling-cluster"))
	}
	if resp.Header.Get("x-found-handling-instance") != "" {
		ctx.Writer.Header().Add("x-found-handling-instance", resp.Header.Get("x-found-handling-instance"))
	}
	if resp.Header.Get("x-found-handling-server") != "" {
		ctx.Writer.Header().Add("x-found-handling-server", resp.Header.Get("x-found-handling-server"))
	}

	body, err := httput.GetBody(ctx, resp.Body, nil)
	if err != nil {
		ctx.Status(http.StatusInternalServerError)
		return
	}

	ctx.String(resp.StatusCode, body)
}

(httput は GoでGET/POST を参照してください)

もちろん nginx とか立てた方が性能はいいと思います。
redis へのキャッシュも実際にファイルの更新あるかどうか関係なく時間決め打ちで適当なのでとりあえず動けばいい人向きです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?