Initial commit.

master
Icedream 2018-06-06 20:34:27 +02:00
commit 3a15f46e5a
Signed by: icedream
GPG Key ID: 1573F6D8EFE4D0CF
7 changed files with 522 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -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

71
internal/css/parse.go Normal file
View File

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

127
internal/css/parser.go Normal file
View File

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

71
internal/css/token.go Normal file
View File

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

View File

@ -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",
}

7
internal/css/util.go Normal file
View File

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

200
main.go Normal file
View File

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