cleanup
This commit is contained in:
parent
10e0bde07b
commit
4540797cce
|
@ -1,26 +0,0 @@
|
|||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
users/
|
||||
|
||||
.vscode/
|
||||
*.swp
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
}
|
33
Dockerfile
33
Dockerfile
|
@ -1,33 +0,0 @@
|
|||
##
|
||||
## Build Container
|
||||
##
|
||||
FROM golang:1.19-alpine as build
|
||||
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /tmp/owl
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go build -o ./out/owl ./cmd/owl
|
||||
|
||||
|
||||
##
|
||||
## Run Container
|
||||
##
|
||||
FROM alpine:3.9
|
||||
RUN apk add ca-certificates
|
||||
|
||||
COPY --from=build /tmp/owl/out/ /bin/
|
||||
|
||||
# This container exposes port 8080 to the outside world
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the binary program produced by `go install`
|
||||
ENTRYPOINT ["/bin/owl"]
|
BIN
assets/owl.png
BIN
assets/owl.png
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
200
assets/owl.svg
200
assets/owl.svg
|
@ -1,200 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="100mm"
|
||||
height="100mm"
|
||||
viewBox="0 0 100 100"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="owl.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:path-effect
|
||||
effect="mirror_symmetry"
|
||||
start_point="101.5,113.98198"
|
||||
end_point="101.5,177.55836"
|
||||
center_point="101.5,145.77017"
|
||||
id="path-effect4762"
|
||||
is_visible="true"
|
||||
mode="free"
|
||||
discard_orig_path="false"
|
||||
fuse_paths="true"
|
||||
oposite_fuse="false" />
|
||||
<inkscape:path-effect
|
||||
effect="mirror_symmetry"
|
||||
start_point="101.6,77.962793"
|
||||
end_point="101.6,178.13471"
|
||||
center_point="101.6,128.04875"
|
||||
id="path-effect4630"
|
||||
is_visible="true"
|
||||
mode="free"
|
||||
discard_orig_path="false"
|
||||
fuse_paths="true"
|
||||
oposite_fuse="false" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.98994949"
|
||||
inkscape:cx="-5.4384962"
|
||||
inkscape:cy="476.07169"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer5"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1391"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Body"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="translate(0,-197)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer6"
|
||||
inkscape:label="Front"
|
||||
transform="translate(0,-197)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer5"
|
||||
inkscape:label="Ref"
|
||||
style="display:inline"
|
||||
transform="translate(0,-197)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Feet"
|
||||
transform="translate(0,-197)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Nose"
|
||||
transform="translate(0,-197)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Eyes"
|
||||
transform="translate(0,-197)">
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#686560;fill-opacity:1;stroke:#000000;stroke-width:1.46500003;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 72.665922,98.468004 c -3.804657,-4.064063 -8.937651,-7.726186 -7.559523,-20.505208 1.511903,2.173362 6.898064,8.031993 12.095236,8.031993 7.291682,0 22.616673,0.08369 24.398365,0.09355 1.78169,-0.0099 17.10668,-0.09355 24.39836,-0.09355 5.19718,0 10.58334,-5.858631 12.09524,-8.031993 1.37813,12.779022 -3.75487,16.441145 -7.55952,20.505208 0,0 4.84169,14.668796 9.88119,20.229646 10.76577,11.87955 9.28012,38.96737 -13.7775,54.51257 -4.9083,3.30912 -14.73063,5.13452 -25.03777,4.90519 -10.307138,0.22933 -20.129467,-1.59607 -25.037771,-4.90519 -23.057616,-15.5452 -24.543272,-42.63302 -13.777496,-54.51257 5.039494,-5.56085 9.881189,-20.229646 9.881189,-20.229646 z"
|
||||
id="path46"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccsccssc"
|
||||
inkscape:path-effect="#path-effect4630"
|
||||
inkscape:original-d="m 72.665922,98.468004 c -3.804657,-4.064063 -8.937651,-7.726186 -7.559523,-20.505208 1.511903,2.173362 6.898064,8.031993 12.095236,8.031993 7.748513,0 24.568455,0.0945 24.568455,0.0945 9.17095,40.967981 -1.73329,86.382581 24.08866,86.819411 -14.73188,7.45015 -40.288984,6.3743 -49.296521,0.30152 -23.057616,-15.5452 -24.543272,-42.63302 -13.777496,-54.51257 5.039494,-5.56085 9.881189,-20.229646 9.881189,-20.229646 z"
|
||||
transform="matrix(0.89013051,0,0,0.89013051,-40.43726,131.70004)" />
|
||||
<path
|
||||
style="display:inline;fill:#fbe9c4;fill-opacity:1;stroke:#262626;stroke-width:4.06500006;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 74.568197,129.95654 c -1.207452,8.188 -1.187282,34.70878 17.572973,44.03265 3.623482,1.36045 5.80487,3.13501 9.35883,3.62684 3.55396,-0.49183 5.73535,-2.26639 9.35883,-3.62684 18.76025,-9.32387 18.78042,-35.84465 17.57297,-44.03265 C 126.73222,117.4929 107.35585,115.8851 101.5,115.68436 95.644152,115.8851 76.267784,117.4929 74.568197,129.95654 Z"
|
||||
id="path4760"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccscc"
|
||||
inkscape:path-effect="#path-effect4762"
|
||||
inkscape:original-d="m 74.568197,129.95654 c -1.207452,8.188 -1.187282,34.70878 17.572973,44.03265 4.717147,1.77107 6.990299,4.24396 13.02939,3.67496 5.2517,-4.28707 -4.51571,-38.74118 3.27405,-50.98166 11.76071,-18.48023 -5.27857,-11.02487 -5.27857,-11.02487 0,0 -26.593322,-0.4009 -28.597843,14.29892 z"
|
||||
transform="matrix(0.89013051,0,0,0.89013051,-40.43726,131.70004)" />
|
||||
<rect
|
||||
style="opacity:1;fill:#674808;fill-opacity:1;stroke:#efc48c;stroke-width:2.72825003;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4767"
|
||||
width="23.046696"
|
||||
height="2.0186887"
|
||||
x="39.805622"
|
||||
y="258.96619" />
|
||||
<rect
|
||||
style="opacity:1;fill:#674808;fill-opacity:1;stroke:#efc48c;stroke-width:2.72825003;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4767-7"
|
||||
width="23.046698"
|
||||
height="2.0186887"
|
||||
x="39.805622"
|
||||
y="266.51617" />
|
||||
<rect
|
||||
style="opacity:1;fill:#674808;fill-opacity:1;stroke:#efc48c;stroke-width:2.72825003;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4767-7-0"
|
||||
width="23.046698"
|
||||
height="2.0186887"
|
||||
x="39.80563"
|
||||
y="274.1886" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#f1cd6b;fill-opacity:1;stroke:#000000;stroke-width:1.21502817;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 37.113903,280.91958 a 11.943909,11.943909 0 0 0 -11.943581,11.94359 h 23.88762 A 11.943909,11.943909 0 0 0 37.113903,280.91958 Z"
|
||||
id="path4732"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="display:inline;opacity:1;fill:#f1cd6b;fill-opacity:1;stroke:#000000;stroke-width:1.21502817;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 62.767977,281.00369 A 11.943909,11.943909 0 0 0 50.824392,292.94728 H 74.712015 A 11.943909,11.943909 0 0 0 62.767977,281.00369 Z"
|
||||
id="path4732-2" />
|
||||
<path
|
||||
style="fill:#dd9829;fill-opacity:1;stroke:#000000;stroke-width:1.21502817;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 45.440938,246.0794 c 0,0 2.139571,9.72892 5.947619,9.7541 3.839356,0.0254 6.126056,-9.7541 6.126056,-9.7541 l -6.140761,-8.69398 z"
|
||||
id="path4721"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="caccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#fedf89;fill-opacity:1;stroke:#000000;stroke-width:1.95828712;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 34.506691,216.30236 a 18.692076,18.692076 0 0 0 -18.692058,18.69205 18.692076,18.692076 0 0 0 18.692058,18.69206 18.692076,18.692076 0 0 0 16.928008,-10.81661 18.692078,18.692078 0 0 0 16.968945,10.90079 18.692078,18.692078 0 0 0 18.69206,-18.69206 18.692078,18.692078 0 0 0 -18.69206,-18.69252 18.692078,18.692078 0 0 0 -16.929847,10.81983 18.692076,18.692076 0 0 0 -16.967106,-10.90354 z"
|
||||
id="path4607"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#fffefc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 64.955116,221.31207 a 13.794374,13.429993 0 0 0 -13.301924,9.88009 13.794374,13.429993 0 0 0 -13.277543,-9.79636 13.794374,13.429993 0 0 0 -13.794111,13.43025 13.794374,13.429993 0 0 0 13.794111,13.4298 13.794374,13.429993 0 0 0 13.301003,-9.92837 13.794374,13.429993 0 0 0 13.278464,9.8442 13.794374,13.429993 0 0 0 13.794572,-13.4298 13.794374,13.429993 0 0 0 -13.794572,-13.42981 z"
|
||||
id="path4609"
|
||||
inkscape:connector-curvature="0" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4611"
|
||||
cx="40.394402"
|
||||
cy="235.28864"
|
||||
rx="8.9999876"
|
||||
ry="8.8738194" />
|
||||
<circle
|
||||
style="opacity:1;fill:#fffefc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4609-3"
|
||||
cy="230.89021"
|
||||
cx="35.68087"
|
||||
r="5.6899729" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4611-9"
|
||||
cx="-63.020538"
|
||||
cy="235.37274"
|
||||
rx="8.9999876"
|
||||
ry="8.8738194"
|
||||
transform="scale(-1,1)" />
|
||||
<circle
|
||||
style="opacity:1;fill:#fffefc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4609-3-1"
|
||||
cx="-67.734055"
|
||||
cy="230.97433"
|
||||
transform="scale(-1,1)"
|
||||
r="5.0387244" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 10 KiB |
48
auth_test.go
48
auth_test.go
|
@ -1,48 +0,0 @@
|
|||
package owl_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetRedirctUrisLink(t *testing.T) {
|
||||
html := []byte("<link rel=\"redirect_uri\" href=\"http://example.com/redirect\" />")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
uris, err := parser.GetRedirctUris(constructResponse(html))
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
|
||||
assertions.AssertArrayContains(t, uris, "http://example.com/redirect")
|
||||
}
|
||||
|
||||
func TestGetRedirctUrisLinkMultiple(t *testing.T) {
|
||||
html := []byte(`
|
||||
<link rel="redirect_uri" href="http://example.com/redirect1" />
|
||||
<link rel="redirect_uri" href="http://example.com/redirect2" />
|
||||
<link rel="redirect_uri" href="http://example.com/redirect3" />
|
||||
<link rel="foo" href="http://example.com/redirect4" />
|
||||
<link href="http://example.com/redirect5" />
|
||||
`)
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
uris, err := parser.GetRedirctUris(constructResponse(html))
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
|
||||
assertions.AssertArrayContains(t, uris, "http://example.com/redirect1")
|
||||
assertions.AssertArrayContains(t, uris, "http://example.com/redirect2")
|
||||
assertions.AssertArrayContains(t, uris, "http://example.com/redirect3")
|
||||
assertions.AssertLen(t, uris, 3)
|
||||
}
|
||||
|
||||
func TestGetRedirectUrisLinkHeader(t *testing.T) {
|
||||
html := []byte("")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
resp := constructResponse(html)
|
||||
resp.Header = http.Header{"Link": []string{"<http://example.com/redirect>; rel=\"redirect_uri\""}}
|
||||
uris, err := parser.GetRedirctUris(resp)
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
assertions.AssertArrayContains(t, uris, "http://example.com/redirect")
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var domain string
|
||||
var singleUser string
|
||||
var unsafe bool
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(initCmd)
|
||||
|
||||
initCmd.PersistentFlags().StringVar(&domain, "domain", "http://localhost:8080", "Domain to use")
|
||||
initCmd.PersistentFlags().StringVar(&singleUser, "single-user", "", "Use single user mode with given username")
|
||||
initCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "Allow raw html")
|
||||
}
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Creates a new repository",
|
||||
Long: `Creates a new repository`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
_, err := owl.CreateRepository(repoPath, owl.RepoConfig{
|
||||
Domain: domain,
|
||||
SingleUser: singleUser,
|
||||
AllowRawHtml: unsafe,
|
||||
})
|
||||
if err != nil {
|
||||
println("Error creating repository: ", err.Error())
|
||||
} else {
|
||||
println("Repository created: ", repoPath)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var repoPath string
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "owl",
|
||||
Short: "Owl Blogs is a not so static blog generator",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&repoPath, "repo", ".", "Path to the repository to use.")
|
||||
rootCmd.PersistentFlags().StringVar(&user, "user", "", "Username. Required for some commands.")
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
Execute()
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var postTitle string
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(newPostCmd)
|
||||
newPostCmd.PersistentFlags().StringVar(&postTitle, "title", "", "Post title")
|
||||
}
|
||||
|
||||
var newPostCmd = &cobra.Command{
|
||||
Use: "new-post",
|
||||
Short: "Creates a new post",
|
||||
Long: `Creates a new post`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if user == "" {
|
||||
println("Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
if postTitle == "" {
|
||||
println("Post title is required")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := owl.OpenRepository(repoPath)
|
||||
if err != nil {
|
||||
println("Error opening repository: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repo.GetUser(user)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
post, err := user.CreateNewPost(owl.PostMeta{Type: "article", Title: postTitle, Draft: true}, "")
|
||||
if err != nil {
|
||||
println("Error creating post: ", err.Error())
|
||||
} else {
|
||||
println("Post created: ", postTitle)
|
||||
println("Edit: ", post.ContentFile())
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var user string
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(newUserCmd)
|
||||
}
|
||||
|
||||
var newUserCmd = &cobra.Command{
|
||||
Use: "new-user",
|
||||
Short: "Creates a new user",
|
||||
Long: `Creates a new user`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if user == "" {
|
||||
println("Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := owl.OpenRepository(repoPath)
|
||||
if err != nil {
|
||||
println("Error opening repository: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = repo.CreateUser(user)
|
||||
if err != nil {
|
||||
println("Error creating user: ", err.Error())
|
||||
} else {
|
||||
println("User created: ", user)
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"h4kor/owl-blogs"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(resetPasswordCmd)
|
||||
}
|
||||
|
||||
var resetPasswordCmd = &cobra.Command{
|
||||
Use: "reset-password",
|
||||
Short: "Reset the password for a user",
|
||||
Long: `Reset the password for a user`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if user == "" {
|
||||
println("Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := owl.OpenRepository(repoPath)
|
||||
if err != nil {
|
||||
println("Error opening repository: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repo.GetUser(user)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// generate a random password and print it
|
||||
password := owl.GenerateRandomString(16)
|
||||
user.ResetPassword(password)
|
||||
|
||||
fmt.Println("User: ", user.Name())
|
||||
fmt.Println("New Password: ", password)
|
||||
|
||||
},
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
web "h4kor/owl-blogs/cmd/owl/web"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var port int
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(webCmd)
|
||||
|
||||
webCmd.PersistentFlags().IntVar(&port, "port", 8080, "Port to use")
|
||||
}
|
||||
|
||||
var webCmd = &cobra.Command{
|
||||
Use: "web",
|
||||
Short: "Start the web server",
|
||||
Long: `Start the web server`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
web.StartServer(repoPath, port)
|
||||
},
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedirectOnAliases(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - /foo/bar\n"
|
||||
content += " - /foo/baz\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", "/foo/bar", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusMovedPermanently)
|
||||
// Check that Location header is set correctly
|
||||
assertions.AssertEqual(t, rr.Header().Get("Location"), post.UrlPath())
|
||||
}
|
||||
|
||||
func TestNoRedirectOnNonExistingAliases(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - /foo/bar\n"
|
||||
content += " - /foo/baz\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", "/foo/bar2", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusNotFound)
|
||||
|
||||
}
|
||||
|
||||
func TestNoRedirectIfValidPostUrl(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
post2, _ := user.CreateNewPost(owl.PostMeta{Title: "post-2"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - " + post2.UrlPath() + "\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", post2.UrlPath(), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
|
||||
}
|
||||
|
||||
func TestRedirectIfInvalidPostUrl(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - " + user.UrlPath() + "posts/not-a-real-post/" + "\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", user.UrlPath()+"posts/not-a-real-post/", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusMovedPermanently)
|
||||
|
||||
}
|
||||
|
||||
func TestRedirectIfInvalidUserUrl(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - /user/not-real/ \n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", "/user/not-real/", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusMovedPermanently)
|
||||
|
||||
}
|
||||
|
||||
func TestRedirectIfInvalidMediaUrl(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - " + post.UrlMediaPath("not-real") + "\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", post.UrlMediaPath("not-real"), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusMovedPermanently)
|
||||
|
||||
}
|
||||
|
||||
func TestDeepAliasInSingleUserMode(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{SingleUser: "test-1"})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Create tileable textures with GIMP\n"
|
||||
content += "author: h4kor\n"
|
||||
content += "type: post\n"
|
||||
content += "date: Tue, 13 Sep 2016 16:19:09 +0000\n"
|
||||
content += "aliases:\n"
|
||||
content += " - /2016/09/13/create-tileable-textures-with-gimp/\n"
|
||||
content += "categories:\n"
|
||||
content += " - GameDev\n"
|
||||
content += "tags:\n"
|
||||
content += " - gamedev\n"
|
||||
content += " - textures\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", "/2016/09/13/create-tileable-textures-with-gimp/", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusMovedPermanently)
|
||||
|
||||
}
|
|
@ -1,396 +0,0 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"h4kor/owl-blogs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
type IndieauthMetaDataResponse struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
|
||||
ScopesSupported []string `json:"scopes_supported"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
GrantTypesSupported []string `json:"grant_types_supported"`
|
||||
}
|
||||
|
||||
type MeProfileResponse struct {
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
Photo string `json:"photo"`
|
||||
}
|
||||
type MeResponse struct {
|
||||
Me string `json:"me"`
|
||||
Profile MeProfileResponse `json:"profile"`
|
||||
}
|
||||
|
||||
type AccessTokenResponse struct {
|
||||
Me string `json:"me"`
|
||||
TokenType string `json:"token_type"`
|
||||
AccessToken string `json:"access_token"`
|
||||
Scope string `json:"scope"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
func jsonResponse(w http.ResponseWriter, response interface{}) {
|
||||
jsonData, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
println("Error marshalling json: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal server error"))
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write(jsonData)
|
||||
}
|
||||
|
||||
func userAuthMetadataHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
jsonResponse(w, IndieauthMetaDataResponse{
|
||||
Issuer: user.FullUrl(),
|
||||
AuthorizationEndpoint: user.AuthUrl(),
|
||||
TokenEndpoint: user.TokenUrl(),
|
||||
CodeChallengeMethodsSupported: []string{"S256", "plain"},
|
||||
ScopesSupported: []string{"profile"},
|
||||
ResponseTypesSupported: []string{"code"},
|
||||
GrantTypesSupported: []string{"authorization_code"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
// get me, cleint_id, redirect_uri, state and response_type from query
|
||||
me := r.URL.Query().Get("me")
|
||||
clientId := r.URL.Query().Get("client_id")
|
||||
redirectUri := r.URL.Query().Get("redirect_uri")
|
||||
state := r.URL.Query().Get("state")
|
||||
responseType := r.URL.Query().Get("response_type")
|
||||
codeChallenge := r.URL.Query().Get("code_challenge")
|
||||
codeChallengeMethod := r.URL.Query().Get("code_challenge_method")
|
||||
scope := r.URL.Query().Get("scope")
|
||||
|
||||
// check if request is valid
|
||||
missing_params := []string{}
|
||||
if clientId == "" {
|
||||
missing_params = append(missing_params, "client_id")
|
||||
}
|
||||
if redirectUri == "" {
|
||||
missing_params = append(missing_params, "redirect_uri")
|
||||
}
|
||||
if responseType == "" {
|
||||
missing_params = append(missing_params, "response_type")
|
||||
}
|
||||
if len(missing_params) > 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Missing parameters",
|
||||
Message: "Missing parameters: " + strings.Join(missing_params, ", "),
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if responseType == "id" {
|
||||
responseType = "code"
|
||||
}
|
||||
if responseType != "code" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Invalid response_type",
|
||||
Message: "Must be 'code' ('id' converted to 'code' for legacy support).",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if codeChallengeMethod != "" && (codeChallengeMethod != "S256" && codeChallengeMethod != "plain") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Invalid code_challenge_method",
|
||||
Message: "Must be 'S256' or 'plain'.",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
client_id_url, err := url.Parse(clientId)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Invalid client_id",
|
||||
Message: "Invalid client_id: " + clientId,
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
redirect_uri_url, err := url.Parse(redirectUri)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Invalid redirect_uri",
|
||||
Message: "Invalid redirect_uri: " + redirectUri,
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if client_id_url.Host != redirect_uri_url.Host || client_id_url.Scheme != redirect_uri_url.Scheme {
|
||||
// check if redirect_uri is registered
|
||||
resp, _ := repo.HttpClient.Get(clientId)
|
||||
registered_redirects, _ := repo.Parser.GetRedirctUris(resp)
|
||||
is_registered := false
|
||||
for _, registered_redirect := range registered_redirects {
|
||||
if registered_redirect == redirectUri {
|
||||
// redirect_uri is registered
|
||||
is_registered = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !is_registered {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Invalid redirect_uri",
|
||||
Message: redirectUri + " is not registered for " + clientId,
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Double Submit Cookie Pattern
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
|
||||
csrfToken := owl.GenerateRandomString(32)
|
||||
cookie := http.Cookie{
|
||||
Name: "csrf_token",
|
||||
Value: csrfToken,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
reqData := owl.AuthRequestData{
|
||||
Me: me,
|
||||
ClientId: clientId,
|
||||
RedirectUri: redirectUri,
|
||||
State: state,
|
||||
Scope: scope,
|
||||
ResponseType: responseType,
|
||||
CodeChallenge: codeChallenge,
|
||||
CodeChallengeMethod: codeChallengeMethod,
|
||||
User: user,
|
||||
CsrfToken: csrfToken,
|
||||
}
|
||||
|
||||
html, err := owl.RenderUserAuthPage(reqData)
|
||||
if err != nil {
|
||||
println("Error rendering auth page: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal Server Error",
|
||||
Message: "Internal Server Error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
}
|
||||
|
||||
func verifyAuthCodeRequest(user owl.User, w http.ResponseWriter, r *http.Request) (bool, owl.AuthCode) {
|
||||
// get form data from post request
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
println("Error parsing form: ", err.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Error parsing form"))
|
||||
return false, owl.AuthCode{}
|
||||
}
|
||||
code := r.Form.Get("code")
|
||||
client_id := r.Form.Get("client_id")
|
||||
redirect_uri := r.Form.Get("redirect_uri")
|
||||
code_verifier := r.Form.Get("code_verifier")
|
||||
|
||||
// check if request is valid
|
||||
valid, authCode := user.VerifyAuthCode(code, client_id, redirect_uri, code_verifier)
|
||||
if !valid {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Invalid code"))
|
||||
}
|
||||
return valid, authCode
|
||||
}
|
||||
|
||||
func userAuthProfileHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
valid, _ := verifyAuthCodeRequest(user, w, r)
|
||||
if valid {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
jsonResponse(w, MeResponse{
|
||||
Me: user.FullUrl(),
|
||||
Profile: MeProfileResponse{
|
||||
Name: user.Name(),
|
||||
Url: user.FullUrl(),
|
||||
Photo: user.AvatarUrl(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthTokenHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
valid, authCode := verifyAuthCodeRequest(user, w, r)
|
||||
if valid {
|
||||
if authCode.Scope == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Empty scope, no token issued"))
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, duration, err := user.GenerateAccessToken(authCode)
|
||||
if err != nil {
|
||||
println("Error generating access token: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal server error"))
|
||||
return
|
||||
}
|
||||
jsonResponse(w, AccessTokenResponse{
|
||||
Me: user.FullUrl(),
|
||||
TokenType: "Bearer",
|
||||
AccessToken: accessToken,
|
||||
Scope: authCode.Scope,
|
||||
ExpiresIn: duration,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthVerifyHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// get form data from post request
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
println("Error parsing form: ", err.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Error parsing form",
|
||||
Message: "Error parsing form",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
password := r.FormValue("password")
|
||||
client_id := r.FormValue("client_id")
|
||||
redirect_uri := r.FormValue("redirect_uri")
|
||||
response_type := r.FormValue("response_type")
|
||||
state := r.FormValue("state")
|
||||
code_challenge := r.FormValue("code_challenge")
|
||||
code_challenge_method := r.FormValue("code_challenge_method")
|
||||
scope := r.FormValue("scope")
|
||||
|
||||
// CSRF check
|
||||
formCsrfToken := r.FormValue("csrf_token")
|
||||
cookieCsrfToken, err := r.Cookie("csrf_token")
|
||||
|
||||
if err != nil {
|
||||
println("Error getting csrf token from cookie: ", err.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "CSRF Error",
|
||||
Message: "Error getting csrf token from cookie",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if formCsrfToken != cookieCsrfToken.Value {
|
||||
println("Invalid csrf token")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "CSRF Error",
|
||||
Message: "Invalid csrf token",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
password_valid := user.VerifyPassword(password)
|
||||
if !password_valid {
|
||||
redirect := fmt.Sprintf(
|
||||
"%s?error=invalid_password&client_id=%s&redirect_uri=%s&response_type=%s&state=%s",
|
||||
user.AuthUrl(), client_id, redirect_uri, response_type, state,
|
||||
)
|
||||
if code_challenge != "" {
|
||||
redirect += fmt.Sprintf("&code_challenge=%s&code_challenge_method=%s", code_challenge, code_challenge_method)
|
||||
}
|
||||
http.Redirect(w, r,
|
||||
redirect,
|
||||
http.StatusFound,
|
||||
)
|
||||
return
|
||||
} else {
|
||||
// password is valid, generate code
|
||||
code, err := user.GenerateAuthCode(
|
||||
client_id, redirect_uri, code_challenge, code_challenge_method, scope)
|
||||
if err != nil {
|
||||
println("Error generating code: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal Server Error",
|
||||
Message: "Error generating code",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r,
|
||||
fmt.Sprintf(
|
||||
"%s?code=%s&state=%s&iss=%s",
|
||||
redirect_uri, code, state,
|
||||
user.FullUrl(),
|
||||
),
|
||||
http.StatusFound,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,428 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"h4kor/owl-blogs/test/mocks"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAuthPostWrongPassword(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "wrongpassword")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("response_type", "code")
|
||||
form.Add("state", "test_state")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("POST", user.AuthUrl()+"verify/", strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertContains(t, rr.Header().Get("Location"), "error=invalid_password")
|
||||
}
|
||||
|
||||
func TestAuthPostCorrectPassword(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "testpassword")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("response_type", "code")
|
||||
form.Add("state", "test_state")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
req, err := http.NewRequest("POST", user.AuthUrl()+"verify/", strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertContains(t, rr.Header().Get("Location"), "code=")
|
||||
assertions.AssertContains(t, rr.Header().Get("Location"), "state=test_state")
|
||||
assertions.AssertContains(t, rr.Header().Get("Location"), "iss="+user.FullUrl())
|
||||
assertions.AssertContains(t, rr.Header().Get("Location"), "http://example.com/response")
|
||||
}
|
||||
|
||||
func TestAuthPostWithIncorrectCode(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
user.GenerateAuthCode("http://example.com", "http://example.com/response", "", "", "profile")
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("code", "wrongcode")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestAuthPostWithCorrectCode(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", "", "", "profile")
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
// parse response as json
|
||||
type responseType struct {
|
||||
Me string `json:"me"`
|
||||
}
|
||||
var response responseType
|
||||
json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assertions.AssertEqual(t, response.Me, user.FullUrl())
|
||||
|
||||
}
|
||||
|
||||
func TestAuthPostWithCorrectCodeAndPKCE(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
// Create Request and Response
|
||||
code_verifier := "test_code_verifier"
|
||||
// create code challenge
|
||||
h := sha256.New()
|
||||
h.Write([]byte(code_verifier))
|
||||
code_challenge := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "S256", "profile")
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("code_verifier", code_verifier)
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
// parse response as json
|
||||
type responseType struct {
|
||||
Me string `json:"me"`
|
||||
}
|
||||
var response responseType
|
||||
json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assertions.AssertEqual(t, response.Me, user.FullUrl())
|
||||
|
||||
}
|
||||
|
||||
func TestAuthPostWithCorrectCodeAndWrongPKCE(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
// Create Request and Response
|
||||
code_verifier := "test_code_verifier"
|
||||
// create code challenge
|
||||
h := sha256.New()
|
||||
h.Write([]byte(code_verifier + "wrong"))
|
||||
code_challenge := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "S256", "profile")
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("code_verifier", code_verifier)
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestAuthPostWithCorrectCodePKCEPlain(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
// Create Request and Response
|
||||
code_verifier := "test_code_verifier"
|
||||
code_challenge := code_verifier
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "plain", "profile")
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("code_verifier", code_verifier)
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestAuthPostWithCorrectCodePKCEPlainWrong(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
// Create Request and Response
|
||||
code_verifier := "test_code_verifier"
|
||||
code_challenge := code_verifier + "wrong"
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "plain", "profile")
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("code_verifier", code_verifier)
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestAuthRedirectUriNotSet(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockParseLinksHtmlParser{
|
||||
Links: []string{"http://example2.com/response"},
|
||||
}
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "wrongpassword")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example2.com/response_not_set")
|
||||
form.Add("response_type", "code")
|
||||
form.Add("state", "test_state")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("GET", user.AuthUrl()+"?"+form.Encode(), nil)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAuthRedirectUriSet(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockParseLinksHtmlParser{
|
||||
Links: []string{"http://example.com/response"},
|
||||
}
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "wrongpassword")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("response_type", "code")
|
||||
form.Add("state", "test_state")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("GET", user.AuthUrl()+"?"+form.Encode(), nil)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestAuthRedirectUriSameHost(t *testing.T) {
|
||||