Initial commit.
commit
3a15f46e5a
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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",
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue