remote-darkness/main.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)
}
}