-
-
Notifications
You must be signed in to change notification settings - Fork 78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added GitHub Enterprise and Private GitHub support #83
base: master
Are you sure you want to change the base?
Changes from all commits
039f38b
5e3a3e3
19cf7e8
1772eb3
69b16ff
5a19513
1985ea2
c58dd06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,40 +9,61 @@ Jekyll plugin for building Jekyll sites with any public GitHub-hosted theme | |
|
||
1. Add the following to your Gemfile | ||
|
||
```ruby | ||
gem "jekyll-remote-theme" | ||
``` | ||
```ruby | ||
gem "jekyll-remote-theme" | ||
``` | ||
|
||
and run `bundle install` to install the plugin | ||
and run `bundle install` to install the plugin | ||
|
||
2. Add the following to your site's `_config.yml` to activate the plugin | ||
|
||
```yml | ||
plugins: | ||
- jekyll-remote-theme | ||
``` | ||
Note: If you are using a Jekyll version less than 3.5.0, use the `gems` key instead of `plugins`. | ||
```yml | ||
plugins: | ||
- jekyll-remote-theme | ||
``` | ||
Note: If you are using a Jekyll version less than 3.5.0, use the `gems` key instead of `plugins`. | ||
|
||
3. Add the following to your site's `_config.yml` to choose your theme | ||
|
||
```yml | ||
remote_theme: benbalter/retlab | ||
``` | ||
or <sup>1</sup> | ||
```yml | ||
remote_theme: http[s]://github.<Enterprise>.com/benbalter/retlab | ||
``` | ||
<sup>1</sup> The codeload subdomain needs to be available on your github enterprise instance for this to work. | ||
```yml | ||
remote_theme: benbalter/retlab | ||
``` | ||
or | ||
```yml | ||
remote_theme: https://github.<Enterprise>.com/benbalter/retlab | ||
``` | ||
or | ||
```yml | ||
repository: https://github.<Enterprise>.com | ||
remote_theme: benbalter/retlab | ||
``` | ||
|
||
|
||
## Declaring your theme | ||
|
||
Remote themes are specified by the `remote_theme` key in the site's config. | ||
Remote themes are specified by the `remote_theme` key in the site's config. By default, this will use `https://github.com` as the host. If you would like to specify your own host, either specify the host part of the `remote_theme` (e.g., `https://github.com/benbalter/retlab`) or by specifying `repository` (e.g., `https://github.com`). | ||
|
||
For public GitHub, remote themes must be in the form of `OWNER/REPOSITORY`, and must represent a public GitHub-hosted Jekyll theme. See [the Jekyll documentation](https://jekyllrb.com/docs/themes/) for more information on authoring a theme. Note that you do not need to upload the gem to RubyGems or include a `.gemspec` file. | ||
For GitHub, remote themes must be in the form of `OWNER/REPOSITORY`, and must represent a Jekyll theme. See [the Jekyll documentation](https://jekyllrb.com/docs/themes/) for more information on authoring a theme. Note that you do not need to upload the gem to RubyGems or include a `.gemspec` file. | ||
|
||
You may also optionally specify a branch, tag, or commit to use by appending an `@` and the Git ref (e.g., `benbalter/[email protected]` or `benbalter/retlab@develop`). If you don't specify a Git ref, the `master` branch will be used. | ||
|
||
For Enterprise GitHub, remote themes must be in the form of `http[s]://GITHUBHOST.com/OWNER/REPOSITORY`, and must represent a public (non-private repository) GitHub-hosted Jekyll theme. Other than requiring the fully qualified domain name of the enterprise GitHub instance, this works exactly the same as the public usage. | ||
To use your own host, such as for Enterprise GitHub, you can specify `remote_theme` providing the full url (e.g., `https://GITHUBHOST.com/OWNER/REPOSITORY`), and must represent GitHub-hosted Jekyll theme. Alternatively, you can specify `repository`. This works exactly the same as the GitHub usage. | ||
|
||
## Private and Internal Themes | ||
|
||
If you would like to use a private or internal hosted theme (or have other specific headers needs): | ||
|
||
1. Create a Personal Access Token following the [Creating a personal access token on GitHub guides](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) | ||
|
||
2. Add the `remote_header` in your `_config.yml` | ||
|
||
```yml | ||
remote_header: | ||
Authorization: token <personal autorization token> | ||
``` | ||
Where the personal autorization token is the token provided by the github repo setting page. | ||
|
||
> :warning: **Storing credentials can lead to security issues** - As stated in the [Creating a personal access token on GitHub guides](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) Treat your tokens like passwords and keep them secret. When working with the API, use tokens as environment variables instead of hardcoding them into your programs. | ||
|
||
## Debugging | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,10 @@ class DownloadError < StandardError; end | |
autoload :Theme, "jekyll-remote-theme/theme" | ||
autoload :VERSION, "jekyll-remote-theme/version" | ||
|
||
CONFIG_KEY = "remote_theme" | ||
CONFIG_REPOSITORY_KEY = "repository" | ||
CONFIG_THEME_KEY = "remote_theme" | ||
CONFIG_HEADERS_KEY = "remote_headers" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would a user need to be able to specify custom headers? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To allow downloading a private hosted theme and additional headers required in a large organization. |
||
|
||
LOG_KEY = "Remote Theme:" | ||
TEMP_PREFIX = "jekyll-remote-theme-" | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,8 +11,9 @@ class Downloader | |
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, | ||
].freeze | ||
|
||
def initialize(theme) | ||
def initialize(theme, remote_headers) | ||
@theme = theme | ||
@remote_headers = remote_headers | ||
end | ||
|
||
def run | ||
|
@@ -29,6 +30,16 @@ def downloaded? | |
@downloaded ||= theme_dir_exists? && !theme_dir_empty? | ||
end | ||
|
||
def remote_headers | ||
default_headers = { | ||
"User-Agent" => USER_AGENT, | ||
"Accept" => "application/vnd.github.v3+json", | ||
} | ||
|
||
@remote_headers ||= default_headers | ||
@remote_headers.merge(default_headers) | ||
end | ||
|
||
private | ||
|
||
attr_reader :theme | ||
|
@@ -37,31 +48,55 @@ def zip_file | |
@zip_file ||= Tempfile.new([TEMP_PREFIX, ".zip"], :binmode => true) | ||
end | ||
|
||
def download | ||
Jekyll.logger.debug LOG_KEY, "Downloading #{zip_url} to #{zip_file.path}" | ||
Net::HTTP.start(zip_url.host, zip_url.port, :use_ssl => true) do |http| | ||
http.request(request) do |response| | ||
raise_unless_sucess(response) | ||
enforce_max_file_size(response.content_length) | ||
response.read_body do |chunk| | ||
zip_file.write chunk | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reorganizing code for better readability and usability |
||
def create_request(url) | ||
req = Net::HTTP::Get.new url.path | ||
|
||
remote_headers&.each do |key, value| | ||
req[key] = value unless value.nil? | ||
end | ||
|
||
req | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Create a request including the remote_headers configuration provided to the downloader |
||
|
||
def handle_response(response) | ||
raise_unless_success(response) | ||
enforce_max_file_size(response.content_length) | ||
response.read_body do |chunk| | ||
zip_file.write chunk | ||
end | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reading the archive file from the server as per the download code above |
||
|
||
def fetch(uri_str, limit = 10) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you describe the change here? I'm having trouble following. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It takes the remote_headers from the configuration file and ensures that the custom headers are passed to the request of the download of the remote_theme. I had to reorganize the download function code for readability and usability. More details are provided with additional comments of the relevant code. |
||
Jekyll.logger.debug LOG_KEY, "Finding redirect of #{uri_str}" | ||
|
||
raise DownloadError, "Too many redirect" if limit.zero? | ||
|
||
url = URI.parse(uri_str) | ||
req = create_request(url) | ||
|
||
Net::HTTP.start(url.host, url.port, :use_ssl => true) do |http| | ||
http.request(req) do |response| | ||
case response | ||
when Net::HTTPSuccess | ||
handle_response(response) | ||
when Net::HTTPRedirection | ||
fetch(response["location"], limit - 1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our organization is using a custom DNS, and the REST Api are creating some redirections. This new download code supports HTTP Redirections to a maximum of 10 hops. |
||
else | ||
raise DownloadError, "#{response.code} - #{response.message}" | ||
end | ||
end | ||
end | ||
end | ||
|
||
def download | ||
Jekyll.logger.debug LOG_KEY, "Downloading #{zip_url} to #{zip_file.path}" | ||
fetch(zip_url) | ||
@downloaded = true | ||
rescue *NET_HTTP_ERRORS => e | ||
raise DownloadError, e.message | ||
end | ||
|
||
def request | ||
return @request if defined? @request | ||
|
||
@request = Net::HTTP::Get.new zip_url.request_uri | ||
@request["User-Agent"] = USER_AGENT | ||
@request | ||
end | ||
|
||
def raise_unless_sucess(response) | ||
def raise_unless_success(response) | ||
return if response.is_a?(Net::HTTPSuccess) | ||
|
||
raise DownloadError, "#{response.code} - #{response.message}" | ||
|
@@ -91,8 +126,8 @@ def unzip | |
def zip_url | ||
@zip_url ||= Addressable::URI.new( | ||
:scheme => theme.scheme, | ||
:host => "codeload.#{theme.host}", | ||
:path => [theme.owner, theme.name, "zip", theme.git_ref].join("/") | ||
:host => theme.host, | ||
:path => [theme.owner, theme.name, "archive", theme.git_ref + ".zip"].join("/") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to the GitHub API documentation, host "api.github.com" and rest endpoint "zipball" is the proper endpoint for getting the repository archive zip file. As per the documentation, this requires support for redirections. Using "archive" should extend the the support to other git private repository such as gitlab and still work for github repos. |
||
).normalize | ||
end | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,24 +3,35 @@ | |
module Jekyll | ||
module RemoteTheme | ||
class Theme < Jekyll::Theme | ||
OWNER_REGEX = %r!(?<owner>[a-z0-9\-]+)!i.freeze | ||
NAME_REGEX = %r!(?<name>[a-z0-9\._\-]+)!i.freeze | ||
REF_REGEX = %r!@(?<ref>[a-z0-9\._\-]+)!i.freeze # May be a branch, tag, or commit | ||
THEME_REGEX = %r!\A#{OWNER_REGEX}/#{NAME_REGEX}(?:#{REF_REGEX})?\z!i.freeze | ||
DEFAULT_SCHEME = "https" | ||
DEFAULT_HOST = "github.com" | ||
ALPHANUMERIC_WITH_DASH_REGEX = %r![\w\-]+!i.freeze | ||
ALPHANUMERIC_WITH_DASH_DOT_REGEX = %r![\w\-\.]+!i.freeze | ||
OWNER_REGEX = %r!(?<owner>#{ALPHANUMERIC_WITH_DASH_REGEX})!i.freeze | ||
NAME_REGEX = %r!(?<name>#{ALPHANUMERIC_WITH_DASH_DOT_REGEX})!i.freeze | ||
REF_REGEX = %r!@(?<ref>#{ALPHANUMERIC_WITH_DASH_DOT_REGEX})!i.freeze | ||
THEME_REGEX = %r!\A/?#{OWNER_REGEX}/#{NAME_REGEX}(?:#{REF_REGEX})?\z!i.freeze | ||
|
||
# Initializes a new Jekyll::RemoteTheme::Theme | ||
# | ||
# raw_theme can be in the form of: | ||
# here are the valid combinations for repository/remote_theme | ||
# | ||
# 1. owner/theme-name - a GitHub owner + theme-name string | ||
# 2. owner/theme-name@git_ref - a GitHub owner + theme-name + Git ref string | ||
# 3. http[s]://github.<yourEnterprise>.com/owner/theme-name | ||
# - An enterprise GitHub instance + a GitHub owner + a theme-name string | ||
# 4. http[s]://github.<yourEnterprise>.com/owner/theme-name@git_ref | ||
# - An enterprise GitHub instance + a GitHub owner + a theme-name + Git ref string | ||
def initialize(raw_theme) | ||
@raw_theme = raw_theme.to_s.downcase.strip | ||
super(@raw_theme) | ||
# [scheme://host/]owner/theme-name[@git_ref] | ||
# | ||
# optional scheme://host | ||
# scheme (default: https): Could be any scheme but generally should be http, https or git | ||
# host (default: github.com): Could be any host | ||
# | ||
# owner: Git repo owner | ||
# theme-name: Git repo name | ||
# optional @git_ref (default: master): Git Reference hash, tag or branch | ||
# | ||
# Header to pass to remote call | ||
# | ||
def initialize(repository, remote_theme) | ||
@repository = repository | ||
@remote_theme = remote_theme.to_s.downcase | ||
super(@remote_theme) | ||
end | ||
|
||
def name | ||
|
@@ -63,26 +74,45 @@ def inspect | |
" ref=\"#{git_ref}\" root=\"#{root}\">" | ||
end | ||
|
||
def to_s | ||
uri.to_s | ||
end | ||
|
||
private | ||
|
||
def default_host | ||
ENV["GITHUB_HOSTNAME"] || ENV["PAGES_GITHUB_HOSTNAME"] || DEFAULT_HOST | ||
end | ||
|
||
def uri | ||
return @uri if defined? @uri | ||
|
||
@uri = if @raw_theme =~ THEME_REGEX | ||
Addressable::URI.new( | ||
:scheme => "https", | ||
:host => "github.com", | ||
:path => @raw_theme | ||
) | ||
else | ||
Addressable::URI.parse @raw_theme | ||
end | ||
remote_theme_parsed = Addressable::URI.parse(@remote_theme) | ||
@uri = if @repository | ||
# Use the remote host as the uri and the remote theme as the path | ||
repository_parsed = Addressable::URI.parse(@repository) | ||
Addressable::URI.new( | ||
:scheme => repository_parsed.scheme, | ||
:host => repository_parsed.host, | ||
:path => remote_theme_parsed.path | ||
) | ||
elsif remote_theme_parsed.scheme && remote_theme_parsed.host | ||
# Use the remote theme as the uri | ||
remote_theme_parsed | ||
else | ||
# Otherwise, make some assumptions, using remote theme as the path | ||
Addressable::URI.new( | ||
:scheme => DEFAULT_SCHEME, | ||
:host => default_host, | ||
:path => remote_theme_parsed.path | ||
) | ||
end | ||
rescue Addressable::URI::InvalidURIError | ||
@uri = nil | ||
end | ||
|
||
def theme_parts | ||
@theme_parts ||= uri.path[1..-1].match(THEME_REGEX) if uri | ||
@theme_parts ||= uri.path.match(THEME_REGEX) if uri | ||
end | ||
|
||
def gemspec | ||
|
@@ -91,9 +121,8 @@ def gemspec | |
|
||
def valid_hosts | ||
@valid_hosts ||= [ | ||
"github.com", | ||
ENV["PAGES_GITHUB_HOSTNAME"], | ||
ENV["GITHUB_HOSTNAME"], | ||
host, | ||
default_host, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As currently written, this is an anti-abuse mechanism, which it looks like this change removes. In an untrusted environment, a malicious actor could request a remote theme from a malicious Git server, leading to a denial of service or other attack. How would we limit hosts in untrusted environments? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem for us is that since we are using custom DNS, privately hosted repos, the themes are not hosted on any of theses endpoints. |
||
].compact.to_set | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
plugins: | ||
- jekyll-remote-theme | ||
|
||
remote_theme: pages-themes/PrImeR | ||
|
||
whitelist: | ||
- jekyll-remote-theme | ||
- jekyll-seo-tag | ||
- jekyll-github-metadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that this is explicitly called out in the documentation, I'd be hesitant to create a workflow that requires committing the secret to the repository. Could we use an environmental variable as suggested?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fully agree that specifying an environment variable here would prevent "hardcoding" any credentials which can be a security hazard. Unfortunately, unless some changes are done on the server side to allow the GitHub Pages to specify credentials for a privately hosted themes, this was the easiest way to solve the issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So does this mean that there is no way for GitHub Secrets to be accessed in the GitHub Pages environment?