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) } }