/**
 * JSON2FILE GO main source file
 *
 * Copyright 2019-2022 Sergio Talens-Oliag <sto@debian.org>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha1"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"github.com/julienschmidt/httprouter"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"os"
	"path"
	"strconv"
	"strings"
	"time"
)

// Types
type DirData struct {
	path  string
	token string
}

// Constants
const DefaultHost = "0.0.0.0"
const DefaultHttpPort = "80"
const DefaultHttpsPort = "443"
const DefaultSignatureHeaders = "X-Hub-Signature"
const DefaultTokenHeaders = "X-Auth-Token,X-Gitlab-Token"
const DefaultTokenFields = "secret"
const DefaultUrlPrefix = ""

// Global variables
var DMap = make(map[string]DirData)
var SignatureHeaders []string
var TokenHeaders []string
var TokenFields []string
var Debug bool

// Functions
func SaveJSON(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	// Check if the passed directory is on DMap
	dname := ps.ByName("dname")
	ddata, _ok := DMap[dname]
	if _ok == false {
		http.Error(w, "Wrong directory name", http.StatusBadRequest)
		if Debug {
			log.Printf("DEBUG: Wrong directory name '%s' requested from %q",
				dname, r.RemoteAddr)
		}
		return
	}
	// Read body as bytes
	body, _err := ioutil.ReadAll(r.Body)
	if _err != nil {
		http.Error(w, "Could not read request body", http.StatusBadRequest)
		if Debug {
			log.Printf("DEBUG: Bad or no JSON data received from %q",
				r.RemoteAddr)
		}
		return
	}
	// Parse JSON data
	var rj map[string]interface{}
	decoder := json.NewDecoder(bytes.NewReader(body))
	_err = decoder.Decode(&rj)
	if _err != nil {
		http.Error(w, "Bad or no JSON data received", http.StatusBadRequest)
		if Debug {
			log.Printf("DEBUG: Bad or no JSON data received from %q",
				r.RemoteAddr)
		}
		return
	}
	// Check tokens
	if ddata.token != "" {
		good_token := false
		if good_token == false {
			// Validate HMAC signatures using GitHub or Gogs format
			// https://developer.github.com/webhooks/securing/
			if len(SignatureHeaders) > 0 {
				s1sig := hmac.New(sha1.New, []byte(ddata.token))
				s1sig.Write(body)
				s1hex := hex.EncodeToString(s1sig.Sum(nil))
				s1str := fmt.Sprintf("sha1=%s", s1hex)
				s256sig := hmac.New(sha256.New, []byte(ddata.token))
				s256sig.Write(body)
				s256hex := hex.EncodeToString(s256sig.Sum(nil))
				s256str := fmt.Sprintf("sha256=%s", s256hex)
				if Debug {
					log.Printf("DEBUG: sha1hex = '%s'", s1hex)
					log.Printf("DEBUG: sha1str = '%s'", s1str)
					log.Printf("DEBUG: sha256hex = '%s'", s256hex)
					log.Printf("DEBUG: sha256str = '%s'", s256str)
				}
				for i := 0; i < len(SignatureHeaders); i++ {
					tv := r.Header.Get(SignatureHeaders[i])
					if Debug {
						log.Printf("DEBUG: remote signature = '%s'", tv)
					}
					if tv == s1hex || tv == s1str || tv == s256hex || tv == s256str {
						good_token = true
						break
					}
				}
			}
		}
		if good_token == false {
			for i := 0; i < len(TokenHeaders); i++ {
				tv := r.Header.Get(TokenHeaders[i])
				if tv == ddata.token {
					good_token = true
					break
				}
			}
		}
		if good_token == false {
			for i := 0; i < len(TokenFields); i++ {
				tv := rj[TokenFields[i]]
				if tv == ddata.token {
					good_token = true
					break
				}
			}
		}
		if good_token == false {
			http.Error(w, "Bad auth token", http.StatusBadRequest)
			if Debug {
				log.Printf("DEBUG: Bad auth token from %q", r.RemoteAddr)
			}
			return
		}
	}
	// Build file_name and file_path, use UTC for timestamp
	// YYYYMMDD-hhmmss.ms == 20060102-150405.000000000 in GO
	fname := time.Now().UTC().Format("20060102-150405.000000000.json")
	fpath := path.Join(ddata.path, fname)
	// Open file for writing
	f, err := os.OpenFile(fpath, os.O_RDWR|os.O_CREATE, 0666)
	if err != nil {
		log.Printf("ERROR: Request from %q, '%s'", r.RemoteAddr, err.Error())
		http.Error(w, err.Error(), 500)
		return
	}
	defer f.Close()
	// Write JSON
	b, err := json.Marshal(rj)
	if err != nil {
		log.Printf("ERROR: Request from %q, '%s'", r.RemoteAddr, err.Error())
		http.Error(w, err.Error(), 500)
		return
	}
	f.Write(b)
	f.Close()
	fmt.Fprintf(w, "JSON data saved to file '%s'\n", fpath)
	log.Printf("JSON data from %q saved to file '%s'\n", r.RemoteAddr, fpath)
}

func InitDMap() (err int) {
	err = 0
	// Read basedir from environment
	base_dir := os.Getenv("J2F_BASEDIR")
	// Check if basedir exists or fail
	stat, _err := os.Stat(base_dir)
	if _err != nil || stat.IsDir() == false {
		fmt.Fprintf(os.Stderr, "J2F_BASEDIR='%s' is not a directory\n",
			base_dir)
		err = 1
		return
	}
	log.Printf("Base directory: '%s'\n", base_dir)
	// Parse DIRLIST variable
	dir_list_str := os.Getenv("J2F_DIRLIST")
	if dir_list_str != "" {
		dir_list := strings.Split(dir_list_str, ";")
		dir_names := []string{}
		for i := 0; i < len(dir_list); i++ {
			dt := strings.Split(dir_list[i], ":")
			if len(dt) > 0 && len(dt) < 3 {
				var dtoken string
				if len(dt) == 2 {
					dtoken = dt[1]
				}
				dpath := path.Join(base_dir, dt[0])
				_, _err = os.Stat(dpath)
				if os.IsNotExist(_err) {
					_err = os.Mkdir(dpath, os.ModeDir|os.ModePerm)
				}
				// Add '/' to the DMap index string, the catch-all route
				// variable starts by '/'
				if _err == nil {
					if dt[0] == "" {
						dt[0] = "/"
					} else if dt[0][0] != '/' {
						dt[0] = strings.Join([]string{"/", dt[0]}, "")
					}
					DMap[dt[0]] = DirData{path: dpath, token: dtoken}
					dir_names = append(dir_names, dt[0])
				}
			}
		}
		log.Printf("Directories: '%s'\n",
			strings.Join(dir_names, "', '"))
	}
	if len(DMap) == 0 {
		fmt.Fprintf(os.Stderr,
			"J2F_DIRLIST='%s' does not contain valid entries\n",
			dir_list_str)
		err = 1
		return
	}
	return
}

func InitTokenVars() {
	// Read signature headers
	sig_headers_str := os.Getenv("J2F_SIGNATURE_HEADERS")
	if sig_headers_str == "" {
		sig_headers_str = DefaultSignatureHeaders
	}
	SignatureHeaders = strings.Split(sig_headers_str, ",")
	log.Printf("Signature Headers: '%s'\n",
		strings.Join(SignatureHeaders, "', '"))
	// Read token headers
	headers_str := os.Getenv("J2F_TOKEN_HEADERS")
	if headers_str == "" {
		headers_str = DefaultTokenHeaders
	}
	TokenHeaders = strings.Split(headers_str, ",")
	log.Printf("Token Headers: '%s'\n",
		strings.Join(TokenHeaders, "', '"))
	// Read token fields
	fields_str := os.Getenv("J2F_TOKEN_FIELDS")
	if fields_str == "" {
		fields_str = DefaultTokenFields
	}
	TokenFields = strings.Split(fields_str, ",")
	log.Printf("Token Fields: '%s'\n",
		strings.Join(TokenFields, "', '"))
}

func main() {
	Debug = os.Getenv("J2F_DEBUG") != ""
	// Init DMap
	ret := InitDMap()
	if ret != 0 {
		os.Exit(ret)
	}
	// Read token vars
	InitTokenVars()
	// Read server parameters from environment or use defaults
	certfile := os.Getenv("J2F_CERTFILE")
	keyfile := os.Getenv("J2F_KEYFILE")
	use_tls := certfile != "" && keyfile != ""
	bind := os.Getenv("J2F_BIND")
	socket := os.Getenv("J2F_SOCKET")
	url_prefix := os.Getenv("J2F_URL_PREFIX")
	if url_prefix != "" && url_prefix[0] != '/' {
		log.Fatal("J2F_URL_PREFIX must start with '/':  ", url_prefix)
	}
	if socket == "" && bind == "" {
		host := os.Getenv("J2F_HOST")
		port := os.Getenv("J2F_PORT")
		if host == "" {
			host = DefaultHost
		}
		if port == "" {
			if use_tls {
				port = DefaultHttpsPort
			} else {
				port = DefaultHttpPort
			}
		}
		bind = fmt.Sprint(host, ":", port)
	}
	router := httprouter.New()
	if url_prefix == "" {
		router.POST("/*dname", SaveJSON)
	} else if url_prefix[len(url_prefix)-1] == '/' {
		router.POST(url_prefix+"*dname", SaveJSON)
		log.Printf("URL Prefix: '%s'\n", url_prefix)
	} else {
		router.POST(url_prefix+"/*dname", SaveJSON)
		log.Printf("URL Prefix: '%s'\n", url_prefix)
	}
	// Check if we are launched from systemd
	if os.Getenv("LISTEN_PID") == strconv.Itoa(os.Getpid()) {
		log.Printf("Using external file descriptor (systemd)\n")
		f := os.NewFile(3, "from systemd")
		l, err := net.FileListener(f)
		if err != nil {
			log.Fatal(err)
		}
		if use_tls {
			http.ServeTLS(l, router, certfile, keyfile)
		} else {
			http.Serve(l, router)
		}
	} else if socket != "" {
		// Start Server
		os.Remove(socket)
		unixListener, err := net.Listen("unix", socket)
		if err != nil {
			log.Fatal("Listen on UNIX socket failed: ", err)
		} else {
			log.Printf("Listening on UNIX socket: '%s'\n", socket)
		}
		defer unixListener.Close()
		log.Fatal(http.Serve(unixListener, router))
	} else {
		log.Printf("Binding to '%s'\n", bind)
		if use_tls {
			log.Fatal(http.ListenAndServeTLS(bind, certfile, keyfile, router))
		} else {
			log.Fatal(http.ListenAndServe(bind, router))
		}
	}
}
