commit 3a15f46e5a78753e04049a5d3b94873593d35d1e Author: Carl Kittelberger Date: Wed Jun 6 20:34:27 2018 +0200 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d96717 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test \ No newline at end of file diff --git a/internal/css/parse.go b/internal/css/parse.go new file mode 100644 index 0000000..e31a2d4 --- /dev/null +++ b/internal/css/parse.go @@ -0,0 +1,71 @@ +package css + +import ( + "fmt" + "io" + + "github.com/davecgh/go-spew/spew" + "github.com/gorilla/css/scanner" +) + +func ParseCSS(cssString string, log io.Writer) (result []*CSSToken, token *scanner.Token, err error) { + cssScanner := &CSSParser{scanner.New(cssString)} + result = make([]*CSSToken, 0) + io.WriteString(log, "/*******************\n") + io.WriteString(log, "PARSER LOG:\n") + token = cssScanner.tolerantRead(result) +generalLoop: + for token.Type != scanner.TokenEOF && token.Type != scanner.TokenError { + token = cssScanner.Next() + + // process token + switch { + case token.Type == scanner.TokenIdent, + token.Type == scanner.TokenChar, + token.Type == scanner.TokenHash: + var intermediate []*CSSToken + + rule := &CSSToken{Type: TokenType_Rule, SubTokens: []*CSSToken{}} + + // selectors + intermediate, token, err = cssScanner.readSelectors(token) + if err != nil { + break generalLoop + } + rule.SubTokens = append(rule.SubTokens, intermediate...) + io.WriteString(log, spew.Sdump(intermediate)+"\n") + + // "{" + if token.Type != scanner.TokenChar || token.Value != "{" { + err = fmt.Errorf("expected closure begin for rule but got %s", token.String()) + break generalLoop + } + rule.SubTokens = append(rule.SubTokens, &CSSToken{Type: TokenType_Native, NativeToken: token}) + + // closure content + intermediate, token, err = cssScanner.readRuleClosure() + if err != nil { + break generalLoop + } + rule.SubTokens = append(rule.SubTokens, intermediate...) + + // "}" + if token.Type != scanner.TokenChar || token.Value != "}" { + err = fmt.Errorf("expected closure end for rule but got %s", token.String()) + break generalLoop + } + rule.SubTokens = append(rule.SubTokens, &CSSToken{Type: TokenType_Native, NativeToken: token}) + + result = append(result, rule) + + default: + result = append(result, &CSSToken{Type: TokenType_Native, NativeToken: token}) + } + + io.WriteString(log, token.String()+"\n") + } + io.WriteString(log, token.String()+"\n") + io.WriteString(log, "*******************/\n\n") + + return +} diff --git a/internal/css/parser.go b/internal/css/parser.go new file mode 100644 index 0000000..17f6a3f --- /dev/null +++ b/internal/css/parser.go @@ -0,0 +1,127 @@ +package css + +import ( + "fmt" + + "github.com/gorilla/css/scanner" +) + +type CSSParser struct { + *scanner.Scanner +} + +func (parser *CSSParser) tolerantRead(extraTokens []*CSSToken) (token *scanner.Token) { +tokenLoop: + for { + token = parser.Scanner.Next() + switch { + case isNativeNonMachineContent(token): + if extraTokens != nil { + extraTokens = append(extraTokens, &CSSToken{Type: TokenType_Native, NativeToken: token}) + } + default: + break tokenLoop + } + } + return +} + +func (parser *CSSParser) readPropertyValue() (result *CSSToken, token *scanner.Token, err error) { + result = &CSSToken{Type: TokenType_PropertyValue, SubTokens: []*CSSToken{}} +propValueLoop: + for { + token = parser.tolerantRead(result.SubTokens) + switch { + case token.Type == scanner.TokenChar && token.Value == "}" || token.Value == ";": // closure or prop end + break propValueLoop + case token.Type == scanner.TokenEOF || token.Type == scanner.TokenError: + err = fmt.Errorf("expected continuation of property value but got %s", token.String()) + default: + result.SubTokens = append(result.SubTokens, &CSSToken{Type: TokenType_Native, NativeToken: token}) + } + } + return +} + +// ",selector,selector,...selector{" +func (parser *CSSParser) readSelectors(currentToken *scanner.Token) (result []*CSSToken, token *scanner.Token, err error) { + result = []*CSSToken{} + currentSelector := &CSSToken{Type: TokenType_Selector, SubTokens: []*CSSToken{}} + token = currentToken +selectorsLoop: + for { + switch { + case token.Type == scanner.TokenChar && token.Value == "{": // closure begin + result = append(result, + currentSelector) + break selectorsLoop + case token.Type == scanner.TokenChar && token.Value == ",": // new selector upcoming + result = append(result, + currentSelector, + &CSSToken{Type: TokenType_Native, NativeToken: token}) + currentSelector = &CSSToken{Type: TokenType_Selector, SubTokens: []*CSSToken{}} + case token.Type == scanner.TokenS && len(currentSelector.SubTokens) <= 0: // space, current selector still empty + result = append(result, + &CSSToken{Type: TokenType_Native, NativeToken: token}) + case token.Type == scanner.TokenEOF || token.Type == scanner.TokenError: + err = fmt.Errorf("expected selector but got %s", token.String()) + return + default: + currentSelector.SubTokens = append(currentSelector.SubTokens, &CSSToken{Type: TokenType_Native, NativeToken: token}) + } + token = parser.tolerantRead(result) + } + return +} + +// "prop;prop;...prop}" +func (parser *CSSParser) readRuleClosure() (result []*CSSToken, token *scanner.Token, err error) { + result = []*CSSToken{} +closureLoop: + for { + token = parser.tolerantRead(result) + switch { + case token.Type == scanner.TokenChar && token.Value == "}": // closure end + break closureLoop + case token.Type == scanner.TokenEOF || token.Type == scanner.TokenError: + err = fmt.Errorf("expected continuation of rule but got %s", token.String()) + return + case token.Type == scanner.TokenChar, + token.Type == scanner.TokenIdent: // property name + property := &CSSToken{Type: TokenType_Property, SubTokens: []*CSSToken{}} + propName := &CSSToken{Type: TokenType_PropertyName, SubTokens: []*CSSToken{}} + propNameLoop: + for { + switch { + case token.Type == scanner.TokenChar && token.Value == ":": // end of prop name + property.SubTokens = append(property.SubTokens, propName, + &CSSToken{Type: TokenType_Native, NativeToken: token}) + break propNameLoop + default: + propName.SubTokens = append(propName.SubTokens, + &CSSToken{Type: TokenType_Native, NativeToken: token}) + } + token = parser.tolerantRead(result) + } + + // prop value + var intermediate *CSSToken + intermediate, token, err = parser.readPropertyValue() + if err != nil { + break closureLoop + } + property.SubTokens = append(property.SubTokens, intermediate) + + result = append(result, property) + if token.Value == ";" { + result = append(result, &CSSToken{Type: TokenType_Native, NativeToken: token}) + } + if token.Value == "}" { + break closureLoop + } + default: + result = append(result, &CSSToken{Type: TokenType_Native, NativeToken: token}) + } + } + return +} diff --git a/internal/css/token.go b/internal/css/token.go new file mode 100644 index 0000000..da17bab --- /dev/null +++ b/internal/css/token.go @@ -0,0 +1,71 @@ +package css + +import ( + "fmt" + "strings" + + "github.com/gorilla/css/scanner" +) + +type CSSToken struct { + Type tokenType + NativeToken *scanner.Token `json:"NativeToken,omitempty"` + SubTokens []*CSSToken `json:"SubTokens,omitempty"` +} + +func (token *CSSToken) Value() string { + if token.NativeToken != nil { + return token.NativeToken.Value + } + + value := "" + for _, token := range token.SubTokens { + value += token.Value() + } + return value +} + +func (token *CSSToken) NonMachineContent() bool { + switch token.Type { + case TokenType_Native: + switch { + case isNativeNonMachineContent(token.NativeToken): + return true + } + } + return false +} + +func (token *CSSToken) MachineSubTokens() (result []*CSSToken) { + result = make([]*CSSToken, 0) + for _, token := range token.SubTokens { + if token.NonMachineContent() { + continue + } + result = append(result, token) + } + return +} + +func (token *CSSToken) Find(tokenTypes ...tokenType) *CSSToken { + for _, token := range token.SubTokens { + for _, tokenType := range tokenTypes { + if token.Type == tokenType { + return token + } + } + } + return nil +} + +func (token *CSSToken) String() string { + retval := fmt.Sprintf("type=%s native=%s", tokenTypeNames[token.Type], token.NativeToken) + if token.SubTokens != nil { + retval += " {\n" + for _, token := range token.SubTokens { + retval += "\t" + strings.Replace(token.String(), "\n", "\n\t", -1) + "\n" + } + retval += "}" + } + return retval +} diff --git a/internal/css/token_type.go b/internal/css/token_type.go new file mode 100644 index 0000000..0331a24 --- /dev/null +++ b/internal/css/token_type.go @@ -0,0 +1,23 @@ +package css + +type tokenType string + +const ( + TokenType_Native tokenType = "native" + TokenType_Rule = "rule" + TokenType_Selector = "selector" + TokenType_Closure = "closure" + TokenType_Property = "property" + TokenType_PropertyName = "propertyName" + TokenType_PropertyValue = "propertyValue" +) + +var tokenTypeNames = map[tokenType]string{ + TokenType_Native: "native", + TokenType_Rule: "rule", + TokenType_Selector: "selector", + TokenType_Closure: "closure", + TokenType_Property: "property", + TokenType_PropertyName: "propertyName", + TokenType_PropertyValue: "propertyValue", +} diff --git a/internal/css/util.go b/internal/css/util.go new file mode 100644 index 0000000..4427951 --- /dev/null +++ b/internal/css/util.go @@ -0,0 +1,7 @@ +package css + +import "github.com/gorilla/css/scanner" + +func isNativeNonMachineContent(token *scanner.Token) bool { + return token.Type == scanner.TokenComment || token.Type == scanner.TokenS +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..71c2213 --- /dev/null +++ b/main.go @@ -0,0 +1,200 @@ +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) + + } +}