201 lines
5.5 KiB
Go
201 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gorilla/css/scanner"
|
|
"gopkg.in/alecthomas/kingpin.v2"
|
|
|
|
"git.icedream.tech/icedream/remote-darkness/internal/css"
|
|
)
|
|
|
|
var (
|
|
cli = kingpin.New("remote-darkness-server", "Transforms CSS and linked images on the fly to darken websites.")
|
|
flagListenAddr = cli.Arg("addr", "The address for the server to listen on.").Default(":8080").TCP()
|
|
)
|
|
|
|
var httpClient = http.DefaultClient
|
|
|
|
func init() {
|
|
kingpin.MustParse(cli.Parse(os.Args[1:]))
|
|
}
|
|
|
|
func must(err error) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
log.Fatal(err)
|
|
}
|
|
|
|
func main() {
|
|
httpRouter := gin.New()
|
|
httpRouter.Any("url/:protocol/:host/*path", handlePathRequest)
|
|
httpRouter.POST("inline/css", handleInlineCssRequest)
|
|
|
|
httpServer := new(http.Server)
|
|
httpServer.Addr = (*flagListenAddr).String()
|
|
httpServer.Handler = httpRouter
|
|
must(httpServer.ListenAndServe())
|
|
}
|
|
|
|
func handleCss(context *gin.Context, cssString string) {
|
|
tokens, token, err := css.ParseCSS(cssString, ioutil.Discard)
|
|
// context.Writer.WriteString("/*******************\n")
|
|
// context.Writer.WriteString("TOKEN LOG:\n")
|
|
// buf := new(bytes.Buffer)
|
|
|
|
// if err != nil {
|
|
// context.Writer.WriteString(fmt.Sprintf("ERROR: %s in line %d, col %d\n", err, token.Line, token.Column))
|
|
// context.Writer.WriteString("\n")
|
|
// lines := strings.Split(cssString, "\n")
|
|
// line := lines[token.Line-1]
|
|
// begin := token.Column - 20 - 1
|
|
// beginEllipsis := "..."
|
|
// if begin < 0 {
|
|
// begin = 0
|
|
// beginEllipsis = ""
|
|
// }
|
|
// end := token.Column + 20 - 1
|
|
// endEllipsis := "..."
|
|
// if end > len(line) {
|
|
// end = len(line)
|
|
// endEllipsis = ""
|
|
// }
|
|
// marker := strings.Repeat(" ", (token.Column-begin)-1+len(beginEllipsis)) + "^"
|
|
// context.Writer.WriteString(beginEllipsis + line[begin:end] + endEllipsis + "\n")
|
|
// context.Writer.WriteString(marker + "\n")
|
|
// context.Writer.WriteString("\n")
|
|
// }
|
|
|
|
// for _, token := range tokens {
|
|
// context.Writer.WriteString(token.String() + "\n")
|
|
// buf.WriteString(token.Value())
|
|
// }
|
|
|
|
// if err != nil {
|
|
// context.Writer.WriteString("\n")
|
|
// context.Writer.WriteString(fmt.Sprintf("ERROR: %s in line %d, col %d\n", err, token.Line, token.Column))
|
|
// context.Status(http.StatusFailedDependency)
|
|
// return
|
|
// }
|
|
|
|
// context.Writer.WriteString("*******************/\n\n")
|
|
// copyHeaders(
|
|
// "Content-type",
|
|
// "Content-length")
|
|
// context.Header("Content-type", "text/css; charset=utf-8")
|
|
// io.Copy(context.Writer, buf)
|
|
errStr := ""
|
|
if err != nil {
|
|
errStr = err.Error()
|
|
}
|
|
context.JSON(200, struct {
|
|
Tokens []*css.CSSToken
|
|
LastToken *scanner.Token `json:"LastToken,omitempty"`
|
|
Error string `json:"Error,omitempty"`
|
|
}{
|
|
Tokens: tokens,
|
|
LastToken: token,
|
|
Error: errStr,
|
|
})
|
|
}
|
|
|
|
func handleInlineCssRequest(context *gin.Context) {
|
|
defer context.Request.Body.Close()
|
|
// TODO - limit size since we are reading everything into a string at once
|
|
cssBytes, err := ioutil.ReadAll(context.Request.Body)
|
|
if err != nil {
|
|
context.AbortWithError(http.StatusPreconditionFailed, err)
|
|
return
|
|
}
|
|
// TODO - support other encodings than UTF-8 properly
|
|
cssString := string(cssBytes)
|
|
handleCss(context, cssString)
|
|
}
|
|
|
|
func handlePathRequest(context *gin.Context) {
|
|
urlProtocol := context.Param("protocol")
|
|
urlHost := context.Param("host")
|
|
urlPath := context.Param("path")
|
|
|
|
// filter colon and slashes from protocol
|
|
if strings.Contains(urlProtocol, ":") {
|
|
urlProtocol = strings.SplitN(urlProtocol, ":", 2)[0]
|
|
}
|
|
|
|
// filter leading slashes from path
|
|
urlPath = strings.TrimLeft(urlPath, "/")
|
|
|
|
u, err := url.Parse(fmt.Sprintf("%s://%s/%s",
|
|
urlProtocol,
|
|
urlHost,
|
|
urlPath))
|
|
if err != nil {
|
|
context.String(http.StatusBadRequest, fmt.Sprintf("Can not parse URL: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
// open connection to download file from URL
|
|
log.Printf("%s %s", context.Request.Method, u)
|
|
downloadRequest, err := http.NewRequest(context.Request.Method, u.String(), context.Request.Body)
|
|
if err != nil {
|
|
context.String(http.StatusBadRequest, fmt.Sprintf("Can not create request: %s", err.Error()))
|
|
return
|
|
}
|
|
downloadResponse, err := httpClient.Do(downloadRequest)
|
|
if err != nil {
|
|
context.AbortWithError(http.StatusFailedDependency, err)
|
|
return
|
|
}
|
|
defer downloadResponse.Body.Close()
|
|
|
|
copyHeaders := func(except ...string) {
|
|
headerLoop:
|
|
for key, values := range downloadResponse.Header {
|
|
for _, exceptKey := range except {
|
|
if strings.EqualFold(exceptKey, key) {
|
|
continue headerLoop
|
|
}
|
|
}
|
|
for _, value := range values {
|
|
context.Header(key, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
if downloadResponse.StatusCode != 200 {
|
|
context.Status(downloadResponse.StatusCode)
|
|
copyHeaders()
|
|
io.Copy(context.Writer, downloadResponse.Body)
|
|
return
|
|
}
|
|
contentType := strings.Split(downloadResponse.Header.Get("Content-type"), ";")
|
|
// TODO - images
|
|
switch strings.ToLower(contentType[0]) {
|
|
case "application/css", "text/css": // Cascading Stylesheet
|
|
// TODO - limit size since we are reading everything into a string at once
|
|
cssBytes, err := ioutil.ReadAll(downloadResponse.Body)
|
|
if err != nil {
|
|
context.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
// TODO - support other encodings than UTF-8 properly
|
|
cssString := string(cssBytes)
|
|
handleCss(context, cssString)
|
|
default: // All other content types
|
|
// Just forward as-is
|
|
context.Status(downloadResponse.StatusCode)
|
|
copyHeaders()
|
|
io.Copy(context.Writer, downloadResponse.Body)
|
|
|
|
}
|
|
}
|